DF-5675 / verdict.json
{ "finding_id": "DF-5675", "status": "reproduced", "reproduced": true, "impact": "panic", "confidence": "certain", "verdict": "Kernel thread-stack exhaustion reproduced on the guest: a 192-level nested <array> plist (2.9KB XML) sent through NETBSD_DM_IOCTL on /dev/mapper/control (0640 root:operator) kills the kernel with Fatal double fault at a page-aligned rsp (0xfffff80117e6b000) \u2014 the recursion in _prop_array_externalize/_prop_dictionary_externalize (64B/frame, objdump-verified in /boot/kernel/kernel) runs the 16KB kstack off its end, while the iterative internalize path accepts any depth. Depths 64/128 survive, 192+ panics; depth 2048 also panics. fix.diff (externalize depth cap 64) validated by full kernel rebuild+reboot on the guest: depths 192 and 2048 now return clean ENOMEM with the guest healthy, depths 32/60 still externalize and echo back (non-regressed).", "exploit_chain": "operator/root opens /dev/mapper/control -> ioctl(NETBSD_DM_IOCTL, plistref{xml with depth-N nested arrays}) -> prop_dictionary_copyin_ioctl internalizes iteratively (any depth OK) -> dmioctl unconditionally copyout_ioctl(dm_dict_in) -> prop_dictionary_externalize recurses pot_extern per nesting level -> kernel stack (16KB) exhausted at ~192 levels -> frames write below kstack into adjacent kmem then fault -> Fatal double fault -> panic. Guest evidence: two independent panics captured; depth ladder attributes the cliff between 128 and 192; userland same-source differential (internalize OK @20000 / externalize SIGSEGV) proves the code asymmetry.", "evidence": "panic.txt (first Fatal double fault, rsp=0xfffff8011891d000); panic.2.txt (attributed: DF5675_DEPTH=64/128 SURVIVED, =192 -> DOUBLE FAULT, rsp=0xfffff80117e6b000); fixtest.console.txt (patched kernel: depths 64/192/2048 all rc=0, ENOMEM for deep); run.userland.log (same-source differential: internalize OK depth 20000, externalize SIGSEGV exit 139); build.kernel.log (kernel rebuild for fix).", "kernel_refs": [ "sys/libprop/prop_object.c:617-659", "sys/libprop/prop_object.c:1061-1107", "sys/libprop/prop_array.c:174-186", "sys/libprop/prop_dictionary.c:455-467", "sys/libprop/prop_kern.c:486-503", "sys/dev/disk/dm/device-mapper.c:181", "sys/dev/disk/dm/device-mapper.c:267-273", "sys/cpu/x86_64/include/param.h:126", "sys/sys/thread.h:472" ], "poc_changes": "Trigger written fresh (no seed): plistref mirror + _IOWR(0xfd,0,16) ioctl; ladder mode prints each depth to the serial console BEFORE the ioctl so the killing depth is attributable after ssh dies; single-depth mode for fix validation.", "attempts": 4, "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": 1500, "guest_dirty": 0, "build_cmd": "sh build.sh (guest root: same-source libprop rig + cc -O2 df5675_dmcrash.c df5675_userland.c)", "run_cmd": "sh run.sh (kernel run: kldload dm; ./df5675_dmcrash [depth]; panic lands on serial console / vm.sh log)", "code_hash": "3f74caf75bceb96703d79d931369b955b33066cb57a649af0d237888b5498ebf", "notes": "Reachability bar identical to DF-2227/DF-2231 (operator group or root; all current kernel echo paths are 0640/0600). The 16KB kstack math (UPAGES=4) and the measured 64B kernel frame put the cliff at 128<N<=192 \u2014 observed exactly. Below-stack frame writes precede the fault (latent adjacent-kmem corruption) but the double-fault handler converts the run to a deterministic panic; uid0 escalation not pursued for that reason.", "recommended_fix": "Cap externalize nesting depth: refuse to externalize containers nested deeper than _PROP_EXTERNALIZE_MAX_DEPTH (64) so the recursive walk can never exhaust the kernel stack.", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "fix.diff applied inside guest /usr/src, kernel rebuilt (nativekernel, 319s) and booted: baseline depth-192 Fatal double fault is GONE; depths 192 and 2048 return ENOMEM with guest healthy; depths 32/60 still externalize and echo (errno=0). Guest reset to clean-source snapshot afterwards.", "fix_evidence": "fixtest.console.txt (FIXTEST_DEPTH=64/192/2048 all FIXTEST_done rc=0), patched-kernel ioctl outputs (errno=12 for 192/2048, errno=0 for 32/60), build.kernel.log, fix.diff" } |