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)
PoC verification
Evidence pack
findings/poc/DF-3057 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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_testablefix.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
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.
No comments yet.