Vega VEGA / Bug List / CVE-2026-43499
Finding · Linux kernel · Futex PI / rtmutex · Jul 07, 2026
CVE-2026-43499

IonStack part II: GhostLock, a 15-year rtmutex/futex-PI stack use-after-free, from unprivileged syscalls to root

kernel/locking/rtmutex.c · remove_waiter
Affects Linux 2.6.39-rc1 - 7.1-rc1 Any distro without the patch (only CONFIG_FUTEX_PI=y, no privilege)
Disclosure timeline
  1. Reported
    Apr 18, 2026
  2. Fixed
    Apr 20, 2026
  3. Backported
    May 04, 2026
  4. Regression CVE-2026-53166 patched
    Jun 02, 2026
  5. kernelCTF reward
    Jun 30, 2026
  6. Disclosed
    Jul 07, 2026

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 flip core_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.39 to 7.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_waiter in the waiter task’s pi_blocked_on.
  • stack-UAF reclaim -> reclaim the freed frame with the user_auxv buffer of PR_SET_MM_MAP and forge a fake rt_mutex_waiter over 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.

Read the deep-dive

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(&current->pi_lock);
rt_mutex_dequeue(lock, waiter);
current->pi_blocked_on = NULL;
raw_spin_unlock(&current->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.

Vega · AI security research

Find bugs in your code. Before anyone else does.

Sign up