# DF-5704 VERDICT — REPRODUCED (deterministic, same-source userland differential; kernel-latent today)

## What was claimed

Auditing `sys/libprop/prop_stack.c` (pass 2) against its contract revealed the
equals walk violates the stack's drain discipline: on a nested **type
mismatch** `prop_object_equals_with_error()` returns `false` immediately
instead of unwinding its `_prop_stack`. Each abandoned stack level represents
an ancestor pair whose *shared locks are still held* (taken by
`_prop_array_equals`/`_prop_dictionary_equals` at first iteration, released
only by `pot_equals_finish` during the `finish:` drain) plus — beyond 16
levels — a heap-allocated `_prop_stack_extern_elem` that only
`_prop_stack_pop()` frees.

## Why it is a real bug (path:line)

- `sys/libprop/prop_object.c:1155-1156` — `if (po1->po_type != po2->po_type) return (false);` — the only exit of the walk that skips the `finish:` drain at `prop_object.c:1184-1189`.
- `sys/libprop/prop_array.c:219-227` — both `pa_rwlock`s rd-locked at `idx==0` (address-ordered);
- `sys/libprop/prop_array.c:240-243` — `_PROP_OBJECT_EQUALS_RECURSE` returned **with the locks held**;
- `sys/libprop/prop_array.c:251-256` — `_prop_array_equals_finish` unlocks both (only called from the drain);
- same pattern for dictionaries: `sys/libprop/prop_dictionary.c:503-511`.
- `sys/libprop/prop_stack.c:49-63` — push beyond `PROP_STACK_INTERN_ELEMS` (16) heap-allocates `_prop_stack_extern_elem` (`_PROP_MALLOC`), freed only at `prop_stack.c:101` inside `_prop_stack_pop`.
- Stack contract documented by construction: every other exit drains
  (`prop_object.c:638-641, 650-656, 1106, 1165-1170, 1184-1188`); the equals
  type-mismatch exit is the sole violation.

## How it was proven (guest, DragonFly 6.5-DEVELOPMENT)

Same-source rig (the established pattern): `/usr/src/sys/libprop` compiled
into `libprop_orig.so` on the guest, harness linked against it — the bytes
executed are the kernel's own source text.

**A) Wedge (deterministic, 3/3 runs + 1 initial = 4/4):**
`a1=["x"]` vs `a2=[5]` — type mismatch at depth 1.
- Control (`a1=["x"]` vs `a2=["y"]` — types match): `equals -> 0`, then
  `prop_array_add` returns immediately (FALSE path drains correctly).
- Buggy: `equals -> 0`, then `prop_array_add` **never returns**; SIGALRM(5s)
  handler `_exit(42)`s from inside the blocked call.
  ⇒ the rdlock taken by `_prop_array_equals` on `a1` (and `a2`) was leaked by
  the abandoned stack entry; the write path (`prop_array_add` →
  `_PROP_RWLOCK_WRLOCK`, `prop_array.c:638`) blocks forever.
  In-kernel equivalent: `struct mtx` shared count never drops → any writer of
  the object wedges forever in `_mtx_lock_ex` (kernel thread DoS).

**B) Heap leak of the growth elements:** 17-deep chains, mismatch at the leaf
— exactly one `_prop_stack_extern_elem` is live on the stack at the moment of
abandonment. 400,000 `prop_array_equals()` on the same pair:
`ru_maxrss` 0 → **19,352 kB** (~48 B/iteration = one malloc chunk per call,
matching prediction `iters × 48B ≈ 18,750 kB`).

## Exploit chain / impact ceiling

- **Userland libprop (same source, shipped as /lib/libprop)**: any program
  that compares attacker- or data-influenced nested plists with a nested type
  mismatch permanently wedges a thread on the next mutation of those objects
  and leaks memory per comparison. Local DoS, deterministic.
- **Kernel**: the identical code compiled into the kernel; today **no
  in-tree caller** drives the nested equals walk (verified: only
  `prop_string_equals_cstring` at `sys/kern/kern_udev.c:906` and
  `sys/dev/disk/dm/device-mapper.c:299` — leaf compares, no stack). Any
  future kernel consumer of `prop_array_equals`/`prop_dictionary_equals`/
  `prop_object_equals` on attacker-shaped plists inherits a permanent
  kernel-lock leak → unkillable blocked threads (local DoS). Not uid0-able:
  the primitive is a lock/heap leak, not memory corruption.

## Fix validation

`fix.diff` (one line, `sys/libprop/prop_object.c`): route the type mismatch
through the existing `finish:` drain (`goto finish;`). The drain pops every
ancestor and calls its `pot_equals_finish` (unlock), then returns `false` —
semantics preserved. Validated in the same rig (`propfix/`):
- buggy case: `add returned TRUE (no wedge)` — wedge GONE;
- leak case: `ru_maxrss` flat at 1,008-1,028 kB across 400k equals() — leak GONE;
- control case: unchanged.

## Classification

status: **reproduced**, impact: **dos** (permanent rwlock wedge + unbounded
heap leak; deterministic in userland libprop today, latent in kernel),
confidence: **certain** (mechanism proven at 4/4 determinism with control,
leak quantified to the byte, fix validated same-source).

## 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.
