From 6f56985815eb6376bfea42f103c4ec595ed73b29 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Wed, 5 Aug 2026 15:18:39 +0200 Subject: [PATCH] Increment slab slot tags deterministically on reuse Previously every reuse of a slab slot picked a fresh random MTE tag, excluding the reserved, previous and neighbor tags. MTE tags are 4 bits with tag 0 reserved, leaving 15 usable tags, so a stale pointer to tag t evaded use-after-free detection whenever a reallocation happened to draw t again. With the random scheme the previous tag is excluded, so the first reuse is always detected, but every reuse from the second onward independently draws t with probability 1/(15 - n), where n is the number of usable tags excluded that cycle (previous tag plus neighbors, n in 1..3), i.e. ~7% (1/14) to ~8% (1/12) per cycle. That risk compounds: the chance of evasion within N reuses is ~1 - (13/14)^(N-1), already ~49% by 10 reuses. Keep a random tag for the first use of a slot, but on every subsequent reuse derive the next tag by incrementing the stored previous tag, skipping excluded values. The tag advances monotonically around the ring and only coincides with the stale tag t once per lap, so detection is deterministic across a full lap of the usable tags instead of leaking ~7% per cycle. A lap is up to 14 reuse cycles; because the two neighbor tags are also excluded each cycle, the increment can skip past them and complete a lap in fewer steps, so 14 is an upper bound rather than a guaranteed floor. Neighbor tags remain excluded, so deterministic linear-overflow detection is unchanged. This trades the random scheme's per-allocation unpredictability for deterministic multi-cycle detection: a slot's tag sequence is now fully determined by its previous and neighbor tags, so leaking one tag reveals the slot's tag for every other cycle. Slot selection remains randomized, and this matches the deterministic tagging already used for linear overflows. Note that since my laptop doesn't have MTE, this commit couldn't be tested on MTE hardware. --- README.md | 17 ++++++++++++----- h_malloc.c | 22 ++++++++++++++++++++-- 2 files changed, 32 insertions(+), 7 deletions(-) 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);