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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5838 Β· 5 files
FileTypeDescriptionSize
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_bsize block 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:

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.

VERDICT.md
↓ download raw

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)

  1. sys/kern/vfs_cluster.c:1746-1748 β€” getcacheblk(vp, loffset, last_bp->b_bcount, GETBLK_SZMATCH|GETBLK_NOWAIT): SZMATCH compares the pre-existing buffer's b_bcount against last_bp->b_bcount (sys/kern/vfs_bio.c:2799-2802), not blksize.
  2. 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 uses blksize: len (:1737), stride (:1745), cluster_write's endbp and realloc teardown (:1312-1342).
  3. Downstream consumer ffs_reallocblks assumes one fs_bsize block per child: block-map rewrite loop sys/vfs/ufs/ffs_alloc.c:470-497 and the old-block free ffs_blkfree(..., fs->fs_bsize) at ffs_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.
  4. Caller sweep confirms no current caller passes bp->b_bcount != blksize: - UFS full-block-only gate sys/vfs/ufs/ufs_readwrite.c:377-380; - 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; - the only other cluster_write path, cluster_awrite β†’ cluster_wbuild, never reaches cluster_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.

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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5838

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.