DF-4800 / verdict.json
{ "finding_id": "DF-4800", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "Concurrent revive_block() calls race sd->revived (read at vinumrevive.c:89, advanced at :187 across two biowaits, no lock on the whole VINUM_SETSTATE->start_object path): both callers copy the same block and both advance the cursor, so the block between the advances is silently skipped; set_sd_state(sd_up) at :190 then declares the subdisk up with never-revived content. Reproduced 3/3 on async (virtio) backing with 152/152/153 corrupted 4k blocks of 1536 persisting after a clean single-reviver finish (~620 KB of silent stale data served by an up mirror; no error logged). On md (malloc) backing the non-MPSAFE vinum ioctls mplock plus inline completions serialize the callers by accident - real async disks release the mplock in biowait and the race becomes the default. Fix (per-sd revive_busy cmpset interlock returning EBUSY to the second caller) validated on the same guest: second racer refused at ioctl 1, survivor copies every block, data windows identical.", "exploit_chain": "root: mirrored volume on async-backed drives, sd stale, TWO concurrent vinum start (or one VINUM_SETSTATE(object_up) ioctl racing the revive) -> both read cursor R, both copy [R,R+N), both advance -> [R+N,R+2N) skipped -> sd up with holes -> unprivileged readers of a mounted filesystem silently receive the pre-revive content; concurrent completion also races set_sd_state/save_config on the shared config", "evidence": "run.log/run.2.log/run.3.log (stock: total corrupted 4k blocks: 152/152/153 of 1536, state lists plexes up); fix_run.log (patched: child 907: 1 ioctls, final error=16 + DATA WINDOWS IDENTICAL)", "kernel_refs": [ "sys/dev/raid/vinum/vinumrevive.c:89", "sys/dev/raid/vinum/vinumrevive.c:187", "sys/dev/raid/vinum/vinumrevive.c:190", "sys/dev/raid/vinum/vinumstate.c:880", "sys/dev/raid/vinum/vinumioctl.c:230" ], "poc_changes": "wrote racer.c (N tight-loop VINUM_SETSTATE(object_up,blocksize) drivers) after discovering userland vinum start sleeps between ioctls (revive interval) which hides the race; async virtio backing required because md inline completions keep the non-MPSAFE ioctls mplock held end-to-end", "attempts": 3, "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": 1500, "guest_dirty": 0, "build_cmd": "cc -O -o /tmp/racer /tmp/racer.c (fix: cd /usr/src/sys/dev/raid/vinum && make obj && make, module)", "run_cmd": "sh setup_async.sh && sh run.sh (2 racers x 150 ioctls + single-reviver finish + data-window compare)", "code_hash": "f338d6256cc9c2238293af35fb3845b911c001bccf2e263b8319be7c5c4c2c8a", "notes": "corruption impact=none in the memory-safety sense; classified dos/data-integrity. md-only guests cannot reproduce (mplock+inline serialization) - use async backing", "recommended_fix": "per-sd revive_busy atomic interlock in revive_block(): second concurrent caller gets EBUSY (fix.diff, validated)", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "patched module: second racer refused with EBUSY on its first ioctl; surviving racer advanced the cursor exactly once per block; after the single-reviver finish the two plex data windows were byte-identical (fix_run.log) vs 152-153 corrupted 4k blocks on stock in 3/3 runs", "fix_evidence": "fix_run.log" } |