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)
PoC verification
Evidence pack
findings/poc/DF-4895 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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)
sh ./setup_vinum.shβ 2 md drives, 3-sd RAID-5 volumerv, sanity 32KB roundtrip OK (proves vp path works when up).vinum stop -f d2β real down: daemonvn_closes vp, sds cascade crashed (dmesg shows the state cascade).sleep 3vinum 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.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.
Related
sdio() (vinumrequest.c:980) uses the same vn_strategy(drive->vp)
pattern for raw-subdisk I/O and needs the same fix (different file).
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
fixedRebuilt 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
Confirmed kernel references
- sys/dev/raid/vinum/vinuminterrupt.c:427
- sys/dev/raid/vinum/vinuminterrupt.c:464
- sys/dev/raid/vinum/vinuminterrupt.c:194
- sys/dev/raid/vinum/vinuminterrupt.c:129
- sys/dev/raid/vinum/vinumstate.c:87
- sys/dev/raid/vinum/vinumstate.c:942
- sys/dev/raid/vinum/vinumio.c:225
- sys/dev/raid/vinum/vinumrequest.c:436
- sys/kern/vfs_bio.c:3375
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.
No comments yet.