# DF-2448 — `dm_table_load_ioctl` NULL-deref via missing `DM_IOCTL_CMD_DATA`

## Verdict

**REPRODUCED (panic / local DoS) + FIX VALIDATED.** The bug is real and
deterministically crashes the kernel the moment the inbound dictionary OMITS
the `"cmd_data"` key. The escalation to `uid=0` is **blocked by a valid hard
blocker**: this is a pure NULL-deref **read-fault-equivalent** (a `mtx_lock`
on `&NULL->pa_rwlock` — a kernel write to virt addr 0x40, taken before any
attacker-controlled content is read or written) and yields **no write
primitive**, so there is no corruption to groom and no escalation chain.
Realistic impact ceiling: **local DoS** (root/operator can panic the kernel).
The authored `fix.diff` is built as a single-fix `dm.ko` module, installed,
and confirmed to close the bug (panic → clean `EINVAL`).

## Mechanism (trigger → primitive → effect)

`dm_table_load_ioctl()` in `sys/dev/disk/dm/dm_ioctl.c` fetches the
`"cmd_data"` array and passes it straight to `prop_array_iterator()` with no
NULL check:

```
 673: int
 674: dm_table_load_ioctl(prop_dictionary_t dm_dict)
 ...
 707:     cmd_array = prop_dictionary_get(dm_dict, DM_IOCTL_CMD_DATA);
 708:     iter = prop_array_iterator(cmd_array);     <-- NULL deref
```

* `prop_dictionary_get()` (`sys/libprop/prop_dictionary.c:933`) returns
  `NULL` when the key is absent (`_prop_dictionary_get` at line 909: `pde`
  stays NULL → `po` stays NULL → returns NULL).
* `prop_array_iterator()` (`sys/libprop/prop_array.c:538`) does:

```
 538: prop_array_iterator(prop_array_t pa)
 539: {
 540:     prop_object_iterator_t pi;
 541:
 542:     _PROP_RWLOCK_RDLOCK(pa->pa_rwlock);        <-- FIRST statement
 543:     pi = _prop_array_iterator_locked(pa);       <-- only HERE does it
 544:     _PROP_RWLOCK_UNLOCK(pa->pa_rwlock);              check is_array(pa)
 545:     return (pi);
 546: }
```

Under `_KERNEL`, `_PROP_RWLOCK_RDLOCK(pa->pa_rwlock)` expands
(`sys/libprop/prop_object_impl.h:297`) to:

```
 mtx_lock(&(pa->pa_rwlock))
```

i.e. `mtx_lock(&(((struct prop_array *)NULL)->pa_rwlock))`. With `pa == NULL`
that is a kernel write to virt address `offsetof(struct prop_array, pa_rwlock)`
= `0x40` — and crucially it happens **before** the
`prop_object_is_array(pa)` guard inside `_prop_array_iterator_locked()`
(line 517) can run, so the NULL is never caught. Result: page fault in
kernel mode → panic.

### Observed crash signature (boot.log)

```
Fatal user address access from kernel mode from dm_nulldata_dere at ffffffff809d9a7b
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x40
fault code         = supervisor write data, page not present
Stopped at      prop_array_iterator+0x1b:  lock cmpxchgl %edx,0x40(%rdi)
db>
```

The fault VA `0x40` and the stopped-at instruction `cmpxchgl %edx,0x40(%rdi)`
with `%rdi==0` exactly match the macro expansion `mtx_lock(&(pa->pa_rwlock))`
with `pa==NULL`.

### Trigger

A `NETBSD_DM_IOCTL` (`sys/dev/disk/dm/netbsd-dm.h:41`,
`_IOWR(DM_IOCTL, 0, struct plistref)`) carrying a libprop dictionary with:

* `"version"` = `[4, 0, 0]` — passes `dm_check_version()` (major==4, minor<=16),
* `"command"` = `"reload"` — routes through `dm_cmd_to_fun()` in
  `device-mapper.c:286` to `dm_table_load_ioctl` (cmd_fn table line 131),
* `"name"` = any string,
* **`"cmd_data"` OMITTED** — the trigger.

The NULL deref at line 708 fires **before** the `dm_dev_lookup` at line 711,
so no device needs to exist; the PoC creates one (`command="create"`) only to
mirror a realistic scenario (legitimate device + malformed reload).

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

1. **Module load:** the `dm` driver is a KLD module; reaching the ioctl
   requires `kldload dm`, which is a root-only operation (`PRIV_KLD_LOAD`).
2. **Device node:** `/dev/mapper/control` is created as
   `make_dev(&dmctl_ops, 0, UID_ROOT, GID_OPERATOR, 0640, "mapper/control")`
   (`device-mapper.c:181`) — `crw-r----- root operator`. The `maxx` user
   (uid 1001, not in `operator`/`wheel`) gets `EACCES` on `open()`.
3. So the bug is reachable only by **root or an `operator`-group member**.
   Root→kernel is game-over by definition; an `operator`-group member
   panicking the kernel is a hardening/robustness gap.

Combined with the fact that the primitive is a **read-fault-equivalent NULL
deref** (no attacker-controlled bytes are read or written before the trap),
there is **no escalation chain to develop** — this is the valid hard blocker
for Phase 6.

## Exploit chain

`none` — pure NULL-deref panic, no write primitive (valid hard blocker).
No escalation file (`exploit.c`) is produced because there is no corruption
to convert.

## Fix (`fix.diff`)

Minimal, root-cause-targeted: check the `prop_dictionary_get` return for NULL
before calling `prop_array_iterator`, returning `EINVAL` if `"cmd_data"` is
missing/non-array; and also check the iterator return for NULL (ENOMEM):

```c
cmd_array = prop_dictionary_get(dm_dict, DM_IOCTL_CMD_DATA);
if (cmd_array == NULL) {
    dmdebug("%s: DM_IOCTL_CMD_DATA missing\n", __func__);
    return EINVAL;
}
iter = prop_array_iterator(cmd_array);
if (iter == NULL) {
    dmdebug("%s: prop_array_iterator failed\n", __func__);
    return ENOMEM;
}
```

(There is no finding markdown for DF-2448 yet — `findings/DF-2448-*.md` does
not exist in this tree. The fix.diff is the authoritative verified fix.)

## Fix validation (Phase 8)

1. **Baseline** (`with-src` snapshot, kernel `6.5-DEVELOPMENT #0`,
   unpatched `dm.ko`): PoC panics deterministically — `Fatal trap 12`,
   `Stopped at prop_array_iterator+0x1b`. Guest DDB, must reset.
2. **Patched**: applied `fix.diff` to `/usr/src/sys/dev/disk/dm/dm_ioctl.c`,
   built the dm module alone (`make` in `sys/dev/disk/dm` — module build,
   ~30 s, no full kernel rebuild needed), installed
   `dm.ko` → `/boot/kernel/dm.ko`, `kldload dm`.
3. **Re-run**: same PoC returns `EINVAL` (errno 22) —
   `reload ioctl returned rv=22 (Invalid argument)`. Guest stays up. Repeated
   3× — deterministic.
4. Verdict: **fix closes the bug** (panic → clean EINVAL).

## PoC

`dm_nulldata_deref.c` — libprop `NETBSD_DM_IOCTL`:
1. `command="create"`, `name="df2448dev"` (cosmetic; the deref is upstream
   of the device lookup).
2. `command="reload"`, `name="df2448dev"`, **`"cmd_data"` omitted** →
   `prop_dictionary_get` returns NULL → `prop_array_iterator(NULL)` → panic.

Build: `cc -o dm_nulldata_deref dm_nulldata_deref.c -lprop`
Run (as root, after `kldload dm`): `./dm_nulldata_deref`
