⬢ DragonFlyBSD Kernel Audit
DF-5674 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5674",
  "status": "reproduced",
  "verdict": "Race reproduced on-guest against libprop built from the exact kernel sources (/usr/src/sys/libprop). _prop_dict_keysym_free (prop_dictionary.c:233-242) runs _prop_rb_tree_remove_node on the global keysym rb-tree with NO mutex because the keysym object type (prop_dictionary.c:148-153) registers no pot_lock; prop_object_release (prop_object.c:1077) only serializes frees through po_type->pot_lock. A trylock probe counted 88-1145 unlocked keysym frees per short run; 11/11 baseline runs SIGSEGV inside _prop_rb_tree_remove_node/_prop_rb_tree_insert_node (the LOCKED inserter died on a tree torn by the UNLOCKED remover); control with removes disabled survived 3/3. Fix (keysym type takes the global lock + recursive userland mutex init) validated: 10/10 identical workloads survive. Impact: userland heap corruption/UAF in any multithreaded libprop consumer using prop_dictionary_remove/all_keys; in-kernel the defect is compiled in and the plist copyin path (sys_vquotactl, vfs_quota.c:346) provides the locked side from unprivileged users, but the only in-tree unlocked side (udev_dict_delete_key -> prop_dictionary_remove, kern_udev.c:233/405) is dead code with zero callers, so kernel exploitation is latent, not currently triggerable. Fix validated in userland only; kernel rebuild not performed (no in-kernel trigger to A/B).",
  "reproduced": true,
  "impact": "none",
  "confidence": "certain",
  "exploit_chain": "userland: thread A prop_dictionary_remove(d1,K) drops the last keysym ref -> unlocked _prop_rb_tree_remove_node rebalances the global tree; thread B prop_dictionary_set(d2,K,...) -> _prop_dict_keysym_alloc find/insert (mutex-held) walks the torn tree -> wild rb_node pointer chase -> heap UAF/corruption (SIGSEGV on guest; with attacker-shaped heap reuse this is the standard rb-tree-race corruption primitive). kernel (latent, requires one future caller of prop_dictionary_remove / all_keys): unprivileged user pins CPU0 with vquotactl plist parses (locked inserts of chosen key strings) while CPU1 runs the unlocked keysym release -> kernel rb-tree corruption -> UAF. No in-kernel PoC today (dead trigger).",
  "evidence": "run.log (5/5 SIGSEGV + serial console signal-11 lines); run.2.log (dladdr: crashes in _prop_rb_tree_remove_node 5/5); run.3.log (probe DF5674_unlocked_frees=88..1145, victim _prop_rb_tree_insert_node = locked side; noremove control survived); fixrun-summary.txt + fixrun logs (fixed lib 10/10 survived); fix.diff (validated); console_crash.txt; env.txt",
  "kernel_refs": [
    "sys/libprop/prop_dictionary.c:148-153",
    "sys/libprop/prop_dictionary.c:203-206",
    "sys/libprop/prop_dictionary.c:233-242",
    "sys/libprop/prop_dictionary.c:294-341",
    "sys/libprop/prop_dictionary.c:843-871",
    "sys/libprop/prop_dictionary.c:1006-1011",
    "sys/libprop/prop_dictionary.c:1082-1107",
    "sys/libprop/prop_dictionary.c:1123",
    "sys/libprop/prop_object.c:1061-1107",
    "sys/libprop/prop_array.c:72-80",
    "sys/kern/kern_udev.c:229-235",
    "sys/kern/kern_udev.c:405",
    "sys/kern/vfs_quota.c:344-349"
  ],
  "poc_changes": "PoC authored from scratch (no seed): race_keysym.c minimal reproducer; race_attr.c adds SIGSEGV handler with dladdr symbol attribution (guest gdb cannot enumerate non-primary threads); race_probe.c adds unlocked-free counter fed by a trylock probe compiled into an instrumented copy of prop_dictionary.c; libprop baseline/probe/fixed variants built directly from /usr/src/sys/libprop on the guest (no installed prop headers in the image; include path -I/usr/src/sys, <libprop/proplib.h>).",
  "attempts": 24,
  "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": 2700,
  "guest_dirty": 1,
  "build_cmd": "sh build.sh  (see pack: builds /tmp/proporig,/tmp/proprop,/tmp/propfix libs + 4 harnesses from /usr/src/sys/libprop)",
  "run_cmd": "sh run.sh   (baseline 5 runs / control / probe 2 runs / fixed 10 runs)",
  "code_hash": "0e88dca92043f63bac47fdea3661ce9e89290710b368d003118825bd1521c5d2 (race_keysym.c); 6912f2bf238d8364664b506a4c92feeb34259cdf53a06b65c971640913609561 (race_attr.c); 4d0ebaad6349ca3175f5a359df7bda972a8dfa6941f69fde6039c22980349b59 (race_probe.c)",
  "notes": "Excluded per instructions: DF-2225 (once-init race), DF-2226 (empty key). Cross-file leads for the orchestrator: (1) prop_kern.c:383 prop_object_copyin_limit is DEFINED but never enforced \u2014 _prop_object_copyin (prop_kern.c:398) kmallocs pref->pref_len+1 straight from the user-supplied plistref with no clamp; reachable unprivileged via sys_vquotactl (vfs_quota.c:346); pref_len=SIZE_MAX wraps the +1 to 0. prop_kern.c is marked pass-1 clean \u2014 recommend re-opening. (2) vfs_quota.c:404 reassigns dict, leaking the copyind dictionary every vquotactl call (kernel memory exhaustion). (3) prop_number.c:152-173 has the same interned-rb-tree + static mutex pattern as the keysym tree (check its free paths). (4) prop_rb.c leads (next file): _prop_rb_tree_find/_prop_rb_tree_insert_node assume caller-held exclusion (prop_dictionary provides it only partially \u2014 this finding); DF-2227 already filed for the attr-value scan. Guest reset with-src performed before returning.",
  "recommended_fix": "Register .pot_lock/_prop_dictionary_lock and .pot_unlock/_prop_dictionary_unlock on _prop_object_type_dict_keysym (prop_dictionary.c:148-153) and make the userland _PROP_MUTEX_INIT use PTHREAD_MUTEX_RECURSIVE (prop_object_impl.h:417) so the nested dictionary-free -> keysym-free path can re-take the same mutex (kernel lockmgr side is already LK_CANRECURSE); validated in fix.diff with 10/10 surviving runs.",
  "fix_status": "fixed",
  "fix_baseline_reproduced": true,
  "fix_patched_reproduced": false,
  "fix_verdict": "Library rebuilt from /usr/src/sys/libprop with fix.diff (userland A/B, identical workload): baseline 11/11 SIGSEGV vs fixed 10/10 survived. Bad behavior gone. Kernel rebuild not performed: the unprotected release has no live in-tree kernel caller (udev_dict_delete_key dead code), so no in-kernel A/B exists; kernel lockmgr is already recursive so the fix nests there by construction.",
  "fix_evidence": "fixrun-summary.txt; fixrun.1-10.log (survived/exit=0); run.log & run.2.log (baseline crashes); fix.diff"
}