DF-5037 / verdict.json
{ "finding_id": "DF-5037", "status": "reproduced", "reproduced": true, "impact": "dos", "confidence": "certain", "verdict": "dm_pdev_insert() holds the global dm_pdev_mutex EXCLUSIVE across vn_open()/VOP_GETATTR()/dev_dioctl() (dm_pdev.c:128-182); with dm-on-dm stacking vn_open re-enters dm via dmopen()->dm_dev_lookup() and needs dm_dev_mutex, while dm_dev_remove()/dm_dev_remove_all() hold dm_dev_mutex across disable_dev()s ref-drain cv_wait and across dm_table_destroy()->target dtor->dm_pdev_decr()->dm_pdev_mutex: a textbook AB-BA inversion, root-triggerable with two ioctls (reload with a dm backing vs remove/remove_all). Reproduced on the stock guest: both workers froze in D state on the complementary locks (wchan dmdevlis vs dm_dev) for 84+ seconds of captured snapshots, all dm ioctls (dmsetup canaries) wedged behind dm_dev_mutex, unkillable, reboot required. Fixed by narrowing dm_pdev_mutex to lookup/insert (re-check after vn_open, loser closes its vnode) and by moving the ref-drain wait and device destruction out of dm_dev_mutex in dm_dev.c; the rebuilt kernel survived the identical duel (two runs, ~4.5k ioctls/s collision rate) with zero wedges and correct dm-on-dm I/O.", "exploit_chain": "root: dmsetup-equivalent ioctls on /dev/mapper/control. (1) create pbase + load linear table over a raw disk + RESUME it (resume is required: until dm_dev_resume_ioctl->dmsetdiskinfo initializes the disk info the disk layer rejects opens of the node with ENXIO). (2) create stk. (3) P1: hammer reload(stk) with a linear table backed by /dev/mapper/pbase \u2014 each reload holds a dm_dev busy ref on stk for the whole ioctl and inside it takes dm_dev_mutex twice via vn_close(pbase)->dmclose->dm_dev_lookup and vn_open(pbase)->dmopen->dm_dev_lookup. (4) P2: hammer remove(stk) \u2014 dm_dev_remove_ioctl unbusies then dm_dev_remove takes dm_dev_mutex and disable_dev cv_waits for ref_cnt==0 still holding dm_dev_mutex. Interleave: P2 sleeps holding dm_dev_mutex waiting for P1s busy ref; P1 sleeps in dmclose/dmopen->dm_dev_lookup waiting for dm_dev_mutex. Permanent kernel deadlock of the whole dm subsystem; any I/O to any dm device hangs; unkillable D-state threads; reboot required.", "evidence": "run.log (full serial console of the wedged run: 42 consecutive 2s ps snapshots with 886 D0 dmdevlis dm_abba / 887 D3 dm_dev dm_abba / two wedged dmsetup canaries); ps_detail.txt (process tree incl. the parents wedged deadlock-detector canary); run_patched.log (fix validation: counters race, clean exits, dm-on-dm dd I/O + removes OK on the patched kernel); panics_observed.txt (pre-empting known-bug manifestations on the same surface: DF-4474 leak->malloc-limit panic, DF-2443/2447-adjacent devfs sysref/devstat panics, ENXIO-until-resume gotcha); VERDICT.md (full narrative with path:line lock-order proof).", "kernel_refs": [ "sys/dev/disk/dm/dm_pdev.c:128", "sys/dev/disk/dm/dm_pdev.c:143", "sys/dev/disk/dm/dm_pdev.c:168", "sys/dev/disk/dm/dm_pdev.c:249", "sys/dev/disk/dm/dm_dev.c:105", "sys/dev/disk/dm/dm_dev.c:66", "sys/dev/disk/dm/dm_dev.c:308", "sys/dev/disk/dm/dm_dev.c:326", "sys/dev/disk/dm/device-mapper.c:200", "sys/dev/disk/dm/dm_ioctl.c:711", "sys/dev/disk/dm/dm_table.c:136", "sys/dev/disk/dm/linear/dm_target_linear.c:67" ], "poc_changes": "Rewrote the seeded approach entirely into a raw NETBSD_DM_IOCTL proplib client: struct plistref mirror (guest lacks sys/plistref.h), prop_dictionary_sendrecv_ioctl(dict, fd, cmd, &out) argument order, version array [4,16,0]. Three decisive discoveries vs the naive PoC: (1) the guests remove-hammer trips the known DF-2443/2447 remove-path UAF (devstat_remove_entry NULL deref) unless remove/recreate is serialized with pacing; (2) kHz churn whose reloads fail ENOENT feeds the known DF-4474 prop leak into the per-malloc-type limit (RAM/10) and panics the guest in ~16-100s; (3) a dm node is unopenable (ENXIO from the disk layer) until dm_dev_resume_ioctl->dmsetdiskinfo runs, so the backing dm device MUST be create+load+resumed or every stacked pdev vn_open fails \u2014 which silently turns the deadlock PoC into (2). With those tamed, the deterministic duel (reload-vs-remove of a stacked device) froze the kernel on the first properly configured run.", "attempts": 8, "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 (fix-validation kernel: #1: Sat Sep 26 08:14:53 UTC 2026)", "runtime_sec": 5400, "guest_dirty": 0, "build_cmd": "in-guest: cc -O2 -Wall -I/tmp/propinc -o dm_abba dm_abba.c -lprop -lpthread (proplib headers shipped from sys/libprop); fix kernel: make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel KERNCONF=X86_64_GENERIC", "run_cmd": "nohup /tmp/guest_duel.sh 90 (duel: ./dm_abba duel /dev/vbd0 90 with console ps-watcher + dmsetup canary loops)", "code_hash": "0bf447097299f8c730e5269848b7b19df3eecfc9042f75eb2cbc173799c92a38", "notes": "Privilege bar: /dev/mapper/control is 0640 root:operator (group read-only), so all dm ioctls require root \u2014 same bar as the other dm-family findings (DF-2450 Medium precedent), hence Medium. First-ever manifestation (before instrumentation) additionally blocked new ssh sessions (devfs/pty cascade), i.e. impact can exceed the dm subsystem. Pre-empting manifestations on the same surface corroborate known findings DF-4474 and DF-2443/DF-2447 (panics_observed.txt) and include a devfs sysref_get-on-PUTAWAY assertion panic (reference_dev via v_associate_rdev during devfs nresolve racing destroy_dev) worth tracking under the dm_dev remove-lifecycle family. The ENXIO-until-resume behavior of unresumed dm nodes is a functional quirk every raw-ioctl dm reproducer must know.", "recommended_fix": "Do not hold dm_pdev_mutex across vn_open/VOP_GETATTR/dev_dioctl in dm_pdev_insert (lookup/insert under lock only, re-check after open, loser closes its vnode), and do not hold dm_dev_mutex across the ref-drain cv_wait or device destruction in dm_dev_remove/dm_dev_remove_all (wait/destroy outside the lock).", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "Applied fix.diff to the guests /usr/src, rebuilt (make nativekernel) and installed the kernel, rebooted, re-ran the identical duel twice (90s + 60s at ~4.5k ioctls/s collision rate): no deadlock, no freeze, workers exit cleanly, dm-on-dm create/dd-I/O/remove all functional. Baseline (unpatched) wedged permanently within one 90s run (42 frozen ps snapshots in run.log).", "fix_evidence": "run_patched.log; VERDICT.md Fix and validation section; fix.diff" } |