DF-4801 / verdict.json
{ "finding_id": "DF-4801", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "sd->waitlist is drained ONLY from the tail of a successful revive_block() (vinumrevive.c:198-218; the sole drain site in the tree). The revive is driven one block per ioctl by userland vinum start; when that process stops (killed, SIGSTOP, crash), conflicting writes queued on the waitlist (vinumrequest.c:327-355) sleep in PCATCH-less biowait forever. Reproduced deterministically on the stock kernel: one VINUM_SETSTATE(object_up) ioctl primes sd_reviving with the cursor at 64k; four writers straddling [64k,96k) all queue; all four freeze in D-state, survive kill -9 of writers and dd children, and REMAIN hung after an explicit vinum setstate -f down revive abort (run.log). The queued struct requests and buffers leak until reboot. With a filesystem mounted on the volume, the victims are unprivileged writers. Fix (drain_sd_waitlist() hooked into set_sd_state() on every real transition out of sd_reviving: relaunch if the sd came up, biodone(EIO)+freerq otherwise) validated on the same guest: post-abort ps shows no hung writers (fix_run.log) vs stocks four immortal D-state dds.", "exploit_chain": "root: mirrored volume + revive started then stopped (kill the vinum start process mid-revive, or let one VINUM_SETSTATE(object_up) ioctl prime sd_reviving and never drive it again); unprivileged: any write straddling the frozen cursor through a mounted filesystem queues on sd->waitlist -> uninterruptible biowait -> permanent unkillable hang, survives kill -9 and revive abort, only reboot clears it", "evidence": "run.log (stock: 4/4 writers D-state, >>> UNKILLABLE HUNG WRITERS <<< after kill -9 and >>> STILL HUNG AFTER ABORT <<< after setstate -f down); fix_run.log (patched: writers gone (drained: fix working))", "kernel_refs": [ "sys/dev/raid/vinum/vinumrevive.c:198", "sys/dev/raid/vinum/vinumrevive.c:216", "sys/dev/raid/vinum/vinumrequest.c:327", "sys/dev/raid/vinum/vinumstate.c:286" ], "poc_changes": "wrote prime.c (single-ioctl revive primer) because userland vinum setstate -f up loops the ioctl internally until the revive completes; writers restricted to the straddle window [64k,96k) so the trigger never touches the REQUEST_DOWN/trailing-BAD launch path (DF-4721 arm) and shows the hang deterministically", "attempts": 2, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 900, "guest_dirty": 0, "build_cmd": "cc -O -o /tmp/prime /tmp/prime.c (fix: module rebuild with fix.diff stacked on DF-4482 v2)", "run_cmd": "sh hang.sh (setup + prime + 4 straddle writers + kill -9 tests + setstate -f down abort test)", "code_hash": "ee4df2159fe26a239b37cb150056131dfcac5b974c8e0e98b0a84c436dc182c2", "notes": "deterministic trigger; no race required. The waitlist also leaks the queued requests (kernleak aspect). Fix stacks on the DF-4482 v2 fix (cmpset head-push append + steal drain make the new drain race-safe)", "recommended_fix": "drain_sd_waitlist() on every set_sd_state() transition out of sd_reviving: relaunch queued requests when the sd comes up, biodone(EIO)+freerq otherwise (fix.diff, validated)", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "patched module: identical workload, writers queue as before while the revive is frozen, and the setstate -f down abort now drains them - post-abort ps shows no dd/writers left (fix_run.log), vs stock where all four remained D-state indefinitely", "fix_evidence": "fix_run.log" } |