DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2747

Journal memfifo reservation protocol is not MP-safe: concurrent VOPs on a journaled mount overlap reservations and corrupt the raw record chain (unprivileged reproducible panic; release-kernel OOB-read/livelock)

Field Value
ID DF-2747
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H
CWE CWE-362 (→CWE-125/CWE-787-class corruption)
File sys/kern/vfs_journal.c
Lines 527-585 (reserve), 625-652 (extend), 685-696 (abort), 742-756 (commit); walk :220-243/:261/:279/:383/:397
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

journal_reserve/extend/abort/commit and the worker threads manipulate jo->fifo.{w,x,r}index and jo->transid with unsynchronized RMW. Journal VOP shims execute on every syscall thread touching a journaled mount, and VOP dispatch has no BGL/mount token, so concurrent VOPs on different vnodes run the FIFO protocol in parallel. Two CPUs reserve the same offset, producing overlapping records, corrupted recsize chains and duplicate transids; journal_wthread then parses the broken chain, where journaled user payload can be interpreted as raw record headers.

Threat model & preconditions

Root enables journaling once (mountctl -a); afterwards any unprivileged local user doing concurrent file operations on the mount triggers it. On INVARIANTS kernels: instant reproducible panic (2/2 demonstrated). On release kernels: the desynced walk can take attacker-poisoned headers (begmagic 0x1234 planted in file payload) with oversized/ negative recsize, driving res past the physical fifo end → fp_write streams kernel heap past the buffer to the journal target (info disclosure), desyncs rindex, or livelocks the worker.

Proof of concept

VERIFIED on the guest (findings/poc/DF-2747/): mountctl journal on /tmp:race; unpriv 12-proc open/write/close+mkdir/rmdir loops with forged journal_rawrecbeg headers in payload → stock INVARIANTS panic assertion "bytes >= 0 && bytes <= rawp->recsize - ..." failed in journal_commit at vfs_journal.c:741 twice (<90s), once via journal_extend←jrecord_data←jrecord_leaf_uio←journal_write (CPU1), once via jrecord_write_path (CPU0). Patched kernel (journal_fifo_lock spinlock wrapping all critical sections, no lock across sleeps): 4/4 clean, 29,880-record chain with 0 monotonicity violations, 0 duplicate transids.

Serialize the protocol with a spinlock (interim global, upstream per journal) wrapping journal_reserve (release before the stall tsleep), extend/abort/commit, commit_wakeup, and the wthread/rthread index updates; also serialize jreclist_init's mnt_jbitmap/mnt_streamid RMW (vfs_jops.c:545-556). Validated fix.diff in the pack.

Timeline

  • 2026-08-30 Discovered during pass-2 audit of vfs_journal.c (GLM 5.3); unpriv panic reproduced 2/2 + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2747 · 10 files
FileTypeDescriptionSize
df2747.c 2.3 KB view raw
parse_stream.py 2.3 KB view raw
build.sh 78 B view raw
run.sh 579 B view raw
build.log 195.9 KB view raw
run.log 2.3 KB view raw
panic.txt 1.5 KB view raw
env.txt 416 B view raw
fix.diff 6.9 KB view raw
verdict.json 5.4 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel #1 (fix.diff applied to /usr/src/sys/kern/vfs_journal.c, nativekernel BUILD_RC=0, installkernel, reboot): the exact baseline trigger (12x400 unpriv hammer, x4 runs) completed 4/4 with no panic and a fully valid stream chain (29,880 records, chain_break=no, transid_monotonic_violations=0, duplicate_transids=0). Baseline stock kernel panicked 2/2.

run.log (baseline panics + patched RUN1..RUN4_OK); parse output in run.log; build.log BUILD_RC=0
↓ 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 -w -o memfifo=64k :race (one-time) -> unpriv user: 12 concurrent processes doing open/write/close+mkdir/rmdir on the mount -> two CPUs interleave journal_reserve's unlocked windex/transid RMW -> overlapping raw records, corrupted recsize chain -> journal_wthread walks attacker-poisoned payload as headers (forged begmagic 0x1234 + recsize 0x7ffffff0 in file content) -> INVARIANTS: KKASSERT panic (demonstrated 2/2); release: res>avail fp_write reads past the 64KB fifo into kernel heap and streams it to the journal fd (info disclosure to the mirror target), or rindex desync/livelock. uid=0 not achieved.

Evidence (decisive lines)

['panic.txt: two independent panic captures, journal_commit:741 assertion via journal_extend on CPU1 and CPU0', 'run.log: baseline 2/2 panics (<90s, unpriv user); patched kernel 4/4 HAMMER DONE + parse 29880 records chain_break=no violations=0', 'df2747.c: hammer with forged journal_rawrecbeg payload (0x1234/0x7ffffff0)', 'parse_stream.py + out/j1_fixed.bin (pack run.log): clean chain on patched kernel', 'fix.diff: validated global-spinlock serialization', 'build.log: make nativekernel BUILD_RC=0 with the patch (-Werror clean)']

PoC changes

Orchestrator gave no seed for this pass-2 finding; PoC written fresh. Guest quirks handled: mountctl CLI wants 'mountpt:tag' order and memfifo=64k suffix; scratch dir must be chmod 777 for the unprivileged user; guest lacks base64/mdconfig/procstat (heredoc/scp transfer, ps -o wchan for sleep-channel evidence).

Verified recommended fix

Serialize the memfifo protocol: per-journal (or global interim) spinlock around journal_reserve/extend/abort/commit critical sections and worker index updates; see validated fix.diff.

Verdict

The journal memfifo reservation protocol (journal_reserve/extend/abort/commit plus the worker-thread index updates) performs unsynchronized RMW on jo->fifo.{w,x,r}index and jo->transid, and journal VOP shims run concurrently on SMP (vop_write_ap is a bare DO_OPS), so unprivileged concurrent VOPs on a root-journaled mount overlap reservations and corrupt the raw record chain. Reproduced twice from an unprivileged user on the stock INVARIANTS guest: KKASSERT 'bytes >= 0 && bytes <= rawp->recsize - 24' failed in journal_commit (vfs_journal.c:741) via journal_extend<-jrecord_data<-jrecord_leaf_uio<-journal_write (CPU1) and via jrecord_write_path<-jrecord_write_vnode_ref (CPU2 of run 2), both within 90 s. On release kernels the same overlap yields a desynced record chain that journal_wthread parses: attacker-poisoned payload headers (begmagic 0x1234) drive res past the physical buffer end (fp_write OOB heap read to the journal fd) or desync rindex/livelock the worker. Not escalated to uid=0: corruption is largely fifo-contained; demonstrated hard impact is the reproducible unprivileged panic. Fix (global spinlock serializing the protocol, plus DF-2748 gating and DF-2749 accounting) built as kernel #1 and validated: 4/4 identical hammer runs completed cleanly with a perfect 29,880-record stream chain (0 monotonic violations, 0 duplicates) vs 2/2 panics on stock.