DF-5699 / verdict.json
{ "finding_id": "DF-5699", "status": "reproduced", "verdict": "prop_dictionary_ingest() (sys/libprop/prop_ingest.c:121-159) obtains each rules value with the borrowing accessor prop_dictionary_get() (prop_ingest.c:133), which copies pde->pde_objref out without prop_object_retain and drops the dictionary rwlock before returning (prop_dictionary.c:933-945, unlock at :944). The walker then runs the arbitrary driver handler (prop_ingest.c:152) for unbounded time on that unpinned pointer with no lock held, repeating for every table entry: any concurrent prop_dictionary_remove()/same-key set/release of the dictionary frees the object underneath the handler \u2014 a use-after-free handed directly to consumer callback code, and handlers are documented to stash the object into driver state, extending the dangling lifetime arbitrarily. Reproduced deterministically (3/3 runs, SIGBUS on attempt 1) on the same-source userland rig (/usr/src/sys/libprop compiled as-is, unprivileged guest user): control (spray, no free) stable; experiment (prop_dictionary_remove under the sleeping handler) faults inside prop_object_type(borrowed) on the 0x4141-reclaimed header. Fixed by pinning (prop_object_retain/release around the dispatch): patched rig survives identical abuse 2/2 with type/value stable and ingest=true. Severity Low because the kernel instantiation is latent today \u2014 sys/conf/files:2249 compiles the file standard into every kernel, but rg shows zero in-tree callers; the same source ships as the base userland libprop (/usr/include/libprop/prop_ingest.h) where the hazard is live now for multi-threaded programs of any privilege.", "reproduced": true, "impact": "dos", "confidence": "certain", "exploit_chain": "Same-source userland rig: thread A prop_dictionary_ingest() on {\"victim\": prop_number 0x1337beef00} with a handler that snapshots obj and sleeps 400ms; thread B prop_dictionary_remove(dict,\"victim\") during the sleep (number not interned -> dict held the only ref -> freed on the spot) then sprays 0x41-filled 40..96B allocations; handlers post-sleep prop_object_type(borrowed) derefs po_type=0x4141414141414141 -> SIGBUS (exit 138) on attempt 1, 3/3 runs. In-kernel: no chain exists yet \u2014 zero in-tree callers (rg prop_ingest|PROP_INGEST sys/ outside libprop matches only sys/conf/files:2249); the primitive for a future kernel consumer is freed-object reuse read (and stale pointer retained into driver state) inside a supposedly-safe callback.", "evidence": "run.log/run.2.log/run.3.log: control stable then attempt 1 exit=138 ... Bus error (core dumped) between post-sleep read-back begins and the absent survived line \u2014 crash is exactly the UAF read. fix_run.log/fix_run.2.log: FIX VALIDATION PASSED (handler survives, type 1852662386 -> 1852662386, value 0x1337beef00 -> 0x1337beef00, ingest=true). VERDICT.md has the full path:line trace.", "kernel_refs": [ "sys/libprop/prop_ingest.c:121", "sys/libprop/prop_ingest.c:131", "sys/libprop/prop_ingest.c:133", "sys/libprop/prop_ingest.c:147", "sys/libprop/prop_ingest.c:152", "sys/libprop/prop_dictionary.c:909", "sys/libprop/prop_dictionary.c:933", "sys/libprop/prop_dictionary.c:944", "sys/conf/files:2249" ], "poc_changes": "No seed existed (pass-2 new finding); harness authored from scratch: control/experiment differential (DF5699_CONTROL env), 0x41 sentinel size-class spray 40..96B, 400ms free window, prop_number victim (not interned \u2014 prop_number.c:273 has no intern tree), crash-or-differential dual detection.", "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": 90, "guest_dirty": 0, "build_cmd": "sh /tmp/df5699/build.sh", "run_cmd": "sh /tmp/df5699/run.sh", "code_hash": "ece6127af81ec22135a713669ac703459f7dbd15404f850872ac4989b8105547", "notes": "Guest stayed up throughout (userland crash only; /tmp/df5699 incl. core dumps removed before return). Full fixed-kernel build skipped as meaningless: the function has zero in-tree callers, so kernel behavior cannot differ; rig A/B isolates the one-hunk fix exactly. Same borrowed-pointer class as DF-5690/DF-5691 but in the core walk API \u2014 every consumer inherits it for every key. Pass-2 negative results for this file (terminator walk, type-gate semantics, error-path/no-unwind contract, ctx reuse staleness, NULL handling of prop_object_type/prop_dictionary_get) recorded in VERDICT.md.", "recommended_fix": "Pin the object across the handler callback: prop_object_retain(obj) before the dispatch at prop_ingest.c:152, prop_object_release(obj) on the handler-failed return and after the successful call (see fix.diff).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Rig-level A/B: baseline (unmodified /usr/src/sys/libprop) SIGBUS 3/3 at the handlers UAF read; patched rig (identical sources + only the retain/release hunk from fix.diff) survives the identical abuse 2/2 with type/value stable and ingest=true. Kernel rebuild not performed \u2014 zero in-tree callers means no kernel behavior can differ; the rig compiles the exact audited source.", "fix_evidence": "fix_run.log + fix_run.2.log end with FIX VALIDATION PASSED: object pinned across handler, no crash, no differential; compare run.log/run.2.log/run.3.log baseline attempt 1 exit=138 ... Bus error (core dumped). fix.diff sha256 a14cf526a13fbbaa21f98e558b643f5d80a3bef86c23e04e83a42eee30c6ed51." } |