November 25th, 2022
Chris Rohlf
In April of 2022 I added an experimental feature to IsoAlloc that implemented a memory tagging model. The idea is very similar to Chrome's MTECheckedPtr which itself is very similar to ARM's upcoming Memory Tagging Extension (MTE) due in ARM v8.5-A.
ARM MTE is a hardware based solution for detecting both spatial and temporal memory safety issues in software with little to no overhead. It works by using the Top Byte Ignore (TBI) feature to transparently tag pointers with metadata or a 'tag'. With MTE this tag is transparently added, verified, and removed from pointers in hardware. MTE errors can be configured to be delivered synchronously or asynchronously.
Pointer tagging as implemented here in IsoAlloc is conceptually very similar, except these operations are implemented in software. There are of course limitations to the IsoAlloc implementation such as performance overhead and less flexibility in how errors are delivered in comparison to MTE. But it is a usable software alternative for systems without TBI or MTE like x86_64. If your target system has TBI you can still use IsoAlloc pointer tagging as described here.
Memory Allocator Support
Implementing a pointer tagging scheme in software is relatively straightforward. On 64 bit architectures such as aarch64 and x86_64 there is only 48 bits of addressable virtual memory which means for canonical addresses (0-0x00007FFFFFFFFFFF) there are 16 unused bits in each pointer. Storing a random 1 byte value in those unused bytes is as simple as:
tagged_pointer = ((random_tag << 56) | raw_pointer);
size_t tag_mapping_size = ROUND_UP_PAGE((new_zone->chunk_count * MEM_TAG_SIZE));
if(internal == false) {
total_size += (tag_mapping_size + g_page_size)
new_zone->tagged = true;
} else {
tag_mapping_size = 0;
}
if(new_zone->tagged == true) {
create_guard_page(p + g_page_size + tag_mapping_size);
new_zone->user_pages_start = (p + g_page_size + tag_mapping_size + g_page_size);
uint64_t *_mtp = p + g_page_size;
/* (>> 3) == sizeof(uint64_t) == 8 */
uint64_t tms = tag_mapping_size >> 3;
/* Generate random tags */
for(uint64_t o = 0; o < tms; o++) {
_mtp[o] = us_rand_uint64(&_root->seed);
}
} else {
new_zone->user_pages_start = (p + g_page_size);
}

INTERNAL_HIDDEN uint8_t _iso_alloc_get_mem_tag(void *p, iso_alloc_zone_t *zone) {
void *user_pages_start = UNMASK_USER_PTR(zone);
uint8_t *_mtp = (user_pages_start - g_page_size - ROUND_UP_PAGE(zone->chunk_count * MEM_TAG_SIZE));
const uint64_t chunk_offset = (uint64_t)(p - user_pages_start);
/* Ensure the pointer is a multiple of chunk size */
if(UNLIKELY((chunk_offset & (zone->chunk_size - 1)) != 0)) {
LOG_AND_ABORT("Chunk offset %d not an alignment of %d", chunk_offset, zone->chunk_size);
}
_mtp += (chunk_offset >> zone->chunk_size_pow2);
return *_mtp;
}
INTERNAL_HIDDEN void *_tag_ptr(void *p, iso_alloc_zone_t *zone) {
if(UNLIKELY(p == NULL || zone == NULL)) {
return NULL;
}
const uint64_t tag = _iso_alloc_get_mem_tag(p, zone);
return (void *) ((tag << UNTAGGED_BITS) | (uintptr_t) p);
}
INTERNAL_HIDDEN void *_untag_ptr(void *p, iso_alloc_zone_t *zone) {
if(UNLIKELY(p == NULL || zone == NULL)) {
return NULL;
}
void *untagged_p = (void *) ((uintptr_t) p & TAGGED_PTR_MASK);
const uint64_t tag = _iso_alloc_get_mem_tag(untagged_p, zone);
return (void *) ((tag << UNTAGGED_BITS) ^ (uintptr_t) p);
}
// _iso_alloc_get_mem_tag returns 0xed, the correct tag for the chunk
((0xed << 56) ^ 0xed000b8066c1a000) = 0xb8066c1a000
// An incorrect tag will result in an incorrect untagged pointer
((0xed << 56) ^ 0xab000b8066c1a000) = 0x46000b8066c1a000
template <typename T>
class IsoAllocPtr {
public:
IsoAllocPtr(iso_alloc_zone_handle *handle, T *ptr) : eptr(nullptr), zone(handle) {
eptr = iso_alloc_tag_ptr((void *) ptr, zone);
}
T *operator->() {
T *p = reinterpret_cast<T *>(iso_alloc_untag_ptr(eptr, zone));
return p;
}
void *eptr;
iso_alloc_zone_handle *zone;
};
// Allocates a chunk from zone and returns a tagged pointer void *iso_alloc_from_zone_tagged(iso_alloc_zone_handle *zone) // Tags a pointer from a private zone void *iso_alloc_tag_ptr(void *p, iso_alloc_zone_handle *zone) // Untags a pointer from a private zone void *iso_alloc_untag_ptr(void *p, iso_alloc_zone_handle *zone) // Retrieves the 1 byte tag for an untagged pointer uint8_t iso_alloc_get_mem_tag(void *p, iso_alloc_zone_handle *zone) // Verifies the tag on a tagged pointer, aborts if incorrect void iso_alloc_verify_ptr_tag(void *p, iso_alloc_zone_handle *zone)