# DF-5680 — `<data size="N">` plist attribute drives an unbounded `kmalloc(M_WAITOK)` in `_prop_data_internalize` (kernel panic / system wedge)

**File:** `sys/libprop/prop_data.c` (pass 2, GLM 5.3)
**Verdict:** reproduced (stock-kernel panic + system-wide wedge; both eliminated by fix.diff)
**Impact:** kernel panic (availability), memory-drain DoS — via root/operator-gated kernel plist entry points (DM ioctl, udev `UDEVPROP`, quotactl-prop, tbridge)

## Root cause (path:line)

`_prop_data_internalize()` (sys/libprop/prop_data.c:543) accepts an
attacker-controlled `size` attribute on `<data>` elements:

```xml
<plist version="1.0"><dict><key>a</key><data size="N">QQ==</data></dict></plist>
```

* `len = strtoul(ctx->poic_tagattrval, &cp, 0)` — prop_data.c:572
* the `ERANGE` overflow guard is **compiled out of the kernel**
  (`#ifndef _KERNEL`, prop_data.c:573-576 — “XXX can't check for ERANGE
  in the kernel”), so in-kernel `strtoul` saturation is silently accepted
  (`size="999999999999999999999999999999"` → `len = SIZE_MAX`); a plain
  `size="-1"` bypasses the guard even in userland (negation is in-range
  modulo 2^64 → no ERANGE, verified: run.userland.log `neg1` errno=0)
* `buf = _PROP_MALLOC(len + 1, M_PROP_DATA)` — prop_data.c:588 — which is
  `kmalloc(len + 1, M_PROP_DATA, M_WAITOK)` (prop_object_impl.h:260)
  with **no upper bound of any kind** on the attacker string
* DF `kmalloc` semantics for such sizes (sys/kern/kern_slaballoc.c):
  * `len+1 == 0` (len = SIZE_MAX) → `kmalloc(0)` returns the special
    non-NULL `ZERO_LENGTH_PTR` (:888-891) — today still contained by the
    decoder's `targsize==0` bounds checks + the `alen != len` check
    (prop_data.c:597), i.e. only by luck
  * size ≥ kernel_map KVA span (e.g. 2^63) → `kmem_slab_alloc` fails
    `vm_map_findspace` and **panics** `“kmem_slab_alloc(): kernel_map ran
    out of space!”` (:1721-1724) — `M_WAITOK` (0x0002) does not include
    `M_NULLOK` (0x0400), so NULL is not an option
  * size > physical RAM (e.g. 8 GiB on a 4 GiB guest) → the M_WAITOK page
    loop **retries forever** in `vm_wait(0)` (:1778-1804): the ioctl never
    returns, the thread is not signal-interruptible, and the drain takes
    the whole guest down (`pagedaemon: WARNING! Continuous paging`)

Kernel reachability (all root/operator gated on stock configs):
`/dev/mapper/control` 0640 root:operator → `NETBSD_DM_IOCTL` →
`prop_dictionary_copyin_ioctl` (sys/dev/disk/dm/device-mapper.c:267);
`/dev/udev` 0600 → `UDEVPROP` (sys/kern/kern_udev.c:892); quotactl-prop
(sys/kern/vfs_quota.c:346); tbridge (sys/dev/misc/tbridge/tbridge.c:258).

## Pack contents

| file | what |
|---|---|
| `df5680_userland.c` | userland differential harness (6 scenarios) |
| `df5680_dmcrash.c`  | kernel trigger via DM ioctl (`panic`/`hang`/custom size) |
| `df5680_mod.c`      | KLD probe calling `prop_dictionary_internalize` in-kernel (fix validation, unambiguous vs DM's own EIO) |
| `build.sh` / `run.sh` | exact runnable commands |
| `build.log` | userland rig build (guest) |
| `run.userland.log` | userland differential battery |
| `panic.txt` | serial-console panic from the stock kernel (`size="2^63"`) — stack: `_prop_data_internalize → _kmalloc → kmem_slab_alloc` |
| `hang.console.txt` | serial console of the 8 GiB wedge (`pagedaemon: WARNING! Continuous paging for 1 minutes`, ssh dead) |
| `fixtest.console.txt` | patched kernel: panic + hang inputs both return cleanly (ioctl EIO), guest alive |
| `fixtest.module.txt` | patched kernel KLD probe: `control`/`plain` → dict OK size=5 (non-regression); `huge`/`hang` → NULL |
| `build.kernel.log` | fix kernel rebuild (`make -j6 nativekernel`, installkernel) |
| `env.txt` | guest uname / physmem / toolchain |
| `fix.diff` | git-apply-able unified diff (authored against read-only sys/; applied only inside guest /usr/src for validation) |

## Reproduce

```sh
# on the audit guest (root), stock kernel:
cd /tmp/df5680 && sh build.sh && sh run.sh          # userland differential
kldload dm
./df5680_dmcrash panic        # size=2^63  -> guest panics (see panic.txt)
# after vm.sh reset with-src + re-rig:
./df5680_dmcrash hang         # size=8GiB  -> guest wedges (hang.console.txt)
```

Expected (stock): panic with the exact stack in `panic.txt`; system-wide
memory drain for the hang variant. Userland same-source rig: every
scenario exits cleanly (malloc NULL → internalize NULL).

## Fix

`fix.diff` bounds `len` by the number of characters remaining in the XML
document (`len > strlen(ctx->poic_cp)` → reject): a base64 blob can never
decode to more bytes than its own encoding occupies, so this rejects
2^63 / 8 GiB / SIZE_MAX / strtoul-overflow values while accepting every
legitimate plist (validated in-kernel and in userland).

Validated by full kernel rebuild + reboot on the guest: baseline panic and
wedge inputs are gone (clean EIO rejection, guest healthy), legit
`size="5"` and attribute-free data plists still internalize (kernel probe
+ userland rig).
