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

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:
KaraZajac
2026-07-23 17:49:40 -04:00
parent d466fbfdcb
commit e46a32f11e
19 changed files with 1914 additions and 31 deletions
+6 -2
View File
@@ -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