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

Unchecked copyin() in jrecord_data leaves stale kernel data in the journal stream

Field Value
ID DF-0029
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
CWE CWE-252 Unchecked Return Value
File sys/kern/vfs_journal.c
Lines 1093, 1140
Area kern
Confidence certain
Discovered 2026-06-29
Reported pending

Summary

Both copyin() call sites in jrecord_data() discard the return value. If the user buffer is invalidated between the size check and the copy (e.g. another thread munmap()s the range, or the page cannot be paged in), the FIFO destination retains whatever bytes it previously held (typically prior journal records) and the journal stream has no in-band marker to detect the substitution β€” silent corruption of the authoritative recovery stream.

Root cause

sys/kern/vfs_journal.c:1093 (in-loop fill, while iterating stream_residual chunks) and :1140 (tail fill) both call copyin(buf, jrec->stream_ptr, ...) without inspecting the return value. On a non-zero return the destination is left unwritten (stale).

Threat model & preconditions

  • Attacker position: unprivileged user with write permission on a file on a journaled mount (journal installed by root).
  • Privileges gained or impact: no kernel memory-safety impact. Silent corruption of the journal stream: a recovery consumer replays substituted (stale) bytes as if they were the user's data, undermining trust in recovered filesystem state. Independent of DF-0028 β€” any normal-sized writev() can trigger it via a racing munmap.
  • Reachability: thread A holds/munmaps the user buffer while thread B writev()s on the journaled FS; intermittently the journal FILEDATA leaf contains previous-transaction bytes.

Proof of concept (sketch)

thread A: mmap a page, writev() it on the journaled FS, and munmap() it in a
          tight loop from another thread racing the journal's copyin.
thread B: repeatedly writev(fd, &iov{mmap'd page}, 1).
Observe (offline, from the journal consumer) that some FILEDATA leaves carry
bytes that were not in the user buffer (previous-transaction residue).

Impact

Low β€” journal integrity only, no kernel memory-safety impact. A maintainer- actionable robustness/correctness fix.

Check the copyin() return; on failure zero-fill the destination and log (the zero-fill makes the substitution detectable in the stream):

--- a/sys/kern/vfs_journal.c
+++ b/sys/kern/vfs_journal.c
@@ -1093 +1093,4 @@
-       copyin(buf, jrec->stream_ptr, jrec->stream_residual);
+       if (copyin(buf, jrec->stream_ptr, jrec->stream_residual) != 0)
+       bzero(jrec->stream_ptr, jrec->stream_residual);
@@ -1140 +1143,4 @@
-       copyin(buf, jrec->stream_ptr, bytes);
+       if (copyin(buf, jrec->stream_ptr, bytes) != 0)
+       bzero(jrec->stream_ptr, bytes);

References

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/vfs_journal.c.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0029 Β· 5 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able unified diff 835 B view raw
VERDICT.md verdict source-trace and fix validation 1.3 KB ↓ raw
README.md readme reproduce instructions 787 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-0029 β€” 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 integrity only) on the unfixed kernel; after applying fix.diff the cited defect is closed. This finding was verified by source-tracing sys/kern/vfs_journal.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

Both copyin() sites in jrecord_data (in-loop fill at :1093 and tail fill at :1140) discard the return value. If the user buffer is invalidated between size check and copy (racing munmap/unpageable), the FIFO destination retains prior journal-record bytes silently.

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

DF-0029 β€” REPRODUCED (source-only)

Verdict

REPRODUCED (source-only)

Mechanism

Both copyin() sites in jrecord_data (in-loop fill at :1093 and tail fill at :1140) discard the return value. If the user buffer is invalidated between size check and copy (racing munmap/unpageable), the FIFO destination retains prior journal-record bytes silently.

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).

Check copyin() return; on failure bzero the destination so the substitution is detectable in the stream. Matches finding proposal.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via combined kernel build: all 39 Low-severity fix.diffs applied to /usr/src; make -j6 nativekernel KERNCONF=X86_64_GENERIC completed rc=0 with zero -Werror warnings (combined_40_low_severity_kernel_build.log).

baseline: source-trace confirms copyin return discarded at vfs_journal.c:1093,1140 / patched: fix.diff applies, combined kernel build NK_DONE rc=0.
↓ fix.diffDragonFly 6.5-DEVELOPMENT combined-build rc=0 (NK_DONE rc=0 Wed Jul 22 20:15:53 UTC 2026)

Confirmed kernel references

Detail

Exploit chain

none (non-corruption: journal integrity only, no memory-safety impact)

Evidence (decisive lines)

Source trace confirms both copyin sites at vfs_journal.c:1093,1140 discard return values. fix.diff checks return and bzeros on failure.

PoC changes

Authored fix.diff (check copyin return, bzero on failure). Wrote VERDICT.md/README.md/build.sh/run.sh/manifest.json.

Verified recommended fix

Check copyin() return at vfs_journal.c:1093 and :1140; on failure bzero the destination. Matches finding proposal.

Verdict

REPRODUCED (source-only). vfs_journal.c:1093 (in-loop fill) and :1140 (tail fill) both call copyin() and discard the return value. If the user buffer is invalidated between size check and copy (racing munmap), the FIFO destination retains prior journal-record bytes silently.