DF-4751 / verdict.json
{ "finding_id": "DF-4751", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Reproduced 4/4 on the QEMU guest (2 baseline + 2 with patched modules): a root-driven create/remove churn on /dev/mapper/control racing `kldunload dm` reliably panics the kernel with Fatal trap 12 executing freed module text (signatures: dev_dioctl+0x1b read of freed ops; IP=0x0 call through zeroed ops; freed-text exec at 0xffffffff82601029; dmioctl+0x2bf inside the freed handler with the sync_devs()-augmented fix installed). Root cause is two-fold: (1) dm_modcmd(MOD_UNLOAD) reads dm_dev_counter unlocked (device-mapper.c:161, race window here) vs dm_dev_insert()s locked increment (dm_dev.c:193) \u2014 every crash log shows malloc_uninit: 1984 bytes of dm still allocated proving devices outlived the check; (2) framework-level: dev_dioctl dispatches through dev->si_ops with no ops/module reference (kern_device.c:258) and destroy_devs dead_dev_ops revector is asynchronous (devfs_core.c:759), so an ioctl already executing dmioctl survives into freed text. Trigger requires root (kldunload) on one side; ioctl side alone is operator-group reachable \u2014 hence Low severity, panic impact.", "exploit_chain": "No unprivileged chain: kldunload requires root. Ceiling is a code-execution-class UAF (execution continues in freed kld text) groomed by root against itself; treated as correctness + framework hardening (gap affects every loadable driver, incl. nmdm.ko).", "evidence": "panic.txt (all four Fatal trap 12 signatures + malloc_uninit lines); run.log (40 benign unloads then crash); fix_build.log (patched dm.ko build); VERDICT.md fix-validation section (v1 destroy_dev+re-arm and v2 +sync_devs both still panic)", "kernel_refs": [ "sys/dev/disk/dm/device-mapper.c:154", "sys/dev/disk/dm/device-mapper.c:161", "sys/dev/disk/dm/device-mapper.c:163", "sys/dev/disk/dm/device-mapper.c:190", "sys/dev/disk/dm/device-mapper.c:241", "sys/dev/disk/dm/dm_dev.c:193", "sys/kern/kern_device.c:237", "sys/kern/kern_device.c:258", "sys/vfs/devfs/devfs_core.c:759" ], "poc_changes": "Harness adapted from the DF-4473 proplib client (create/remove only, no targets); header comment de-nested to compile; include order fixed (sys/types.h first).", "attempts": 4, "guest_uname": "DragonFly dfbsd 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": 480, "guest_dirty": 1, "build_cmd": "cc -O -Wall -o /tmp/dm_unloadrace dm_unloadrace.c -lprop -lpthread", "run_cmd": "kldload dm; /tmp/dm_unloadrace 30", "code_hash": "61dc9d24e54ca4e979c860cfda7744e30e87a1a7d8d510dd2791e5cd3fcfc653", "notes": "40+ unloads typically succeed benignly before the interleaving hits; every crashing run also printed the counter-race corroboration (malloc_uninit dm bytes). Guest reset with-src performed after the runs.", "recommended_fix": "Framework: reference dev_ops (and its module) across every dev_d* dispatch or make the revector synchronous + kldunload respect per-open-cdev module refs; driver-side shrink: fix.diff (destroy+sync_devs+stable counter+re-arm) plus an in-flight-ioctl drain that MOD_UNLOAD waits on.", "fix_status": "fix_failed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 1, "fix_verdict": "Two in-driver fixes were built as dm.ko and A/B-tested with the identical PoC: v1 (destroy_dev before counter check + re-arm on counter>0) still panicked (run 3, freed-text exec); v2 (v1 + sync_devs() per the kern_conf.c unload contract) still panicked INSIDE dmioctl (run 4, dmioctl+0x2bf) because sync_devs only drains the devfs message queue, not calls that already loaded si_ops->d_ioctl. The window is a dev-dispatch framework gap; fix.diff (v2) is kept as a documented best-effort shrink, NOT a complete fix.", "fix_evidence": "panic.txt runs 3 and 4; fix_build.log; VERDICT.md Why the in-driver fixes failed" } |