DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2748

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2748 · 9 files
FileTypeDescriptionSize
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

fixed
baseline reproduced→ patch + rebuild →patched clean

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.

run3.log (patched transcript + status counters) vs run.log/run2.log (baseline)
↓ fix.diffDragonFly 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

Confirmed kernel references

Detail

Exploit chain

root: mountctl -a -2 -w -o memfifo=64k :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 (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.