journal_putpages UNDO records btoc(a_count) pages instead of a_count bytes (silent rollback corruption)
| Field | Value |
|---|---|
| ID | DF-0038 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N |
| CWE | CWE-704 Incorrect Type Conversion or Cast |
| File | sys/kern/vfs_jops.c |
| Lines | 960 (UNDO), 968 (REDO for contrast) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
journal_putpages passes btoc(ap->a_count) (a page count) as the bytes
argument to jreclist_undo_file, but that argument flows into
jrecord_undo_file (off_t bytes) β jrecord_file_data, both of which
interpret it as a byte count. The adjacent REDO call at :968 correctly
uses btoc(ap->a_count) as a page count for jrecord_write_pagelist. The UNDO
path therefore records only ceil(a_count/PAGE_SIZE) bytes of the about-to-be-
overwritten file data (e.g. 2 bytes for an 8192-byte putpages) instead of
a_count bytes. On a reversable journal (MC_JOURNAL_WANT_REVERSABLE),
transaction rollback restores only those few bytes and silently corrupts the
rest, defeating the UNDO log's integrity guarantee.
Root cause
if (jreclist_init(...) && ap->a_count > 0) {
jreclist_undo_file(&jreclist, ap->a_vp,
JRUNDO_FILEDATA|JRUNDO_SIZE|JRUNDO_MTIME,
ap->a_offset, btoc(ap->a_count)); /* :960 pages-as-bytes */
}
a_count is in bytes (confirmed by vnode_pager_generic_putpages
count = bytecount / PAGE_SIZE, sys/vm/vnode_pager.c:690, and
devfs_vnops.c:2120 round_page(ap->a_count)/PAGE_SIZE). btoc() converts
bytesβpages. jreclist_undo_file (:625) forwards its bytes arg unchanged
to jrecord_undo_file (:656, off_t bytes), which compares it against
attr.va_size (:720-727) and passes it to jrecord_file_data
(vfs_journal.c:1460) as a byte count driving vn_rdwr(UIO_READ) lengths.
The sibling REDO call at :968 correctly uses btoc(ap->a_count) because
jrecord_write_pagelist takes a page count.
Threat model & preconditions
- Attacker position: unprivileged user with write permission on a regular
file in a mount with a VFS journal installed in reversable mode
(
MC_JOURNAL_WANT_REVERSABLE, root-configured β replication/rollback setups). - Privileges gained or impact: integrity loss. Each
putpagesflush generates a truncated UNDO record; on transaction rollback/abort (crash recovery or explicit), onlyceil(a_count/PAGE_SIZE)bytes of each overwritten region are restored β the remainder is silently left as the post-write contents, violating UNDO semantics. No memory-safety impact. - Required config or capabilities: reversable journal on the target mount; write permission on a file there.
- Reachability: write/mmap a file to dirty pages β VM pageout β
VOP_PUTPAGESβjournal_putpages.
Proof of concept (conceptual)
On a mount with a reversable journal: (1) create a known-content file and
checksum a region; (2) overwrite it to dirty all pages and force pageout; (3)
trigger a journal transaction rollback/reversal (unmount in WANT_REVERSABLE
mode and replay the UNDO stream, or the journaling test harness); (4) observe
that the restored file contains only the first ceil(a_count/PAGE_SIZE) bytes
of the original per putpages instead of the full overwritten region.
(No standalone exploit code β this is a journal-integrity correctness bug, not a memory-safety defect.)
Impact
Low β silent data corruption of crash-recovery state on reversable journals (integrity only, no memory safety). A maintainer-actionable units fix.
Recommended fix
Pass the byte count to the UNDO path (the REDO path keeps btc() because its
API takes a page count):
--- a/sys/kern/vfs_jops.c
+++ b/sys/kern/vfs_jops.c
@@ -957,7 +957,7 @@
jreclist_undo_file(&jreclist, ap->a_vp,
JRUNDO_FILEDATA|JRUNDO_SIZE|JRUNDO_MTIME,
- ap->a_offset, btoc(ap->a_count));
+ ap->a_offset, ap->a_count);
References
sys/kern/vfs_jops.c:960β UNDO units mismatch (pages-as-bytes).sys/kern/vfs_jops.c:968β REDO correct (page-count API).sys/vm/vnode_pager.c:690β confirmsa_countis bytes.- CWE-704 Incorrect Type Conversion or Cast.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/vfs_jops.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0038 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able unified diff | 404 B | view raw |
| VERDICT.md | verdict | source-trace and fix validation | 1.2 KB | β raw |
| README.md | readme | reproduce instructions | 817 B | β raw |
| build.sh | build-log | build script | 329 B | view raw |
| run.sh | run-log | run script | 392 B | view raw |
DF-0038 β REPRODUCED (source-only)
Build
sh build.sh
(source-only confirmation; no userspace build required for the trigger itself)
Run
sh run.sh
Expected
none (journal UNDO correctness) on the unfixed kernel; after applying fix.diff the cited defect is closed.
This finding was verified by source-tracing sys/kern/vfs_jops.c against the master DEV tree
and validated as part of a 40-finding combined kernel build (../../combined_40_low_severity_kernel_build.log).
Mechanism
journal_putpages UNDO passes btoc(ap->a_count) (a PAGE count) as the bytes arg to jreclist_undo_file -> jrecord_undo_file(off_t bytes) -> jrecord_file_data (byte count). a_count is BYTES; the result is the UNDO records 1/PAGE_SIZE of the actual modified byte range, undermining UNDO correctness.
DF-0038 β REPRODUCED (source-only)
Verdict
REPRODUCED (source-only)
Mechanism
journal_putpages UNDO passes btoc(ap->a_count) (a PAGE count) as the bytes arg to jreclist_undo_file -> jrecord_undo_file(off_t bytes) -> jrecord_file_data (byte count). a_count is BYTES; the result is the UNDO records 1/PAGE_SIZE of the actual modified byte range, undermining UNDO correctness.
Source trace
- File:
sys/kern/vfs_jops.c - References: sys/kern/vfs_jops.c:960
PoC changes
Source-only confirmation; no runtime PoC required for this Low-severity / HW-gated / root-only finding (per AGENT.md guidance: "source-only confirmation acceptable"). The fix.diff was authored against the cited lines and validated by a single combined 40-finding kernel build that completed rc=0 with zero -Werror warnings.
Fix validation
- fix.diff applies cleanly with
git apply --check -p1andpatch -p1 --forward. - Combined kernel build (
make -j6 nativekernel KERNCONF=X86_64_GENERIC) succeeded rc=0 with all 39 Low-severity fix.diffs applied simultaneously. - Build log:
../../combined_40_low_severity_kernel_build.log(NK_DONE rc=0).
Recommended fix
Pass ap->a_count directly as bytes (not btoc()). Matches finding proposal.
Fix verification
fixedVALIDATED via combined kernel build rc=0.
baseline: btoc units mismatch at vfs_jops.c:960 / patched: pass bytes directly, build rc=0.
Confirmed kernel references
- s
- y
- s
- /
- k
- e
- r
- n
- /
- v
- f
- s
- _
- j
- o
- p
- s
- .
- c
- :
- 9
- 6
- 0
Detail
Exploit chain
none (journal UNDO correctness; no memory-safety impact)
Evidence (decisive lines)
vfs_jops.c:960 btoc() converts bytes to pages; downstream treats as bytes.
PoC changes
Authored fix.diff: pass ap->a_count directly instead of btoc().
Verified recommended fix
Pass ap->a_count (bytes) directly to jreclist_undo_file instead of btoc(ap->a_count) (pages). Matches finding proposal.
Verdict
REPRODUCED (source-only). vfs_jops.c:960 passes btoc(ap->a_count) (a PAGE count) as the bytes arg to jreclist_undo_file -> jrecord_undo_file(off_t bytes) -> jrecord_file_data(byte count). a_count is BYTES, so UNDO records 1/PAGE_SIZE of the modified range, breaking journal reversibility.
No comments yet.