A vulnerability doesn’t have to be clever to be dangerous. Sometimes it just has to sit unnoticed in old code that nobody has looked at in years. That’s exactly what happened with GhostLock.
Nebula Security disclosed the flaw this week as CVE-2026-43499, but the detail that really stands out is its age. The bug has been sitting in the Linux kernel’s real time mutex code for roughly 15 years. Since 2011, it has quietly shipped in the default kernels of almost every major Linux distribution because a cleanup routine was clearing the state of the wrong task.
The bug was uncovered by VEGA, Nebula’s automated code scanning system, while it was piecing together a two stage exploit chain the company calls IonStack. The first stage targets Firefox’s IonMonkey JIT engine through CVE-2026-10702, a vulnerability fixed in Firefox 151.0.3. Exploiting it gives an attacker code execution inside the browser’s renderer process. GhostLock handles the second stage by escaping that sandbox and turning renderer level access into full root privileges.
Nebula demonstrated the complete chain against Firefox on Android with a single click attack. A user only has to open a malicious link for the exploit to take over the device. The company says a separate technical write-up covering the Android side of the attack is coming soon. Google’s kernelCTF program awarded $92,337 for the GhostLock portion alone, which says a lot about how valuable a reliable Linux kernel privilege escalation is. Nebula reports the exploit succeeds about 97 percent of the time, requires no Linux capabilities, and works without unusual system configuration.
GhostLock carries a CVSS score of 7.8, which places it in the “High” severity category instead of “Critical” because it requires local access. In practice, that distinction doesn’t make it much less dangerous. Local access is exactly what an attacker gains after compromising a browser renderer, escaping a container, abusing a CI pipeline, or landing on a shared system. GhostLock was almost made for that role. It takes an initial foothold and turns it into complete control over the machine.
What broke
The root cause sits inside remove_waiter(), a function in kernel/locking/rtmutex.c. Its job is fairly simple. Whenever a thread stops waiting on a priority inheritance (PI) mutex, whether it successfully acquires the lock or backs out, remove_waiter() cleans up the kernel’s bookkeeping.
For years, the function only had to deal with one situation. A thread blocked on a mutex for itself, then later cleaned up its own wait state. Because of that, the code made a perfectly reasonable assumption: the thread currently executing (current) was also the thread that owned the rt_mutex_waiter structure being removed. It simply cleared current->pi_blocked_on and returned.
That assumption stopped being true when the kernel gained support for PI futex requeue operations. The FUTEX_CMP_REQUEUE_PI syscall, built on top of rt_mutex_start_proxy_lock(), allows one thread to create or cancel a PI wait on behalf of another thread that is asleep. In other words, the thread making the syscall is no longer the thread that owns the waiter object.
At that point, current refers to the thread performing the requeue operation, not the sleeping thread whose waiter is actually being cleaned up. The real owner is still blocked in FUTEX_WAIT_REQUEUE_PI, with its rt_mutex_waiter structure sitting on its own kernel stack. When remove_waiter() clears current->pi_blocked_on, it updates the wrong task. The sleeping thread’s state is left untouched, creating the stale kernel state that GhostLock later turns into a privilege escalation primitive.

Turning a dangling stack pointer into a write primitive
This is where the bug stops being a kernel mistake and starts becoming an exploit. A dangling pointer into freed kernel stack memory does not automatically give an attacker anything useful. The hard part is reclaiming that exact stack space with attacker-controlled data before the kernel follows the stale pointer again.
📬 Stay Ahead of Cyber Threats
Get the latest cybersecurity news, critical vulnerabilities, threat intelligence, tutorials, and exclusive giveaways delivered straight to your inbox. No spam. Unsubscribe anytime.
Subscribe to the Newsletter →Nebula solves that problem with prctl(PR_SET_MM, PR_SET_MM_MAP, ...). Inside the kernel, prctl_set_mm_map() copies a user supplied auxiliary vector into a fixed size stack buffer called user_auxv[AT_VECTOR_SIZE]. By coincidence, that buffer sits at almost the same stack depth as the rt_mutex_waiter object that was just freed. It is naturally aligned, large enough to overwrite the old object, accessible from unprivileged code, and requires no namespace tricks. Right after the waiter thread returns from its futex syscall, it issues this prctl() call. The copied auxv data lands directly on top of the freed rt_mutex_waiter, replacing it with attacker controlled bytes.
Those bytes are not random. The auxiliary vector is carefully arranged so that the overlapping memory looks like a valid rt_mutex_waiter structure. The forged object contains an rb tree node that promotes a chosen child pointer during removal, a task pointer aimed at &init_task so the kernel can safely walk the chain without crashing, and a lock pointer positioned eight bytes before the final write target.
To make the timing more reliable, a second thread repeatedly races fallocate(PUNCH_HOLE) against the last page of a memfd backed auxiliary vector buffer while the kernel is copying it. That race stretches the execution time of copy_from_user(), creating a wider window for another CPU to enter sched_setattr() and traverse the forged waiter before the stack contents change again.
The write primitive comes from rt_mutex_dequeue(), which eventually performs an rb tree erase. When the root node has a single child, removing it replaces the root pointer with that child. By setting the forged waiter’s lock field to target - 8, the fake rt_mutex_base structure overlaps real kernel memory around the chosen address. The tree erase then becomes a constrained 8 byte write:
*(uint64_t *)target = W0_BASE
The constraints matter. The eight bytes before the target must look like an unlocked spinlock, with the low bits set to zero. The surrounding qwords also need to contain values that keep the tree walk valid. Otherwise, the kernel follows an invalid pointer, panics, and the exploit dies before the write completes.
That leaves one final question. Where can you place a forged kernel structure at a predictable address while also satisfying all of those surrounding memory constraints? Nebula’s answer is the CPU entry area.
Leaks and the CEA
Before any of the write primitive becomes useful, the exploit still has to defeat KASLR. Nebula does that with a prefetch timing side channel. By issuing prefetch instructions against kernel addresses and measuring how long they take to complete, it becomes possible to tell whether a particular address is mapped. Linux only randomizes the kernel text base with about nine bits of entropy by default, so collecting enough timing samples is usually enough to recover the KASLR offset with very high confidence.
On x86 systems without Kernel Page Table Isolation (KPTI), which is how the kernelCTF environment is configured, this leak is almost effortless. Systems with KPTI enabled make things harder, but not impossible. Nebula combines the same timing technique with the EntryBleed side channel through the syscall trampoline and is still able to recover the kernel base.
The next problem is finding the CPU entry area, or CEA. This per CPU region stores entry and exception stacks. Before Linux 6.2, its virtual address never changed, which made it a common target for kernel exploits. That changed after Project Zero published its stack attack research. Linux 6.2 introduced strong randomization for the CEA’s virtual address, removing the predictable target attackers had relied on.
Rather than trying to recover the randomized virtual address, Nebula sidesteps the problem. The CEA’s physical offset stays fixed, so its direct map alias can be calculated as physmap_base plus a kernel version specific offset. The physmap_base itself can be recovered with the same prefetch timing attack. The signal is noisier than the KASLR leak, but after collecting enough samples and filtering out neighboring page artifacts, it is reliable enough for exploitation. With that address in hand, the exploit gains about 120 bytes of contiguous, attacker controlled memory at a predictable kernel location. Triggering an exception causes the entry code to spill pt_regs directly into that region.
Those 120 bytes end up doing a surprising amount of work. They first hold the forged rt_mutex_waiter and rt_mutex_base structures that survive the kernel’s consistency checks during the tree walk. Later, the same memory becomes the destination for a forged function table entry. Finally, it serves as a small ROP stack after execution pivots into it. Fitting all of that into roughly 120 bytes is probably one of the cleaner parts of the entire exploit chain.
From one write to control flow
With the write primitive working, the next question is where to point it. Nebula scanned writable kernel memory for structures whose surrounding fields already satisfied the primitive’s constraints. One target stood out: inet6_protos[IPPROTO_UDP]. The qword immediately before it is zero, which satisfies the unlocked spinlock check. The following qwords are also zero, so the rb_leftmost and owner checks pass as well. Triggering the corrupted pointer is surprisingly simple. An unprivileged process only needs to open a loopback IPv6 UDP socket and send a packet.
The overwrite replaces inet6_protos[IPPROTO_UDP] with a pointer into the CPU entry area. From that point on, whenever the kernel looks up the IPv6 UDP protocol handler, it expects to find a valid inet6_protocol structure at that address. Nebula reshapes the CEA memory into exactly that, including a forged handler function pointer. Sending the loopback packet causes the kernel to call straight through the corrupted pointer. In the end, a futex cleanup bug leads to an rb tree write, which corrupts an IPv6 protocol table entry and hands over control of the instruction pointer. It’s an unusual chain, but every step fits together.
Gaining control of the program counter is not the end of the exploit. Execution still has to pivot onto attacker controlled memory. The target kernel did not contain a convenient one gadget stack pivot, so Nebula used an extra load and call sequence to place the CEA address into rbp. From there, the familiar mov rsp, rbp; pop rbp; ret sequence pivots execution onto a ROP stack stored in the CPU entry area.
Instead of building a long return oriented programming chain that writes directly to /proc/%P/fd or another privileged target, Nebula takes a much shorter route that it calls DirtyMode. The ROP chain performs just one additional kernel write. It changes the permission bits on the core_pattern sysctl, located in coredump_sysctls, so the file becomes world writable. After that, the exploit no longer needs kernel execution. A normal unprivileged process opens /proc/sys/kernel/core_pattern, writes a pipe directive that points to a helper binary, then deliberately crashes a process to trigger a coredump. The kernel launches the helper as root, completing the privilege escalation. Everything after the permission change happens entirely in userspace.
According to Nebula, the full exploit completes in roughly five seconds against the remote kernelCTF target. Considering how many different kernel subsystems it crosses, that’s remarkably fast.
The fix, and the fix’s fix
The upstream fix, commit 3bfdc63936dd, is surprisingly straightforward. Instead of assuming current always owns the waiter being cleaned up, remove_waiter() now takes the actual task as an explicit argument. It acquires that task’s pi_lock with scoped_guard and only clears pi_blocked_on if it still points to the waiter being removed. Callers on the normal self blocking path continue to pass current, while the proxy locking rollback path passes the sleeping task that actually owns the waiter. Once you know where the bug is, the fix feels almost obvious. That is probably why it managed to survive code review for nearly 15 years.
The first patch was not the end of the story, though. It introduced its own regression, a null pointer dereference that later received CVE-2026-53166. A follow up patch fixed that issue. If you’re updating systems now, make sure your kernel includes both changes. Seeing “GhostLock fixed” in a changelog is not enough. The difference between the initial patch and the follow up is the difference between fixing the vulnerability and replacing it with a different kernel bug.
There are also a couple of build time mitigations that make exploitation harder, although neither one addresses the underlying flaw. RANDOMIZE_KSTACK_OFFSET disrupts the predictable overlap between the freed rt_mutex_waiter stack frame and the PR_SET_MM_MAP auxiliary vector buffer. The exploit can still work, but the reliable stack reclaim becomes roughly a one in thirty two guess. Neither of the kernelCTF targets Nebula tested had this option enabled by default.
STATIC_USERMODE_HELPER blocks the specific core_pattern technique used at the end of the exploit chain. Nebula notes that the broader idea still applies, though. Any /proc/sys setting protected by a writable ctl_table::mode field in predictable kernel memory could become a similar target. These mitigations raise the bar, but they do not remove the vulnerability. They should be treated as defense in depth, not as a replacement for the kernel update itself.
The rollout across Linux distributions has also been uneven enough that it is worth checking rather than assuming. As of early July, Ubuntu had released fixes for its newest version and some cloud kernel packages, while Ubuntu 24.04, 22.04, and 20.04 LTS were still marked as vulnerable or under active update. The best approach is to check your distribution’s advisory and verify the exact kernel package version you are running. The kernel update in May addressed this issue and Arch promptly released subsequent updates due to critical security enhancements. If your system has been updated since that time, it is unlikely to be susceptible to this vulnerability, unless you are utilizing a customized kernel that has not received recent updates. This was resolved in version 7.0.4, with the LTS version fixed in 6.18.27.
Why GhostLock Deserves Attention
Most local privilege escalation vulnerabilities come with a catch. Maybe they depend on a kernel module that isn’t enabled by default. Maybe they require an uncommon capability or a configuration option that hardened systems disable. GhostLock doesn’t have much of a checklist. As Nebula points out, it only needs CONFIG_FUTEX_PI=y, which was enabled on every distribution they examined, along with access to standard threading syscalls that every unprivileged process already has. There are no user namespaces to enable, no unusual mount options, and no configuration mistake an attacker has to hope for.
That is what makes this bug particularly relevant for shared environments. On a multi tenant Kubernetes node, a CI runner executing untrusted code, or a university shell server, getting a local foothold is often the easy part. The challenge is turning that foothold into root access. GhostLock was built for exactly that scenario, which is why it quickly became a high priority issue despite requiring local code execution.
It is also worth looking at the timing. GhostLock was disclosed only days after another Linux kernel privilege escalation, Bad Epoll (CVE-2026-46242). That bug lived in another long standing piece of kernel infrastructure, was independently demonstrated through kernelCTF, and also worked on Android. The similarities are difficult to ignore. Both bugs came from mature kernel subsystems that had received relatively little scrutiny over the years.
Priority inheritance futexes have been part of Linux since 2011. GhostLock shows that old code is not necessarily well understood code. Fifteen years without a public exploit did not make this path safe. It simply meant nobody had found the right bug yet.
If your organization has treated kernel updates as something that can wait until the next maintenance window, this is probably a good time to revisit that approach. GhostLock is a reminder that some of the most valuable vulnerabilities are not hiding in new features. They are sitting in code that everyone assumed had already been picked clean.









