diff --git a/README.md b/README.md index 1ae3de32..e58ba44c 100644 --- a/README.md +++ b/README.md @@ -483,7 +483,7 @@ was a bit less important and if a core goal was finding latent bugs. against accessing freed memory * guarantee distinct tags for adjacent memory allocations by incrementing past matching values for deterministic detection of linear overflows - * [future] store previous random tag and increment it to get the next tag + * store the previous tag for each slot and increment it to get the next tag for that slot to provide deterministic use-after-free detection through multiple cycles of memory reuse @@ -727,22 +727,29 @@ freeing as there would be if the kernel supported these features directly. ## Memory tagging -Random tags are set for all slab allocations when allocated, with 4 excluded values: +The first time a slab slot is used, a random tag is set for the allocation, with +4 excluded values: 1. the reserved `0` tag 2. the previous tag used for the slot 3. the current (or previous) tag used for the slot to the left 4. the current (or previous) tag used for the slot to the right +On each subsequent reuse of the slot, the tag is instead derived deterministically +by incrementing the previous tag for the slot, skipping past the same 4 excluded +values. This makes each slot cycle through all of the usable tags before any tag +is repeated. + When a slab allocation is freed, the reserved `0` tag is set for the slot. Slab allocation slots are cleared before reuse when memory tagging is enabled. This ensures the following properties: - Linear overflows are deterministically detected. -- Use-after-free are deterministically detected until the freed slot goes through - both the random and FIFO quarantines, gets allocated again, goes through both - quarantines again and then finally gets allocated again for a 2nd time. +- Use-after-free accesses are deterministically detected until the slot has been + reused enough times for the incrementing tag to wrap around to the stale + pointer's tag, i.e. through a full cycle of the usable tags rather than being + probabilistically reused after a single reallocation. - Since the default `0` tag is reserved, untagged pointers can't access slab allocations and vice versa. diff --git a/h_malloc.c b/h_malloc.c index 46a6d481..ce8b6329 100644 --- a/h_malloc.c +++ b/h_malloc.c @@ -609,11 +609,29 @@ static void *tag_and_clear_slab_slot(struct slab_metadata *metadata, void *slot_ // current or previous tag of left neighbor or 0 if there's no left neighbor or if it was never used tem |= (1 << u4_arr_get(slot_tags, slot_idx)); // previous tag of this slot or 0 if it was never used - tem |= (1 << u4_arr_get(slot_tags, slot_idx + 1)); + u8 prev_tag = u4_arr_get(slot_tags, slot_idx + 1); + tem |= (1 << prev_tag); // current or previous tag of right neighbor or 0 if there's no right neighbor or if it was never used tem |= (1 << u4_arr_get(slot_tags, slot_idx + 2)); - void *tagged_ptr = arm_mte_create_random_tag(slot_ptr, tem); + void *tagged_ptr; + if (prev_tag == RESERVED_TAG) { + // A stored tag of 0 (RESERVED_TAG) means the slot was never used, since a used slot always + // stores a tag in [1, 15]. Pick a random tag as the baseline, excluding the reserved tag + // and the neighboring slots' tags. + tagged_ptr = arm_mte_create_random_tag(slot_ptr, tem); + } else { + // Derive the next tag by advancing past the previous one, skipping excluded tags and + // wrapping around within the tag space. This makes a slot deterministically cycle through + // all usable tags before repeating, extending use-after-free detection across multiple + // cycles of reuse. + const u8 tag_mask = (1 << TAG_WIDTH) - 1; + u8 tag = prev_tag; + do { + tag = (tag + 1) & tag_mask; + } while (tem & (1 << tag)); + tagged_ptr = set_pointer_tag(slot_ptr, tag); + } // slot addresses and sizes are always aligned by 16 arm_mte_tag_and_clear_mem(tagged_ptr, slot_size);