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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2765 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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
fixedpost-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']
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.
No comments yet.