DF-2748 / verdict.json
{ "finding_id": "DF-2748", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "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.", "exploit_chain": "root: mountctl -a -2 -w <plain-file> -o memfifo=64k <mp>:wedge -> rthread reads EOF, exits (xindex frozen forever) -> unpriv writers fill 64K un-acked -> permanent uninterruptible 'jwrite' sleep (unkillable, holds vnode locks; every user of the mount affected) -> root 'mountctl -d' frees jo+membase under the sleepers -> heap reuse at the same addresses (e.g. new journal, same size classes) -> new journal's wakeup(&jo->fifo.windex) wakes the zombie -> journal_reserve/commit resume on freed/reallocated state; rawp = membase+(windex&mask) writes attacker-payload records into whatever now occupies the old addresses (UAF write; demonstrated only in the benign same-journal-reuse layout).", "evidence": [ "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" ], "kernel_refs": [ "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)" ], "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).", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC x86_64 (baseline) / #1 Mon Aug 31 16:22:33 (fix kernel)", "runtime_sec": 3600, "guest_dirty": 0, "build_cmd": "cc -O -o df2748 df2748.c [fix validated in DF-2747's kernel build]", "run_cmd": "mountctl -a -2 -w /root/j2.bin -o memfifo=64k /tmp:wedge ; ./df2748 /tmp/jstress", "code_hash": "803d6fa41dd95e7d7fd224411bb80270ec463c98820bc53a07b0092b7fedcf9d", "notes": "Severity High: unkillable-process wedge of arbitrary length for unprivileged users plus demonstrated post-free execution through stale journal pointers (memcorrupt bucket). The journal wire protocol is unauthenticated (magic constants), so a dead/lying remote mirror triggers the same rthread exit over the network.", "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.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Aug 31 16:22:33 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Patched 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.", "fix_evidence": "run3.log (patched transcript + status counters) vs run.log/run2.log (baseline)" } |