DF-5692 / verdict.json
{ "finding_id": "DF-5692", "status": "reproduced", "reproduced": true, "impact": "none", "confidence": "certain", "verdict": "sys/libkern/fnmatch.c is memory-safe and unreachable by unprivileged input (only consumers are ipfw/ipfw3/ip6fw iface_match() with root-installed rule globs matched against kernel-set if_xname, flags=0, raw-socket creation priv-gated at raw_ip.c:473/raw_ip6.c:530). A differential rig compiling the kernel source verbatim (md5-verified against /usr/src) against DragonFly libc fnmatch on the guest found 67,195 divergences in 32,303,668 cases across four classes: (A) trailing backslash treated as literal where libc returns FNM_NOMATCH (66,505; fnmatch.c:140-146); (B) signed-char bracket ranges wrong for bytes >=0x80 (651; fnmatch.c:170,215); (C) unterminated [ + / under FNM_PATHNAME hard-fails where libc falls back to literal (~39; fnmatch.c:198-199); (D) MAXNEST=20 recursion cap makes >=20-star-glob patterns silently FNM_NOMATCH (fnmatch.c:51,62-63,112). None is reachable through current consumers: flags are always 0 (kills C), ifname globs are root-authored ASCII (kills A/B), and if_xname is capped at 15 chars (IFNAMSIZ=16, if_var.h:326) while a successful depth-20 match needs >=19 string chars (kills D). Guard-page harness: 0 overreads in 32,445,104 cases (no write exists in the file; no leak surface). Worst-case runtime at the 16-byte ifname bound is 0.45 ms, so no ReDoS either. Impact ceiling: silent misbehavior of root-authored firewall globs only; filed as Info hardening. fix.diff (unsigned-char ranges) validated in-rig: class B eliminated (690->39 other-class divergences, targeted probes 4/4 kernel==libc after fix).", "exploit_chain": "", "evidence": "difftest2.log: TOTAL divergences 67195/32303668 (trailing-backslash 66505, other 690) and [maxnest] first divergence at 20 stars and [signed] kernel=1 libc=0 for [a-\\xff] vs 0x80/0xfe; guardfuzz.log: 32445104 cases, 0 overreads; difftest_fixed.log: TOTAL 66544 (other 39) after fix.diff; [time-ifname-worst] stars=10 n=16 kernel: 0.000449 s; VERDICT.md for the full per-class trace with kernel citations.", "kernel_refs": [ "sys/libkern/fnmatch.c:51", "sys/libkern/fnmatch.c:62-63", "sys/libkern/fnmatch.c:112", "sys/libkern/fnmatch.c:140-146", "sys/libkern/fnmatch.c:170", "sys/libkern/fnmatch.c:198-199", "sys/libkern/fnmatch.c:215", "sys/net/ipfw/ip_fw2.c:1355-1360", "sys/net/ipfw3_basic/ip_fw3_basic.c:134-140", "sys/net/ip6fw/ip6_fw.c:323-330", "sys/netinet/raw_ip.c:473", "sys/netinet6/raw_ip6.c:530", "sys/net/if_var.h:326", "sys/net/if.h:54-56" ], "poc_changes": "No runnable seed existed; built the full rig: shim.h (kernel headers swapped for userland equivalents, flag values verified identical to sys/sys/libkern.h:177-186), fnmatch_k.c (verbatim kernel source, include-swap only, diff-verified), difftest.c (32.2M-case differential + bracket sweep + MAXNEST probe + signed-range probes + worst-case timing), guardfuzz.c (guard-page overread detector), fnmatch_k_fixed.c + fix.diff (signedness fix A/B). First calibration run hit the exponential path on a 40-char/26-star probe and was killed; probes rescaled to string length k+2.", "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": 480, "guest_dirty": 0, "build_cmd": "cc -O2 -Wall -o difftest difftest.c fnmatch_k.c && cc -O2 -Wall -o guardfuzz guardfuzz.c fnmatch_k.c (plus *_fixed variants)", "run_cmd": "./difftest > difftest2.log; ./guardfuzz > guardfuzz.log; ./difftest_fixed > difftest_fixed.log; ./guardfuzz_fixed", "code_hash": "sha256:3d28d1a449b03812dd506e8332c3d79adcb0408f3b74e54b55386a22b4580401", "notes": "Memory-safety negative result is machine-checked (guard pages, 32.4M cases, 0 overreads, baseline and fixed); the file contains zero stores, so memory-corruption classes are structurally absent. Orchestrator hint that devfs rules / jail params consume fnmatch was checked and refuted (rg over sys/: only ipfw/ipfw3/ip6fw). ReDoS angle killed by measurement: worst case at the if_xname 15-char bound is 0.45 ms per call; exponential growth (~x5.5 per +4 chars) only matters for string lengths no kernel consumer supplies. IDs DF-5693..DF-5704 reserved but unused (single merged Info finding). Guest left up and healthy; only ~/fnm rig files remain on it.", "recommended_fix": "Apply the 3-line unsigned-char signedness fix to rangematch() (fix.diff); for full semantic parity with userland fnmatch(3) (trailing-backslash, PATHNAME-unterminated-bracket, MAXNEST>=20 silent NOMATCH), replace the recursive kernel matcher with the iterative algorithm from lib/libc/gen/fnmatch.c (byte-oriented variant).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff (unsigned char c,c2 + two (unsigned char)test casts in rangematch) eliminates the signed-range divergence class in the rig: baseline 690 other-class divergences -> 39 after fix (all remaining are the separate unterminated-bracket/PATHNAME class), targeted [signed] probes agree with libc 4/4 afterwards, guard-page harness still 0 overreads on the patched source. Classes A/C/D are algorithm-level and require adopting the libc algorithm; documented in VERDICT.md, not addressed by this diff.", "fix_evidence": "difftest_fixed.log: TOTAL divergences: 66544 / 32303668 cases (trailing-backslash class: 66505, other: 39) and [signed] lines showing kernel=0 libc=0 for [a-\\xff] cases; fix_guard_run.log: 0 overreads; compare difftest2.log baseline other: 690.", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 x86_64 (rig-level validation: verbatim kernel source compiled userland; kernel rebuild not required for an Info/no-impact finding)" } |