# DF-2610 — icmp6_redirect_input stale `ip6`/`nd_rd` (latent UAF-read)

## What this investigates

`icmp6_redirect_input` captures `ip6 = mtod(m, ...)` at `icmp6.c:2160`
BEFORE `IP6_EXTHDR_CHECK` at `icmp6.c:2187`. In the non-PULLDOWN build
(no `PULLDOWN_TEST` anywhere in `sys/` — verified) that macro can replace
`m` via `m_pullup` when the mbuf is `M_LOOP` and short
(`netinet/ip6.h:277-306`), and `m_pullup`'s slow path frees the old first
mbuf (`uipc_mbuf.c:2125-2153`). Every later use — `nd_rd` (2188),
`redtgt6/reddst6` (2196-2197), `ip6->ip6_hlim` (2211) — would then read
freed memory. This violates the file's own documented invariant
(`icmp6.c:412`: "m might change if M_LOOP. So, call mtod after this").

## Verdict

**Code-level defect CONFIRMED in the current tree (identical guest
/usr/src, md5-matched). Live manifestation: NOT REPRODUCIBLE from
userspace on this kernel — the reallocating `m_pullup` branch is provably
unreachable for any packet a userspace process can cause to be delivered
to `icmp6_redirect_input`.** The finding's own assessment ("latent class
violation", confidence: speculative) is accurate.

## Live evidence (run.log, full untrimmed)

- **Path traversal proven**: raw-socket ND_REDIRECT (type **137**) sends
  to `::1` with src forced to `fe80::1` and hlim 255 traverse the entire
  validation block — dmesg shows
  `ICMP6 redirect rejected; not equal to gw-for-src=::0001 (must be same): (src=fe80:0002::0001 dst=::0001 tgt=fe80:0002::)`
  which is the log at `icmp6.c:2241-2246`, i.e. execution passed the
  `nd_rd` reads (2196-2197), the hlim check (2211) and `rtpurelookup`
  (2227). `icp6s_inhist[137]` counted every send (1 -> 71 over the run).
  `raw137listen` confirms the kernel-chosen source/hlim on the wire
  (`from fe80::1 scope=2 ... hlim=255`).
- **No corruption observed**: sends at payload sizes 40..8000 (single
  mbufs AND `[40B header mbuf][cluster]` chains — `lo0= 23` multi-mbuf
  inputs in ip6 stats), multi-iov sendmsg shapes, and a 50-packet burst
  all left the guest alive, no INVARIANTS trip, no panic (`uptime` after).

## Why the stale pointer cannot fire today (trace)

1. The dispatch copy `n = m_copym(m, 0, M_COPYALL, M_NOWAIT)`
   (`icmp6.c:787`) never carries `M_LOOP`: `M_COPYFLAGS`
   (`sys/sys/mbuf.h:281-284`) excludes it. With `!M_LOOP` the macro takes
   the `M_EXT`/plain else-branches (`ip6.h:285-297`) which either pass
   (data contiguous) or free-and-return — **never** `m_pullup`.
2. The only `M_LOOP` packet that can reach `icmp6_redirect_input` is the
   ORIGINAL loopback mbuf, delivered solely when that `m_copym` fails
   (`icmp6.c:789-791`) — requiring cluster-zone exhaustion.
3. For the pullup to *reallocate* (free the first mbuf `ip6` points
   into), the first mbuf must be `M_EXT` (or lack room) with
   `off+icmp6len <= MHLEN` (`uipc_mbuf.c:2125-2135`).
4. No userspace producer creates such a chain: `sosend` sizes the first
   mbuf to the full send via `m_getl(resid, ...)` (`uipc_socket.c:866-887`)
   — first mbuf is either a full/small internal mbuf or a FULL cluster;
   `rip6_output`'s `M_PREPEND` (`raw_ip6.c:326`) then splits off a new
   *internal* header mbuf (cluster has no headroom). So chains are always
   `[internal hdr][...]`, taking the **in-place** pullup path
   (`uipc_mbuf.c:2119-2124`) that leaves the first mbuf (and `ip6`)
   valid. Want > MCLBYTES (e.g. 2100/3000/8000-byte sends) makes
   `m_pullup` return NULL -> macro frees and returns before line 2188.
   bpf/tun injection produce single mbufs.
5. Therefore the dangling dereference requires a future mbuf-producer
   change (e.g. an LRO/driver change emitting `M_EXT`-short-first chains
   over loopback, plus mbuf pressure for step 2). A one-line fix removes
   the class.

## Fix

`fix.diff` re-derives `ip6 = mtod(m, ...)` after the check, exactly as
`icmp6_input` (420-421), `icmp6_notify_error` (861/978) and `icmp6_error`
(340) already do. Kernel-rebuild validation not performed: the bug did not
reproduce live (nothing to observe a behavioral delta against); the change
is mechanical and mirrors the sibling functions' proven pattern.
