cluster_collectbufs collects buffers matched against last_bp->b_bcount instead of blksize, breaking the documented size contract feeding VOP_REALLOCBLKS
Summary
The write-cluster reallocblk helper's own comment says only buffers whose block size matches blksize are collected, but the getcacheblk SZMATCH filter uses last_bp->b_bcount while len, the stride, and cluster_write's teardown are all in blksize units. A mismatched caller yields a mixed-size children list into ffs_reallocblks, which walks block maps one fs_bsize per child and frees each old block as a full fs_bsize block β fragment-backed children would corrupt the cylinder-group allocation bitmap, with the only catching panics behind #ifdef DIAGNOSTIC. Latent: all current callers pass b_bcount == blksize and an unprivileged user cannot decouple the two through any VOP. One-line blksize fix git-apply-clean.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5838 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 2.6 KB | β raw | |
| fix.diff | β | 515 B | view raw | |
| verdict.json | β | 3.3 KB | view raw | |
| manifest.json | β | 828 B | view raw |
DF-5838 β cluster_collectbufs matches collected buffer size against last_bp->b_bcount, not blksize
File: sys/kern/vfs_cluster.c (pass-3 fresh-perspective re-audit)
What (code facts, certain)
cluster_collectbufs() (sys/kern/vfs_cluster.c:1726-1783) is the
buffer-gathering helper for the write-side VOP_REALLOCBLKS optimization
called from cluster_write() (sys/kern/vfs_cluster.c:1310-1311).
Its own contract comment says:
"Only pre-existing buffers whos block size matches blksize are collected." β
sys/kern/vfs_cluster.c:1719-1721
But the getcacheblk lookup matches the size of the last buffer instead of the cluster block size:
bp = getcacheblk(vp, loffset,
last_bp->b_bcount, GETBLK_SZMATCH |
GETBLK_NOWAIT);
β sys/kern/vfs_cluster.c:1746-1748
Everything else in the write-cluster machinery is computed in blksize
units: the number of children (len = (cc->v_lastw - cc->v_cstart) / blksize,
sys/kern/vfs_cluster.c:1737), the loop stride (loffset += blksize,
:1745), and cluster_write's endbp/bdwrite/brelse teardown
(:1312-1342).
Impact (latent)
If any caller ever passes bp with bp->b_bcount != blksize,
cluster_collectbufs collects a children array with mixed logical sizes and
hands it to VOP_REALLOCBLKS. For FFS, ffs_reallocblks
(sys/vfs/ufs/ffs_alloc.c:349-567) then:
- walks the inode/indirect block maps assuming one
fs->fs_bsizeblock per child (ffs_alloc.c:470-497), and - frees each child's old physical block as a full block:
ffs_blkfree(ip, dofftofsb(fs, buflist->bs_children[i]->b_bio2.bio_offset), fs->fs_bsize)(ffs_alloc.c:535-541).
Freed-as-full-block fragment-backed buffers corrupt the cylinder-group
allocation bitmap (space leak / overlapping allocations β silent on-disk
corruption). The DIAGNOSTIC panics that would catch a mismatched list
(ffs_alloc.c:375-388, "non-logical cluster" / "non-physical cluster") are
compiled out of release kernels.
Reachability today
All current callers pass equal values, so this is a latent contract violation rather than a live bug:
- FFS:
cluster_writeis only reached for full blocks (xfersize + blkoffset == fs->fs_bsize,sys/vfs/ufs/ufs_readwrite.c:377-380) withblksize = fs->fs_bsizeand VOP_BALLOC-created full-block buffers. - HAMMER1:
cluster_write(bp, cluster_eof, blksize, seqcount)withbp->b_bcount == blksize(sys/vfs/hammer/hammer_vnops.c:841). - HAMMER2: same shape (
sys/vfs/hammer2/hammer2_vnops.c:1177). - tmpfs: buffers created with
node->tn_blksize(sys/vfs/tmpfs/tmpfs_vnops.c:864-881).
One adjacent wrinkle noted while verifying: when a gap exists, j is
recomputed per-NULL (sys/kern/vfs_cluster.c:1750-1752), so only buffers
after the last gap survive the compaction (:1762-1775) β earlier valid
buffers are dropped. That is safe (VOP_REALLOCBLKS needs contiguous runs)
but silently shrinks the optimization window; not a defect on its own.
Why not verified on the guest
Triggering requires a filesystem caller that passes a short/oversized bp
against blksize β none exists in this tree, and no unprivileged local
program can manufacture the mismatch through the VOP layer. Phase V
verification is skipped; the fix is a one-line contract alignment.
Fix
fix.diff: pass blksize to getcacheblk instead of last_bp->b_bcount.
DF-5838 β VERDICT
status: untested (deliberately not executed β latent contract defect, no reachable trigger in this tree) impact: none demonstrated (latent on-disk-corruption footgun) confidence: certain (code discrepancy), speculative (impact until a mismatching caller exists)
Static verification (line-accurate)
sys/kern/vfs_cluster.c:1746-1748βgetcacheblk(vp, loffset, last_bp->b_bcount, GETBLK_SZMATCH|GETBLK_NOWAIT): SZMATCH compares the pre-existing buffer'sb_bcountagainstlast_bp->b_bcount(sys/kern/vfs_bio.c:2799-2802), notblksize.- The documented contract at
sys/kern/vfs_cluster.c:1719-1721("Only pre-existing buffers whos block size matches blksize are collected") and every other computation in the function/callers usesblksize:len(:1737), stride (:1745),cluster_write'sendbpand realloc teardown (:1312-1342). - Downstream consumer
ffs_reallocblksassumes one fs_bsize block per child: block-map rewrite loopsys/vfs/ufs/ffs_alloc.c:470-497and the old-block freeffs_blkfree(..., fs->fs_bsize)atffs_alloc.c:535-541. A mixed-size children list frees fragment-backed blocks as full blocks β allocation-bitmap corruption. The guards that would catch it (ffs_alloc.c:375-388) are#ifdef DIAGNOSTIC. - Caller sweep confirms no current caller passes
bp->b_bcount != blksize: - UFS full-block-only gatesys/vfs/ufs/ufs_readwrite.c:377-380; - HAMMER1sys/vfs/hammer/hammer_vnops.c:841; - HAMMER2sys/vfs/hammer2/hammer2_vnops.c:1177; - tmpfssys/vfs/tmpfs/tmpfs_vnops.c:864-881; - the only other cluster_write path,cluster_awriteβcluster_wbuild, never reachescluster_collectbufs.
Why not executed on the guest
No trigger exists: the mismatch requires a future/unknown filesystem caller
passing a short or oversized buffer. An unprivileged user cannot influence
bp->b_bcount vs the blksize argument independently β both are fixed by
the filesystem per write. Guest left untouched.
Recommended fix
fix.diff: last_bp->b_bcount β blksize at sys/kern/vfs_cluster.c:1747.
One line; aligns the collection filter with the documented contract and the
surrounding arithmetic. Not built (behavior is identical for every existing
caller; fix_status: not_testable).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff authored against the read-only sys/ tree (one line); not built on the guest because behavior is byte-identical for every existing caller (b_bcount == blksize in all reachable paths) - nothing observable to measure (fix_status not_testable).
findings/poc/DF-5838/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-5838/VERDICT.md (full path:line analysis); findings/poc/DF-5838/fix.diff (one-line contract alignment); decisive lines: sys/kern/vfs_cluster.c:1746-1748 (b_bcount used) vs :1719-1721 (contract) vs :1737/:1745 (blksize arithmetic); sys/vfs/ufs/ffs_alloc.c:470-497+535-541 (full-block assumption downstream); sys/kern/vfs_bio.c:2799-2802 (SZMATCH semantics)
PoC changes
no PoC built - latent defect with no reachable trigger in this tree (all callers pass b_bcount == blksize); verification intentionally skipped per contract for Low hardening findings
Verified recommended fix
Pass blksize (not last_bp->b_bcount) to getcacheblk in cluster_collectbufs so the SZMATCH filter matches the documented contract and the surrounding blksize-based cluster arithmetic.
Verdict
Contract defect confirmed statically: cluster_collectbufs() documents that only buffers whose block size matches blksize are collected (sys/kern/vfs_cluster.c:1719-1721), but the getcacheblk SZMATCH filter at sys/kern/vfs_cluster.c:1746-1748 uses last_bp->b_bcount instead, while len (1737), the collection stride (1745) and cluster_write's endbp/teardown (1312-1342) are all computed in blksize units. A caller passing bp->b_bcount != blksize would hand ffs_reallocblks a mixed-size children list; ffs walks the block maps one fs_bsize block per child (sys/vfs/ufs/ffs_alloc.c:470-497) and frees every child's old block as a full fs_bsize block (ffs_alloc.c:535-541), so fragment-backed children corrupt the cylinder-group allocation bitmap - and the DIAGNOSTIC guards that would panic (ffs_alloc.c:375-388) are compiled out of release kernels. Not executed on the guest: every current caller (UFS sys/vfs/ufs/ufs_readwrite.c:377-380 full-block gate; HAMMER1 sys/vfs/hammer/hammer_vnops.c:841; HAMMER2 sys/vfs/hammer2/hammer2_vnops.c:1177; tmpfs sys/vfs/tmpfs/tmpfs_vnops.c:864-881) passes b_bcount == blksize, and an unprivileged user cannot decouple the two values through any VOP path, so the defect is latent. One-line fix authored.
No comments yet.