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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2747 · 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedPatched 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
Confirmed kernel references
- sys/kern/vfs_journal.c:527-585 (journal_reserve unlocked windex/transid RMW)
- sys/kern/vfs_journal.c:625-652 (journal_extend fast paths RMW windex+recsize)
- sys/kern/vfs_journal.c:685-696 (journal_abort windex rewind)
- sys/kern/vfs_journal.c:742-756 (journal_commit backindex + dead-space pad)
- sys/kern/vfs_journal.c:741 (panicked assertion)
- sys/kern/vfs_journal.c:236-243 (journal_wthread chain walk sink)
- sys/kern/vfs_journal.c:220,261,279,383,397 (worker rindex/xindex RMW)
- sys/kern/vfs_vopops.c:1869 (vop_write_ap bare DO_OPS - no serialization)
- sys/kern/vfs_jops.c:545-556 (adjacent: mnt_jbitmap/mnt_streamid RMW, same class)
Detail
Exploit chain
root: mountctl -a -w
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.
No comments yet.