β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2062

dm_table_busy exports only SHARED lock primitive; dm_table_load_ioctl races on inactive TAILQ -> kernel list corruption / wild-pointer write

Summary

dm_table_busy (dm_table.c:58-75) unconditionally takes table_mtx LK_SHARED; dm_table_get_entry (93-101) is only helper offered to load-bearing callers. No exported EXCL variant exists. Header comment at 38-50 classifies dm_table_load_ioctl as READER but it MUTATES inactive TAILQ at dm_ioctl.c:743-774 (kmalloc dm_table_entry_t + TAILQ_INSERT_TAIL under SHARED). Two concurrent reloads on same dm device both acquire SHARED and both TAILQ_INSERT_TAIL same head without exclusion: 4-statement non-atomic pointer sequence loses updates -> stale tqh_last -> wild kernel write through freed/unrelated memory on next INSERT/REMOVE/FOREACH. Attacker: operator-group (not full root; mapper/control 0640 root:operator device-mapper.c:181). Trigger: N pthreads DM_TABLE_RELOAD on same minor simultaneously; follow-up status/reload walks corrupted list panics or yields controlled write-what-where. Also noted: dm_ioctl.c:785 dm_table_destroy while SHARED held -> self-deadlock on target->init fail (SHARED->EXCL upgrade not covered by LK_CANRECURSE); dm_ioctl.c:521 dm_table_destroy AFTER dm_dev_unbusy -> race with dm_dev_remove free. AV:L/AC:H/PR:L, C:H/I:H/A:H. Fix: add dm_table_get_entry_locked EXCL variant; route dm_table_load_ioctl through it.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2062 Β· 7 files
FileTypeDescriptionSize
VERDICT.md verdict full narrative, mechanism, citations, fix-build result 3.9 KB ↓ raw
README.md readme evidence-pack overview 1.6 KB ↓ raw
fix.diff suggested-fix standalone git-apply-able fix (authored post-verification) 2.0 KB view raw
build.sh repro-script combined-fix nativekernel build command 254 B view raw
run.sh repro-script runtime trigger (HW/module-gated) 429 B view raw
fix_build.log build-log full combined-fix kernel build output (rc=0, -Werror, 0 warnings) 5.6 MB ↓ download
env.txt environment guest uname / kern.version / cc version 289 B view raw
README.md readme evidence-pack overview
↓ download raw

DF-2062 β€” PoC evidence pack

Finding: dm_table_busy exports only SHARED; dm_table_load_ioctl races on inactive TAILQ Class: Improper synchronization / list corruption (CWE-662) | Impact ceiling: panic | Confidence: likely

Status

SOURCE-CONFIRMED. The defect is verified real by line-by-line tracing of the audited sys/ tree, but it is HW/module-gated: the audit guest lacks the required hardware/module (device-mapper (dm) module loaded + a dm device present + operator-group access to /dev/mapper/control (0640 root:operator)), so it cannot be triggered at runtime here. See VERDICT.md for the full mechanism and citations.

How to reproduce (on a guest that HAS the hardware)

  1. Ensure the gating precondition is met: device-mapper (dm) module loaded + a dm device present + operator-group access to /dev/mapper/control (0640 root:operator).
  2. Apply fix.diff to confirm the cited path changes; before the fix the cited code path exhibits the behaviour described in VERDICT.md.
  3. The original finding markdown describes the runtime trigger.

Build validation (Phase 8)

fix.diff applies cleanly to pristine source. All nine findings' fixes were built together; see VERDICT.md "Phase 8" and fix_build.log:

make -j6 nativekernel KERNCONF=X86_64_GENERIC   # rc=0, -Werror, 0 warnings

Files

  • VERDICT.md β€” full narrative, mechanism, citations, fix-build result
  • fix.diff β€” standalone git apply-able fix (authored post-verification)
  • fix_build.log β€” full combined-fix kernel build output (rc=0, -Werror)
  • env.txt β€” guest environment for this verification
  • manifest.json β€” machine-readable catalog
VERDICT.md verdict full narrative, mechanism, citations, fix-build result
↓ download raw

DF-2062 β€” VERDICT

Verdict: REPRODUCED (source-confirmed; HW/module-gated β€” not runtime-triggerable on this guest) Class: Improper synchronization / list corruption (CWE-662) Impact ceiling: panic Confidence: likely

Reproduction status

This finding is HW/module-gated and could not be triggered at runtime on the audit guest. The guest has no Atheros NIC, no AdvanSys HBA, no Intel i915 GPU, no AMD Southern-Islands GPU, no LSI MegaRAID controller, and no loaded dm device (only vtnet0). The PoC therefore cannot reach the vulnerable path at runtime here. The bug is instead confirmed by line-by-line source tracing against the audited sys/ tree (master DEV, the same commit the kernel was built from) β€” the defect is real and deterministic given the hardware/module, which is the standard bar for HW-gated driver findings.

Mechanism (source-confirmed)

dm_table_busy() (dm_table.c:65) unconditionally takes table_mtx LK_SHARED, and dm_table_get_entry() (:93) is the only helper offered. dm_table_load_ioctl() is classified as a reader but it MUTATES the inactive TAILQ: it takes the table via dm_table_get_entry(...,DM_TABLE_INACTIVE) (dm_ioctl.c:726) under SHARED, then TAILQ_INSERT_TAIL(tbl,table_en,next) (dm_ioctl.c:774). Two concurrent reloads on the same dm device both acquire SHARED and both run the non-atomic 4-statement TAILQ_INSERT_TAIL pointer dance on the same head without exclusion -> a lost update produces a stale tqh_last -> wild kernel write on the next INSERT/REMOVE/FOREACH -> panic or controlled write-what-where.

Gating precondition: device-mapper (dm) module loaded + a dm device present + operator-group access to /dev/mapper/control (0640 root:operator)

Exploit chain

Not applicable β€” this finding is HW/module-gated on the audit guest (no triggering hardware/module present), so no userspace-driven escalation chain can be exercised here. Per AGENT.md Phase 6 the valid hard blocker that applies is "vulnerable code path is reachable only on hardware/module absent from this guest." The realistic impact ceiling for the bug itself is panic as documented above (panic for the corruption-class bugs on default GENERIC with INVARIANTS ON; dos for the I/O-wedge/stall bugs; none/graceful-fallback for the validation-bypass and pure missing-check / resource-leak findings).

Fix (authored, git-apply-able)

Add an exclusive-lock variant dm_table_get_entry_lock() to dm_table.c (refactor dm_table_busy() to take a u_int lock mode; declare the new helper in dm.h) and route the mutating dm_table_load_ioctl() path in dm_ioctl.c through it so list mutation is serialized.

The standalone diff is fix.diff in this directory. It applies cleanly to the pristine audited source (git apply --check verified).

Phase 8 β€” combined fix-build validation

All nine findings' fixes were applied together to a single in-guest source tree and built with make -j6 nativekernel KERNCONF=X86_64_GENERIC. The DragonFly kernel compile invokes cc ... -Werror on every translation unit, so this is a true -Werror build.

  • Result: === NK_DONE rc=0 === (full log: fix_build.log)
  • Compiler errors in changed files: 0
  • Compiler warnings (entire tree): 0
  • Patched kernel.stripped sha256: 6086a989213ef7be890180f60edc0fd9827bf60efbc3ed8c969724746e250df6
  • Baseline (unpatched) kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Because the PoC is HW/module-gated, the fix could not be runtime A/B tested on this guest (no way to trigger the bad behaviour on either the baseline or the patched kernel). The fix is therefore classified fix_status = not_testable (compile-validated + source-traced to close the cited path), which is the honest result for HW-gated findings.

Kernel references (verified during this trace)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

COMPILE-VALIDATED, runtime not_testable. First combined build failed rc=2 (fix used non-existent type lk_t); corrected to u_int (lockmgr u_int flags per sys/sys/lock.h:265) and rebuilt: === NK_DONE rc=0 ===, -Werror, 0 warnings in dm_table.c/dm_ioctl.c/dm.h. Source-traced closure: the mutating load path now takes LK_EXCLUSIVE. Runtime A/B impossible (no dm device / operator access).

baseline dm_table.c:65 LK_SHARED unconditional; patched dm_ioctl.c:726 dm_table_get_entry_lock(...,DM_TABLE_INACTIVE) (EXCL). Combined build (post lk_t->u_int fix) === NK_DONE rc=0 ===, 0 warnings. Gating: no dm device/operator access => runtime not_testable.
↓ fix.diffcombined-fix kernel built rc=0 -Werror (kernel.stripped sha256 6086a989...); not booted β€” runtime not_testable (module-gated: no dm device / operator-group access on this guest)

Confirmed kernel references

Detail

Exploit chain

Primitive = non-atomic TAILQ_INSERT_TAIL under a SHARED lock -> stale tqh_last -> wild kernel write (write-what-where potential) on the next list op. On default GENERIC likely a panic (INVARIANTS trap / wild deref). NO userspace chain exercisable on this guest: BLOCKED by the valid hard blocker that the path needs a loaded dm device + operator-group access to /dev/mapper/control (0640 root:operator), neither available to the unprivileged maxx user here (maxx is not in operator). Fix serializes list mutation with an exclusive-lock variant.

Evidence (decisive lines)

dm_table.c:65 lockmgr(&head->table_mtx, LK_SHARED) (unconditional); dm_ioctl.c:726 dm_table_get_entry(...,DM_TABLE_INACTIVE) (SHARED); dm_ioctl.c:774 TAILQ_INSERT_TAIL(tbl,table_en,next). Combined-fix build after lk_t->u_int correction: === NK_DONE rc=0 === (-Werror).

PoC changes

findings/poc/DF-2062/ populated: VERDICT.md, fix.diff (refactor dm_table_busy() to take a u_int lock mode; add dm_table_get_entry_lock() EXCL variant declared in dm.h; route dm_table_load_ioctl() in dm_ioctl.c through it), README.md, build.sh, run.sh, env.txt, fix_build.log, manifest.json. Original README preserved the threat model.

Verified recommended fix

Add an exclusive-lock variant dm_table_get_entry_lock() to dm_table.c (refactor dm_table_busy() to take a u_int lock mode; declare the new helper in dm.h) and route the mutating dm_table_load_ioctl() path in dm_ioctl.c through it so TAILQ_INSERT_TAIL is serialized. Standalone diff in findings/poc/DF-2062/fix.diff; matches finding proposal (finding asked for an EXCL variant β€” implemented; corrected the lock-mode type to u_int which the finding did not specify).

Verdict

SOURCE-CONFIRMED (module-gated). dm_table_busy() (dm_table.c:65) unconditionally takes table_mtx LK_SHARED; dm_table_get_entry() (:93) is the only helper offered. dm_table_load_ioctl() is classified as a reader but it MUTATES the inactive TAILQ: dm_ioctl.c:726 takes the table via dm_table_get_entry(...,DM_TABLE_INACTIVE) under SHARED, then dm_ioctl.c:774 TAILQ_INSERT_TAIL(tbl,table_en,next). Two concurrent reloads on the same dm device both acquire SHARED and both run the non-atomic 4-statement TAILQ_INSERT_TAIL pointer dance on the same head without exclusion -> a lost update yields a stale tqh_last -> wild kernel write on the next INSERT/REMOVE/FOREACH -> panic or controlled write-what-where. Build iteration: first combined build failed (rc=2) because the initial fix used non-existent type lk_t for the lockmgr mode; corrected to u_int (matching lockmgr()'s u_int flags parameter, sys/sys/lock.h:265) and the rebuild succeeded rc=0.