# DF-5678 — prop_array_set/prop_array_remove bounds enforced only by INVARIANTS-gated assert

`sys/libprop/prop_array.c` — pass 2 (GLM 5.3 re-audit).

## The defect

`prop_array_set()` and `prop_array_remove()` validate their index argument
only via `_PROP_ASSERT`:

* `sys/libprop/prop_array.c:648` — `_PROP_ASSERT(idx < pa->pa_count);`
  (prop_array_set), immediately followed by OOB read `opo = pa->pa_array[idx]`
  (:650), OOB write `pa->pa_array[idx] = po` (:654), and
  `prop_object_release(opo)` on the OOB-read value (:657).
* `sys/libprop/prop_array.c:701` — same assert (prop_array_remove), followed
  by OOB read `po = pa->pa_array[idx]` (:709) and
  `prop_object_release(po)` of the garbage pointer (:719).

In the kernel `_PROP_ASSERT` == `KKASSERT` (`sys/libprop/prop_object_impl.h:258`),
which is compiled in **only when `options INVARIANTS` is set**
(`sys/sys/systm.h:94-122`):

* stock X86_64_GENERIC (has INVARIANTS): kernel **panic** — reproduced live.
* any kernel built without INVARIANTS (a fully supported production
  configuration): the check is compiled to nothing and both functions
  perform a silent **heap OOB write of a kernel pointer at an arbitrary
  32-bit index** plus **`prop_object_release()` of an out-of-bounds-read
  pointer** (arbitrary-pointer refcount decrement / potential free).

Contrast `prop_array_get()` at :591, which does a real `if (idx >= pa->pa_count)`
bounds check — the intended contract the two writers fail to mirror.

No *current* in-tree kernel caller passes an attacker-derived index
(audited: dm_ioctl.c fixed indices 0..2 are provably safe *because* of the
`idx == pa_count` append design at :643; dm_ioctl.c:649 loops over
`prop_array_count`; vfs_quota.c/kern_udev.c use add/iterator only) — so the
kernel reachability today is latent, one API misuse away. proplib is the
kernel's untrusted-plist container library, which is why this is filed.

## Reproduce (evidence classes: same-source userland differential + KLD)

On the audit guest (source snapshot, `/usr/src/sys/libprop` present):

```sh
scp df5678_userland.c guardalloc.h df5678_mod.c build.sh run.sh fix.diff dfbsd:/tmp/
vm.sh run_root 'mkdir -p /tmp/df5678 && mv /tmp/df5678_* /tmp/guardalloc.h /tmp/build.sh /tmp/run.sh /tmp/fix.diff /tmp/df5678/ 2>/dev/null; cd /tmp/df5678 && sh build.sh'
vm.sh run_root 'cd /tmp/df5678 && sh run.sh'          # userland differential
vm.sh run_root 'kldload -v /tmp/df5678/kmod_set/df5678.ko'   # PANIC at :648
# ... reset, then:
vm.sh run_root 'kldload -v /tmp/df5678/kmod_rem/df5678rem.ko' # PANIC at :701
```

### Expected output (baseline)

* `df5678_assert set`    → `Assertion failed: (idx < pa->pa_count), function
  prop_array_set, file prop_array.c, line 648` exit 134 (stock-kernel analog).
* `df5678_ndebug set`    → SIGSEGV in `prop_object_release` on an OOB-read
  garbage pointer (wild release), exit 139 (non-INVARIANTS-kernel analog).
* `df5678_guard set`     → SIGSEGV with `si_addr` in the guard page at exactly
  `pa_array[512]`; the `remove` fault address differs by exactly
  `(600-512)*8 = 704` bytes → the touched slots are `pa_array[512]`/`[600]`.
* KLD loads → `panic: assertion "idx < pa->pa_count" failed in
  prop_array_set/prop_array_remove at .../prop_array.c:648/:701`
  (panic.set.txt / panic.remove.txt).

### After fix.diff (validated)

* userland battery: every variant/scenario exit 0; `set(512)` returns
  **0 (false)**; `remove(600)` no-ops; guard rig raises no fault
  (run.userland.fixed.log).
* full `make -j8 nativekernel` + `installkernel` + reboot: both KLDs load,
  `set(5) returned 0, count=2` / `remove(7) returned, count=2`,
  `module completed without panic` (fixtest.console.txt).

## Files

* `df5678_userland.c` — 3-variant differential harness
* `guardalloc.h` — rig-only guard-page allocator (patches the *rig copy* of
  prop_object_impl.h; `sys/` is never modified)
* `df5678_mod.c` — kernel trigger modules (MODE_SET / MODE_REM)
* `build.sh`, `run.sh` — exact runnable commands
* `run.userland.log`, `run.userland.fixed.log` — baseline / fixed batteries
* `panic.set.txt`, `panic.remove.txt` — stock-kernel panics (serial console)
* `fixtest.console.txt` — fixed-kernel module runs, no panic
* `build.baseline.log`, `build.fixed.log` — rig build logs
* `env.txt` — guest environment (fixed-kernel phase; baseline uname in
  VERDICT.md)
* `fix.diff` — git-apply-able unified diff (validated)
