# DF-3450 VERDICT

**Status: reproduced (race-condition kernel panic, 2/2 baseline runs, both
fault variants). Fix: validated (two-sided; single-sided attempt failed and
is itself documented).**
**Impact: panic (root-gated kldunload racing unprivileged/remote-triggered
proto-4 packet dispatch; control-flow divert — NULL-call or jump into
unmapped module text).**

## Root cause (source)

- `sys/netinet/ip_encap.c:231-235` — encap4_input's backward-compat path:
  ```c
  if (proto == IPPROTO_IPV4 && ipip_input) {   /* read #1 */
	  *mp = m;
	  ipip_input(mp, offp, proto);         /* read #2 + call */
  ```
  No lock/token around the read-test-call, and the global is read TWICE.
  `ipip_input` storage lives in this (always-compiled) file; the value
  points into the ip_mroute KLD.
- `sys/net/ip_mroute/ip_mroute.c:3266/3285` — MOD_LOAD/MOD_UNLOAD swap the
  hook under `mroute_token` — a token the netisr-side reader never takes,
  so it provides no mutual exclusion; MOD_UNLOAD additionally performs **no
  netisr drain** before returning, after which the linker unmaps the module
  text (`vm_map_remove`-style teardown ⇒ execute faults, not just stale
  heap reads).

Two fatal interleavings, both observed:
1. **call-NULL**: reader passes test on the old value, unload stores NULL
   between read #1 and read #2, reader calls 0x0.
2. **execute-after-unmap**: reader calls the (valid) old pointer and is
   executing X_ipip_input/rip_input when the unload completes and the text
   is unmapped mid-execution.

Distinct from DF-0689 (the encaptab *list* walk vs LIST mutation — different
data structure and different fix) and not covered by the DF-3425/DF-3433
mp_token fix (that patch touches encap_attach/attach_func/detach only).
This is the `ipip_input` *function-pointer hook* read at ip_encap.c:231.

## Reproduction (guest)

Harness: `sprayer.c` floods crafted proto-4 packets to 127.0.0.1 (correct
IP checksum; IP_HDRINCL raw socket). Non-TCP/UDP protocols hash to 0
(ip_demux.c:315-318) so every packet is processed on netisr0 — the exact
context the `IN_NETISR(0)` path requires. 96 raw IPPROTO_IPV4 sockets fan
out per packet through rip_input (delivery counter proves X_ipip_input ran:
201 M receives over the run) and widen the in-module execution window.
`racer.sh` loops kldload/kldunload ip_mrace (mrouting never configured, so
`ip_mrouter == NULL` and unload is permitted, ip_mroute.c:3281).

- Run 1: panic; `fault virtual address = 0x0`, `fault code = supervisor read
  instruction`, `instruction pointer = 0x8:0x0`, `current process = Idle`
  (netisr0). Variant (1).
- Run 2 (fresh reset, same harness): panic after **23 s**; `instruction
  pointer = 0x8:0xffffffff82600275` — inside the freshly-unloaded module's
  text (`kldstat` showed ip_mrace.ko at 0xffffffff82600000+0x12000).
  Variant (2).

Setup is root-gated (kldunload); the racing packet stream can be produced by
any local user or an on-link remote peer (proto 4 to a local address).

## Fix

`fix.diff` (two sides, both required):

1. **Reader** (sys/netinet/ip_encap.c): snapshot the hook once into a local
   and call the local — eliminates the double-read call-NULL window.
2. **Unloader** (sys/net/ip_mroute.c): after NULLing the hooks, drain netisr
   (`netmsg_service_sync()`) before returning so the module text cannot be
   unmapped under a live reader — the same discipline the dummynet module
   unload already uses.

Validation history (honest):
- Attempt 1 (unload-side sync only): **still panicked** at 89 s with RIP=0x0
  (`fixattempt1_single_sided.log`) — proving the reader's double-read is
  independently fatal and the reader-side hunk is required.
- Attempt 2 (two-sided, kernel rebuilt: `X86_64_GENERIC #1 Sat Sep 12
  23:27:53 UTC 2026`, module rebuilt): **480 s continuous race, 115,650
  load/unload cycles, 16.8 M sprayed packets, zero crashes, guest alive**
  (baseline died in 23–89 s).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
