# DF-2541 — ip_mroute X_ipip_input / pim_input race (NULL-deref / panic)

## Verdict: REPRODUCED (panic); fix_status = fix_failed (minimal token fix insufficient)

## What the finding claimed
`X_ipip_input` (sys/net/ip_mroute/ip_mroute.c:1710) and the PIM-REGISTER branch of
`pim_input` (:3020) read `viftable`/`numvifs`/`last_encap_vif`/`reg_vif_num` **without**
holding `mroute_token`, while `del_vif` (:873) and `X_ip_mrouter_done` (:605) mutate exactly
those fields **under** `mroute_token` (including `bzero()` of `viftable` slots at :909 and
:634). A concurrent teardown can therefore zero a vif between validation and use.

## Reachability established on this guest (6.5-DEVELOPMENT #0, X86_64_GENERIC)
1. **`pim_input` is DEAD CODE here.** It is entirely inside `#ifdef PIM`
   (sys/net/ip_mroute/ip_mroute.c:3250 `#endif /* PIM */`). The GENERIC kernel has no
   `options PIM` (sys/config/X86_64_GENERIC), and the **`ip_mroute.ko` module Makefile
   defines only `-DMROUTING`, not `-DPIM`** (sys/net/ip_mroute/Makefile). Confirmed at
   runtime: `sysctl net.inet.pim` ⇒ *unknown oid*; `nm /boot/kernel/kernel | grep pim_input`
   ⇒ empty. The PIM-REGISTER NULL-deref at :3230 is unreachable on this guest.
2. **`X_ipip_input` is reachable** via the `encap4_input()` backward-compat hook
   (sys/netinet/ip_encap.c:231 `if (proto == IPPROTO_IPV4 && ipip_input)`) once the module
   is `kldload`'d (MOD_LOAD sets `ipip_input = X_ipip_input`, :3266). But its first guard
   (:1716 `if (!have_encap_tunnel)`) returns early unless **root has added a `VIFF_TUNNEL`
   vif** (set in `add_vif` :785-786). `MRT_INIT` requires `SOCK_RAW`/`IPPROTO_IGMP` and
   root (`ip_mrouter_init` :571-572). So this is a **root-configured** race — the realistic
   threat is an admin running mrouted while an attacker floods during MRT_DONE/restart.

## Reproduction (root-configured race → panic)
`poc.c` (run as root): opens the IGMP mrouter socket, `MRT_INIT`, adds a `VIFF_TUNNEL`
vif (rmt=10.0.2.99) → `have_encap_tunnel=1`, primes the `last_encap_vif` cache by flooding
IP-in-IP packets (outer proto=4, inner dst=multicast), then races `MRT_DEL_VIF(0)`+re-add
against the packet flood. del_vif `bzero()`s `viftable[0]` (v_ifp→0) while a packet reuses
the stale cached `last_encap_vif`; the decapsulated multicast packet is then forwarded into
torn-down state.

**Observed (unpatched #0, twice):** `Fatal trap 12: page fault while in kernel mode`,
`instruction pointer = 0x8:0x0` (NULL function-pointer call), `current process = Idle`
(netisr context). Reproducible in <2 s of the race loop. See `panic.txt`, `run.log`,
`run.2.log`.

## Mechanism (cited hops)
- `X_ipip_input` :1734-1747 — walks `viftable`/`numvifs` and caches `last_encap_src`/`
  `last_encap_vif` **with no token**.
- `del_vif` :883 acquires `mroute_token`; :888-891 resets the cache (only if
  `vifp == last_encap_vif`); :909 `bzero((caddr_t)vifp, sizeof(*vifp))` → `v_ifp` = 0.
- `X_ipip_input` :1763 `m->m_pkthdr.rcvif = last_encap_vif->v_ifp;` (stale cache ⇒ NULL),
  :1765 `netisr_queue(NETISR_IP, m)` ⇒ ip_input runs with a NULL/torn-down rcvif ⇒
  NULL function-pointer call ⇒ panic.
- Downstream: the decapsulated multicast packet is forwarded by `X_ip_mforward` (:1154) /
  `ip_mdq`, which access `viftable`/`numvifs` **without** the token at entry, so a
  concurrent teardown also corrupts the forwarding path.

## Fix attempt + validation
`fix.diff` adds `lwkt_gettoken(&mroute_token)` to `X_ipip_input` (entry + all 4 return
paths). `lwkt_gettoken` does serialize cross-CPU on contention
(sys/kern/lwkt_token.c:686-716: `_lwkt_trytokref_spin` then `lwkt_switch()`).

Built the patched `ip_mroute.ko`, **objdump-verified** `lwkt_gettoken` at offset 0x14e +
4 `lwkt_reltoken` in `X_ipip_input`, installed (sha256 de5fc1… vs original 7a4a68…),
reloaded, re-ran the SAME PoC. **Result: still panics, identical signature.**

**Why the minimal fix is insufficient (fix_failed):** protecting only `X_ipip_input`'s
`v_ifp` read does not close the race, because the decapsulated multicast packet is
subsequently forwarded by `X_ip_mforward`/`ip_mdq` (:1154+), which read `viftable`/
`numvifs` **without** the token at entry; a concurrent `del_vif`/`X_ip_mrouter_done`
`bzero()`s `viftable` under the token, so the forwarding path still derefs zeroed vif
state ⇒ same NULL-pointer panic.

**Concrete next iteration for a complete fix:** the teardown paths (`del_vif` and
`X_ip_mrouter_done`) must synchronize against in-flight netisr packets before `bzero()` —
e.g. a `netisr` drain/barrier (analogous to `netisr_wait`) on teardown — **and/or** the
entry of `X_ip_mforward`/`ip_mdq` must acquire `mroute_token` for the full viftable walk.
This is a broader locking change beyond a single-function patch.

## PoC changes
- Wrote `poc.c` from scratch (no prior scaffold existed). Fixed `vifi_t` (was u_int8_t; the
  kernel's is `u_short`, sys/net/ip_mroute/ip_mroute.h:82 — first build returned EINVAL),
  and used the correct `SOCK_RAW`/`IPPROTO_IGMP` mrouter socket required by
  `ip_mrouter_init` (:571).

## Impact
Kernel panic / DoS. Reachable only with **root-configured multicast routing** (MRT_INIT is
privileged) plus concurrent reconfiguration; an unprivileged user cannot trigger it. Not a
privilege escalation — no memory-corruption primitive is attacker-shaped here, it is a
torn-down-state deref. Medium severity is appropriate (admin-config DoS; pim_input half is
dead code on GENERIC).
