Module-unload handler destroys shared objcache even when dm_target_remove fails EBUSY causing NULL-deref panic on subsequent I/O
Summary
dmtd_mod_handler MOD_UNLOAD case calls _objcache_destroy() unconditionally instead of only on successful unload. When delay target still loaded dm_target_remove returns EBUSY at :441 but _objcache_destroy() at :444 still runs freeing global obj_cache and setting it NULL. Module handler returns EBUSY so module stays registered but obj_cache gone. Next I/O against existing delay target reaches _strategy :245 objcache_get(obj_cache M_WAITOK) and objcache_get dereferences oc->cache_percpu[mycpuid] with oc==NULL panicking kernel. Attacker: privileged root or principal able to open /dev/mapper/control and issue kldunload2(2). Trigger: load dm_target_delay create delay table kldunload dm_target_delay (EBUSY but corrupts state) then any subsequent I/O panics.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2437 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | libprop NETBSD_DM_IOCTL: create/reload/resume/teardown delay device | 5.8 KB | view raw |
| run.sh | run-script | orchestrates kldload + setup + I/O + kldunload + crash-trigger | 2.1 KB | view raw |
| build.sh | build-script | cc -O2 -o poc poc.c -lprop | 62 B | view raw |
| run.log | run-log | baseline unpatched run: SSH hangs, guest panicked | 757 B | view raw |
| fix_run.log | run-log | patched-module run: EBUSY, no panic, guest stays up | 966 B | view raw |
| fix_build.log | build-log | single-fix dm_target_delay.ko module build output (rc=0) | 275 B | view raw |
| fix.diff | suggested-fix | move _objcache_destroy() inside if(err==0) block | 415 B | view raw |
| panic.txt | panic-signature | Fatal trap 12 / objcache_put+0x3a / fault from destroyed objcache | 337 B | view raw |
| env.txt | environment | uname, cc, kern.version, /dev/mapper/control perms | 326 B | view raw |
| VERDICT.md | verdict | full mechanism walkthrough + Phase 8 fix validation | 5.7 KB | β raw |
DF-2437 β dm_target_delay module-unload handler destroys shared objcache
Verdict
REPRODUCED (panic / local DoS) + FIX VALIDATED. The bug is real and
deterministically crashes the kernel when kldunload dm_target_delay is
attempted while a delay dm device is active. The MOD_UNLOAD handler calls
_objcache_destroy() unconditionally β even when dm_target_remove returns
EBUSY (active device). The objcache is destroyed (set to NULL) while the
module stays loaded and devices keep doing I/O. The next objcache_put(NULL,
...) in the delay I/O path page-faults β panic. The escalation to uid=0
is blocked by a valid hard blocker: this is a NULL-deref (read/write fault
on destroyed/freed objcache) reachable only by root (kldunload + dm control
device are root-only), with no attacker-controlled write primitive. Realistic
impact ceiling: local DoS (root/operator can panic the kernel). The
authored fix.diff is built as a single-fix dm_target_delay.ko module,
installed, and confirmed to close the bug (panic β clean EBUSY, guest stays up).
Mechanism (trigger β primitive β effect)
dmtd_mod_handler() in sys/dev/disk/dm/delay/dm_target_delay.c:
440: case MOD_UNLOAD:
441: err = dm_target_remove("delay");
442: if (err == 0)
443: kprintf("dm_target_delay: unloaded\n");
444: _objcache_destroy(); <-- UNCONDITIONAL! runs even when err=EBUSY
445: break;
_objcache_destroy() (lines 403β410):
403: static void
404: _objcache_destroy(void)
405: {
406: if (obj_cache) {
407: objcache_destroy(obj_cache); <-- frees the objcache
408: obj_cache = NULL; <-- sets global to NULL
409: }
410: }
The sequence
- A delay dm device is created and its table loaded+resumed. The table
entry holds a reference to the delay target (
dm_target_lookupindm_table_load_ioctlatdm_ioctl.c:737incrementsref_cnt). The delay kernel thread (dmdl0) is running. kldunload dm_target_delaycalls the module'sMOD_UNLOADhandler.dm_target_remove("delay")findsref_cnt > 0β returns EBUSY (dm_target.c:181-184). The target is NOT removed.- But
_objcache_destroy()runs ANYWAY (line 444 is outside theifblock). The globalobj_cacheis freed and set to NULL. - The handler returns EBUSY, so the linker aborts the unload β the module stays loaded. But the objcache is gone.
- The next I/O on the still-active delay device enters
_strategy(dm_target_delay.c:241) which callsobjcache_get(obj_cache=NULL, ...). Atkern_objcache.c:429:c struct percpu_objcache *cpucache = &oc->cache_percpu[mycpuid];Withoc==NULL, this dereferences NULL β page fault. Alternatively, the delay kernel thread's_submit_queuecallsobjcache_put(obj_cache=NULL, dp)(dm_target_delay.c:307) which also crashes.
Observed crash signature (boot.log)
Fatal user address access from kernel mode from dd at ffffffff8064b175 Fatal trap 12: page fault while in kernel mode cpuid = 2; Stopped at objcache_put+0x3a: addq $0x1,0xa8(%r12) db>
The crash is at objcache_put+0x3a (addq $0x1,0xa8(%r12)) β incrementing
a statistics counter at offset 0xa8 in the (now-destroyed/NULL) objcache
structure. %r12 holds the NULL/invalid objcache pointer.
Trigger
kldload dm; kldload dm_target_delay- Create a delay dm device:
command="create",name="df2437". - Reload a delay table with delay > 0:
command="reload"with target type"delay", params/dev/md0 0 100(100ms read delay). - Resume:
command="resume"β makes the table ACTIVE, device usable. - Generate I/O on
/dev/mapper/df2437(e.g.dd if=/dev/mapper/df2437). kldunload dm_target_delayβ objcache destroyed (EBUSY returned, module stays loaded).- More I/O (or the delay thread processing queued bufs) β crash.
Privilege analysis β why this is DoS, not privesc
- Module load: reaching the dm ioctl requires
kldload dm, root-only. - Module unload:
kldunloadis also root-only (PRIV_KLD_LOAD). - Device node:
/dev/mapper/controlis0640 root:operator(device-mapper.c:181). - The primitive is a NULL/destroyed-pointer deref β no attacker-controlled bytes are written before the trap.
Valid hard blocker for Phase 6.
Exploit chain
none β NULL/destroyed-objcache deref panic, no write primitive (valid hard blocker).
Fix (fix.diff)
Move _objcache_destroy() inside the if (err == 0) block so the objcache
is only destroyed when the unload actually succeeds (target removed, no active
devices):
case MOD_UNLOAD:
err = dm_target_remove("delay");
if (err == 0) {
kprintf("dm_target_delay: unloaded\n");
_objcache_destroy();
}
break;
Fix validation (Phase 8)
- Baseline (
with-src, kernel6.5-DEVELOPMENT #0, unpatcheddm_target_delay.ko): PoC panics deterministically βFatal trap 12,Stopped at objcache_put+0x3a. Guest in DDB. - Patched: applied
fix.diffto/usr/src/sys/dev/disk/dm/delay/dm_target_delay.c, built the module alone, installeddm_target_delay.koβ/boot/kernel/, reloaded. - Re-run (Γ2): same PoC sequence β
kldunload dm_target_delayreturnskldunload: can't unload file: Device busy(EBUSY). I/O continues normally. No panic. Guest stays up. Deterministic. - Verdict: fix closes the bug (panic β clean EBUSY, guest survives).
PoC
poc.c β libprop NETBSD_DM_IOCTL helper for create/reload/resume/teardown.
run.sh β orchestrates the full trigger sequence (kldload, setup, I/O,
kldunload, more I/O).
Build: cc -O2 -o poc poc.c -lprop
Run (as root): ./run.sh
Fix verification
fixedVALIDATED: PoC panics deterministically on unpatched dm_target_delay.ko baseline (Fatal trap 12, Stopped at objcache_put+0x3a) and does NOT panic on single-fix module (kldunload returns 'Device busy', objcache preserved, I/O continues, guest stays up). Ran twice β deterministic. Fix closes the bug.
baseline: Fatal trap 12 / Stopped at objcache_put+0x3a: addq $0x1,0xa8(%r12) / db> (guest dead). patched: kldunload: can't unload file: Device busy / guest up, I/O continues normally.
Confirmed kernel references
- sys/dev/disk/dm/delay/dm_target_delay.c:441
- sys/dev/disk/dm/delay/dm_target_delay.c:444
- sys/dev/disk/dm/delay/dm_target_delay.c:403
- sys/dev/disk/dm/delay/dm_target_delay.c:407
- sys/dev/disk/dm/delay/dm_target_delay.c:245
- sys/dev/disk/dm/delay/dm_target_delay.c:307
- sys/dev/disk/dm/dm_target.c:181
- sys/kern/kern_objcache.c:429
Detail
Exploit chain
none β NULL/destroyed-objcache deref panic, no write primitive (valid hard blocker). kldunload root-only (PRIV_KLD_LOAD), dm control device root/operator-only. No corruption to groom, no escalation chain possible.
Evidence (decisive lines)
Fatal user address access from kernel mode from dd at ffffffff8064b175 / Fatal trap 12: page fault while in kernel mode / cpuid = 2; Stopped at objcache_put+0x3a: addq $0x1,0xa8(%r12) / db>. AFTER FIX: kldunload: can't unload file: Device busy / guest stays up, no panic.
PoC changes
Created poc.c (libprop NETBSD_DM_IOCTL helper for create/reload/resume/teardown of delay device) and run.sh (orchestrates kldload + setup + background dd I/O + kldunload dm_target_delay + crash trigger). Trigger sequence: create delay device with 100ms read delay, resume, start dd reads, then kldunload dm_target_delay which destroys objcache despite EBUSY. build.sh, VERDICT.md, fix.diff, manifest.json.
Verified recommended fix
Move _objcache_destroy() inside 'if (err == 0)' block in MOD_UNLOAD case of dmtd_mod_handler (dm_target_delay.c:441-444). Ensures objcache only destroyed when unload actually succeeds (dm_target_remove returned 0 = no active devices), preventing objcache from being destroyed while module still loaded and devices active. Supersedes finding proposal (none existed).
Verdict
REPRODUCED. The bug is real and deterministic: the MOD_UNLOAD handler in dmtd_mod_handler (dm_target_delay.c:440-445) calls _objcache_destroy() UNCONDITIONALLY β even when dm_target_remove returns EBUSY (active delay device). The objcache is freed and set to NULL while the module stays loaded and devices keep doing I/O. The next objcache_get/put(NULL,...) page-faults. Confirmed by panic: Fatal trap 12, Stopped at objcache_put+0x3a (addq $0x1,0xa8(%r12) with r12=NULL/freed).
No comments yet.