2026-08-04, kernel 7.2.0-rc5+ #86 (master testing + zb64), 3-device
raid5 data / raid1 metadata, directed-writehole.sh DW_SURGERY=parity.
The original directed test (DW_SURGERY=data, the default) drops a data
block of an in-flight write and shows that a committed neighbour sharing the
full stripe becomes unrecoverable. That is the write hole most people mean.
This variant drops the parity update instead: the data sectors of the
in-flight write reach the disk, the parity block for that row does not. The
same dm-log-writes replay machinery is used -- replay to the afterA mark,
then splice the pre-transaction contents of exactly one physical block back
over the final image. The block is chosen from the chunk map:
data column -> member (fsn + col) % 3
parity -> member (fsn + 2) % 3 # ndata = 2, 3 devices
so DW_SURGERY=parity recomputes the physical target as the parity member of
the same row the write touched, and reverts that one block.
########## legacy ##########
f1 @ 105644032, f17 @ 105709568 (logical)
OK: stock allocator packed f17 into the committed stripe
f17: fullstripe 267 col 1 mode parity -> member 2 phys 67174400
spliced pre-txn-2 block over the parity update of f17's row
all-present reads clean: stale parity is LATENT
dropping member 0 (holds f1's data column)
degraded read of committed f1: f1 EIO (torn parity detected by csum)
OK: WRITE HOLE demonstrated deterministically at 3 devices
degraded read of writing-file f17: CORRECT
########## stripe_alloc ##########
f1 @ 105644032, f17 @ 105775104 (logical)
OK: stripe_alloc kept f17 out of the committed stripe
f17: fullstripe 268 col 0 mode parity -> member 0 phys 39583744
spliced pre-txn-2 block over the parity update of f17's row
all-present reads clean: stale parity is LATENT
dropping member 0 (holds f1's data column)
degraded read of committed f1: f1 CORRECT (reconstructed through consistent parity)
OK: stripe_alloc survives the surgery
degraded read of writing-file f17: CORRECT
DIRECTED_RC=0
It is a real second hole, not a variation of the first. Legacy loses
the committed neighbour f1 exactly as in the data-dropped case, by a
different mechanism: nothing about f1's own bytes was touched, and no
f1 block was lost. The damage is entirely in the redundancy: parity now
describes col1_old while the disk holds col1_new, so reconstruction
computes P_old ^ col1_new = col0 ^ col1_old ^ col1_new -- garbage.
It is latent. all-present reads clean in both legs: with every
device present, btrfs never reads parity, so nothing detects the
divergence. Scrub would (it recomputes parity), an ordinary read never
will. The corruption is created at crash time and stays invisible until a
device is lost, which is precisely when it is least welcome.
The victim's csum decided whether it was loud or silent. Here f1 is
ordinary csummed data, so reconstruction produced garbage, the csum failed,
and the read returned EIO -- loud, and the data is still gone. A victim
without csums (nodatasum/nodatacow file, or free space that a later
allocation trusts) would have been reconstructed to garbage silently.
That is the worse half of this failure mode and is not exercised here.
stripe_alloc confines it to the writer's own stripe. Under the series
f17 was placed in full stripe 268 rather than joining f1 in 267, so the
reverted parity belongs to a stripe with no committed data in it. f1
reconstructs through stripe 267's parity, which the crash never touched:
f1 CORRECT. This is the same confinement argument as the data-dropped
case and it holds for the same structural reason -- a committed stripe is
never rewritten, so nothing in flight can invalidate its parity.
The writer's own file survives in both legs (f17: CORRECT). Not a
property of either allocator: f17's data column is on a surviving member
and is read directly, no reconstruction involved. Recorded so the line is
not mistaken for a stripe_alloc guarantee.
The level-2 case was assumed to be covered by the level-1 result and had never actually been executed. It is worth keeping as a distinct gate: the two modes exercise different halves of the RMW write (data vs parity), and a fix that happened to protect one could leave the other open.
Companion: directed-writehole.sh (DW_SURGERY=data|parity),
NOTE-b-eio-cluster.md, NOTE-stripe-alloc-reservation-and-reclaim.md.