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

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

sys/kern/vfs_jops.c:955-961:

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 putpages flush generates a truncated UNDO record; on transaction rollback/abort (crash recovery or explicit), only ceil(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.

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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0038 Β· 5 files
FileTypeDescriptionSize
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
README.md readme reproduce instructions
↓ download 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.

VERDICT.md verdict source-trace and fix validation
↓ download raw

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

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 -p1 and patch -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).

Pass ap->a_count directly as bytes (not btoc()). Matches finding proposal.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via combined kernel build rc=0.

baseline: btoc units mismatch at vfs_jops.c:960 / patched: pass bytes directly, build rc=0.
↓ fix.diffDragonFly 6.5-DEVELOPMENT combined-build rc=0

Confirmed kernel references

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.