# DF-2231 — `_prop_object_copyin()` unbounded `pref_len` (prop_kern.c)

## Verdict
**REPRODUCED** — the code claim is **CONFIRMED**: `prop_object_copyin_limit`
(declared at `sys/libprop/prop_kern.c:383` precisely to bound the copy-in) is
**never consulted**; `_prop_object_copyin()` feeds the fully attacker-controlled
`pref->pref_len` (a `size_t`) straight into `kmalloc(pref_len + 1, …)` (line 398)
and `copyin(…, pref_len)` (line 399) with **no upper-bound check**. The proposed
fix was **VALIDATED** on a single-fix kernel (E2BIG, zero allocation; legit path
intact).

## Impact (honest, on the default GENERIC guest)
**Privileged memory-exhaustion DoS + dead-code hardening gap.** Impact ceiling,
not an unprivileged escalation:
- The four kernel callers of `prop_*_copyin[_ioctl]()` are **all privileged** on
  the default guest (verified):
  - `sys/kern/kern_udev.c:892` — `UDEVPROP` ioctl on `/dev/udev` — node is
    `root:wheel 0600` (maxx: `Permission denied`, confirmed).
  - `sys/kern/vfs_quota.c:346` — `vquotactl(530)` — gated by `vfs_quota_enabled`
    which is `0` by default and `CTLFLAG_RD` (root-only tunable); returns
    `EOPNOTSUPP` at `vfs_quota.c:342` **before** reaching the vulnerable call.
  - `sys/dev/disk/dm/device-mapper.c:267` — `NETBSD_DM_IOCTL` — dm module **not
    loaded** on default guest; `/dev/mapper/control` would be `0640 root:operator`.
  - `sys/dev/misc/tbridge/tbridge.c:258` — `TBRIDGE_LOADTEST` — module **not
    loaded**; `0600 root:wheel`.
- => No **unprivileged** trigger exists on the default kernel. This is the valid
  hard blocker for an `uid=0` chain (root→kernel is game-over by definition; the
  bug is a *privileged* DoS / hardening gap here, not an unpriv→root privesc).
- Demonstrated as root via `/dev/udev` (the only caller compiled into the base
  kernel): `pref_len = 1 GiB` → `kmalloc(1 GiB)` succeeds (`EFAULT` on the
  truncated user source); `pref_len = 384 MiB` with a fully user-mapped source →
  the kernel **fully allocates and copies 384 MiB** (`EIO` from internalize, not
  `EFAULT` — copyin ran end-to-end). An attacker-controlled `pref_len` thus
  forces unbounded kernel allocations with no validation.
- No single-shot panic observed on this guest: `copyin()` swallows the destination
  fault (`EFAULT`) before the unconditional `buf[pref_len] = '\0'` store at
  `prop_kern.c:404`, and the slab allocator's `ks_limit` precise-recompute
  (`kern_slaballoc.c:867-869`) prevents a burst-driven `panic("malloc limit
  exceeded")` on a single host (12× 256 MiB parallel burst: all returned `EIO`,
  no panic).

## Mechanism (trigger → primitive → effect)
1. **Trigger.** A privileged caller (`/dev/udev` `UDEVPROP`, root) issues an
   ioctl whose `ap->a_data` is a user `struct plistref`
   `{ void *pref_plist; size_t pref_len; }` (`sys/libprop/plistref.h:43-45`).
2. **Sink (no bound).** `udev_dev_ioctl()` (`sys/kern/kern_udev.c:892`) →
   `prop_dictionary_copyin_ioctl()` (`prop_kern.c:478`) →
   `_prop_object_copyin_ioctl()` (`prop_kern.c:429`, only checks `cmd & IOC_IN`)
   → `_prop_object_copyin()` (`prop_kern.c:386`). At `prop_kern.c:398-399`:
   ```c
   buf = kmalloc(pref->pref_len + 1, M_TEMP, M_WAITOK);
   error = copyin(pref->pref_plist, buf, pref->pref_len);
   ```
   `prop_object_copyin_limit` (line 383, `= 65536`) is **never compared** against
   `pref_len`.
3. **Primitive.** Attacker-controlled `pref_len` drives an unbounded kernel
   allocation + user→kernel copy. Two failure modes observed:
   - Large `pref_len`, small user mapping → `kmalloc` succeeds (huge transient
     kernel buffer), `copyin` faults off the user source → `EFAULT`, `kfree`.
     Net: attacker forces a gigabyte-scale transient kernel allocation per call.
   - `pref_len == SIZE_MAX` → `pref_len + 1` integer-overflows to `0` →
     `kmalloc(0)` returns `ZERO_LENGTH_PTR` (`(void *)-8`, `kern_slaballoc.c:193`)
     → `copyin` into it faults → `EFAULT` (no panic: copyin handles the fault
     before the `buf[pref_len]='\0'` store at line 404).
4. **Effect.** Privileged memory-exhaustion DoS (unbounded per-call allocation)
   + a latent integer-overflow / dead-code gap. The `prop_object_copyin_limit`
   guard was clearly *intended* (it exists solely for this) but is dead code.

## Fix (validated)
Enforce `prop_object_copyin_limit` against `pref_len` **before** `kmalloc`+
`copyin`, and make the `kmalloc` `M_NULLOK` + `NULL`-check so a future bypass or
genuine kmem exhaustion cannot deref `NULL`. Full diff in `fix.diff`:
```c
size_t len = pref->pref_len;
if (len == 0 || len > (size_t)prop_object_copyin_limit)
    return (E2BIG);
buf = kmalloc(len + 1, M_TEMP, M_WAITOK | M_NULLOK);
if (buf == NULL)
    return (ENOMEM);
```

### Fix validation (single-fix kernel, `make -j6 nativekernel`)
- **Baseline `#0`** (unpatched, Jul 2 2026, sha256
  `5dc83dac…`): `pref_len=1 GiB` → `EFAULT` (1 GiB kmalloc succeeded);
  `pref_len=384 MiB` mapped → `EIO` (full 384 MiB copyin succeeded).
- **Single-fix `#1`** (Aug 8 16:25:24 2026, sha256 `2dbf4696…`): all oversized
  `pref_len` → **errno=7 `E2BIG`** (the fix's guard fires before `kmalloc`);
  `vmstat -m` shows `temp` memuse flat at `332K` before/after a 1 GiB call
  (only the request counter ticks); benign 64-byte input still reaches
  `internalize` and returns `EIO` (legit path intact).
- `git apply --check`: **APPLIES CLEAN**.

## PoC changes
Rewrote `df_poc.c` from the reviewer draft (which used a stale hand-rolled
`struct plistref` and a syscall-number literal) into a parameterised
demonstrator that uses the real `<sys/udev.h>` `UDEVPROP` macro and
`<libprop/plistref.h>`, accepts a hex `pref_len` plus an optional `mapmb`
(to mmap a large source so `copyin` runs to completion), and documents the
four privileged callers + the unprivileged-reachability blocker in its header.
Added `build.sh`/`run.sh`. Original README claim ("kernel panic") was
over-optimistic; the honest impact on this guest is privileged
memory-exhaustion DoS (no single-shot panic; see VERDICT.md for why).

## Files
- `df_poc.c` — parameterised privileged demonstrator via `/dev/udev UDEVPROP`.
- `build.sh` / `run.sh` — exact build/run.
- `build.log` — final clean userspace build.
- `run.log` — baseline (#0) bad-behavior markers (EFAULT/EIO) + reachability notes.
- `fix_build.log` — single-fix `nativekernel` build output (`NK_DONE rc=0`).
- `fix_run.log` — patched (#1) good-behavior markers (E2BIG; legit path EIO).
- `fix_kmem_proof.txt` — `vmstat -m` showing `temp` memuse flat across a 1 GiB call on #1.
- `env.txt` — guest `uname`, `cc`, `vfs.quota_enabled`, `/dev/udev` perms, M_TEMP limit.
- `fix.diff` — `git apply`-able fix (enforce the limit + M_NULLOK + NULL check).
- `manifest.json` — machine-readable catalog.
