Full-duplex journal ack-path loss permanently wedges all writers in uninterruptible, SIGKILL-immune kernel sleep — and mountctl -d frees the journal out from under the sleepers (demonstrated wake-through-reuse UAF)
| Field | Value |
|---|---|
| ID | DF-2748 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-667 / CWE-832 (→CWE-416 on teardown) |
| File | sys/kern/vfs_journal.c |
| Lines | 543-548 (stall sleep), 330-346 (rthread death), 383/397 (xindex), 204-207 (jpad) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
In full-duplex mode xindex is advanced only by journal_rthread on acks; the rthread silently exits on the first EOF/error/short read or bad ack magic (plain-file or dead/lying socket target = immediate). journal_reserve's stall path then tsleeps uninterruptibly ('jwrite', flags=0, no timeout, no STOP_REQ escape; STOP_IMM is unimplemented) forever once the memfifo fills with un-acked bytes — every VOP on the mount wedges unkillably. mountctl -d completes anyway, kfree'ing struct journal and the fifo while the sleepers persist on &jo->fifo.windex inside freed memory; a later same-size allocation whose wakeup hits the recycled address wakes them to execute on freed→reused state (rawp = membase + (windex & mask) writes attacker-payload records into whatever now occupies the old addresses).
Threat model & preconditions
Root configures two-way journaling (the feature's stated purpose is off-site mirroring over untrusted networks; the wire protocol is unauthenticated — magic constants only, so a dead, blocking or malicious mirror triggers it remotely). Thereafter every unprivileged user writing to the mount becomes unkillably wedged (D-state, holds vnode locks); the mount cannot be used or unmounted; after cleanup, the freed-under-sleeper condition plus heap reuse yields a UAF write primitive (demonstrated wake; arbitrary-object write traced).
Proof of contest
VERIFIED on the guest (findings/poc/DF-2748/): mountctl -2 -w j2.bin memfifo=64k → rthread reads EOF and exits instantly; unpriv writers → D4/D5 wchan=jwrite from t+5s; SIGKILL ineffective through t+60s; membufused/unacked pinned at 63K (frozen xindex), fifo_stalls=2; mountctl -d succeeds → jo+membase freed while sleepers persist; installing a new 64K journal and generating load WOKE the zombies through the reused wchan — they ran on freed→reused journal state, completed the syscall, and only then took the pending SIGKILL. Patched kernel (gate full-duplex on a live MC_JOURNAL_RACTIVE, degrading to half-duplex): never wedges, killable, fifo drains to 0.
Recommended fix
Gate full-duplex behavior on a live rthread so the wthread takes over xindex advancement when the ack path dies (validated fix.diff); additionally implement the STOP_IMM/STOP_REQ escape in the 'jwrite'/'jpad' sleeps and drain/abort outstanding reservers in journal_destroy before kfree.
Timeline
- 2026-08-30 Discovered during pass-2 audit of vfs_journal.c (GLM 5.3); unpriv wedge + wake-through-reuse reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2748 · 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df2748.c | — | 2.0 KB | view raw | |
| build.sh | — | 35 B | view raw | |
| run.sh | — | 509 B | view raw | |
| run.log | — | 2.4 KB | view raw | |
| run2.log | — | 2.6 KB | view raw | |
| run3.log | — | 1.1 KB | view raw | |
| env.txt | — | 416 B | view raw | |
| fix.diff | — | 6.9 KB | view raw | |
| verdict.json | — | 5.2 KB | view raw |
Fix verification
fixedPatched kernel #1: exact same PoC never wedges - children ran continuously past the baseline wedge point (file #11) for the entire window, no D-state leftovers, SIGKILL effective, fifo fully drained (membufused=0/membufunacked=0), journal deleted cleanly. Stock baseline: unkillable D jwrite wedge >= 45 s + free-under-sleeper + wake-through-reuse.
run3.log (patched transcript + status counters) vs run.log/run2.log (baseline)
Confirmed kernel references
- sys/kern/vfs_journal.c:543-548 (uninterruptible stall sleep, no STOP_REQ escape)
- sys/kern/vfs_journal.c:330-336 (rthread exits on EOF/error/short read)
- sys/kern/vfs_journal.c:341-346 (rthread exits on bad ack magic)
- sys/kern/vfs_journal.c:383,397 (only xindex advancers, rthread-only in fullduplex)
- sys/kern/vfs_journal.c:204-207 (wthread jpad loop ignores MC_JOURNAL_STOP_REQ)
- sys/kern/vfs_journal.c:544 (MC_JOURNAL_STOP_IMM unimplemented)
- sys/kern/vfs_journal.c:140-158 (journal_destroy_threads jwait loop)
- sys/kern/vfs_jops.c:433-452 (journal_destroy frees jo/fifo without draining reservers)
Detail
Exploit chain
root: mountctl -a -2 -w
Evidence (decisive lines)
['run.log: 60 s transcript, children 1009 D4 jwrite / 1010 D5 jwrite, SIGKILL ineffective t+15s..t+60s', 'run2.log: membufused=63K membufunacked=63K stalls=2; mountctl -d completes (memory freed under sleepers); new journal + 15 stalls wake the zombies; ps empty afterwards', 'run3.log: patched kernel - no wedge at all, killable, fifo drains', 'fix.diff: DF-2748 gating hunks (MC_JOURNAL_RACTIVE), validated']
PoC changes
No seed; written fresh. Guest notes: -2 for two-way, memfifo=64k, mountpt:tag argument order; plain-file target makes rthread death deterministic (shared-offset fp_read returns our own journal bytes or EOF -> break).
Verified recommended fix
Gate full-duplex behavior on a live rthread (MC_JOURNAL_RACTIVE) so the wthread takes over xindex advancement when the ack path dies (validated in fix.diff); upstream should additionally drain/abort outstanding reservers in journal_destroy before kfree.
Verdict
With a full-duplex journal installed (root, mountctl -2), journal_rthread exits silently on the first fp_read EOF/error/short-read (vfs_journal.c:330-336) or bad ack magic (:341-346), after which nothing ever advances fifo.xindex; journal_reserve's stall tsleep(&jo->fifo.windex,0,"jwrite",0) (:543-548) is uninterruptible, unbounded, has no STOP_REQ escape (STOP_IMM is unimplemented), so every journaled VOP wedges in an unkillable D-state once the memfifo fills with un-acked bytes. Demonstrated deterministically on the stock guest: rthread gone at install; both writer children D4/D5 wchan=jwrite from t+5s through t+60s, SIGKILL at t+15s ineffective; membufused/membufunacked pinned at 63K (frozen xindex). mountctl -d then SUCCEEDED, freeing struct journal and the fifo while the sleepers persisted inside freed memory; installing a new same-size journal and generating load (15 stalls) woke the zombies through the recycled wchan address - they executed journal_reserve on freed->reused state and only then took the pending SIGKILL. Wake-through-reuse demonstrated; the arbitrary-object UAF write under a hostile heap layout is traced but not demonstrated. Fix (wthread gates full-duplex behavior on a live MC_JOURNAL_RACTIVE, degrading to half-duplex so xindex keeps advancing) validated on patched kernel #1: same PoC never wedges (children ran past file #24 indefinitely), killable, fifo drained (membufused=0), clean delete.
No comments yet.