# DF-5678 VERDICT — reproduced (kernel panic proven live; silent OOB write + wild release proven on the same source with the assert compiled out)

**File**: `sys/libprop/prop_array.c` (pass 2, GLM 5.3)
**Baseline guest kernel**: `DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64` (INVARIANTS on — verified:
`strings /boot/kernel/kernel | grep -c 'assertion "%s" failed'` == 3, and both panics below).
**Fixed guest kernel**: `#1: Fri Oct 9 23:00:04 UTC 2026` (full `make -j8 nativekernel
KERNCONF=X86_64_GENERIC` + `installkernel` of fix.diff).

## Root cause (path:line)

* `sys/libprop/prop_array.c:648` — `_PROP_ASSERT(idx < pa->pa_count);` in
  `prop_array_set()` is the ONLY index bound before:
  * `:650` `opo = pa->pa_array[idx];` — OOB **read** (8-byte kernel pointer)
  * `:654` `pa->pa_array[idx] = po;` — OOB **write** (kernel pointer at arbitrary 32-bit index)
  * `:657` `prop_object_release(opo);` — **release of the OOB-read pointer**
    (refcount decrement at `garbage+offsetof(po_refcnt)`; on 0 → `pot_free`
    dispatch on a fake object)
* `sys/libprop/prop_array.c:701` — same in `prop_array_remove()`, followed by
  the OOB read at `:709` and `prop_object_release(po)` of garbage at `:719`;
  the `pa->pa_count--` at `:714` additionally underflows when count==0.
* `_PROP_ASSERT` is `KKASSERT` in the kernel
  (`sys/libprop/prop_object_impl.h:258`) and `KKASSERT` is active **only with
  `options INVARIANTS`** (`sys/sys/systm.h:94-122` — the `#else` at :116-118
  defines it to `do {} while(0)`).  Stock `X86_64_GENERIC` has INVARIANTS
  (`sys/config/X86_64_GENERIC:56`); kernels built without it — a fully
  supported, common production configuration — compile the bound check away.
* The intended contract is visible in the same file: `prop_array_get()` does a
  real check at `:591` (`if (idx >= pa->pa_count) goto out;`).

## What was reproduced, and how

### 1. Stock-kernel analog + live kernel proof (assert live)

* Same-source userland rig (assert build): `set` aborts with
  `Assertion failed: (idx < pa->pa_count), function prop_array_set, file
  prop_array.c, line 648` (exit 134); `remove` aborts at **line 701**
  (run.userland.log, section A).
* **Live kernel**: `kldload df5678.ko` (calls `prop_array_set(pa,5,po)` on a
  count=2 array inside MOD_LOAD) →
  `panic: assertion "idx < pa->pa_count" failed in prop_array_set at
  /usr/src/sys/libprop/prop_array.c:648` with the full backtrace through
  `df5678_modevent` (panic.set.txt).  After reset, `kldload df5678rem.ko`
  (`prop_array_remove(pa,7)`) → panic at **prop_array.c:701**
  (panic.remove.txt).  This proves the defective code is compiled into and
  executes in the running stock kernel.

### 2. Non-INVARIANTS-kernel analog (assert compiled out)

* NDEBUG rig (plain libc malloc): `set(512)` on a count=capacity=510 array →
  SIGSEGV `si_addr=0x89736c6162` — the fault is inside
  `prop_object_release(opo)` at :657 dereferencing the **garbage pointer read
  out of bounds at :650**: the wild-release primitive demonstrating itself.
  `remove(600)` → SIGSEGV `si_addr=0x0` (release of an OOB-read NULL-ish
  value).  (run.userland.log, section B; the handler's fixed text says
  "guard page" — in section B the faults are wild-pointer derefs, in section
  C they are true guard-page hits.)
* Guard rig (NDEBUG + guard-page allocator around every `_PROP_*` allocation;
  array backing store of 510*8+16 == 4096 bytes ends flush with a `PROT_NONE`
  page): `set(512)` faults at `si_addr=0x8004f3010`, `remove(600)` at
  `si_addr=0x8004f32d0`.  **The addresses differ by exactly 0x2c0 = 704 =
  (600-512)*8** — i.e. the faulting accesses are precisely at
  `pa_array[512]` and `pa_array[600]`, the slots the source analysis
  predicts (:650/:654 and :709).  No sanitizer needed.

### 3. Reachability (honest scope)

No in-tree kernel caller passes an attacker-derived index today:

* `sys/dev/disk/dm/dm_ioctl.c:1090-1092` — `prop_array_set_uint32(ver, 0/1/2,
  ...)`: fixed indices 0..2 are provably safe because `prop_array_set`'s
  `idx == pa_count` branch (:643) appends, and the three sequential calls
  raise count to ≥2 before index 2 is used as a replace.
* `sys/dev/disk/dm/dm_ioctl.c:649` — get loop bounded by
  `prop_array_count(array)`; `prop_array_get` real-checks anyway (:591).
* `sys/kern/vfs_quota.c`, `sys/kern/kern_udev.c`, dm_dev/dm_target — only
  add/iterator/copyout usage.

So the kernel impact today is **latent**: one future (or currently unknown)
caller that forwards a plist-derived index — the exact use pattern this
container library exists for — turns this into a stock-kernel panic
(INVARIANTS) or, on non-INVARIANTS kernels, an attacker-shaped heap OOB write
plus arbitrary-pointer release (refcount decrement → UAF).  The primitive is
the reason this is Medium and not Low.

### 4. Fix validation (full kernel rebuild)

* `fix.diff` adds real bounds checks mirroring `prop_array_get`: `idx >
  pa->pa_count → goto out` (rv=false) in `prop_array_set`, and `idx >=
  pa_count → unlock+return` in `prop_array_remove`.  git-apply and patch(1)
  clean against the read-only tree; applied only inside the guest's
  `/usr/src` copy.
* Patched-source userland rig: **all** variants/scenarios exit 0; `set(512)`
  returns **0**; `remove(600)` no-ops; guard rig raises **no** fault
  (run.userland.fixed.log).  Controls and the legal `idx == count` append
  path are unchanged.
* `make -j8 nativekernel KERNCONF=X86_64_GENERIC` + `installkernel` + reboot
  into kernel #1 (23:00:04): both trigger modules load and print
  `set(5) returned 0, count=2` / `remove(7) returned, count=2` /
  `module completed without panic` (fixtest.console.txt).  Baseline panics:
  panic.set.txt / panic.remove.txt.  **Bad behavior gone in both builds.**
* The kernel-build log was lost to the guest's /tmp-cleaning reboot; the
  kernel identity (`#1: Fri Oct 9 23:00:04` vs baseline `#0: Thu Jul 2`),
  env.txt, and the before/after behavioral evidence carry the validation.

## Differential-harness notes

* The segv_handler message text ("guard page after prop_array backing
  store") is generic for all three binaries; it is literally true for the
  guard rig (C) and means "wild pointer deref" for the NDEBUG rig (B).
* `df5678_ndebug set` faulting instead of "surviving silently" is a stronger
  result than the README predicted: the wild release at :657/:719 fires
  immediately in this heap layout; on other layouts the corruption is silent
  until later (that silence is exactly the non-INVARIANTS production risk).

## Verdict

**reproduced** — both halves of the differential (assert-live panic at
prop_array.c:648/:701 in the running stock kernel; assert-compiled-out OOB
read/write + release-of-garbage with slot-exact fault localization on the
same source), and **fixed** on a full kernel rebuild (panics gone, bounds
enforced, legal paths unchanged).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
