Re: [REPORT] futex: private hash refcount corruption and UAF via CLONE_VM PR_FUTEX_HASH race

From: Davidlohr Bueso

Date: Thu Apr 30 2026 - 21:13:02 EST


On Thu, 30 Apr 2026, ????????? wrote:

Hello,

I found a race in the futex private hash implementation that can
corrupt the private-hash reference state and free a live `struct
futex_private_hash`. I can trigger a KASAN slab-use-after-free in
`futex_hash_put()` from an unprivileged process inside QEMU.

## Summary

`futex_hash_allocate()` lazily initializes `current->mm->futex_ref`
without serialization. The code comment assumes the allocation is
always performed by the first thread, but `clone(CLONE_VM | SIGCHLD)`
creates another task that shares the same `mm_struct` while not
setting `CLONE_THREAD`. Such tasks bypass
`need_futex_hash_allocate_default()` and can concurrently call
`prctl(PR_FUTEX_HASH, PR_FUTEX_HASH_SET_SLOTS, ...)` while
`mm->futex_ref` is still `NULL`.

So the code doesn't hold with that comment in futex_hash_allocate()
when things get creative.

afaict we can either assume some unlikely concurrency in futex_hash_allocate()
and do something like this:

@@ -1798,15 +1798,20 @@ static int futex_hash_allocate(unsigned int hash_slots, unsigned int flags)
}
}

- if (!mm->futex_ref) {
+ if (!READ_ONCE(mm->futex_ref)) {
+ unsigned int __percpu *ref;
+
+ ref = alloc_percpu(unsigned int);
+ if (!ref)
+ return -ENOMEM;
+
+ this_cpu_inc(*ref); /* 0 -> 1 */
/*
- * This will always be allocated by the first thread and
- * therefore requires no locking.
+ * Ensure a task observing futex_ref non-nil will always
+ * see the counter with its first reference.
*/
- mm->futex_ref = alloc_percpu(unsigned int);
- if (!mm->futex_ref)
- return -ENOMEM;
- this_cpu_inc(*mm->futex_ref); /* 0 -> 1 */
+ if (unlikely(cmpxchg_release(&mm->futex_ref, NULL, ref)))
+ free_percpu(ref);
}

fph = kvzalloc(struct_size(fph, queues, hash_slots),

... or keep the current assumption and update need_futex_hash_allocate_default()
such that we only consider CLONE_VM (maybe also CLONE_VFORK check to return false).

@@ -1948,8 +1948,11 @@ static void rv_task_fork(struct task_struct *p)

static bool need_futex_hash_allocate_default(u64 clone_flags)
{
- if ((clone_flags & (CLONE_THREAD | CLONE_VM)) != (CLONE_THREAD | CLONE_VM))
+ if (!(clone_flags & CLONE_VM))
return false;
return true;
}

I ran the reproducer but was unable to trigger the reported kasan splat altogether.

Thanks,
Davidlohr