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

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.

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1965 Β· 5 files
FileTypeDescriptionSize
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
README.md readme PoC trigger description
↓ download raw

DF-1965 PoC β€” NULL obj_cache deref after failed kldunload of dm_target_delay

Build & run

  1. Boot DragonFlyBSD with dm_target_delay KLD loaded.
  2. 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
  3. 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.

VERDICT.md verdict verification narrative
↓ download raw

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.

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 --check on 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

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.