DF-5697 / verdict.json
{ "finding_id": "DF-5697", "status": "reproduced", "reproduced": true, "impact": "dos", "confidence": "certain", "verdict": "Reproduced against the verbatim sys/libkern/qsort.c in the userland rig (host zig cc 18.1.6 AND DragonFly guest gcc 8.3): with caller-supplied es == 0 and n >= 7 the sort never terminates - every pointer advance is a multiple of es == 0, so the partition loop (qsort.c:136-159) spins forever calling the comparator with pb frozen (5,000,000+ comparator calls observed for n=64, which needs <3000) - and the very first pivot swap (qsort.c:132, pm == a because the n<7 insertion path is skipped) drives swapfunc(a, a, 0, 1) whose swapcode do-while (qsort.c:48-57) executes its body once at n == 0, an out-of-contract 8-byte load/store at the zero-length arrays base (proven by SIGSEGV with base inside a PROT_NONE page, before any comparator call). n <= 6 returns cleanly; the two-line if (es == 0) return; guard eliminates both symptoms (guard-only variant f7 returns for n=0..8 with 0 comparator calls and no fault). No in-tree caller passes es == 0 (all five call sites use sizeof-constants: ext2_htree.c:587, linux_sort.c:36, vinumio.c:760, ng_ppp.c:1747, ng_ppp7:2292), so this is hardening of a core kernel primitive - severity Info; the demonstrated artifact is a non-terminating kernel-context spin (impact dos) should any future caller derive es from untrusted input.", "exploit_chain": "", "evidence": "run.log (host): ES0 HANG CONFIRMED: comparator called 5,000,000 times with es=0, ES0 ACCESS CONFIRMED (verbatim): SIGSEGV inside kqsort with es=0 n=7, es0_segv(2): child exited st=0 (no fault) for the guard-only fix, ALL_EXPECTATIONS_MET; run_es0.log (guest gcc 8.3, same symptoms); VERDICT.md (mechanism + boundary n<=6 vs n>=7); fix.diff (2-line guard)", "kernel_refs": [ "sys/libkern/qsort.c:48-57", "sys/libkern/qsort.c:62-69", "sys/libkern/qsort.c:71-77", "sys/libkern/qsort.c:113-119", "sys/libkern/qsort.c:132", "sys/libkern/qsort.c:136-159", "sys/libkern/qsort_r.c:9" ], "poc_changes": "Authored from scratch (no seed): fork+waitpid SIGSEGV proof for the 8-byte access (an ASan build was impossible on this host - no system cc and the zig/musl ASan runtime lacks __asan_stack_malloc; the PROT_NONE page proof is equally decisive and runs on the guests native cc too), comparator-call watchdog exit for the hang proof, guard-only fixed variant qsort_fixed7.c to isolate the fix ingredient from DF-5696s type fix.", "attempts": 2, "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": 95, "guest_dirty": 0, "build_cmd": "sh build.sh (host zig cc; guest build uses the DF-5696 packs guest_build.sh pattern)", "run_cmd": "sh run.sh", "code_hash": "7bf6733a06a63fe243efca8622f5cd46c8cda0d88072b7c4cea7752097a155a7", "notes": "exit code 7 = hang confirmed, es0_segv rc=0 = SIGSEGV confirmed. The 8-byte self-swap writes back the same value on writable pages - it is an out-of-contract access/UB, not value corruption; the hang is the practical kernel impact. Guest es0 evidence was collected during the DF-5696 guest session (run_es0.log); guest has since been reset with-src.", "recommended_fix": "Reject the degenerate element size at function entry: if (es == 0) return; before the loop label in the shared qsort.c body (covers kqsort and kqsort_r).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "A/B on identical verbatim source: without the guard, es=0 n>=7 hangs (5M+ comparator calls) and faults on the 8-byte access (SIGSEGV at the PROT_NONE base); with the 2-line guard only (qsort_fixed7.c), n=0..8 all return immediately with 0 comparator calls and no memory access. A patched-kernel build is not applicable: no in-tree caller can pass es==0, so a kernel build would show no observable delta.", "fix_evidence": "run.log: hang rc=7 and SIGSEGV on verbatim; guarded f7 (es==0 guard only) es=0 n=0..8: returned, cmp_calls=0 and es0_segv(2): child exited st=0 (no fault); fix.diff" } |