Adds the ghostlock module for CVE-2026-43499 ("GhostLock", VEGA / Nebula
Security's "IonStack part II") — a ~15-year race UAF on kernel STACK memory in
the rtmutex/futex requeue-PI path (kernel/locking/rtmutex.c). On the -EDEADLK
deadlock-rollback, remove_waiter() runs against `current` instead of the waiter
task, so a concurrent sched_setattr()-driven PI-chain priority walk on another
CPU clears pi_blocked_on on the wrong task and leaves an on-stack rt_mutex_waiter
dangling. Reachable by any unprivileged user (CVSS 7.8, PR:L) — plain
futex(2) + sched_setattr(2), no userns/CONFIG beyond CONFIG_FUTEX_PI. The
corpus's first rtmutex/futex-PI module and first kernel-stack UAF (all others
are heap/slab).
Introduced 2.6.39; fixed 3bfdc63936dd (7.1-rc1), stable backports
7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175; 5.15/5.10/5.4/4.19 affected with
no upstream fix. CWE-416; not in KEV.
detect() is a pure version gate over the five-branch backport table. exploit()
forks an isolated child that (A) deterministically confirms the -EDEADLK
remove_waiter() rollback path is reachable (safe — no concurrent walk means no
dangling pointer; validated on real hardware) then (B) exercises the actual race
a hard-bounded 24 iters / 2s with a sibling-CPU sched_setattr(SCHED_BATCH) storm,
and stops. Deliberately under-driven: no copy_from_user widening, no stack-frame
spray/reoccupation, and the KernelSnitch leak -> forged-waiter ->
fops/ashmem/pipe R/W -> cred-patch chain (Android/Pixel-specific, per-build
offsets) is NOT bundled. Returns EXPLOIT_FAIL. Lowest --auto safety rank (11) —
a won race corrupts the kernel stack. Unlike most races it ships a real
detection signature (futex requeue-PI returning EDEADLK + sibling sched_setattr);
auditd/sigma anchor on sched_setattr, falco/eBPF on the EDEADLK tell; no yara.
Wired: registry, Makefile, safety rank 11, version 0.9.13, 9 detect() test rows
(incl. 6.13.0 -> VULNERABLE, the multi-branch "newer than all" case), CVE
metadata (CWE-416 / T1068 / not-KEV; cve_metadata.c + KEV_CROSSREF.md regenerated
-> 13 of 40), README + CVES.md + website counts (45 modules / 40 CVEs),
RELEASE_NOTES v0.9.13, verify-vm target. Also corrects pre-existing
docs/index.html drift left by v0.9.12 (body counts stuck at 43/38 and a missing
bad_epoll corpus pill). Tests: 33 kernel_range + 101 detect, all pass.
Credit: VEGA / Nebula Security (nebusec.ai, NebuSec/CyberMeowfia, Apache-2.0);
upstream fix 3bfdc63936dd (Keenan Dong / Thomas Gleixner).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUq4DGXSPBmPkAyJ9WnM9n
6.5 KiB
ghostlock — CVE-2026-43499
"GhostLock" — a race-condition use-after-free on kernel stack memory in
the Linux rtmutex / futex requeue-PI code path (kernel/locking/rtmutex.c),
reachable by any unprivileged local user (CVSS PR:L). No user namespace,
no capability, no special CONFIG beyond CONFIG_FUTEX_PI (universally
enabled). It has existed since PI-futex requeue landed — ~15 years, across
every distribution — which is what makes it remarkable.
The bug
On the deadlock-rollback path, remove_waiter() operates on current
instead of the actual waiter task while unwinding a proxy lock in
rt_mutex_start_proxy_lock() — reached from futex_requeue(). If a
concurrent PI-chain priority walk (driven from another CPU via
sched_setattr()) runs at that instant, pi_blocked_on is cleared on the
wrong task and an on-stack struct rt_mutex_waiter is left dangling in a
task's waiter / pi tree. When the kernel later rotates that rbtree over the
(now-reused) stack frame, the forged node fields become a controlled kernel
write → use-after-free.
The public research + PoC ("IonStack part II: GhostLock", VEGA / Nebula
Security) builds the requeue-PI cycle so FUTEX_CMP_REQUEUE_PI hits
-EDEADLK (the rollback) while a sibling-core consumer thread hammers
sched_setattr(SCHED_BATCH) on the waiter's tid to win the race. A separate
full Android/Pixel LPE then forges the on-stack rt_mutex_waiter on a leaked
kernel page (the "KernelSnitch" futex-bucket timing side channel), overwrites
a struct file f_op → configfs/ashmem arbitrary R/W → pipe physical R/W →
cred patch → root. ~97% stable on kernelCTF; Google awarded $92,337.
Affected range
| Introduced | PI-futex requeue — 2.6.39 (commit 8161239a8bcc) |
| Fixed upstream | commit 3bfdc63936dd ("rtmutex: Use waiter::task instead of current in remove_waiter()") — merged 7.1-rc1 |
| Stable backports | 7.0.4 · 6.18.27 · 6.12.86 (LTS) · 6.6.140 (LTS) · 6.1.175 (LTS) |
| Affected, no upstream fix | 5.15.x / 5.10.x / 5.4.x / 4.19.x (kernel CNA lists no stable fix) |
| Not affected | < 2.6.39 (predates PI-futex requeue) |
| NVD class | CWE-416 (Use After Free) via CWE-362 (race); CVSS 7.8, PR:L |
| CISA KEV | no (brand new) |
The kernel_range table carries one entry per backported branch;
kernel_range_is_patched() marks any branch strictly newer than all of them
(7.1+) patched-via-mainline and everything below the on-branch threshold
vulnerable — including the 5.x LTS lines that have no published fix. Extend
the table as more branches backport (the drift checker flags them). Source:
the Linux kernel CNA record (git.kernel.org/stable/c/<hash>), corroborated
by the Debian / Ubuntu / SUSE trackers.
Trigger / detection
detect() is a pure version gate — no active probe, because there is no
cheap, safe way to distinguish vulnerable from patched without winning the
race. It returns OK below 2.6.39 or on a patched kernel, and VULNERABLE
in range. CONFIG_FUTEX_PI is a (near-universal) precondition detect()
assumes rather than probes; there is no userns / capability precondition
(any local user — CVSS PR:L).
exploit() forks an isolated child and runs two phases:
- (A) deterministic + safe — builds the requeue-PI cycle (a waiter
holding a "chain" PI-futex and parked in
FUTEX_WAIT_REQUEUE_PI; an owner holding the "target" PI-futex and blocked on the chain) and firesFUTEX_CMP_REQUEUE_PI, confirming the kernel returns -EDEADLK. That proves theremove_waiter()rollback path — where the bug lives — is reachable here. Without a concurrent priority walk the rollback is the kernel's normal, correct deadlock rejection: it creates no dangling pointer, so this phase is safe on any kernel. (Validated on real hardware: the cycle returns-EDEADLKdeterministically.) - (B) hard-bounded window exercise — repeats (A) a small, wall-clock-
capped number of times (24 iterations / 2 s) with a sibling-CPU
sched_setattr(SCHED_BATCH)storm on the waiter's tid, overlapping the priority walk with the rollback (the actual race). Then it stops.
It is deliberately under-driven. A won race corrupts the kernel
stack and drives a near-arbitrary pointer write — near-certain panic on a
vulnerable host. So this module does not widen the copy_from_user
window (no memfd / PUNCH_HOLE), does not spray or reoccupy the freed
stack frame, and does not bundle the KernelSnitch leak → forged-waiter →
fops/configfs/ashmem/pipe R/W → cred-patch chain (Android/Pixel-specific,
per-build offsets). The trigger is reconstructed from the public PoC and is
not VM-verified. It returns EXPLOIT_FAIL and never claims root it did not
get.
Because a kernel race that corrupts the stack is the least predictable class
in the corpus, ghostlock carries the lowest --auto safety rank (11 —
just below bad_epoll), so --auto only reaches for it after every safer
vulnerable module.
Detection — better than most kernel races, but read this
Unlike bad_epoll (whose epoll syscalls are indistinguishable from every
event loop), GhostLock has a genuinely distinctive tell: a futex
requeue-PI op (FUTEX_WAIT_REQUEUE_PI / FUTEX_CMP_REQUEUE_PI) returning
-EDEADLK, which glibc's requeue-PI usage inside pthread_cond_wait never
provokes, interleaved with sched_setattr(SCHED_BATCH) on a sibling
thread and sched_setaffinity CPU pinning. The catch: auditd/sigma see the
futex syscall but not its op-vs-return cheaply, and a bare -S futex watch
would flood any host. So:
- auditd / sigma anchor on the far rarer
sched_setattr/sched_setaffinitydrivers plus the post-exploitation euid-0 transition. - falco / eBPF carries the high-fidelity rule (futex requeue-PI returns
EDEADLK+ siblingsched_setattr) — it can see the op and the return value.
There is no yara rule (in-kernel race, no file artifact). Tune the
sched_setattr anchor per environment — real-time and scheduler-tuning
daemons will false-positive.
Fix / mitigation
Upgrade the kernel (>= 7.0.4 / 6.12.86 / 6.6.140 / 6.1.175 on-branch, or
7.1+). There is no partial mitigation: PI futexes cannot be disabled at
runtime, and no unprivileged_userns_clone / sysctl toggle closes this path.
mitigate() is NULL for that reason.
Credit
Discovery, research, and the public PoC: VEGA / Nebula Security
(@nebusecurity, nebusec.ai). Upstream fix 3bfdc63936dd (Keenan Dong /
Thomas Gleixner). See NOTICE.md.