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
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
124 lines
6.5 KiB
Markdown
124 lines
6.5 KiB
Markdown
# 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`.
|