# DF-5675 — Recursive proplib externalize vs. iterative internalize → kernel thread-stack exhaustion

**Severity:** High · **Class:** kernel stack exhaustion via unbounded recursion (CWE-674 → panic/memcorrupt)
**File:** sys/libprop/prop_object.c (externalize machinery) — recursion sites: prop_array.c:177-181, prop_dictionary.c:458-467
**Reachability:** NETBSD_DM_IOCTL on /dev/mapper/control (0640 root:operator) — device-mapper.c:267-273 internalizes the attacker plist and unconditionally externalizes THE SAME dict back out.

## Why it is a bug

proplib was explicitly hardened to be iterative for untrusted input: internalize
(`_prop_object_internalize_by_tag`, prop_object.c:617-659), release
(`prop_object_release`, prop_object.c:1061-1107 — "Do not recurse to avoid stack
overflows", prop_object.c:999-1000) and equals (prop_object.c:1128-1190) all use
the explicit `prop_stack`. The externalize walk is the one exception: each
nested `<array>`/`<dict>` costs one real C stack frame
(`pot_extern` → `_prop_array_externalize` → `pot_extern` → …).

The DM ioctl echoes the attacker's internalized dictionary back to userspace,
so attacker-chosen nesting depth maps 1:1 to kernel recursion depth. The kernel
thread stack is `UPAGES * PAGE_SIZE` = 4 * 4096 = **16KB**
(sys/cpu/x86_64/include/param.h:126); the kernel build of
`_prop_array_externalize` uses a 64-byte frame (measured: objdump of
/boot/kernel/kernel at 0xffffffff809d91c0 — 6 pushes + sub $8 + ret addr).

## Reproduce (guest, root — the device node is 0640 root:operator)

```
build.sh     # same-source libprop rig + df5675_userland + df5675_dmcrash
run.sh       # A: userland differential   B: kernel DM ioctl
```

Expected:

* **A (userland, same source as kernel):** depth 300 @ `ulimit -s 256` —
  internalize OK, externalize OK (95920 bytes). depth 20000 — internalize OK,
  externalize **SIGSEGV (exit 139)**.
* **B (kernel):** serial console shows

```
DF5675_DEPTH=64     → SURVIVED
DF5675_DEPTH=128    → SURVIVED
DF5675_DEPTH=192    → Fatal double fault, rsp = 0xfffff80117e6b000 (page-aligned — stack pointer ran off the kstack), panic: double fault
```

192 nesting levels = 2.9KB of XML (15 bytes per level). The dead 64KB
`prop_object_copyin_limit` (DF-2231) allows far deeper, but ~192 is already
fatal.

## Fix validation

`fix.diff` adds `_PROP_EXTERNALIZE_MAX_DEPTH` (64) and refuses to externalize
deeper nesting (graceful `false` → `prop_dictionary_externalize` → NULL →
copyout returns ENOMEM). Applied inside the guest's /usr/src, kernel rebuilt
(`make nativekernel`, 320s), rebooted:

* baseline kernel: depth 192 → **Fatal double fault** (panic.txt, panic.2.txt)
* patched kernel: depth 192 and 2048 → `ioctl errno=12 (ENOMEM)`, guest stays up
  (fixtest.console.txt)
* non-regression: depth 32 and 60 still externalize and echo back (`errno=0`)

## Files

df5675_dmcrash.c, df5675_userland.c, build.sh, run.sh, fix.diff,
panic.txt (first crash), panic.2.txt (attributed depth ladder crash),
fixtest.console.txt, build.kernel.log, run.userland.log
