# DF-5704 — equals-walk stack abandonment leaks ancestor locks + extern stack elements

**File audited (pass 2):** `sys/libprop/prop_stack.c` (the explicit stack used by
the iterative internalize/release/equals walks).

**Defect:** `prop_object_equals_with_error()` returns early on a nested
**type mismatch** without draining its `_prop_stack`
(`sys/libprop/prop_object.c:1155-1156`), violating the stack's pop-to-empty +
`pot_equals_finish` contract. Because `_prop_array_equals()` /
`_prop_dictionary_equals()` take **both objects' rwlocks on the first
iteration and keep them held across `_PROP_OBJECT_EQUALS_RECURSE`**
(`sys/libprop/prop_array.c:219-243`, `sys/libprop/prop_dictionary.c:503-...`),
and only `pot_equals_finish()` (invoked by the `finish:` drain loop at
`prop_object.c:1184-1189`) releases them, the abandoned stack entries leak:

1. **Permanent shared-lock leak on every ancestor pair** — in the kernel
   (`struct mtx`, `prop_object_impl.h:295-300`) the shared count never drops,
   so any later writer of those objects blocks forever (unkillable-thread
   local DoS); in userland (pthread rwlock, same source) identical.
2. **Heap leak of the stack's growth elements** — beyond 16 nesting levels
   each pushed level is a `kmalloc`/`malloc`'d `_prop_stack_extern_elem`
   (`sys/libprop/prop_stack.c:49-63`, 40/48 bytes) that is only freed by
   `_prop_stack_pop()`; abandoning the stack leaks it.

Kernel reachability today: **latent** — no in-tree kernel caller drives the
nested equals walk (only leaf `prop_string_equals_cstring` at
`sys/kern/kern_udev.c:906`, `sys/dev/disk/dm/device-mapper.c:299`). Userland
libprop builds the *same* source (`lib/libprop/Makefile:6-13`), where
`prop_array_equals`/`prop_dictionary_equals` are public API and the bug is
deterministically reachable by any program comparing nested structures with a
nested type mismatch.

## Reproduce (guest, root, ~2 min)

```
sh build.sh      # same-source rig: /usr/src/sys/libprop -> libprop_orig.so + harness
sh run.sh        # A) wedge: control unwedged / buggy wedged (exit 42 = still blocked)
                  # B) leak: RSS 0 -> 19.3 MB over 400k equals() (~48 B/call)
```

Expected decisive output (run.userland.log):

```
   -> child STILL BLOCKED in prop_array_add after 5s: rdlock LEAKED (exit 42)
DF-5704 wedge result: REPRODUCED (control unwedged, buggy wedged)
  [leak] after 400000 equals(): ru_maxrss = 19352 kB
```

## Fix validation

`fix.diff` routes the type mismatch through the existing `finish:` drain
(one-line change in `sys/libprop/prop_object.c`). Applied to the same rig
(`propfix/`), rebuilt, re-run: buggy case `add returned TRUE (no wedge)`,
RSS flat at ~1010 kB over 400k equals() (`fixtest.userland.log`).
