β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4897

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4897 Β· 3 files
FileTypeDescriptionSize
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_testable
baseline no→ patch + rebuild →patched clean

not_testable: no failure-injection facility on the guest to stage a failed component read

README.md proposed diff
per-fix-DF-4897

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.