# 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:

```c
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:

- FFS: `cluster_write` is only reached for full blocks
  (`xfersize + blkoffset == fs->fs_bsize`, `sys/vfs/ufs/ufs_readwrite.c:377-380`)
  with `blksize = fs->fs_bsize` and VOP_BALLOC-created full-block buffers.
- HAMMER1: `cluster_write(bp, cluster_eof, blksize, seqcount)` with
  `bp->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`.
