# DF-2452 — dm_target_flakey NULL-deref via mismatched feature_cnt

## Verdict

**REPRODUCED (panic / local DoS) + FIX VALIDATED.** The bug is real and
deterministically crashes the kernel when a flakey table is reloaded with a
`feature_cnt` parameter that claims more feature args than actually follow in
the params string. `_init_features` trusts this count and walks past the
populated argv slots into the M_ZERO'd NULL tail, dereferencing NULL in
`strcmp` → page-fault panic. The escalation to `uid=0` is **blocked by a valid
hard blocker**: this is a pure NULL-deref (read fault at VA 0x0) with **no
write primitive**, and the dm control device is root/operator-only. Realistic
impact ceiling: **local DoS** (root/operator can panic the kernel). The
authored `fix.diff` is built as a single-fix `dm_target_flakey.ko` module,
installed, and confirmed to close the bug (panic → clean `EINVAL`).

## Mechanism (trigger → primitive → effect)

`_init_features()` in `sys/dev/disk/dm/flakey/dm_target_flakey.c`:

```c
 122: static int
 123: _init_features(dm_target_flakey_config_t *tfc, int argc, char **argv)
 124: {
 125:     char *arg;
 126:     unsigned int value;
 127:
 128:     if (argc == 0)
 129:         return 0;
 130:
 131:     argc = atoi64(*argv++);  /* # of args for features */
 132:     if (argc > 6) {          /* upper-bound check ONLY */
 133:         kprintf("Invalid # of feature args %d\n", argc);
 134:         return EINVAL;
 135:     }
 136:
 137:     while (argc) {
 138:         argc--;
 139:         arg = *argv++;       /* <-- walks past populated slots */
```

* `dm_table_init` (`sys/dev/disk/dm/dm_ioctl.c:824`) allocates `argv` with
  `kmalloc(sizeof(*argv) * n, M_DM, M_WAITOK | M_ZERO)` and fills only the
  parsed tokens; the rest stay NULL.
* `dm_target_flakey_init` (`dm_target_flakey.c:108`) calls
  `_init_features(tfc, argc - 4, argv + 4)` where `argc - 4` is the number of
  feature args actually present.
* `_init_features` immediately overwrites its `argc` parameter with the count
  embedded in the FIRST feature arg (`atoi64(*argv++)`). The original argc
  parameter is lost — there is **no check** that the declared count <= the
  actual remaining args.
* When the declared count exceeds the actual remaining args, the `while`
  loop reads `*argv++` past the populated slots into the NULL tail. The next
  `strcmp(arg, "drop_writes")` (or `strcmp(arg, "corrupt_bio_byte")`)
  dereferences NULL → kernel page fault.

### Trigger

params: `<dev> <offset> <up_int> <down_int> 5 drop_writes`

The `5` is the declared feature_cnt, but only ONE feature arg (`drop_writes`)
follows. After consuming "drop_writes" (iteration 1), iteration 2 reads
`argv[6]` which is NULL → `strcmp(NULL, ...)` → panic.

### Observed crash signature (boot.log)

```
Fatal user address access from kernel mode from poc at ffffffff809d75cc
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x0
fault code         = supervisor read data, page not present
Stopped at      strcmp+0xc:     movzbl  (%rdi,%rax,1),%edx
db>
```

Fault VA `0x0`, stopped at `strcmp+0xc` with `%rdi==0` (NULL) — exactly the
NULL-deref from reading a zeroed argv slot.

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

1. **Module load:** reaching the dm ioctl requires `kldload dm`, root-only.
2. **Device node:** `/dev/mapper/control` is `0640 root:operator`
   (`device-mapper.c:181`). The `maxx` user (uid 1001) gets EACCES.
3. The primitive is a **read-fault-equivalent NULL deref** — no
   attacker-controlled bytes are written before the trap. There is **no
   corruption to groom**, so no escalation chain.

This is the valid hard blocker for Phase 6.

## Exploit chain

`none` — pure NULL-deref panic, no write primitive (valid hard blocker).

## Fix (`fix.diff`)

Save the original argc parameter before overwriting it. After reading the
declared feature count, validate it against the actual remaining args:

```c
int avail;

if (argc == 0)
    return 0;

avail = argc - 1;  /* remaining args after consuming the count */
argc = atoi64(*argv++);  /* # of args for features */
if (argc > 6 || argc > avail) {
    kprintf("Invalid # of feature args %d (available %d)\n", argc, avail);
    return EINVAL;
}
```

## Fix validation (Phase 8)

1. **Baseline** (`with-src` snapshot, kernel `6.5-DEVELOPMENT #0`,
   unpatched `dm_target_flakey.ko`): PoC panics deterministically —
   `Fatal trap 12`, fault VA `0x0`, `Stopped at strcmp+0xc`. Guest in DDB.
2. **Patched**: applied `fix.diff` to
   `/usr/src/sys/dev/disk/dm/flakey/dm_target_flakey.c`, built the module
   alone (`make` in `sys/dev/disk/dm/flakey`), installed
   `dm_target_flakey.ko` → `/boot/kernel/dm_target_flakey.ko`, reloaded.
3. **Re-run**: same PoC returns `EINVAL` (errno 22) —
   `reload ioctl returned rv=22 (Invalid argument)`. Guest stays up.
4. Verdict: **fix closes the bug** (panic → clean EINVAL).

## PoC

`poc.c` — libprop `NETBSD_DM_IOCTL`:
1. `command="create"`, `name="df2452dev"`.
2. `command="reload"` with target type `"flakey"` and params
   `/dev/md0 0 10 5 5 drop_writes` (feature_cnt=5 but only 1 arg follows).

Build: `cc -O2 -o poc poc.c -lprop`
Run (as root, after `kldload dm; kldload dm_target_flakey`): `./poc`
