β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2765

journal_nrename records both path leaves after cache_rename mutated the namecache: PATH1==PATH2==target and the source path is never journaled

Field Value
ID DF-2765
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
CWE CWE-1051 Incomplete Data Processing
File sys/kern/vfs_jops.c
Lines 1318 (op), 1322-1323 (path leaves)
Area kern
Confidence certain
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

journal_nrename writes JLEAF_PATH1 from a_fnch->ncp and JLEAF_PATH2 from a_tnch->ncp only after vop_journal_operate_ap. The underlying nrename implementations call cache_rename, which relinks the SOURCE ncp to the target name in place and unlinks the target ncp. Decoded guest stream proves it: PATH1@0x60 and PATH2|LAST@0x78 both = 'jt/b/dst.txt'; the string 'jt/a/src.txt' occurs 0 times in the entire stream. Rename records are unreplayable (degenerate rename(new,new)) β€” silent replica divergence and impossible undo. Any rename on a root-journaled mount (unpriv once the journal exists).

Proof of concept

VERIFIED on the guest (findings/poc/DF-2765/): create src before journal install, mountctl -a, mv to dst, mountctl -d, decode the stream β†’ baseline source count 0 / target count 2; fix-validated kernel β†’ source count 1 / target count 1 (PATH1 written between jreclist_init and the op; PATH2 post-op β€” a failed op aborts the whole stream in jreclist_done, so the pre-op write is free).

fix.diff hunk 4 in the pack (validated in the same rebuilt kernel as DF-2763).

Timeline

  • 2026-08-30 Discovered during pass-2 audit of vfs_jops.c (GLM 5.3); stream-decode proof + fix validation same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2765 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 1.4 KB ↓ raw
df2765_run.sh β€” 816 B view raw
journal_stream.bin β€” 192 B ↓ download
run.log β€” 321 B view raw
decode.py β€” 994 B view raw
decode_out.txt β€” 341 B view raw
run_fixed.log β€” 321 B view raw
journal_stream_fixed.bin β€” 192 B ↓ download
fix.diff β€” 3.6 KB view raw
verdict.json β€” 2.0 KB view raw

DF-2765 β€” journal_nrename records post-rename namecache paths: the source path is never journaled (PATH1 == PATH2)

What

journal_nrename() (sys/kern/vfs_jops.c:1301-1328) writes its REDO path leaves AFTER vop_journal_operate_ap() (:1318). But a successful underlying rename calls cache_rename(ap->a_fnch, ap->a_tnch) (sys/kern/vfs_cache.c:2085-2142, called from every nrename implementation, e.g. sys/kern/vfs_default.c:1064), which relinks the SOURCE ncp to the target name in place (fncp->nc_name = nname, :2118-2120) and destroys the target ncp (_cache_unlink(tncp), :2141).

So the post-op walk records: - JLEAF_PATH1 from ap->a_fnch->ncp = the target path (post-rename), - JLEAF_PATH2 from ap->a_tnch->ncp = the target path again,

and the original (source) path never enters the journal stream. A mirror/replica replaying rename records cannot reconstruct the operation (it degenerates to rename(new,new)), silently diverging from the master; reversable-journal rollback of a rename is likewise impossible.

PoC (reproduced first try, stock kernel)

mkdir -p /tmp/jt/a /tmp/jt/b; echo hi > /tmp/jt/a/src.txt   # BEFORE journal install
mountctl -a -w /root/jr.bin -o memfifo=64k /tmp:rename1      # root
mv /tmp/jt/a/src.txt /tmp/jt/b/dst.txt
mountctl -d /tmp:rename1
# decode /root/jr.bin (see run.log + decode.py)

Observed in the 192-byte stream (raw hex @0x60 and @0x78): - JLEAF_PATH1 (0x0402) = "jt/b/dst.txt\0" - JLEAF_PATH2|LAST (0x4403) = "jt/b/dst.txt\0" - string "jt/a/src.txt" occurs 0 times in the entire stream.

Fix

Capture PATH1 before the op (validated in the DF-2763 fix kernel): write JLEAF_PATH1 from ap->a_fnch->ncp between jreclist_init and vop_journal_operate_ap(); on op failure the stream is aborted by jreclist_done() so the pre-op write costs nothing. See fix.diff.

Fix validation (kernel #2, same build as DF-2763)

Re-run of df2765_run.sh on the patched kernel: stream now contains BOTH paths β€” 'jt/a/src.txt' (PATH1, captured pre-op, count 1) and 'jt/b/dst.txt' (PATH2, count 1). Baseline stream had the source path 0 times / target twice. See run_fixed.log, journal_stream_fixed.bin.

VERDICT.md
↓ download raw

DF-2765 β€” VERDICT

Status: reproduced (first run), fix validated.

Baseline (stock kernel)

Journal installed on /tmp (tmpfs); rename of /tmp/jt/a/src.txt -> /tmp/jt/b/dst.txt through the shim; stream decoded:

  • RAW sid=0xc100 (JREC_STREAMCTL_BEGIN|END, streamid 0x100) = the JTYPE_RENAME transaction.
  • PATH1 leaf (0x0402) @0x60 = "jt/b/dst.txt\0"
  • PATH2 leaf|LAST (0x4403) @0x78 = "jt/b/dst.txt\0"
  • "jt/a/src.txt" count in the whole 192-byte stream: 0

Root cause: journal_nrewrite records both paths AFTER vop_journal_operate_ap (vfs_jops.c:1322-1323); the underlying nrename calls cache_rename (vfs_cache.c:2085) which relinks fnch->ncp to the target name in-place (:2118-2120) and unlinks tnch->ncp (:2141). The source path is thereby unobservable after the op.

Fix (validated on kernel #2)

PATH1 written from ap->a_fnch->ncp between jreclist_init and the op (hunk 4 of fix.diff). On op failure the whole stream is aborted by jreclist_done(error), so the pre-op write is free. Post-fix stream: source count 1, target count 1. run_fixed.log + journal_stream_fixed.bin.

Impact

Journal-record correctness: replicas/mirrors replaying rename records degenerate to rename(new,new) and silently diverge; reversable-journal undo of renames is impossible. Requires a root-installed journal; trigger is any rename on the journaled mount (unprivileged ok). Severity Low.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

post-fix stream contains PATH1='jt/a/src.txt' (pre-op) and PATH2='jt/b/dst.txt'; baseline had source absent / target duplicated

['run_fixed.log', 'journal_stream_fixed.bin']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #2: Mon Aug 31 23:12:27 UTC 2026

Confirmed kernel references

Detail

Evidence (decisive lines)

["decode_out.txt (PATH1@0x60='jt/b/dst.txt', PATH2|LAST@0x78='jt/b/dst.txt', source count 0)", 'journal_stream.bin (192 B raw stream)', 'run_fixed.log + journal_stream_fixed.bin (post-fix: source present)']

PoC changes

authored from scratch; reused the DF-2747-proven mountctl recipe

Verified recommended fix

snapshot the source ncp path before vop_journal_operate_ap in journal_nrename (fix.diff hunk 4)

Verdict

journal_nrename journals both path leaves after the underlying op (vfs_jops.c:1322-1323), but cache_rename relinked fnch->ncp to the target name and destroyed tnch->ncp, so PATH1==PATH2==target and the source path never enters the stream (reproduced: 'jt/a/src.txt' count 0, target count 2). Journal replay/undo of renames is therefore impossible - silent replica divergence. Fix (capture PATH1 pre-op) validated on the patched kernel: source count 1, target count 1.