{
  "finding_id": "DF-5704",
  "status": "reproduced",
  "reproduced": true,
  "impact": "dos",
  "confidence": "certain",
  "verdict": "REPRODUCED deterministically (4/4 runs) via the established same-source userland differential rig on the guest: prop_object_equals_with_error returns early on a nested type mismatch (prop_object.c:1155-1156) without draining its _prop_stack, so every ancestor pairs shared rwlocks (taken at prop_array.c:219-227 / prop_dictionary.c:503-511 and held across RECURSE, released only by pot_equals_finish in the finish: drain) leak permanently, and beyond 16 levels each abandoned _prop_stack_extern_elem (prop_stack.c:49-63, freed only by _prop_stack_pop at prop_stack.c:101) leaks. Wedge proof: a1=[\"x\"] vs a2=[5] then prop_array_add blocks forever (SIGALRM exit 42) while the type-matched control completes; leak proof: ru_maxrss 0 -> 19,352 kB over 400k equals() on 17-deep chains (~48 B/call = exactly one extern element). Kernel impact is latent today (no in-tree kernel caller drives the nested equals walk; only leaf prop_string_equals_cstring at kern_udev.c:906 / device-mapper.c:299) but the same code compiles into the kernel, where the mtx shared count never drops and any future writer wedges forever. Not a memory-corruption primitive: no uid0 path, impact ceiling is unkillable-thread DoS + unbounded heap leak. fix.diff (goto finish;) validated in the same rig: wedge GONE (add returns), RSS flat ~1,010 kB, control unchanged.",
  "exploit_chain": "program (or future kernel caller) compares two nested prop arrays/dicts whose element types differ at some nesting depth -> _prop_array_equals takes both objects rwlocks and returns RECURSE -> walk pushes the pair onto _prop_stack -> next levels type check fails -> early return abandons the stack -> ancestors rdlocks never released (pot_equals_finish never runs) + kmallocd extern elements never popped -> next mutation of any ancestor (prop_array_add/set -> wrlock) blocks forever; each additional comparison leaks another extern element past depth 16. Guest evidence: run.userland.log (control unwedged / buggy wedged exit 42; RSS 19,352 kB @400k), run.2.log (3/3 deterministic), fixtest.userland.log (patched: no wedge, RSS flat).",
  "evidence": "run.userland.log (decisive: STILL BLOCKED in prop_array_add after 5s: rdlock LEAKED (exit 42); ru_maxrss = 19352 kB after 400k equals); run.2.log (wedge REPRODUCED 3/3); fixtest.userland.log (fixed build: add returned TRUE (no wedge), RSS flat 1008-1028 kB); build.log (same-source rig build); env.txt (guest uname/cc); fix.diff (one-line goto finish)",
  "kernel_refs": [
    "sys/libprop/prop_stack.c:49-63",
    "sys/libprop/prop_stack.c:86-117",
    "sys/libprop/prop_stack.h:50-62",
    "sys/libprop/prop_object.c:1149-1190",
    "sys/libprop/prop_array.c:219-243",
    "sys/libprop/prop_array.c:251-256",
    "sys/libprop/prop_dictionary.c:503-511",
    "sys/libprop/prop_object_impl.h:295-300",
    "sys/kern/kern_udev.c:906",
    "sys/dev/disk/dm/device-mapper.c:299"
  ],
  "poc_changes": "No seed. Harness written fresh: fork+alarm(5) wedge probe with type-matched control, and a 17-deep RSS leak probe reusing one object pair (isolates the ~48B extern-element leak from object churn). Built against the same-source libprop_orig.so rig per the established pattern (lib/libprop/Makefile .PATH sys/libprop).",
  "attempts": 2,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026     root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64",
  "runtime_sec": 420,
  "guest_dirty": 0,
  "build_cmd": "sh build.sh  (guest root: cc -O2 rig of /usr/src/sys/libprop -> libprop_orig.so; cc -O2 df5704_userland.c)",
  "run_cmd": "sh run.sh   (./df5704_userland wedge ; ./df5704_userland leak 400000)",
  "code_hash": "f0f963cc705aa68df42183200d66660ed5946d9603ba77ad348997bca02bec76",
  "notes": "Pass-2 audit of sys/libprop/prop_stack.c; the stack itself (init/push/pop LIFO discipline, fixed 40-byte growth allocation, write-before-read on intern slots, invariant extern non-empty => used==16, refcount hand-off push<->pop in the release walk) verified clean. The finding is the equals walks violation of the stacks drain contract. Kernel-unreachable today via in-tree callers (grep: only leaf string equals); userland libprop (same source, /lib/libprop) deterministically affected. uid0 escalation not applicable (lock/heap leak, not corruption). Guest left clean (userland-only run, no kernel dirt).",
  "recommended_fix": "Route the equals type-mismatch through the existing finish: drain: replace the early return (false) at prop_object.c:1155-1156 with goto finish; so every pushed ancestor gets pot_equals_finish (unlock) and its extern stack element popped/freed.",
  "fix_status": "fixed",
  "fix_baseline_reproduced": true,
  "fix_patched_reproduced": false,
  "fix_verdict": "fix.diff applied to the same-source rig inside the guest (propfix/), rebuilt, re-run: baseline wedge (add blocked forever, exit 42) and 19.3 MB RSS leak are GONE (add returns TRUE; RSS flat ~1,010 kB over 400k equals); type-matched control unchanged; equals still returns false on mismatch (semantics preserved).",
  "fix_evidence": "fixtest.userland.log (fixed build full output: buggy case add returned TRUE (no wedge), ru_maxrss 1008/1028 kB flat), fix.diff"
}