Summary
GhostLock (CVE-2026-43499) is a use-after-free in the Linux kernel’s rtmutex/futex-PI code (remove_waiter() in kernel/locking/rtmutex.c). On the requeue-PI proxy path, remove_waiter() clears pi_blocked_on on the requeuer instead of the waiter, so the waiter task can return to userspace with pi_blocked_on still pointing at the rt_mutex_waiter on its own FUTEX_WAIT_REQUEUE_PI stack frame. That frame is freed the moment the syscall returns, and any later PI chain walk through the task follows the dangling pointer.
GhostLock was introduced by the 2011 rtmutex rework and sat untouched for about fifteen years. Triggering it needs only ordinary threading and futex syscalls: no config beyond CONFIG_FUTEX_PI=y, and no capabilities or namespaces. We turned the stack-UAF into a 97% stable privilege escalation and container escape, and won $92,337 in Google’s kernelCTF.
Impact & exploitability
Regular syscalls mint a dangling pointer into freed kernel stack, which the attacker reclaims with controlled bytes and dereferences on demand as a forged rt_mutex_waiter. The whole chain runs locally from an unprivileged process.
- Attacker model: a local unprivileged process using ordinary threading and futex syscalls; no capabilities, no user or network namespaces
- Primitive: a dangling kernel-stack pointer, refined into one constrained pointer write whose stored value the attacker also chooses
- Practical impact: overwrite
inet6_protos[IPPROTO_UDP]to point into the CPU entry area, hijack control flow from a loopback IPv6 UDP packet, and flipcore_pattern’s mode bits with a single write (DirtyMode) - End result: privilege escalation and container escape to root, about 97% stable, landing in roughly 5 seconds on the remote
Am I affected?
The vulnerable code is built in whenever PI futexes are configured. A target is vulnerable if:
- its kernel is in the affected range
2.6.39to7.1(May 2011 to about May 2026), and - it is built with
CONFIG_FUTEX_PI=y, which is the default on essentially EVERY distribution.
No capabilities, user namespaces, or network access are needed: the trigger is a set of ordinary threading and futex syscalls available to any local process. In practice, every unpatched Linux distribution is affected.
Root cause
remove_waiter() was written for the self-blocking slow path, where current is the waiter, so it has always cleared current->pi_blocked_on. Requeue-PI reuses the same helper through rt_mutex_start_proxy_lock() on behalf of a sleeping task, so current is the requeuer, not the waiter. When the proxy enqueue hits -EDEADLK and rolls back, remove_waiter() dequeues the waiter from the lock but scrubs pi_blocked_on on the wrong task. lockdep does not catch it, because it only checks that a pi_lock is held, not whose.
The waiter task then returns to userspace with pi_blocked_on still pointing at its freed FUTEX_WAIT_REQUEUE_PI frame. Reaching the rollback needs a PI dependency cycle across three futex words and three threads (waiter -> f_pi_target -> owner -> f_pi_chain -> waiter), which closes on its own once staged. After that there is no time pressure: a follow-up sched_setattr() walks the chain through the task whenever it likes, and dereferences the dangling pointer.
Exploit
The chain is short:
- prefetch -> leak the kernel image slide and the physmap base.
- GhostLock -> leave a dangling
rt_mutex_waiterin the waiter task’spi_blocked_on. - stack-UAF reclaim -> reclaim the freed frame with the
user_auxvbuffer ofPR_SET_MM_MAPand forge a fakert_mutex_waiterover it. - one constrained write -> an rtmutex rb-tree erase stores a CEA pointer into
inet6_protos[IPPROTO_UDP]. - CFH -> a loopback IPv6 UDP packet dispatches through the overwritten handler and pivots into a short ROP stack staged in the CPU entry area.
- DirtyMode -> one write flips
core_pattern’s mode bits, then the rest of the LPE is pure userspace.
The full write-up covers the three-futex deadlock, the stack reclaim with
PR_SET_MM_MAP, forging the waiter past its structural checks, and the CEA
overlay that pivots and flips core_pattern in a single write.
Patch
The upstream fix reads the owning task out of waiter->task instead of current, so the proxy rollback scrubs the right pi_blocked_on:
static void __sched remove_waiter(struct rt_mutex_base *lock, struct rt_mutex_waiter *waiter){ bool is_top_waiter = (waiter == rt_mutex_top_waiter(lock)); struct task_struct *owner = rt_mutex_owner(lock); struct task_struct *waiter_task = waiter->task; struct rt_mutex_base *next_lock;
raw_spin_lock(¤t->pi_lock); rt_mutex_dequeue(lock, waiter); current->pi_blocked_on = NULL; raw_spin_unlock(¤t->pi_lock); scoped_guard(raw_spinlock, &waiter_task->pi_lock) { rt_mutex_dequeue(lock, waiter); waiter_task->pi_blocked_on = NULL; } ... rt_mutex_adjust_prio_chain(owner, RT_MUTEX_MIN_CHAINWALK, lock, next_lock, NULL, current); next_lock, NULL, waiter_task);}Mitigation
Update to a fixed kernel (the latest LTS), carrying both the fix and its follow-up. There is no clean site-level workaround, since the trigger is ordinary futex and threading syscalls that any local process can issue. Two config options raise the bar without removing the bug: RANDOMIZE_KSTACK_OFFSET turns the deterministic stack reuse into a roughly 1/64 (>5-bit) guess. But it is not a substitute for patching.
Timeline & credit
Found by VEGA at Nebula Security. We reported the bug with a draft patch to security@kernel.org on Apr 18, 2026; it was fixed two days later and backported on May 4. A regression introduced by the first fix (CVE-2026-53166) was patched on Jun 2. Google acknowledged the kernelCTF submission and rewarded us $92,337 on Jun 30.
For all bugs found by VEGA, we follow a 90+30 day disclosure policy.