# 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`:

```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):
```c
 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

1. A delay dm device is created and its table loaded+resumed. The table
   entry holds a reference to the delay target (`dm_target_lookup` in
   `dm_table_load_ioctl` at `dm_ioctl.c:737` increments `ref_cnt`). The
   delay kernel thread (`dmdl0`) is running.
2. `kldunload dm_target_delay` calls the module's `MOD_UNLOAD` handler.
   `dm_target_remove("delay")` finds `ref_cnt > 0` → returns **EBUSY**
   (`dm_target.c:181-184`). The target is NOT removed.
3. But `_objcache_destroy()` runs ANYWAY (line 444 is outside the `if`
   block). The global `obj_cache` is freed and set to NULL.
4. The handler returns EBUSY, so the linker **aborts the unload** — the
   module stays loaded. But the objcache is gone.
5. The next I/O on the still-active delay device enters `_strategy`
   (`dm_target_delay.c:241`) which calls `objcache_get(obj_cache=NULL, ...)`.
   At `kern_objcache.c:429`:
   ```c
   struct percpu_objcache *cpucache = &oc->cache_percpu[mycpuid];
   ```
   With `oc==NULL`, this dereferences NULL → page fault.
   Alternatively, the delay kernel thread's `_submit_queue` calls
   `objcache_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

1. `kldload dm; kldload dm_target_delay`
2. Create a delay dm device: `command="create"`, `name="df2437"`.
3. Reload a delay table with delay > 0: `command="reload"` with target
   type `"delay"`, params `/dev/md0 0 100` (100ms read delay).
4. Resume: `command="resume"` — makes the table ACTIVE, device usable.
5. Generate I/O on `/dev/mapper/df2437` (e.g. `dd if=/dev/mapper/df2437`).
6. `kldunload dm_target_delay` → objcache destroyed (EBUSY returned,
   module stays loaded).
7. More I/O (or the delay thread processing queued bufs) → crash.

## Privilege analysis — why this is DoS, not privesc

1. **Module load:** reaching the dm ioctl requires `kldload dm`, root-only.
2. **Module unload:** `kldunload` is also root-only (`PRIV_KLD_LOAD`).
3. **Device node:** `/dev/mapper/control` is `0640 root:operator`
   (`device-mapper.c:181`).
4. 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):

```c
case MOD_UNLOAD:
    err = dm_target_remove("delay");
    if (err == 0) {
        kprintf("dm_target_delay: unloaded\n");
        _objcache_destroy();
    }
    break;
```

## Fix validation (Phase 8)

1. **Baseline** (`with-src`, kernel `6.5-DEVELOPMENT #0`, unpatched
   `dm_target_delay.ko`): PoC panics deterministically — `Fatal trap 12`,
   `Stopped at objcache_put+0x3a`. Guest in DDB.
2. **Patched**: applied `fix.diff` to
   `/usr/src/sys/dev/disk/dm/delay/dm_target_delay.c`, built the module
   alone, installed `dm_target_delay.ko` → `/boot/kernel/`, reloaded.
3. **Re-run** (×2): same PoC sequence — `kldunload dm_target_delay` returns
   `kldunload: can't unload file: Device busy` (EBUSY). I/O continues
   normally. **No panic. Guest stays up.** Deterministic.
4. 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`
