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

bre5() second-bad-subdisk guard requires checksdstate() to fail: badsdno silently overwritten for reviving/reborn data subdisks -> parityless-write conversion defeated and two-bad-column writes accepted (silent data loss, parity scribbled onto down subdisk)

Summary

The refusal requires checksdstate to fail, but it returns OK for reborn data subdisks on writes and for reviving in a parity plex - badsdno is then overwritten. With the parity subdisk down plus one reborn data subdisk, the degraded path launches I/O against the DOWN parity subdisk, never writes the data block, and reports SUCCESS; with two reborn data subdisks, writes spanning both are accepted with neither written. GUEST-VERIFIED: 16KB write returned success while data was never written and parity was physically rewritten on the down subdisk; 32KB two-reborn write panics via DF-4721. Fixed kernel: EIO on both, correct degraded writes unaffected. Refuse-on-badsdno-alone fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4714 Β· 10 files
FileTypeDescriptionSize
setup_vinum.sh β€” 2.1 KB view raw
run.sh β€” 111 B view raw
trigger_a.c β€” 2.2 KB view raw
trigger.c β€” 2.4 KB view raw
run.log β€” 2.4 KB view raw
run.patched.log β€” 1.2 KB view raw
panic.txt β€” 802 B view raw
fix.diff β€” 1.7 KB view raw
VERDICT.md β€” 4.3 KB ↓ raw
env.txt β€” 211 B view raw
VERDICT.md
↓ download raw

DF-4714 β€” VERDICT

status: reproduced (4/4 decisive runs across 2 baseline boots + patched control boot) impact: silent data loss on RAID-5 writes (data corruption) + kernel panic via the DF-4721 arm fix: validated β€” bad behaviors eliminated on patched kernel

Root cause (code trace)

sys/dev/raid/vinum/vinumraid5.c, write path of bre5():

  • :363-364 β€” if the parity subdisk's state < sd_reborn (down/crashed), m.badsdno = m.psdno.
  • :403-404 β€” for each participating data subdisk with state != sd_up, s = checksdstate(...); the refusal for a second bad subdisk is if (s && (m.badsdno >= 0)) β€” it requires checksdstate() to have FAILED.
  • sys/dev/raid/vinum/vinumstate.c:676-678 β€” checksdstate() returns REQUEST_OK (0) for sd_reborn data subdisks on writes (and for sd_reviving in a parity plex with sddowncount <= 1, :651-661).
  • :420 β€” therefore m.badsdno = mysdno overwrites the earlier (parity) marker, or the first bad data subdisk's marker.
  • :443-453 β€” the badsdno == psdno β†’ XFR_PARITYLESS_WRITE conversion consequently never runs for the parity-down+reborn-data case.

Reachability of the states: a drive flap (down→up) moves every subdisk on the drive sd_up→sd_crashed→sd_reborn (vinumstate.c:427-438, set_drive_state calls update_sd_state per subdisk). Two subdisks of a RAID-5 plex on one drive is a legal vinum configuration (used here; also arises with reborn+reviving pairs because sd_reborn is not counted in sddowncount, vinumstate.c:744-745).

What was run (guest DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC)

Setup: 2 md (malloc) disks, GPT vinum partitions, RAID-5 plex stripesize 32s, sds: s0β†’d1, s1β†’d2@0, s2β†’d2@4361s. Volume sectors [128,192) are stripe group 2 β†’ parity ordinal 0 (s0), data ordinals 1 (s1) and 2 (s2). All triggers use SINGLE write()/read() calls (a 512-byte dd issues per-sector bios that only ever touch one data column and hide the bug β€” see run.log history for that trap).

Baseline boot 1 (run.log)

  1. Scenario A β€” s0 forced down, s1 forced reborn, 16KB single write of 0xBB at [128,160): - write returned SUCCESS; - readback returned the OLD contents (0x75) β†’ *** SILENT DATA LOSS ***; - physical evidence: s0's (DOWN subdisk) drive region changed to 0xCE = 0xBB^0x75 β€” the user's new data was folded into parity and written to the down subdisk while the data block itself was discarded; s1's region never changed.
  2. Scenario B first attempt (pre-reset boot, panic.txt): 32KB single write with s1+s2 reborn β†’ Fatal trap 12, launch_requests+0xc2: movq (%rax),%rax, fault VA 0x0 (the DF-4721 launch_requests UAF arm β€” reachable here because the overwrite leaves a trailing XFR_BAD_SUBDISK rqe).

Baseline boot 2

Re-ran scenario B cleanly (plex/volume lifted): identical panic, 2/2. (panic.txt.)

Patched boot (run.patched.log)

Kernel rebuilt with fix.diff (this finding + DF-4715 correction):

  1. Scenario A: write β†’ EIO (write: Input/output error) β€” correct refusal instead of silent loss.
  2. Scenario B: write β†’ EIO, no panic.
  3. Degraded-but-correct write (single sd down, DF-4721 case): still SUCCEEDS, readback matches β€” the fix does not break legitimate degraded writes.
  4. Control roundtrip: OK.

Non-goals / limits

  • No uid=0 chain: the bug corrupts storage data / panics; it does not grant privilege. The trigger needs a vinum array (root-created); after that, any user I/O to a mounted filesystem on the volume reaches the path.
  • Scenario B's panic is attributed to the DF-4721 UAF (also reachable without this bug); scenario A is the pure, panic-free demonstration of THIS bug's data-loss impact.

Fix

fix.diff β€” guard on m.badsdno >= 0 alone (refuse regardless of checksdstate() result), returning REQUEST_DOWN when s == REQUEST_OK; co-located with the DF-4715 ordinal-loop correction. Validated above.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

patched kernel (fix.diff incl. DF-4715 correction): scenario A write EIO (was success+loss), scenario B write EIO no panic (was Fatal trap 12), designed degraded write still succeeds with correct readback, control roundtrip OK - run.patched.log

run.patched.log steps 1,3,4,5
↓ fix.diffper-fix-DF-4714

Confirmed kernel references

Detail

Exploit chain

root: 3-sd RAID-5 vinum volume on md disks (s1+s2 share one drive); states placed via raw setstate ioctls exactly as a drive flap leaves them (down/reborn); unprivileged: single 16KB/32KB write() on the volume (e.g. via filesystem) -> silent data loss or kernel panic

Evidence (decisive lines)

run.log (scenario A loss + physical drive dumps), panic.txt (scenario B panic), run.patched.log (EIO on fixed kernel, degraded write still OK), VERDICT.md full matrix

PoC changes

no seeded PoC; built from scratch. Critical detail: triggers must use a SINGLE write() spanning both data blocks (dd bs=32768 or C write()); 512-byte dd hides the bug (per-sector bios touch one column each). vinum config syntax: 'plex org raid5 16k' (positional stripesize), explicit driveoffset must account for the 265-sector vinum label reserve; 'vinum setstate -f' uses the raw force ioctl (no state propagation) which is what makes deterministic state placement possible

Verified recommended fix

in bre5() refuse on m.badsdno >= 0 regardless of checksdstate() result, returning REQUEST_DOWN when s==REQUEST_OK (fix.diff, validated)

Verdict

bre5()'s second-bad-subdisk guard (vinumraid5.c:404) requires checksdstate() to fail, but sd_reborn/sd_reviving data subdisks return REQUEST_OK for writes, so m.badsdno is silently overwritten (:420). With parity down + a reborn data sd the XFR_PARITYLESS_WRITE conversion (:443) never runs: guest-verified silent data loss (16KB write SUCCESS, data block never written, parity recomputed and written onto the DOWN parity subdisk, readback stale). With two reborn data sds (one shared drive flap, vinumstate.c:427-438) writes spanning both columns are accepted with neither written, and the resulting trailing XFR_BAD_SUBDISK layout trips the DF-4721 launch_requests UAF -> kernel panic (Fatal trap 12, launch_requests+0xc2, 2/2 runs). Fixed kernel: both writes EIO, degraded-correct writes unaffected, no panic.