GhostLock, a stack-UAF that has existed in all Linux distributions for 15 years

GhostLock (CVE-2026-43499) is a critical Linux kernel vulnerability affecting almost all distributions since 2011, allowing unprivileged local users to achieve highly stable privilege escalation and container escape.
GhostLock (CVE-2026-43499) is a Linux kernel vulnerability found by VEGA that exists in every major distribution since 2011. Triggering the bug does not require any special kernel config or privilege. By turning it into a 97% stable privilege escalation and container escape, Google has rewarded us $92,337 in kernelCTF. This writeup covers the technical details of the exploit.
Vulnerability Summary
GhostLock (CVE-2026-43499) lets an unprivileged local attacker:
- Get a dangling kernel pointer to kernel stack memory with only regular threading syscalls.
- Write a pointer to an almost arbitrary address.
- Hijack a function table to get control flow hijack and eventually get root access.
GhostLock was introduced in Linux 2.6.39 and fixed in Linux 7.1. It has existed in the Linux kernel for more than 15 years. Every Linux distribution without the patch is affected and should consider upgrading to the latest LTS version.
Vulnerability Analysis
Overview
GhostLock was introduced with the rtmutex rework in 8161239a8bcc
(“rtmutex: Simplify PI algorithm and make highest prio task get lock”), and sat untouched for about fifteen years until the April 2026 fix in 3bfdc63936dd
(“rtmutex: Use waiter::task instead of current in remove_waiter()”). The affected range is v2.6.39-rc1
to v7.1-rc1
, with CONFIG_FUTEX_PI=y
the only requirement and no capabilities or user namespaces needed.
remove_waiter()
in kernel/locking/rtmutex.c
clears current->pi_blocked_on
. That is correct on the normal slow path, where current
is the task that owns the waiter. It is wrong on the proxy path. rt_mutex_start_proxy_lock()
enqueues, and on error rolls back, an rt_mutex_waiter
on behalf of another task, so current
is the requeuer rather than the waiter.
The waiter object lives on the stack of a task sleeping in FUTEX_WAIT_REQUEUE_PI
. A FUTEX_CMP_REQUEUE_PI
then proxies that waiter onto the target PI futex. When the rtmutex chain walk reports a deadlock, the rollback dequeues the waiter from the lock but clears pi_blocked_on
on the requeuer. The waiter task keeps pi_blocked_on
pointing at its own stack frame, which is popped the moment the waiter returns to userspace. Any later PI chain walk through that task follows the dangling pointer.
Root cause
This is the same shape as many other life-cycle bugs: a function reused by a caller it was never written for.
The helper function remove_waiter()
was originally written for exactly one scenario: a thread blocks on its own, then cleans up after itself.
So it has always assumed that current
(whichever thread happens to be running) is the waiter
it needs to clean up, and clears current->pi_blocked_on
accordingly.
However, Requeue-PI breaks that assumption. Through rt_mutex_start_proxy_lock()
, this helper is now used to clean up on behalf of a different, sleeping thread.
In that path, current
is the thread that issued FUTEX_CMP_REQUEUE_PI
rather than the actual waiter
.
When __rt_mutex_start_proxy_lock()
returns -EDEADLK
, it rolls back via remove_waiter()
, the misused helper.
remove_waiter()
then scrubs the wrong task.
waiter
is the object that lives on the sleeping thread’s own stack, while current
here is the thread that requested the requeue. The fix locks waiter->task->pi_lock
and clears waiter->task->pi_blocked_on
instead. This issue slips past lockdep, which only checks that a pi_lock
is held but not whose it is.
Triggering the -EDEADLK Path. Reaching the -EDEADLK
rollback needs a PI dependency cycle built from three futex words and three threads.
f_pi_chain
, a PI futex, locked first by thewaiterthread.f_pi_target
, a PI futex, locked first by theownerthread. This is the requeue target.f_wait
, the plain futex the waiter blocks on withFUTEX_WAIT_REQUEUE_PI
.
The sequence is:
- The waiter takes
f_pi_chain
, then blocks inFUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target)
. Itsrt_mutex_waiter
is now on its stack. - The owner takes
f_pi_target
, then blocks onf_pi_chain
, which the waiter holds. - The main thread calls
FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)
.
The requeue tries to proxy the waiter onto f_pi_target
. The owner of f_pi_target
is already blocked behind the waiter through f_pi_chain
, so the chain walk closes the loop waiter -> f_pi_target -> owner -> f_pi_chain -> waiter
. It returns -EDEADLK
and takes the buggy rollback. The waiter wakes with a dangling pi_blocked_on
.
Here the only ordering that matters is the requeuer rolling back the waiter while the waiter still owns the soon-to-be-freed object, and once the cycle is staged that happens on its own. After it resolves there is no time pressure at all. The waiter sits in userspace with a dangling pi_blocked_on
, and the follow-up sched_setattr()
that walks the chain can fire whenever it likes. The UAF window is wide open.
The catch is where the freed object lives on the kernel stack (stack-UAF if we call ret
out of the futex syscall a “free”). To reclaim it, we need to find a syscall that can land controlled bytes back on the same stack at the same depth (offset).
Triggering the stack-UAF
Staging the three-futex cycle leaves the waiter task in userspace with pi_blocked_on
dangling into its old FUTEX_WAIT_REQUEUE_PI
frame. Everything below rides on that one pointer.
Note that three threads is for better understanding. To win the race and trigger UAF, you only need one CPU core.
The initial primitive from GhostLock
By now we hold a pointer into freed kernel stack, and we can trigger, at will, a kernel access that dereferences it as an rt_mutex_waiter
. We can spray controlled bytes onto that stack and forge the rt_mutex_waiter
outright. Depending on the shape we forge, this one access yields several primitives, two main ones:
- write a pointer to an arbitrary (but constrained) address
- write 8 bytes of zero to an arbitrary (but constrained) address
Several pointer dereferences and integrity checks run before the primitive fires, and after it fires the kernel returns normally, no crash.
So our main questions, each answered in a section below:
- how do we get the freed stack memory back (spray)? -> Reusing the stack
- how do we get the fake
rt_mutex_waiter
past its built-in structural checks, and forge pointers that read as valid? -> From fake waiter to a write - which write primitive, and what do we write where? what does the primitive constrain about the “arbitrary” address? -> Use inet6_protos
Exploit Details
Exploit Summary
prefetch-> Leak the kernel image slide and the physmap base.GhostLock-> Leave a danglingrt_mutex_waiter
in the waiter task’spi_blocked_on
.(stack-)UAF Reclaim-> UsePR_SET_MM_MAP
to reclaim the waiter’s own kernel stack and forge a fakert_mutex_waiter
over the freed frame.Arb address writer-> Rtmutex rb-tree erase: one constrained pointer write (which we can reclaim its content), overwrite struct which contains a function table:inet6_protos[IPPROTO_UDP] = <CEA pointer>
.CPU entry area-> Host {fakeinet6_protocol
, pivot slots, ROP stack} all together at a known direct-map address.Trigger CFH-> Trigger a loopback IPv6 UDP packet calls through the overwritten handler and pivots.DirtyMode-> One write flipscore_pattern
’s mode bits, then the rest LPE is pure userspace.
What about Android?
This part we are focusing on basic exploit steps of generic x86 Linux systems, our next blog will discuss how to exploit GhostLock on Android, reclaiming stack frame, bypassing both ASLR and CFI.
Background of used tricks
Prefetch ASLR Leak
A prefetch
on a given address runs in a different number of cycles depending on whether that address is mapped in the current page tables, so an unprivileged process can time prefetch
across the kernel range and read
Source: Hacker News
















