KKASSERT kernel panic in dm_dev_insert on concurrent dm_dev_create name collision
Summary
dm_dev_insert asserts dmv!=NULL in collision else-branch but dmv only ever assigned by uuid lookup. On name/minor collision with non-matching (or empty) uuid control reaches else-branch with dmv==NULL so KKASSERT fires and panics kernel. KKASSERT maps to panic() under INVARIANTS enabled in shipping X86_64_GENERIC kernel. Collision reachable via TOCTOU race between pre-lookup in dm_dev_create_ioctl and insert here between two concurrent create ioctls using same device name. dm_dev_create_ioctl looks name up (taking+releasing dm_dev_mutex) then dm_dev_create does long sequence before dm_dev_insert re-acquires mutex so two concurrent create ioctls for same name both pass pre-lookup second to insert hits KKASSERT. Local DoS deterministic kernel panic.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2442 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | fork-based concurrent create racer: N children race create(same_name) -> KKASSERT panic | 6.6 KB | view raw |
| build.sh | build-script | cc -O2 -o poc poc.c -lprop | 89 B | view raw |
| run.sh | run-script | kldload dm; ./poc [children] [rounds] | 257 B | view raw |
| build.log | build-log | successful build output | 86 B | view raw |
| run.log | run-log | KKASSERT panic on round 1 (unpatched) | 791 B | view raw |
| panic.txt | panic-signature | panic: assertion dmv != NULL failed in dm_dev_insert at dm_dev.c:195 | 755 B | view raw |
| fix_run.log | fix-run-log | fixed dm.ko: 5 rounds clean (created=1 existed=7), RC=0 | 1.4 KB | view raw |
| fix.diff | suggested-fix | remove incorrect KKASSERT(dmv != NULL) in dm_dev_insert else-branch | 523 B | view raw |
| VERDICT.md | verdict | full analysis with secondary devstat race note | 3.5 KB | β raw |
| README.md | readme | build/run instructions | 1.1 KB | β raw |
| env.txt | environment | uname, cc version, devfs perms | 359 B | view raw |
DF-2442: dm_dev_insert KKASSERT panic via concurrent create race
Build & Run
Setup (as root)
kldload dm cc -O2 -o poc poc.c -lprop
Trigger (as root β control dev is 0640 root:operator)
./poc [num_children] [num_rounds] # defaults: 8 children, 200 rounds
Expected Output (unpatched #0 kernel)
Kernel panic on round 1:
panic: assertion "dmv != NULL" failed in dm_dev_insert at dm_dev.c:195
Guest dies immediately.
Expected Output (fixed dm.ko)
[round 1] created=1 existed=7 other=0 [*] All 5 rounds completed without panic.
EEXIST returned cleanly; no KKASSERT. Guest stays up.
Mechanism
Concurrent create ioctls for the same name race past the early
dm_dev_lookup check. The second thread to call dm_dev_insert finds the
name already present, enters the else-branch with dmv==NULL (uuid was
zero-filled, so uuid lookup was skipped), and the KKASSERT(dmv != NULL)
at dm_dev.c:195 fires.
Note
Under sustained racing (50+ rounds), a secondary devstat_remove_entry NULL deref can occur in the error cleanup path β a separate bug.
DF-2442 VERDICT β dm_dev_insert KKASSERT panic via concurrent create race
Verdict: REPRODUCED (panic) β rootβkernel DoS; fix VALIDATED (KKASSERT fixed)
Summary
Racing two or more concurrent create ioctls for the SAME dm device name
triggers a KKASSERT panic in dm_dev_insert (sys/dev/disk/dm/dm_dev.c:195).
Mechanism (trigger β primitive β effect)
- Attacker races N threads, each calling
createioctl with the same name. dm_dev_create_ioctl(dm_ioctl.c:207) first callsdm_dev_lookup(name, uuid, -1). All threads pass this check (device doesn't exist yet). This is the TOCTOU window β the mutex is released between this lookup and the insert.- Each thread proceeds to
dm_dev_create(dm_dev.c:225) βdm_dev_insert(dm_dev.c:173). - The first thread to acquire
dm_dev_mutexindm_dev_insertinserts successfully. - The second thread acquires the mutex and hits the bug at dm_dev.c:190-196: ```c if (memcmp(dev->uuid, dummy_uuid, DM_UUID_LEN)) dmv = dm_dev_lookup_uuid(dev->uuid); // SKIPPED: uuid is zero-filled
if ((dmv == NULL) && // dmv == NULL (uuid was zero)
(_dm_dev_lookup(dev->name, NULL, dev->minor) == NULL)) { // name EXISTS now
...
} else {
KKASSERT(dmv != NULL); // PANIC: dmv is NULL
r = EEXIST;
}
``
Sincedev->uuidis zero-filled (the normal case for create without uuid),
the uuid lookup at line 187-188 is skipped, sodmvstays NULL. But_dm_dev_lookupat line 191 finds the name (inserted by the winning thread),
so we enter the else-branch withdmv == NULL. TheKKASSERT(dmv != NULL)`
at line 195 fires β kernel panic.
Panic signature
panic: assertion "dmv != NULL" failed in dm_dev_insert at /usr/src/sys/dev/disk/dm/dm_dev.c:195 Trace: dm_dev_insert() at dm_dev_insert+0xe1 dm_dev_create() at dm_dev_create+0x223 dm_dev_create_ioctl() at dm_dev_create_ioctl+0xa0 dmioctl() at dmioctl+0x2eb dev_dioctl() at dev_dioctl+0x65
Exploit chain
N/A β valid hard blocker (root-only reachability). /dev/mapper/control
is 0640 root:operator; maxx (uid 1001) is not in operator/wheel. The bug
requires root to trigger. KKASSERT panic is not a write primitive, so no
escalation chain.
Fix
Remove the incorrect KKASSERT(dmv != NULL) at dm_dev.c:195. The assertion is
wrong: dmv can legitimately be NULL here (when uuid is zero-filled and name
lookup matched). The r = EEXIST assignment doesn't use dmv, so removing
the assertion is safe. See fix.diff.
Fix validation
- Unpatched (#0 baseline): PoC panics on round 1 with
panic: assertion "dmv != NULL" failed in dm_dev_insert. - Patched (fixed dm.ko): 5 consecutive rounds complete cleanly
(
created=1 existed=7each round, RC=0). The KKASSERT no longer fires.
Note on secondary devstat race
Under sustained concurrent racing (50+ rounds with 12 children), a separate
panic can occur: Stopped at devstat_remove_entry+0x25: movq (%rdx),%rax (NULL
deref). This is a race in the error cleanup path of dm_dev_destroy when the
losing thread destroys its partially-registered device (disk_destroy +
devstat_remove_entry race under concurrent access). This is a different bug
from DF-2442 β it's in the device teardown path, not the KKASSERT path. It was
previously masked by the KKASSERT firing first. The KKASSERT fix is correct and
sufficient for DF-2442; the devstat race should be filed as a separate finding.
Fix verification
fixedVALIDATED: PoC panics on unpatched #0 (panic: assertion dmv != NULL failed in dm_dev_insert at dm_dev.c:195) on round 1. Fixed dm.ko: 5 consecutive rounds complete cleanly (created=1 existed=7 each round, RC=0), no KKASSERT.
BEFORE (unpatched #0): panic: assertion 'dmv != NULL' failed in dm_dev_insert at dm_dev.c:195 (round 1). AFTER (fixed dm.ko): [round 1] created=1 existed=7 other=0; All 5 rounds completed without panic. RC=0.
Confirmed kernel references
Detail
Exploit chain
none β valid hard blocker (root-only reachability): /dev/mapper/control 0640 root:operator, maxx not in operator/wheel. KKASSERT panic no write primitive, no escalation. root->kernel DoS.
Evidence (decisive lines)
panic: assertion 'dmv != NULL' failed in dm_dev_insert at /usr/src/sys/dev/disk/dm/dm_dev.c:195 | Trace: dm_dev_insert+0xe1 -> dm_dev_create+0x223 -> dm_dev_create_ioctl+0xa0 -> dmioctl+0x2eb -> dev_dioctl+0x65. Guest panicked on round 1 of 8-child concurrent create race.
PoC changes
Authored poc.c from scratch: fork-based concurrent create racer modeled on TOCTOU between dm_dev_create_ioctl:207 (dm_dev_lookup) and dm_dev_insert:182 (lockmgr acquire). N children synchronize on pipe barrier then all fire create(same_name) simultaneously. fix.diff: remove incorrect KKASSERT(dmv != NULL) β dmv can legitimately be NULL when uuid zero-filled; EEXIST return doesn't use dmv.
Verified recommended fix
In dm_dev.c dm_dev_insert, remove the KKASSERT(dmv != NULL) at line 195 in else-branch. Assertion is incorrect: dmv can legitimately be NULL (uuid zero-filled so uuid lookup skipped, but name lookup matched). r = EEXIST assignment doesn't use dmv, so removing assertion safe and correct.
Verdict
REPRODUCED. dm_dev_insert (dm_dev.c:195) has KKASSERT(dmv != NULL) in the else-branch. When dev->uuid is zero-filled (normal case for create without uuid), the uuid lookup at line 187-188 is SKIPPED, so dmv stays NULL. If _dm_dev_lookup at line 191 finds the name (because another thread inserted it between the pre-check and insert β a TOCTOU race), we enter the else-branch with dmv==NULL and the KKASSERT fires. Triggered by racing 8 concurrent create ioctls for the same name: 'panic: assertion dmv != NULL failed in dm_dev_insert at dm_dev.c:195'.
No comments yet.