# DF-3307 — stale `exthdrs[]` pointers across `pfil_run_hooks()` → use-after-free read+write in `ip6_output()` fragmentation

File: `sys/netinet6/ip6_output.c` (pass-2 audit, GLM 5.3)
Class: kernel memory corruption (use-after-free write of one controlled byte
+ stale read that feeds the emitted fragment's `ip6f_nxt`), packet corruption.
Gate: root-set Hop-by-Hop (or Dest1/Rthdr) option + any inet6 `PFIL_OUT` hook
that replaces the mbuf chain (e.g. firewall normalization). No unprivileged
trigger exists on a stock system (hbh/dest/rthdr options are privilege-gated
at `ip6_setpktoption`, ip6_output.c:2763/2794), hence severity **Low**, but
the primitive itself is a genuine kernel UAF and is fully demonstrated here.

## What is in this pack

| file | what |
|---|---|
| `trigger.c` | root UDP6 sendmsg to ::1 with a 256-byte `IPV6_HOPOPTS` cmsg and 5000-byte payload (forces IPv6 fragmentation once `lo0` MTU is lowered to 1500) |
| `instr-ip6_input.c` | the *instrumentation* kernel patch: a demo `PFIL_OUT` hook compiled into the guest's `/usr/src/sys/netinet6/ip6_input.c` copy that deep-copies the >5000-byte probe packet's chain, frees the old chain and returns the copy (`rv=0`) — a perfectly legal `pfil_run_hooks()` action (`sys/net/pfil.c` passes `&m` to every hook) |
| `pfildemo.c` | the same hook as a KLD module (**note: could not be used — see VERDICT.md: kldloading any freshly-built module panics this guest's loader in `strcmp`; the hook was therefore compiled into the test kernels**) |
| `fix.diff` | the verified fix (re-locate the last unfragmentable next-header field in the *current* chain via `m_getptr()` instead of dereferencing pre-pfil `exthdrs[]` pointers) |
| `run.log` | vulnerable-kernel TEST run: wire hex — hbh byte 0 = `0x11` (17) |
| `run.2.log` | vulnerable-kernel CONTROL run (hook passthrough) and FIXED-kernel run — hbh byte 0 = `0x2c` (44) |
| `env.txt` | guest uname/build info |
| `build.sh` / `run.sh` | the exact (final) procedures used |

## Reproduce (summary — full detail in VERDICT.md)

1. Copy `instr-ip6_input.c` over the guest's `/usr/src/sys/netinet6/ip6_input.c`
2. `cd /usr/src && make quickkernel KERNCONF=X86_64_GENERIC -DNO_WERROR && make installkernel KERNCONF=X86_64_GENERIC`, reboot
3. `ifconfig lo0 mtu 1500`; build `trigger.c`; start `tcpdump -i lo0 -U -w cap.pcap ip6`
4. `sysctl net.inet6.ip6.pfildemo_enable=1` then run `./trigger`
   → **vulnerable kernel**: `receiver: no datagram`; wire shows hbh.nxt `11`
   (fragment header unreachable → receivers parse a bogus UDP header)
5. `sysctl net.inet6.ip6.pfildemo_enable=0` (control) then `./trigger`
   → datagram delivered; wire shows hbh.nxt `2c`
6. Apply `fix.diff` to the guest's `ip6_output.c`, `quickkernel`+`installkernel`, reboot, repeat step 4 with `enable=1`
   → **fixed kernel**: datagram delivered, hbh.nxt `2c` even though the chain was replaced

## Expected result table (observed)

| kernel | pfil hook | hbh.nxt on wire | datagram delivered |
|---|---|---|---|
| stock (vulnerable) | passthrough | 0x2c (44) | yes |
| stock (vulnerable) | replaces chain | **0x11 (17) — write hit freed memory** | **no (corrupt packet)** |
| + fix.diff | replaces chain | 0x2c (44) | yes |
