⬢ DragonFlyBSD Kernel Audit
DF-4473 / verdict.json
← back to finding ↓ download raw
{
  "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)"
}