DF-4473 / verdict.json
{ "finding_id": "DF-4473", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "dm_table_load_ioctl is a writer that runs under the SHARED table_mtx taken by dm_table_get_entry (dm_table.c:65), so concurrent reload ioctls append to the same inactive-table TAILQ simultaneously (TAILQ_INSERT_TAIL at dm_ioctl.c:774; queue.h:628 is four plain stores). The conditional destroy at :722-723 normally serializes loads, but it is skipped when DM_INACTIVE_PRESENT_FLAG is clear -- exactly after clear/resume. PoC alternates clear <-> 6 barrier-synchronized 16-entry reloads; the racing inserts tore the tail and the QUEUEDEBUG invariant panicked the kernel inside dm_table_load_ioctl+0x6bf (panic: Bad tailq NEXT(->tqh_last) != NULL); guest required reset. Without QUEUEDEBUG the same interleaving writes through a stale tqh_last and orphans entries (permanent M_DM + dm_target refcount + pdev leaks). Ceiling: uncontrolled kernel list corruption (panic / leak) from racing ioctls by an operator-group user; no controlled write demonstrated. Fix (serialize loads on dmv->dev_mtx EXCLUSIVE + insert entries only after dm_table_init completes) validated: 400/400 rounds, 2400/2400 reloads OK, no panic, no residual dm malloc objects after cleanup.", "exploit_chain": "", "evidence": "run.log (baseline panic trace; patched clean run round 400/400 (ok=2400 err=0), remove_all ok, no residual M_DM); panic.txt (panic: Bad tailq NEXT(0xfffff801173eebf8->tqh_last) != NULL, last 0xfffff80117492080; dm_table_load_ioctl+0x6bf x2)", "kernel_refs": [ "sys/dev/disk/dm/dm_ioctl.c:722-726", "sys/dev/disk/dm/dm_ioctl.c:774", "sys/dev/disk/dm/dm_table.c:59-75", "sys/sys/queue.h:628-634" ], "poc_changes": "Authored fresh (pass-2 finding, no seed). Barrier-synchronized worker threads + per-round clear to reopen the race window (flag must be clear for loads to skip the serializing destroy); M_DM before/after accounting via vmstat -m. One harness bug fixed during validation: unchecked pthread_create failure starved the barrier (userspace wedge, not kernel) -- now aborts loudly.", "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": 2400, "guest_dirty": 0, "build_cmd": "cc -o dm_loadrace dm_loadrace.c -lprop -lpthread", "run_cmd": "./dm_loadrace 6 400 16", "code_hash": "1234353f6cf8a75ddf4a47837711d406793dcd70fcfcdca5eb46e94b14859d61", "notes": "Distinct from known DF-2447 (dmv UAF after unbusy), DF-4466 (dm-crypt async completion vs table destroy) and DF-1640 (params string double-free): this is ioctl-vs-ioctl list mutation under a wrong lock mode. Related hardening (not re-filed per DF-4466 context): suspend sets DM_SUSPEND_FLAG without draining in-flight IO; resume destroys tables without waiting for async target completions -- dm_ioctl-side drain-on-resume remains independently warranted.", "recommended_fix": "Hold dmv->dev_mtx (LK_EXCLUSIVE) across the table-load mutation phase and insert each entry only after dm_table_init succeeds, so concurrent loads cannot race TAILQ_INSERT_TAIL and readers never see half-initialized entries.", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Baseline: panic Bad tailq NEXT inside dm_table_load_ioctl mid-race, guest down. Patched (dm.ko rebuilt from fix.diff, -Werror clean): 400/400 rounds, ok=2400 err=0, no panic, guest healthy, zero residual dm-malloc objects after remove_all (no orphans).", "fix_evidence": "run.log fix-validation section (/tmp/race_fixed.log contents); fix.diff (dev_mtx serialization + insert-after-init hunks)" } |