Files
KaraZajac 03324c8542
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / build (x86_64-static / musl) (push) Waiting to run
release / build (arm64-static / musl) (push) Waiting to run
release / release (push) Blocked by required conditions
modules: add ghostlock (CVE-2026-43499, "GhostLock" rtmutex/futex requeue-PI stack UAF)
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
2026-07-13 15:43:00 -04:00

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 fires FUTEX_CMP_REQUEUE_PI, confirming the kernel returns -EDEADLK. That proves the remove_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 -EDEADLK deterministically.)
  • (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_setaffinity drivers plus the post-exploitation euid-0 transition.
  • falco / eBPF carries the high-fidelity rule (futex requeue-PI returns EDEADLK + sibling sched_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.