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

dirfs_write clobbers the bread() error and ignores bwrite(): a failed block read is still modified and the whole 16KB block persisted β€” stale/foreign buffer data written into the target file (readable back) and failures reported as success

Summary

'error = bread(...)' (:740) is immediately overwritten by 'error = uiomovebp(...)' (:741), and the IO_SYNC bwrite(bp) return is ignored (:752-753). bread/breadnx always returns a buffer but on failed strategy the block is NOT filled; dirfs_write then copies the user's bytes into the unfilled block and flushes the entire block via pwrite(dn_fd) - the bytes outside the user span are stale getblk-reuse contents of OTHER files, persisted into the target file and readable back. vkernel user writes to a dirfs file whose backing host read fails (host I/O error on the backing store, invalidated dn_fd): write() returns success while the file receives 16KB of stale buffer-cache contents from other files on the mount (cross-file disclosure) plus silent corruption; IO_SYNC write failures equally invisible. VERIFIED via harness transcription with real file and read-back (canary from 'another file' persisted and read back; write returned 0; fixed variant reports EIO and persists nothing). Fix: check bread's error before uiomove + propagate bwrite's.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3057 Β· 14 files
FileTypeDescriptionSize
harness.c trigger-source deterministic transcription of the vulnerable code path 6.5 KB view raw
build.sh build-script cc command line 352 B view raw
run.sh run-script runs harness; rc=2 == BUG CONFIRMED + FIX VALIDATED 181 B view raw
build.log build-log final successful build 20 B view raw
run.log run-log decisive run 709 B view raw
run.2.log run-log determinism check 2 709 B view raw
run.3.log run-log determinism check 3 709 B view raw
fix.diff suggested-fix git-apply-able unified diff (never applied to sys/) 934 B view raw
fix_base_vnops.log fix-log baseline compile error (vkernel64 env) 1.4 KB view raw
fix_p3057_vnops.log fix-log patched compile error β€” identical to baseline (compile-neutral) 1.4 KB view raw
env.txt environment uname, compiler, dirfs absence, guest restoration 586 B view raw
VERDICT.md verdict full narrative 2.7 KB ↓ raw
verdict.json verdict machine verdict (persist_poc.py schema) 4.0 KB view raw
README.md readme how to reproduce 602 B ↓ raw
README.md readme how to reproduce
↓ download raw

DF-3057 β€” evidence pack

Finding: dirfs_write clobbers the bread() error and ignores bwrite(): unfilled 16KB blocks (stale data of other files) are persisted to the target file and failures report success

Verdict: REPRODUCED (deterministic harness). Impact: stale/foreign buffer data persisted into an attacker-readable file (CWE-909) + silent data loss; trigger needs a failing host read (Medium).

Reproduce

./build.sh
./run.sh     # expect BUG CONFIRMED + FIX VALIDATED; rc=2

See VERDICT.md for the full narrative; manifest.json / verdict.json for machine-readable results.

VERDICT.md verdict full narrative
↓ download raw

DF-3057 β€” dirfs_write discards the bread() error and the bwrite() result: unfilled blocks are persisted (stale/foreign data written into the target file) and failures reported as success

Verdict

REPRODUCED (deterministic harness, identical over 3 runs) β€” the write loop clobbers the read error and ignores the write result:

:740  error = bread(vp, base_offset, BSIZE, &bp);
:741  error = uiomovebp(bp, (char *)bp->b_data + offset, len, uio);   /* :740 overwritten */
:742  if (error) { brelse(bp); break; }
...
:752  if (ap->a_ioflag & IO_SYNC)
:753          bwrite(bp);          /* return value ignored */
:754  else
:755          bdwrite(bp);

bread/breadnx (sys/kern/vfs_bio.c:900-960) always sets *bpp via getblk but on a failed strategy (dirfs_strategy marks the buffer B_ERROR when the host pread/pwrite fails β€” host I/O error on the backing file, invalid/closed dn_fd, stale dn_size making iosize negative) the buffer contents are NOT filled. dirfs_write then uiomoves the user's bytes into the unfilled 16KB block and flushes the WHOLE block through dirfs_strategy -> pwrite(dn_fd): everything outside the user-modified span is whatever the getblk buffer previously held β€” stale data of ANOTHER file on the same mount β€” and it is persisted into the target file, which the user can read back: an information disclosure into an attacker-readable file (CWE-909), plus silent data corruption, plus write() returning success despite the failed read and despite a failed IO_SYNC flush (:753).

Harness proof (real file, real pread-back):

bread failed with EIO but write() returns: 0 (SUCCESS β€” error swallowed at :741)
read back target file: STALE DATA FROM ANOTHER FILE present at offset 7000
=> INFO LEAK INTO ATTACKER-READABLE FILE CONFIRMED
FIXED: write() returns: 5 (EIO) β€” failure reported, nothing persisted; no stale data

Trigger reality check

The failed read requires the underlying host read of the backing file to fail (host I/O error / device error on the backing store / dn_fd invalidated). The extension case does NOT fail this way (pread past EOF returns 0 and dirfs_strategy bzeros the block), so this needs a genuinely failing backing file β€” narrow but real on vkernel deployments whose backing storage can return EIO. Rated Medium (limited disclosure + silent corruption, unusual precondition), consistent with the severity rubric.

Fix validation

fix.diff: check the bread error before uiomove (brelse+break) and propagate the bwrite error for IO_SYNC. git apply --check RC=0 locally and on guest /usr/src; compile-neutral (identical first compiler error patched vs unpatched); harness FIXED variant reports EIO and persists nothing. Live boot: not_testable (dirfs vkernel-only).

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched reproduced

fix.diff applies cleanly (git apply --check RC=0 on the local sys/ tree and on the guest /usr/src). Compile-neutral: patched vs unpatched dirfs_vnops.o/dirfs_subr.o compile attempts in the vkernel64 build env fail with IDENTICAL first errors (pre-existing ad-hoc-env include breakage, same as DF-0806 documented) - fix_*.log in this pack. Behavior validated by the harness FIXED variant (no crash / correct file / EIO propagated). Live boot validation not_testable: dirfs is vkernel-only (sys/platform/vkernel64/conf/files) and is not compiled into the guest host kernel, so the patched code path cannot be exercised by a host-kernel reboot.

fix.diff; fix_base_vnops.log vs fix_p3057_vnops.log (identical first error); harness FIXED variant output in run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

failing backing file (host EIO) + partial-block write through dirfs -> unfilled buffer span persisted to the target file -> attacker reads back stale buffer-cache contents of other files on the mount (cross-file disclosure within the vkernel) + silent data corruption; write()/fsync-adjacent semantics report success. Trigger is narrow (requires the host read to fail) β€” rated Medium.

Evidence (decisive lines)

harness.c (verbatim loop transcription with bread-fails-without-fill model; getblk-reuse modeled by pre-seeding the buffer with another file's canary); run.log: 'bread failed with EIO but write() returns: 0', 'STALE DATA FROM ANOTHER FILE present at offset 7000', 'INFO LEAK ... CONFIRMED'; FIXED: 'write() returns: 5 (EIO)', 'no stale data => NO LEAK'. Deterministic over 3 runs.

PoC changes

Harness written fresh (no seed). One iteration: replaced memmem() (absent on DF libc) with a portable scan.

Verified recommended fix

dirfs_write: check bread() error before uiomovebp (brelse+break), and propagate the bwrite() return for IO_SYNC writes.

Verdict

REPRODUCED (deterministic harness; dirfs vkernel-only, no live-boot test). dirfs_write :740-741 assigns error=bread(...) then immediately overwrites it with error=uiomovebp(...); :752-755 ignore the bwrite()/bdwrite() results. bread always returns a buffer but on a failed strategy (host pread EIO on the backing file / invalid dn_fd / stale dn_size) the block is NOT filled; dirfs_write then overlays the user's bytes, flushes the whole 16KB block via pwrite(dn_fd), and returns success. Harness (real file + read-back): stale bytes of another file appear in the target file at the expected offset (info leak into attacker-readable file, CWE-909) and write() returns 0 despite EIO; IO_SYNC bwrite failures equally invisible. Fixed variant (check bread error; propagate bwrite error) reports EIO and persists nothing.