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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2442 Β· 11 files
FileTypeDescriptionSize
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
README.md readme build/run instructions
↓ download 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.

VERDICT.md verdict full analysis with secondary devstat race note
↓ download raw

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)

  1. Attacker races N threads, each calling create ioctl with the same name.
  2. dm_dev_create_ioctl (dm_ioctl.c:207) first calls dm_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.
  3. Each thread proceeds to dm_dev_create (dm_dev.c:225) β†’ dm_dev_insert (dm_dev.c:173).
  4. The first thread to acquire dm_dev_mutex in dm_dev_insert inserts successfully.
  5. 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=7 each 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 with fixed dm.ko (SHA256 51ac50c0a24e81c57093f6366bdd478235c850472fa13590b87e4935f0333cbb)

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'.