DF-5696 / verdict.json
{ "finding_id": "DF-5696", "status": "reproduced", "reproduced": true, "impact": "none", "confidence": "certain", "verdict": "Reproduced in the userland differential rig on the DragonFly guest (verbatim sys/libkern/qsort.c, sha256-pinned, compiled by the guests own gcc 8.3): partition byte-distances are kept in int variables (qsort.c:109); at a 2,160,000,000-byte partition the truncated (int)r = -2,134,967,296 passes the if ((r = pd - pc) > es) guard because r (int) is promoted to size_t against es, so a = pn - r; n = r / es; goto loop restarts the sort with the array pointer 2,134,967,296 bytes PAST the end and n ~ 2^64 - the comparator then receives args at base+7,894,967,296 and base+7,628,096,384 (an OOB read in any dereferencing comparator; the following swap(a, pm) is an 8-byte OOB write). Second family member demonstrated at 17.2 GiB: d = (n/8)*es wraps int and med3 feeds the comparator a pointer 2,144,967,296 bytes BELOW the array. Patched variant (size_t d/r2, size_t swapfunc n) sorts the identical 5.76 GiB input correctly (8.8e9 comparisons, 222 s) and shows 0 OOB med3 args. In-tree kernel reachability today: NONE - all five callers (ext2_htree.c:587, linux_sort.c:36, vinumio.c:760, ng_ppp.c:1747, ng_ppp7:2292) sort at most a few KB, far below the 2.16 GiB threshold - so impact is a latent primitive memory-corruption defect (severity Low), not a live kernel bug. Differential fuzz (28k arrays incl. odd es, unaligned bases, kqsort_r thunk checks) and 7k adversarial-comparator runs show all other behavior byte-identical to libc and memory-safe.", "exploit_chain": "", "evidence": "run.log (guest, full matrix, ALL_EXPECTATIONS_MET): lines with TRUNCATION OOB CONFIRMED ... x-base=7894967296 y-base=7628096384, PATCHED(f6) ... patched: array IS SORTED, OOB COMPARATOR ARGS CONFIRMED ... y-base=-2144967296 ... n*es=17200000000; VERDICT.md (byte-for-byte offset derivation vs the trace); fix.diff (size_t d/r2 + size_t swapfunc n); env.txt (guest uname, gcc, pinned sha256 of both kernel files); guest_build.log (sha pin verified against /usr/src)", "kernel_refs": [ "sys/libkern/qsort.c:109", "sys/libkern/qsort.c:125", "sys/libkern/qsort.c:132", "sys/libkern/qsort.c:169-172", "sys/libkern/qsort.c:173", "sys/libkern/qsort.c:179-183", "sys/libkern/qsort.c:63", "sys/libkern/qsort_r.c:9", "sys/vfs/ext2fs/ext2_htree.c:587", "sys/dev/drm/linux_sort.c:36", "sys/dev/raid/vinum/vinumio.c:760", "sys/netgraph/ppp/ng_ppp.c:1747", "sys/netgraph7/ppp/ng_ppp.c:2292" ], "poc_changes": "Authored the rig from scratch (the finding had no seed PoC): verbatim-source compilation with symbol renames, bounds/alignment-checking counting comparator with ring-buffer arg tracing, byte canaries with absolute-pattern phase (an initial slice-relative phase bug produced false canary hits and was fixed after standalone replay disproved them), fork-based verdict exits, chunked MAP_FIXED contiguous mapping helper, patched-variant A/B. Trace mode added to decompose the exact wild-pointer origin; the observed mechanism (unsigned promotion through > es) is stronger than the initially suspected mis-sort.", "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": 418, "guest_dirty": 0, "build_cmd": "sh build.sh (host, zig cc 18.1.6) ; sh guest_build.sh (guest cc 8.3, /usr/src verbatim + sha pin)", "run_cmd": "sh guest_run.sh (guest: fuzz/fuzz_lie/truncate/med3_oob/med3_oob_fixed/es0_*) ; sh run.sh (host small modes)", "code_hash": "05be04f52c6ac1e9c92a8bad7674ec985256615c9740d0b65a48bc4082aa085e", "notes": "Big-array modes need >=8 GiB RAM: the guest was temporarily booted with -m 12288 (vm.sh restored to 4096 and guest reset with-src after the run). The audit host sandbox gates >=3 GiB single mmaps for freshly built binaries (deterministic per-binary, cause not identified), which is why the decisive runs are guest-side. Member-2 demo touches only ~2 pages of a 17.2 GiB NORESERVE mapping (DragonFly lacks MAP_NORESERVE; rig defines it to 0 there). exit codes: 9 = truncate OOB confirmed, 3 = med3 OOB confirmed, 7 = es0 hang (DF-5697).", "recommended_fix": "Keep partition byte-distances and swapfuncs n in size_t: int r, swaptype, swap_cnt; size_t d, r2; with r2 used for the min()/vecswap/recursion/goto distances and swapfunc(char*, char*, size_t, int) - fixes kqsort and kqsort_r identically via the shared body (fix.diff).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Validated by same-input A/B on the guest: verbatim source exits with confirmed OOB comparator args (base+7,894,967,296 / base+7,628,096,384 on the 5.76 GiB input; y-base=-2,144,967,296 on the 17.2 GiB med3 input), while the patched source (fix.diff applied to the identical verbatim copy) sorts the same 5.76 GiB input correctly (8,795,877,536 comparisons) and shows 0 OOB med3 args in the first 1000 calls; 28k-iteration differential fuzz confirms the patched variant is still byte-identical to libc qsort. A patched-kernel build was not applicable: no in-tree caller can reach the >=2.16 GiB threshold, so a kernel build would show no observable delta.", "fix_evidence": "run.log: TRUNCATION OOB CONFIRMED ... (verbatim) vs PATCHED(f6): ... patched: array IS SORTED; OOB COMPARATOR ARGS CONFIRMED ... y-base=-2144967296 vs PATCHED med3: 0 OOB comparator args in first 1000 calls; run_host.log: fuzz 20000 OK with f6 included; fix.diff in this pack" } |