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

complete_raid5_write() launches RAID-4/5 second-phase I/O via vn_strategy(drive->vp) on a teardown-vulnerable vnode β€” deterministic NULL-vp kernel panic (fatal trap 12 in vn_strategy), closed-vnode UAF window in the mid-flight variant

Summary

The second phase launches via vn_strategy(drive->vp) while the identical first phase goes through the teardown-immune cdev stored in bio_driver_info - and the daemon's close_locked_drive does vn_close(vp); vp=NULL with no sync against completions (reachable from the code's OWN ENXIO branch in the same call). REPRODUCED 1/1 deterministic on guest (no fault injection): stop -f -> state pokes -> first RAID-5 write panics at vn_strategy+0x38 (va 0x5c, process dd); fix validated on rebuilt kernel - identical trigger completes clean, second-phase data verifiably lands via the cdev route. dev_dstrategy fix (+ vinumrequest.c:980 twin noted).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4895 Β· 15 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
setup_vinum.sh β€” 1.9 KB view raw
writer.sh β€” 500 B view raw
racer.sh β€” 1.1 KB view raw
run.sh β€” 115 B view raw
trigger.sh β€” 956 B view raw
run.log β€” 2.7 KB view raw
run.patched.log β€” 1.5 KB view raw
panic.txt β€” 4.2 KB view raw
env.txt β€” 529 B view raw
fix.diff β€” 729 B view raw
fix_build.log β€” 1.1 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.9 KB view raw

DF-4895 β€” vinum complete_raid5_write(): second-phase RAID-5 I/O launched via vn_strategy(drive->vp) on a teardown-vulnerable vnode β€” deterministic NULL-vp panic (fatal trap 12 in vn_strategy), closed-vnode UAF in the mid-flight window

What this proves

complete_raid5_write() (sys/dev/raid/vinum/vinuminterrupt.c) launches the second phase of a RAID-4/5 group write (data blocks at :427, parity block at :464) with vn_strategy(drive->vp, ...). The first phase launches the same requests through dev_dstrategy(bio_driver_info=cdev, ...) (vinumrequest.c:436) β€” the cdev survives drive teardown, the vnode does not: set_drive_state(down) queues daemonrq_closedrive, and the vinum daemon runs close_locked_drive() β†’ vn_close(drive->vp); drive->vp=NULL (vinumio.c:225-227) with no synchronization against the completion path. Any group write whose subdisks' states say "up" then faults in vn_strategy (vp->v_flag deref, vfs_bio.c:3375).

Teardown routes that leave/NULL the vp while writes continue: - complete_rqe()'s own ENXIO branch (vinuminterrupt.c:129-139) force-downs the drive and, in the same call, proceeds into complete_raid5_write() which launches more vn_strategy()s β€” racing the daemon's vn_close() (closed-vnode use-after-close / memory corruption window); - vinum stop -f <drive> (the only CLI route to real set_drive_state with force; plain setstate -f down pokes DRIVE[].state directly via setstate_by_force() and never closes anything); - post-stop state-poke recovery (vinum setstate -f up ...), which is the sequence the DF-4714/DF-4482 packs used for years on this guest.

Reproduced (deterministic, no fault injection)

  1. sh ./setup_vinum.sh β€” 2 md drives, 3-sd RAID-5 volume rv, sanity 32KB roundtrip OK (proves vp path works when up).
  2. vinum stop -f d2 β€” real down: daemon vn_closes vp, sds cascade crashed (dmesg shows the state cascade).
  3. sleep 3
  4. vinum setstate -f up d2; vinum setstate -f up rv.p0.s0 rv.p0.s1 rv.p0.s2 rv.p0 rv β€” states "up", vp still NULL.
  5. dd of=/dev/vinum/vol/rv bs=32768 seek=5 count=1 if=/tmp/cc conv=notrunc

Result: fatal trap 12, Stopped at vn_strategy+0x38: testb $0x40,0x5c(%r13), fault address 0x5c (NULL vp + offset), current process = dd. See panic.txt (serial console), run.log (transcript+analysis).

Impact ceiling: privileged local DoS (guaranteed panic) via the deterministic route; vnode use-after-close (memory corruption potential) via the mid-flight/ENXIO route. Requires vinum administrative control (root) or a genuine component ENXIO failure β†’ severity Medium.

Fix

fix.diff: use the cdev the code already stores in bio_driver_info at :404/:440 β€” dev_dstrategy(rqe->b.b_bio1.bio_driver_info, &rqe->b.b_bio1) at both sites, identical to the first-phase launch mechanism.

Fix validation: vm.sh reset with-src, apply fix.diff in guest /usr/src, make quickkernel + installkernel, reboot, re-run the exact trigger (steps 1-5): write must NOT panic (baseline panicked 1/1). See run.patched.log, fix_build.log.

sdio() (vinumrequest.c:980) uses the same vn_strategy(drive->vp) pattern for raw-subdisk I/O and needs the same fix (different file).

VERDICT.md
↓ download raw

DF-4895 β€” complete_raid5_write() launches RAID-5 second-phase I/O via vn_strategy(drive->vp) with no validity guarantee on the vnode

ROOT CAUSE (sys/dev/raid/vinum/vinuminterrupt.c) * :427 vn_strategy(drive->vp, &rqe->b.b_bio1); (second-phase data write) * :464 vn_strategy(drive->vp, &rqe->b.b_bio1); (parity write) The first phase launches the very same requests via dev_dstrategy(rqe->b.b_bio1.bio_driver_info, ...) with the cdev (vinumrequest.c:436, bio_driver_info = DRIVE[].dev set in build_rq_buffer vinumrequest.c:828 and re-set at vinuminterrupt.c:404/440). The cdev stays valid across vinum's drive teardown; the vnode does not: set_drive_state(down) queues daemonrq_closedrive (vinumstate.c:87-89) and the vinum daemon runs close_locked_drive() -> vn_close(drive->vp); drive->vp = NULL (vinumio.c:225-227) with no synchronization against the request completion path. Any group write that was built while the drive was up (or whose states were administratively restored) then faults: vn_strategy() dereferences vp immediately (vfs_bio.c:3368/3375).

Teardown can be triggered by: - complete_rqe()'s own ENXIO error branch (vinuminterrupt.c:129-139) force-downs the drive and then, IN THE SAME CALL, continues into complete_raid5_write() which launches more vn_strategy()s (race vs the daemon's vn_close -> closed-vnode use, or NULL deref); - administrative vinum stop -f <drive> racing in-flight group writes; - state-poke recovery (vinum setstate -f up) after a stop, which restores drive/sd/plex states without re-opening vp.

GUEST REPRODUCTION (deterministic, no fault injection needed) 1. sh setup_vinum.sh (2 md drives, 3-sd RAID-5 "rv", 2 sds on d2) 2. vinum stop -f d2 (real set_drive_state down: daemon vn_close's vp; sds cascade to crashed) 3. sleep 3 4. vinum setstate -f up d2; vinum setstate -f up rv.p0.s0 rv.p0.s1 \ rv.p0.s2 rv.p0 rv (state pokes; vp stays NULL) 5. dd of=/dev/vinum/vol/rv bs=32768 seek=5 count=1 (single group write)

Expected (unpatched): first-phase reads complete via the still-valid cdev, then complete_raid5_write() calls vn_strategy(NULL): Fatal trap 12: page fault while in kernel mode fault virtual address = 0x5c Stopped at vn_strategy+0x38: testb $0x40,0x5c(%r13) (vfs_bio.c:3375 (vp->v_flag & VKVABIO) == 0 β€” NULL vp deref.)

Observed on guest DragonFly 6.5-DEVELOPMENT (x86_64), 2026-09-24: see panic.txt (full serial-console capture), run.log (session transcript).

IMPACT Privileged local kernel DoS (guaranteed panic) once vp is torn down or raced; the mid-flight variant (vn_strategy on a vnode concurrently vn_close'd by the daemon) is a vnode use-after-close -> potential memory corruption. Trigger requires vinum administrative control (root) or a genuine component ENXIO failure, hence severity Medium.

FIX (fix.diff) Launch the second phase exactly like the first phase β€” via the cdev the code already stores in bio_driver_info at :404/:440: - vn_strategy(drive->vp, &rqe->b.b_bio1); + dev_dstrategy(rqe->b.b_bio1.bio_driver_info, &rqe->b.b_bio1); (both sites). dev_dstrategy() on DRIVE[].dev is the proven first-phase mechanism and is unaffected by drive->vp teardown. NOTE: sdio() in vinumrequest.c:980 uses the same vn_strategy(drive->vp) pattern and needs the same treatment (out of scope for this diff).

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

Rebuilt kernel with fix.diff: identical deterministic trigger no longer panics; second-phase writes verifiably reach the raw device through the cdev route (pattern 0xCC at md1s0 sectors 425 and 4521); 32KB RAID-5 roundtrip still correct. Bug considered fixed by this diff.

run.patched.log, fix_build.log, fix.diff
↓ fix.diffper-fix-DF-4895

Confirmed kernel references

Detail

Exploit chain

['root: vinum stop -f d2 -> set_drive_state(down, force) -> queue daemonrq_closedrive -> daemon vn_close(drive->vp), vp=NULL (or, in the ENXIO error path, complete_rqe:129-139 does this and immediately continues into complete_raid5_write in the same call)', 'root: vinum setstate -f up d2 + sds/plex/vol (setstate_by_force pokes states; vp stays NULL) β€” mirrors any admin recovery that does not re-init the drive', 'any volume write: bre5 builds normal RMW group (sd states up); launch_requests fires first-phase reads via cdev (valid) -> complete_rqe -> complete_raid5_write -> vn_strategy(NULL) -> fatal trap 12', 'mid-flight variant (no state pokes needed): group write built while drive up, daemon vn_close lands between first and second phase -> strategy on closing/closed vnode -> vnode use-after-close (memory-corruption potential)']

Evidence (decisive lines)

panic.txt (serial console: 'Fatal trap 12 ... fault virtual address = 0x5c ... Stopped at vn_strategy+0x38: testb $0x40,0x5c(%r13)', process dd); run.log (baseline transcript incl. offset-mapping proof 0xAA->md1s0:425/4521); run.patched.log (fix validation: no panic, 0xCC lands at both offsets, ROUNDTRIP OK); fix_build.log; fix.diff

PoC changes

Trigger developed during verification (no seed). v1 (flap racer: writers + setstate -f down/up loop) failed: CLI 'vinum setstate -f' goes to setstate_by_force() which pokes DRIVE[].state directly and never calls set_drive_state/never queues the daemon close β€” proven live (writes kept landing on the 'down' drive). v2 ('vinum stop d2' non-forced) refused EBUSY: opencount (attached-subdisk count, never decremented) is always nonzero for a drive hosting sds. v3 (final): 'vinum stop -f d2' is the only CLI route to set_drive_state(down, force) -> daemon really vn_close's vp (state cascade visible in dmesg) -> deterministic panic on the next group write after state pokes.

Verified recommended fix

Launch second-phase RAID-5 I/O via dev_dstrategy(rqe->b.b_bio1.bio_driver_info=cdev) exactly like the first phase, at vinuminterrupt.c:427 and :464 (see fix.diff); apply the same to sdio() in vinumrequest.c:980.

Verdict

Deterministic privileged-local kernel panic reproduced 1/1 on the QEMU guest: complete_raid5_write() launches RAID-5 second-phase I/O via vn_strategy(drive->vp) (vinuminterrupt.c:427/:464) while drive->vp can be NULL (daemon close_locked_drive after set_drive_state down) or mid-vn_close (vnode UAF window). First-phase I/O survives the same teardown because it uses the cdev (dev_dstrategy, vinumrequest.c:436); only the second phase is vp-coupled. Trigger: vinum stop -f d2 (real teardown), state pokes back up, one 32KB group write -> Fatal trap 12 at vn_strategy+0x38 (testb $0x40,0x5c(%r13) = vp->v_flag deref of NULL), current process dd. fix.diff (dev_dstrategy via the already-set bio_driver_info at both sites) validated on a rebuilt kernel: identical trigger completes cleanly, second-phase data verifiably lands on the raw device through the cdev route, RAID-5 roundtrip intact.