Bug found 2026-08-07 on kernel #115, fixed and validated on #116.
The growable-frontier path in btrfs_alloc_from_open_stripe() handed out
[r->offset, r->offset + num_bytes) without calling run_live_set(). The
other two allocation sites (fast path, new-run install) both set the bits.
Grow fires when an allocation exceeds the run's remainder, so the allocation
begins at the old tail — usually mid-stripe — and spills across the
full-stripe boundary. The liveness map therefore called exactly those spill
sectors dead. When a later allocation filled the rest of the boundary row
and its write went down as a separate partial rbio, rmw_try_pad_full()
consulted the map, believed the spill was dead, zero-filled it and computed
parity over the zeros.
The failure is asymmetric: a map that under-reports liveness destroys
committed data silently, while one that over-reports only forces an RMW
fallback. That is why the missing set was catastrophic and why the
live_nbits overflow guard (which zeroes the map, making pad decline) is
harmless.
Fix: run_live_set(r, r->offset, num_bytes, true) in the grow path.
stripe_unusable stuck ~1.2 GiB, tree a eio=3081, tree b eio=2751,
at both profiles. Legacy allocator passes both.eio=0 everywhere, stripe_unusable collapses (802->2 MiB raid6,
1181->4 MiB raid5), dmesg clean.Why other suites missed it: fill-edge has no per-file fsync, so boundary rows merge into single full-stripe writes. The window needs a separate final writer on the boundary row, which balance-reclaim's rolling fsync-per-file oracle supplies.
zfscan.py — full-array damage scanner. Parses the chunk tree (handles
variable stripe counts — late chunks can be 4-stripe raid6) and the csum
tree, flags all-zero sectors whose csum entry is not crc32c(zeros), and
XOR-checks parity per damaged row. Paths at the top point at the
evidence array; edit BASE/DEV/IMG to rescan a different one.gencorr.py — correlates damaged rows against an extent map
(key/len/gen/owner extracted from dump-tree -t extent) to show
head/tail ownership and generation.padrepro.sh — fill-only repro with ftrace kprobes on
btrfs_stripe_run_pad_mask / _write_abandoned / _write_done plus the
reserve_extent and raid56_write tracepoints. Never run; kept because the
probe set is the right one for any future pad-mask question.balance-reclaim6-115.{log,dmesg} — the failing raid6 run.damage-map.txt — the full scan output: all 77 damaged rows with chunk,
row, logical address, per-column damaged-sector counts, and the per-row
parity verdict (parity_ok=True on every one).gen-correlation.txt — head/tail extent correlation for the damaged
rows: (logical, len, gen, owner) for the extent whose spill was zeroed
and for the one that survived. 39 rows have both extents present and
all 39 are same-generation; the remaining rows show tail ext None
(the tail extent had been freed by scan time), which is why the counter
reads DIFF(<gen>->-1) — that is a missing tail, not a generation
mismatch./boot/vmlinuz.prev on the test VM is #115 (pre-fix), and
balance-reclaim-test.sh reproduces the damage deterministically at either
profile in well under an hour. Revert the one-line fix on any later kernel
for the same effect. The raw images are therefore a convenience, not the
only path back to a damaged array.