# DF-5692 — kernel kfnmatch() semantic divergences vs userland fnmatch(3) (differential rig)

## What this pack proves

Pass-2 audit of `sys/libkern/fnmatch.c` (225 LOC). The file is **memory-safe**
(machine-proven, see below) and **not reachable by unprivileged users** (only
consumers are ipfw/ipfw3/ip6fw `iface_match()` with root-installed rule
patterns vs kernel-set `if_xname` strings, flags always 0). What the audit
found is a set of **semantic divergences between the kernel matcher and the
DragonFly userland `fnmatch(3)`** running on the same OS — demonstrated by a
differential rig that compiles the kernel source **verbatim** in userland.

### Divergence classes (baseline: 67,195 / 32,303,668 corpus cases)

| class | example | kernel | libc | root cause |
|---|---|---|---|---|
| A. trailing `\` (66,505 cases) | pattern `\ vs string `\ | match | FNM_NOMATCH | fnmatch.c:140-146 treats trailing backslash as literal (POSIX/glibc behavior); libc r254091 returns NOMATCH |
| B. signed-char ranges (651 cases) | pattern `[a-\xff]` vs `\x80` | NOMATCH | match | fnmatch.c:170 `char c, c2` + :215 `c <= test && test <= c2` — bytes ≥ 0x80 are negative on signed-char archs (x86_64) |
| C. unterminated `[` + `/` + FNM_PATHNAME (~39 cases) | pattern `[\/` vs `/[` | NOMATCH | match | fnmatch.c:198-199 returns RANGE_NOMATCH (hard fail) where libc falls back to literal `[` |
| D. MAXNEST depth cap | pattern `*a`×20 vs `a`×22 | NOMATCH | match | fnmatch.c:51,62-63 recursion cap: ≥20 star-globs silently fail |

### Kernel reachability (all four classes: NONE today)

- Consumers: `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` — all `kfnmatch(pattern, ifp->if_xname, 0)`.
- Pattern side root-gated: raw-socket attach `caps_priv_check(SYSCAP_NONET_RAW)`
  (`sys/netinet/raw_ip.c:473`, IPv6: `sys/netinet6/raw_ip6.c:530`).
- String side kernel-bounded: `if_xname[IFNAMSIZ]`, IFNAMSIZ=16
  (`sys/net/if_var.h:326`, `sys/net/if.h:54-56`) ⇒ max 15 chars.
- Class D needs a *successful* libc match at recursion depth 20, which consumes
  ≥19 string chars (one literal between consecutive collapsed stars,
  fnmatch.c:84-87,111-118) — impossible with a 15-char ifname.
- Classes A-C need `\`, high bytes, or `[`/`/`-with-PATHNAME in the *pattern*;
  consumer flags are always 0 (no PATHNAME) and ifname globs are ASCII —
  only a misconfigured root rule could express them.

### Memory-safety negative result (machine-checked)

`guardfuzz.c` places pattern and string so the terminating NUL is the last
readable byte before a PROT_NONE page: **32,445,104 cases, 0 overreads**
(baseline and fixed). The file also contains no writes at all — it cannot
overflow/UAF/leak; worst-case runtime with the 15-char ifname bound is
measured at ≤ 0.45 ms (`*a`×10 failing match vs 16 chars).

## Reproduce

On the DragonFly guest (source tree at /usr/src):

    ./build.sh     # compiles difftest + guardfuzz (baseline and fixed)
    ./run.sh       # runs the full rig, ~4 minutes

Key artifacts: `difftest2.log` (baseline differential), `guardfuzz.log`
(overread proof), `difftest_fixed.log` (after fix.diff), `fix.diff`
(signedness fix, validated: other-class divergences 690 → 39).
