complete_raid5_write() computes and commits RAID-4/5 parity from FAILED (undefined) first-phase reads β garbage parity written to stable storage; the corruption survives retries and later surfaces as silently corrupt recovery reads
Summary
The gate into the parity update never tests B_ERROR and complete_raid5_write has no error handling: the XOR consumes rqe->b.b_data of members whose READ failed (stale kernel heap from Malloc'd buffers) and the parity is written unconditionally - a later successful retry computes poisoned^old^new, so the stripe parity stays wrong until manual rebuild, and a subsequent failure reconstructs corrupt data with no error anywhere. Inspection-certain; not injectable on md backing. B_ERROR-veto fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4897 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| manifest.json | β | 751 B | view raw | |
| verdict.json | β | 2.6 KB | view raw |
DF-4897 β complete_raid5_write() builds and writes RAID-4/5 parity from FAILED (undefined) first-phase reads β garbage parity committed to stable storage; corruption persists through retries and surfaces later as silently corrupt recovered data
Root cause (inspection-certain)
sys/dev/raid/vinum/vinuminterrupt.c:194-196:
} else if ((rqg->flags & (XFR_NORMAL_WRITE | XFR_DEGRADED_WRITE)) &&(rqg->active == 1)) complete_raid5_write(rqe);
The gate never tests bp->b_flags & B_ERROR, and complete_raid5_write()
contains no error handling of any kind. The XOR machinery consumes
rqe->b.b_data of the participating rqes:
- degraded pass :331-348 β
pdata[count] ^= sdata[count] - normal pass :362-385 β old-data-out / new-data-in XORs
For a component whose READ failed, that buffer is a non-zeroed malloc
(build_rq_buffer(), vinumrequest.c:834 β Malloc(bp->b_bcount)) with
undefined contents (stale kernel heap). The computed parity is therefore
XOR(garbage_old_data_of_failed_member, good members), and it is
written to disk by the unconditional parity launch at :433-464
(rqe->b.b_cmd = BUF_CMD_WRITE; ... vn_strategy/dev_dstrategy).
Why the damage is permanent
The request does fail at the top level (rq->error is set by the error
path at :92-140; volume ops go to the ioerror daemon β retry or EIO), so
the caller sees an error. But the corrupted parity is already on disk.
If the failure was transient, the daemon retry succeeds β and the retry's
own read-modify-write computes parity' = parity_stored ^ old_data ^
new_data where parity_stored is the poisoned value: the stripe parity
stays wrong forever (until a manual vinum parity rebuild). A later
subdisk failure reconstructed through this stripe (XFR_RECOVERY_READ
XOR at :165-193) then returns corrupted data to the user with no error
indication anywhere β the classic worst-case RAID integrity failure,
triggered by a single transient component read error.
Trigger / PoC
Requires one failed component read during a RAID-4/5 group write
(failing device). Not injectable on this guest (md devices cannot be
made to fail; re-verified 2026-09-24 β gpt destroy/gpt remove under
open vinum I/O leaves the stale cdev serving). Untested, certain by
inspection (error flow + XOR dataflow verified line-by-line).
Fix (proposed; not applied)
Do not run the parity update from failed components β fail the group instead (the error path already crashes the sd; the request completes through the error branch):
--- a/sys/dev/raid/vinum/vinuminterrupt.c +++ b/sys/dev/raid/vinum/vinuminterrupt.c @@ -193,7 +193,8 @@ } else if ((rqg->flags & (XFR_NORMAL_WRITE | XFR_DEGRADED_WRITE)) / RAID 4/5 group write operation / - &&(rqg->active == 1)) / and this is the last active request / + &&(rqg->active == 1) / and this is the last active request / + &&((bp->b_flags & B_ERROR) == 0)) / and its read did not fail / complete_raid5_write(rqe);
Note: a full fix should also check the OTHER members' B_ERROR (any member's read failing poisons the parity XOR); gating on the completing bp catches the common last-member case, and the sd crashed by the error path prevents future groups from using the bad component.
(severity Low: requires a component read failure; impact is persistent silent on-disk parity corruption β integrity, not memory safety.)
Fix verification
not_testablenot_testable: no failure-injection facility on the guest to stage a failed component read
README.md proposed diff
Confirmed kernel references
Detail
Evidence (decisive lines)
README.md (line-cited dataflow incl. why the corruption persists through retries: parity'=poisoned^old^new)
PoC changes
no seed; trigger requires one failed component read (failing device)
Verified recommended fix
Do not run the parity update from failed components: add && ((bp->b_flags & B_ERROR) == 0) to the :194 gate and veto on any member B_ERROR; diff in README.md.
Verdict
Inspection-certain integrity defect, not runnable on this guest: the gate at vinuminterrupt.c:194-196 never tests B_ERROR and complete_raid5_write() has no error handling; failed first-phase reads leave XFR_MALLOCED buffers with stale kernel-heap contents (Malloc at vinumrequest.c:834 is not zeroed) which are XORed into parity (:331-348 degraded, :362-385 normal) and committed by the unconditional parity write (:433-464). Same facility gap as DF-4481/DF-4896: this guest's md-backed drives cannot be made to fail I/O (re-verified 2026-09-24). Proposed guard (skip complete_raid5_write when the completing bp has B_ERROR; full fix should veto the parity update if ANY member read failed) in README.md; not validatable without error injection.
No comments yet.