DF-5691 / verdict.json
{ "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)." } |