stripe_alloc: reservation accounting bug, balance reclaim, minimal geometry

Findings from the fill-to-ENOSPC test generation (2026-08-02), all on bhive with the full series.

Bug: data reservation ignored trapped free space (fixed, two rounds)

btrfs_space_info_used() counted bytes_zone_unusable but not bytes_stripe_unusable, so the data reservation path admitted write()s against trapped sub-stripe free space the claim rule can never hand out. cow_file_range() then failed the real allocation at writeback and discarded the pages with the reservation -- silently: no message (the "dropping unwritten extent" log is on the ordered-extent path; this failure is earlier), no EIO for a non-fsyncing writer, and the freed reservation let the writer continue indefinitely.

Measured on kernel #77 (unfixed): the fill workload "wrote" 12.5 GiB into a 5 GiB filesystem; 1,008,802 blocks (~3.9 GiB) read back as short/zero on an UNDAMAGED fs.

Full rolling scenario on #85: FIRST FULLY CLEAN PASS. Tree 'a' correct-or-EIO (0 wrong, 0 silent); trees 'b' and 'c' all-zero at every step (#82's 216 warned 'b' edge drops: gone); both scrubs "no errors found". The formerly-separate deterministic 'b' EIO cluster is also gone -- diagnosed as stale rbio-cache pages poisoning the parity of a claimed stripe's first partial write (see NOTE-b-eio-cluster.md) and fixed by making rmw_try_pad_full() win over the cached-pages shortcut in rmw_rbio().

(Kernel #84 also crashed once, via the hang watchdog, during its fill-edge run -- no trace reached disk; not reproduced on #85 with the phantom fixed and zero enospc_debug dumps. Suspected: the free-space-entry dump printing hundreds of lines to the 115200-baud serial console under locks, while the margin code raised sinfo lock traffic. Moot while allocation failures are zero; remember it if dumps ever return.)

Sanity datum: with the fix, stripe_alloc's fill capacity equals legacy's within 2 MiB (4582 vs 4584) -- pure sequential fill traps nothing measurable; trapping starts with deletion holes.

Honest statfs arithmetic confirmed

After deleting 2297 MiB from the full fs: df-avail 1112 MiB + stripe_unusable 1187 MiB = 2299 MiB. The counters add up.

Balance reclaims trapped space (RESOLVED: headroom recipe)

Original finding: on a fill-to-ENOSPC fs, btrfs balance start -d collapsed stripe_unusable 1187 -> 51 MiB and the refill recovered 93% of the deleted bytes, but balance itself exited ENOSPC before the last chunks. Zygo's diagnosis: NOT series work -- balance of a block group requires enough free space outside it to relocate all its extents at their original sizes, and a filesystem filled to ENOSPC has none; correct behavior, wrong test setup.

Test v2 (his recipe): multi-GiB devices, shrink each by 1G after mkfs (btrfs fi resize <devid>:-1g), fill, grow back (btrfs fi resize <devid>:max) before balancing -- the regrown space guarantees one block group's worth of destinations and each freed group cascades more; the same technique admins already use against metadata ENOSPC. Kernel #86 result: BOTH allocators PASS. stripe_alloc: balance completes ("9 out of 11 chunks"), stripe_unusable 1176 -> 1 MiB, tree 'a' byte-perfect through balance and refill, refill absorbs the full regrown capacity.

Metadata raid56 under stripe_alloc (analysis, 2026-08-03)

The machinery below find_free_extent (runs, claim, padding, rescan, central fragment accounting, balance reclaim) is allocator- and content-agnostic and would cover metadata unchanged; metadata is simpler in one way (always CoW: no nocow class, no run persistence). The real work is reservation: metadata reserves per-item via block rsvs and overcommit, not per-byte delalloc, so the pessimism has to move there (roughly one stripe width per active run folded into the overcommit check) -- and the stakes are higher, because a mid- transaction metadata allocation failure is a transaction abort, not a warned dropped writeback.

Performance positioning (Zygo): stripe-alloc'd raid5 metadata would have NO RMW at all -- every write is a full stripe or padded to one -- putting it between upstream raid56 metadata (full RMW) and raid1c3/c4. The mirrors write fewer bytes when a stripe isn't full (3-4 copies of a node vs a padded full stripe) and burn no parity CPU, but cost more space than full-stripe raid5/6 parity. Padded tails also strand run remainders at high fsync rates (log tree), so log blocks likely want a dedicated run.

Mixed block groups: btrfs_is_stripe_alloc_bg() tests the DATA flag and do_allocation() dispatches per block group, so a mixed (DATA|METADATA) raid56 group already routes BOTH classes through the claim rule today -- mechanically complete, but metadata reservations there get no margin pricing, risking abort near-full. Until metadata reservation support exists, mixed groups should be excluded from the gate explicitly (!(flags & METADATA)) or added to the test matrix.

Minimal write-hole geometry

The stock "magic 256K" that lets narrow-array tests pass by accident is find_free_space()'s full-stripe alignment: allocations >= full_stripe_len are stripe-aligned (since the original raid56 merge 53b381b3abeb), so byte-dominant large files tile whole stripes and opt out of every shared-stripe failure mode. Sub-stripe files get no escape at any width.

Falsification suite (two-drive-writehole.sh, now misnamed):

SOLVED: the unrecoverable tree-'b' EIO cluster

In fill-mode rolling runs, tree 'b' (written after wiping devs 1+2, before wiping dev 3) ends with a deterministic EIO cluster reconstruction and scrub cannot fix (112 blocks on #77-78, 48 on #79-80, 34 on #81, 32 on #82). Root cause found by stripe-level post-mortem of the preserved #82 images: a stale cached rbio from the stripe's previous ('a'-era, pre-wipe) life is stolen by the first partial write into the re-claimed stripe; the all-uptodate stolen pages skip both the RMW read and rmw_try_pad_full(), so parity is computed from the cache's memory of columns whose real on-disk content the wipe had replaced. Full evidence chain, fix directions, and forensic method: NOTE-b-eio-cluster.md.