DF-4907 / verdict.json
{ "finding_id": "DF-4907", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "dm_target_delay _destroy() (dm_target_delay.c:341) drains its bio queue via _submit_queue(di,1), which asynchronously vn_strategy()s every queued bio and returns; dm teardown then frees the dm_dev while those bios completion path (nestiobuf_iodone -> nestiobuf_done -> devstat_end_transaction_buf, vfs_bio.c:4505) still writes through the strategy-time-captured &dmv->stats. Verified deterministically by stacking delay targets (d2<-d1<-d0<-vn0): removing the never-opened middle device 0.2s after bios enter its queue frees dmv1 while the bios sit in d0s 1s queue; spraying dm devices reallocates the chunk; the late completion then corrupts the live replacement devices devstat. Serial console 2/2 fresh-boot baseline runs: devstat_end_transaction: HELP!! busy_count for f00 is < 0 (-1)! (naming the sprayed filler that reused the freed chunk - cross-object write-after-free proof) followed by panic: bio_track_rel: bad count in biodone. Impact recorded as panic (the demonstrated primitive is a repeatable write-after-free into a chosen-size reallocated kernel object with semi-controlled values: bytes_read += b_bcount, num_reads++, timestamps, busy_count--; exploit-grade heap corruption root-gated by /dev/mapper/control).", "exploit_chain": "root: dmsetup create d0=delay(vn0,1000ms), d1=delay(d0,1000ms), d2=delay(d1,1000ms) -> 48 threads pread /dev/mapper/d2 (bios reach d1s queue at t0+1.0) -> suspend/reload/resume d2 (releases d1 from pdev-hold so d1.is_open=0) -> dmsetup remove d1 at t0+1.2 (drain submits bios into d0s queue, dm_dev_free(dmv1) microseconds later) -> spray dmsetup create f0..f7 (f0 reuses the freed dm_dev chunk) -> t0+2.2 the orphaned bios complete and devstat_end_transaction writes into the LIVE f0 device -> HELP!! line + secondary bio_track_rel panic.", "evidence": "panic.txt:311-312 and panic.2.baseline.txt:222-223 (HELP!! busy_count for f00 < 0 then panic, identical on two fresh boots); fix_run.log (patched module, 5x96 readers, RC=0, HELP counts 0, guest up); VERDICT.md (full path:line analysis); df4907.c (trigger).", "kernel_refs": [ "sys/dev/disk/dm/delay/dm_target_delay.c:341", "sys/dev/disk/dm/delay/dm_target_delay.c:267", "sys/dev/disk/dm/device-mapper.c:425", "sys/kern/vfs_bio.c:4505", "sys/kern/subr_devstat.c:179", "sys/dev/disk/dm/dm_dev.c:384" ], "poc_changes": "Seed design (from scratch this run): v1 single-layer remove hit dms is_open EBUSY because blocked preads hold the vnode; v2 extra-fd close also fails (dmclose only on last vnode ref); v3 stacks the removed device under a user-facing d2 and uses suspend/reload/resume on d2 to drop d1s pdev-hold before remove, and sprays dm devices to make the UAF hit a live victim.", "attempts": 5, "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 df4907 df4907.c -lpthread", "run_cmd": "sh run.sh 2 48", "code_hash": "013c505797f0b9daa7914f05d10e9c6f048b4ccab6ca11f576a3202f473b4019", "notes": "Delay-over-delay widens the free-vs-completion window to ~1s (vs one disk latency for the generic dm teardown race), making the UAF deterministic. The generic root cause (dm_dev_destroy frees dmv without waiting for in-flight bios whose completion path uses &dmv->stats) affects all targets; this pack fixes the delay-local aggravator. Severity Medium: gated on root access to /dev/mapper/control.", "recommended_fix": "_submit_queue: in the submit_all (teardown) path, fail queued bios with ENXIO (b_error/b_resid/biodone) instead of submitting them asynchronously past the targets lifetime", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 + dm_target_delay_fixed.ko (fix.diff applied to /usr/src copy, module rebuilt in guest)", "fix_verdict": "fix.diff applied to the guest /usr/src copy, module rebuilt (fix_build.log) and loaded as dm_target_delay_fixed.ko; the exact PoC (2x48 and stress 5x96) runs clean: RC=0, no HELP message, no panic, guest alive. Baseline stock module panicked on 2/2 fresh-boot runs. Bad behavior gone.", "fix_evidence": "fix_build.log (patch + module build, untrimmed); fix_run.log (patched runs 2x48 and 5x96 with RC=0 HELP_before=0 HELP_after=0 and uptime confirming guest alive); panic.txt/panic.2.baseline.txt (baseline 2/2 panics)" } |