dm_table_load_ioctl mutates the inactive-table TAILQ under a SHARED table lock β concurrent reload ioctls race TAILQ_INSERT_TAIL (kernel list corruption -> panic / permanent orphan leaks)
Summary
dm_table_get_entry takes LK_SHARED for the caller's whole walk, but table_load is a WRITER under it: TAILQ_INSERT_TAIL at :774 (entries published before dm_table_init completes). The conditional destroy normally serializes loads but is skipped whenever the INACTIVE_PRESENT flag is clear - exactly after clear/resume. Two concurrent reloads append simultaneously: TAILQ_INSERT_TAIL is four plain stores, so threads tear the tail and orphan entries (permanent M_DM leak + target-module refcount pins + pdev refs). VERIFIED on guest: barrier-synchronized reloads panicked 'Bad tailq NEXT != NULL' inside dm_table_load_ioctl+0x6bf; without QUEUEDEBUG the same interleaving is uncontrolled kernel memory corruption. Fix (serialize loads under dev_mtx EXCLUSIVE + publish-after-init) validated 400/400 rounds, no leaks. Distinct from DF-2447 and DF-4466.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4473 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dm_loadrace.c | β | 7.1 KB | view raw | |
| build.sh | β | 103 B | view raw | |
| run.sh | β | 103 B | view raw | |
| run.log | β | 2.4 KB | view raw | |
| panic.txt | β | 3.1 KB | view raw | |
| fix.diff | β | 4.3 KB | view raw | |
| env.txt | β | 414 B | view raw | |
| VERDICT.md | β | 4.1 KB | β raw | |
| README.md | β | 854 B | β raw | |
| manifest.json | β | 973 B | view raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-4473 PoC β concurrent reload TAILQ corruption race
Build (guest, after vm.sh reset with-src + kldload dm):
cc -o dm_loadrace dm_loadrace.c -lprop -lpthread
Run:
./dm_loadrace 6 400 16 # threads, rounds, entries-per-reload
Expected (unpatched): kernel panic mid-run β
panic: Bad tailq NEXT(0x...->tqh_last) != NULL, last 0x... dm_table_load_ioctl() at dm_table_load_ioctl+0x6bf
(QUEUEDEBUG tailq invariant firing inside the racing TAILQ_INSERT_TAIL;
guest down, reset needed.) If the race only orphans entries in a given run,
the post-run vmstat -m "dm" line instead shows residual in-use objects
after remove_all.
Expected (patched with fix.diff): round 400/400 (ok=2400 err=0),
remove_all: rv=0 (ok), no panic, no residual "dm" malloc objects.
Requires root (or operator group).
DF-4473 β dm_table_load_ioctl mutates the inactive table under a SHARED lock: concurrent reloads corrupt the TAILQ
REPRODUCED (kernel panic / list corruption race) + FIX VALIDATED.
The bug
Table readers/writers are supposed to be serialized by table_mtx:
dm_table_get_entry() takes it LK_SHARED (sys/dev/disk/dm/dm_table.c:65)
and holds it for the whole walk; dm_table_destroy()/dm_table_switch_tables()
take it LK_EXCLUSIVE. But dm_table_load_ioctl() is a writer that runs
under the SHARED hold:
722 if (dmv->flags & DM_INACTIVE_PRESENT_FLAG)
723 dm_table_destroy(&dmv->table_head, DM_TABLE_INACTIVE);
726 tbl = dm_table_get_entry(&dmv->table_head, DM_TABLE_INACTIVE); /* SHARED */
...
774 TAILQ_INSERT_TAIL(tbl, table_en, next); /* mutation */
Two concurrent reloads therefore both append to the same TAILQ at once.
TAILQ_INSERT_TAIL (sys/sys/queue.h:628) is four plain stores; racing
threads reading the same tqh_last overwrite each other's publish store,
leaving the tail pointer referencing a non-tail slot (and orphaning entries).
The conditional destroy at :722-723 usually serializes loads against each
other β but it is skipped whenever DM_INACTIVE_PRESENT_FLAG is clear,
i.e. exactly after clear (dm_table_clear_ioctl) or resume
(dm_ioctl.c:505 clears it).
Reproduction (baseline, unpatched)
./dm_loadrace 6 400 16 β 6 threads, 400 rounds of [barrier β one reload
with 16 error-target entries each β barrier β clear]:
panic: Bad tailq NEXT(0xfffff801173eebf8->tqh_last) != NULL, last 0xfffff80117492080 dm_table_load_ioctl() at dm_table_load_ioctl+0x6bf dm_table_load_ioctl() at dm_table_load_ioctl+0x6bf dmioctl() at dmioctl+0x2eb ... Stopped at Debugger+0x7c
The QUEUEDEBUG tailq invariant (QMD_TAILQ_CHECK_TAIL, queue.h:629) fired
inside TAILQ_INSERT_TAIL in dm_table_load_ioctl β two racing inserts had
torn the tail. Without QUEUEDEBUG the same interleaving writes through a
stale tqh_last (mislinked list / orphaned entries that no destroy path can
free: permanent M_DM leak + dm_target_busy reference leak pinning loadable
target modules + pdev leaks). Guest required a hard reset.
Impact ceiling
The demonstrated primitive is uncontrolled kernel list corruption from two racing ioctls (write of a valid pointer through a wrong list slot). On this code the reliable outcome is panic / leaked entries; a groomed exploitation would additionally require controlling which kmalloc'd entry lands in the wrong slot β not demonstrated. Precondition: operator group (mapper/control is 0640 root:operator). Filed High: kernel memory corruption reachable from a semi-privileged local user with a trivially-winnable race window (each entry insert races inside a loop that repeats 16Γ per reload).
Fix validation
fix.diff (shared with DF-4472/DF-4474): the load path now (a) takes
dmv->dev_mtx (recursive, only briefly used by busy/unbusy) exclusively for
the whole load, serializing concurrent loads on the device, and (b) links the
entry with TAILQ_INSERT_TAIL only after dm_table_init() fully
initialized it (readers walking under SHARED can never observe a half-built
entry), with the init-failure path cleaning the entry directly.
- Baseline: panic mid-run (above), guest down.
- Patched (
make-Werror clean in sys/dev/disk/dm, kldload dm.ko): 400/400 rounds, 2400/2400 reloads OK, no panic, guest healthy, and afterremove_allthedmmalloc type shows no residual in-use objects (no orphans). (run.log)
Files
dm_loadrace.cβ PoC (barriered concurrent reloads + clear, M_DM check)run.logβ baseline panic + patched clean runpanic.txtβ full serial-console excerptfix.diffβ combined dm_ioctl.c fix (validated)env.txt,build.sh,run.sh
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedBaseline: 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).
run.log fix-validation section (/tmp/race_fixed.log contents); fix.diff (dev_mtx serialization + insert-after-init hunks)
Confirmed kernel references
Detail
Evidence (decisive lines)
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)
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.
Verified 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.
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.
No comments yet.