{
  "finding_id": "DF-5691",
  "status": "reproduced",
  "reproduced": true,
  "impact": "none",
  "confidence": "certain",
  "verdict": "prop_dictionary_util.cs _nocopy twins (template at :183-218) export the DF-5690 borrowed-pointer class into _KERNEL with, unlike the array twin, 26 live kernel call sites: prop_dictionary_get_cstring_nocopy returns an unpinned pointer into the prop_strings sole-owned buffer that dangles the moment the same dict key is replaced (prop_dictionary.c:988-993 releases the old object), the dict is released, or the key removed; two in-tree dm sites actively create that dangle mid-function \u2014 dm_dev_status_ioctl borrows uuid at dm_ioctl.c:397 then replaces the UUID key at :409, and dm_table_deps_ioctl borrows name+uuid at :596-597 then replaces both keys at :619-620 \u2014 while prop_dictionary_set_cstring_nocopy stores caller-owned kernel memory forever (free skipped at prop_string.c:85-87) with ps_size frozen at creation although externalize/get_cstring re-read the buffer to its CURRENT NUL (prop_string.c:101-105, :302-304). Deterministically proven on the same-source page-guard rig: dict-replace dangle TRAPS (T1/T1b, the two exact dm patterns), dict-release dangle TRAPS (T2), freed-buffer read via the proplist pointer TRAPS (T2b), buffer-reuse externalize leaks <string>PWNED-BY-BUFFER-REUSE</string> (T2c, the dm copyout info-leak semantics), and the frozen-ps_size strcpy overruns its ps_size+1 allocation TRAPPING (T3); control T4 and the exact no-deref in-tree dm sequence (DML) stay clean, proving the in-tree dangle is real but latent today (line-by-line: neither dm site dereferences the borrow after the replace). Kernel impact today none-beyond-latent: dm is optional (sys/conf/files:48) behind a root-only control node, tbridge is LINT64-only, and the set twin has zero kernel callers; filed Low hardening one edit away from a kernel UAF-read/leak/overflow class. Negative results machine-confirmed in the same run: 2517-check differential matrix 0 failures (get-side numeric guard algebra byte-identical to the array twins and exact), DF-2240 signed-creator class ABSENT (set_uintN uses the unsigned creator at :167; UINT64_MAX roundtrips), set_and_rel release dance balanced, all type guards and NULL handling correct.",
  "exploit_chain": "",
  "evidence": "run.log section 2 (baseline): T1 ... TRAP(SIGSEGV) OK, T1b ... TRAP(SIGSEGV) OK, T2 ... TRAP(SIGSEGV) OK, T2b ... TRAP(SIGSEGV) OK, T2c leaked <string>PWNED-BY-BUFFER-REUSE</string> with exit=0 OK, T3 ... TRAP(SIGSEGV) OK, DML ... NOT dereferenced (latent); section 1: MATRIX: 2517 checks, 0 failures, 1 known(DF-2240-diff) rows; section 3 (fixed): NOLIFE(fixed): all expectations met with T2b intact \"transient-buffer\", T2c frozen copy exit=3, T3 len=15. run2.log/run3.log: 3/3 byte-identical stable reruns (18 OK rows each). build.log: md5-identical guest/host sources, redirected allocator macros, fix.diff applied.",
  "kernel_refs": [
    "sys/libprop/prop_dictionary_util.c:183-218",
    "sys/libprop/prop_dictionary_util.c:167",
    "sys/libprop/prop_dictionary.c:988-993",
    "sys/libprop/prop_string.c:85-87",
    "sys/libprop/prop_string.c:188-200",
    "sys/libprop/prop_string.c:101-105",
    "sys/libprop/prop_string.c:302-304",
    "sys/dev/disk/dm/dm_ioctl.c:396-409",
    "sys/dev/disk/dm/dm_ioctl.c:596-620",
    "sys/dev/misc/tbridge/tbridge.c:125",
    "sys/conf/files:48"
  ],
  "poc_changes": "Authored fresh (no seed). Harness revisions across 3 attempts: (1) fixture needed the full plist XML preamble (bare <dict> is rejected by prop_dictionary_internalize); root shell on the guest is csh so all remote commands wrapped in /bin/sh -c. (2) wrong-type sweep originally included right-typed keys (a string key IS a hit for get_cstring, a bool key for get_bool, a dict key for get_dict) and the u>INT64_MAX expected value used a lossy ternary \u2014 both expectation bugs fixed. (3) nolife: borrowed pointers must actually be READ through a volatile sink (assignment alone optimizes away); proplib-returned pointers must never be free()d on the guard rig (DragonFly libc malloc asserts); T1b must read right after the FIRST key replace \u2014 reading after BOTH replaces lets the second set_cstrings own mmap recycle the just-freed page (gap reuse DF-5690 also observed); T3 needs an 8190-byte current NUL to cross the page-guard; fork requires fflush before fork to stop duplicated stdio buffers; T2c uses exit-code semantics (0=leak baseline, 3=frozen fixed).",
  "attempts": 3,
  "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": 150,
  "guest_dirty": 0,
  "build_cmd": "sh /tmp/df5691/build.sh",
  "run_cmd": "sh /tmp/df5691/run.sh",
  "code_hash": "7c1ff1d1387b3f975a78f6817f35d97c0e0579d5974bd68ee980ebe5c6a9dc17",
  "notes": "Reproduced refers to the memory-safety/leak primitives on the same source in the established userland rig (page-guard allocator; guest cc 8.3 lacks usable ASAN). impact=none because no kernel dereference of the dangling borrow exists today (verified line-by-line for both dm sites and by rg for all other callers); severity Low per DF-5682/DF-5690 precedent upgraded one notch because the dangle is actively created by live in-tree code (dm_ioctl.c:409, :619-620), not merely one-caller-away. Cross-ref for the dm re-audit: same callers, plus dm_check_version ignores prop_dictionary_get_uint32 failure (dm_ioctl.c:1074-1086) leaving dm_version uninitialized (DF-5690 note). sys/ untouched; guest scratch removed; no panic, no kernel rebuild.",
  "recommended_fix": "Make prop_string_create_cstring_nocopy copy in _KERNEL builds (see fix.diff); dm callers of get_cstring_nocopy must retain the parent (or use the copy variant) whenever the key can be replaced \u2014 dm_dev_status_ioctl:409 and dm_table_deps_ioctl:619-620 replace borrowed-from keys while the borrows are in scope.",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Fix validated on the same source in-rig (DF-5690/DF-5682 precedent; pure C-level control-flow change on a path the rig compiles identically, zero live kernel callers of the set twin, so no kernel rebuild performed): baseline traps T2b/T3 and leaks T2c; patched copy (exact fix.diff, activated by the rig-only -DPROP_RIG_FORCE_NOCOPY_COPY which is undefined in real builds) reads intact transient-buffer with no trap, externalizes the frozen <string>transient-buffer</string>, and copies exactly 15 bytes with no overrun; T1/T1b/T2 still trap as the documented caller contract (get-side borrowing is an API contract addressed by caller discipline \u2014 the dm sites must retain or use the copy variant \u2014 not by the creator fix); T4/DML/NCD controls unchanged in both.",
  "fix_evidence": "run.log section 2 vs section 3: T2b ... TRAP(SIGSEGV) -> T2b: NO TRAP, intact content \"transient-buffer\"; T2c <string>PWNED-BY-BUFFER-REUSE</string> -> <string>transient-buffer</string> (exit=0 -> exit=3); T3 ... TRAP(SIGSEGV) -> T3: NO TRAP copy=BBBBBBBBBBBBBBB... (len=15); NOLIFE(base): all expectations met and NOLIFE(fixed): all expectations met; fix.diff adds the _KERNEL copying branch at prop_string.c:188 (git-apply-able, hunk headers regenerated from a real diff)."
}