# DF-3360 — pf scrub max-mss: 2-byte stack OOB read/write past `opts[40]` in pf_normalize_tcpopt()

## What
`pf_normalize_tcpopt()` (sys/net/pf/pf_norm.c:1907-1957) clamps the TCP MSS
option for scrub rules with `max-mss N`. The option walk:

```c
u_char opts[TCP_MAXOLEN];                 /* :1915 — exactly 40 bytes  */
cnt = thoff - sizeof(struct tcphdr);      /* == 40 for th_off == 15    */
...
optlen = optp[1];
if (optlen < 2 || optlen > cnt) break;    /* optlen == 2 is accepted   */
switch (opt) {
case TCPOPT_MAXSEG:
        mss = (u_int16_t *)(optp + 2);    /* == opts + 40 with the tile below */
        if ((ntohs(*mss)) > r->max_mss) { /* OOB READ of opts[40..41]  */
                th->th_sum = pf_cksum_fixup(th->th_sum, *mss,
                    htons(r->max_mss), 0);
                *mss = htons(r->max_mss); /* OOB WRITE of opts[40..41] */
                rewrite = 1;
        }
```

A TCP header with `th_off = 15` whose option space tiles as
`[38 bytes of filler][0x02 0x02]` — an MSS option with declared option
length **2** (instead of TCPOLEN_MAXSEG == 4) as the final option — makes
`optp == opts+38`, `optlen == 2` (accepted by the `optlen > cnt` check
since cnt == 2), and `mss == opts+40`: two bytes past the 40-byte stack
array. The read is unconditional; when the adjacent stack word (read
big-endian) exceeds the rule's max-mss, a 2-byte value `htons(max_mss)`
is **written** past the array. An option length of 3 gives a 1-byte OOB.

Secondary effect even when the write does not fire: `th->th_sum` is
"fixed up" with a delta computed from the OOB-read stale-stack bytes and
the header is copied back — a 2-byte stale-kernel-stack oracle in the
forwarded packet's checksum for transit traffic.

## Preconditions
- pf enabled with a scrub rule carrying `max-mss` (common on WAN edges;
  not the default empty ruleset).
- Attacker sends one TCP packet whose header ends in the malformed MSS
  option. Unauthenticated, remote, one packet per execution of the bug.

## Guest verification (see VERDICT.md)
- Disassembly of the shipped `/boot/kernel/pf.ko` proves the unguarded
  access is compiled in: `movzwl 0x2(%r15)` / `mov %ax,0x2(%r15)` with
  `%r15` = opts+38 (pf_normalize_tcpopt+0xba).
- 300,000 malformed-MSS packets through `scrub in on lo0 all max-mss
  {64,1}` executed the path (rule evaluations climbed) with the guest
  staying up: in this gcc -O2 build `opts+40..41` lands in an 8-byte
  frame **alignment-padding slot** (`rbp-0x30`, unreferenced), so the
  OOB is inert on this exact binary — the defect is real (UB) and
  compiler-layout-dependent for impact.

## Reproduce
```
# root on the guest:
kldload pf
printf 'scrub in on lo0 all max-mss 64\n' > /root/poc/mss.conf
pfctl -f /root/poc/mss.conf && pfctl -e
/root/poc/mss_oob 100000        # sends SYN, th_off=15, [38xNOP][0x02 0x02]
pfctl -sr -v                    # evaluations counter climbs; guest stays up
```

## Fix
`if (optlen != TCPOLEN_MAXSEG) break;` in the TCPOPT_MAXSEG case — see
fix.diff.
