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
+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