⬢ DragonFlyBSD Kernel Audit
DF-2748 / verdict.json
← back to finding ↓ download raw
{
  "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_reserves 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 journals 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-2747s 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)"
}