# DF-0747 — check_established reads TCP th_flags from non-first IP fragments without pullup

## Finding
**File:** `sys/net/ipfw3_layer4/ip_fw3_layer4.c:173-191` (function `check_established`)
**Severity:** Low
**Impact:** OOB read + firewall misclassification on fragmented TCP packets

## Bug mechanism
`check_established()` re-reads `L3HDR(struct tcphdr, ip)->th_flags` from the mbuf
for every TCP packet, including non-first IP fragments. The dispatcher
`ip_fw3_chk()` (`ip_fw3.c:384-409`) only calls `PULLUP_TO(hlen + sizeof(tcphdr))`
when `offset == 0` (first/unfragmented). For non-first fragments (`offset > 0`),
the pullup is skipped and the payload at `ip + ip_hl*4` is fragment data, not a
TCP header.

`th_flags` is at offset 13 in `struct tcphdr`, so `L3HDR(tcphdr, ip)->th_flags`
reads at `ip + 20 + 13 = ip + 33`. For a minimum-size fragment (RFC 791: 8-byte
payload, 28-byte total), this is a **5-byte over-read past valid m_len** into the
mbuf backing store. For fragments with ≥ 14 bytes of payload, the read is within
attacker-controlled fragment data.

Since `pfil_run_hooks` runs **before** `ip_reass` (`ip_input.c:631` vs `:858`),
each fragment individually reaches `check_established`. The firewall's
established-rule decision is thus driven by attacker-controlled or stale bytes.

## Build & Run
```sh
# Build the trigger
cc -O2 -Wall -o trigger trigger.c

# Setup (as root — loads ipfw3 with default-accept so SSH survives, adds rules)
sh setup.sh

# Run the trigger (as root — needs raw socket)
sh run.sh
# or directly:
./trigger
ipfw3 show   # check rule-hit counters
```

## Expected output (bug present, unpatched module)
- Rule `100 allow tcp established`: counter increases (Cases 1, 3, 4 match)
- Rule `65534 allow ip from any to any`: counter increases by **2 pkts / 88 bytes**
  for Case 2 (SYN byte at fragment position 13 → NOT established)
- The attacker controls whether a non-first TCP fragment matches "established"

## Expected output (fixed module)
- Rule `100`: counter increases for ALL fragment cases (fid->flags=0 for fragments
  → (0 & mask) != SYN → always matches established)
- Rule `65534`: **0** delta — no fragments fall through

## Precondition
An admin has loaded ipfw3 with a `check_established` rule (e.g. `allow tcp ...
established`). This is a standard firewall configuration. The vulnerability is
triggered by any packet reaching the host's `ip_input` — local (via raw socket
on loopback) or remote.

## Fix
Use the cached `fid->flags` (populated only for first/unfragmented TCP segments
at `ip_fw3.c:391`) instead of re-reading the mbuf. For non-first fragments,
`fid->flags` stays 0, giving a deterministic, safe firewall decision. See
`fix.diff`.

## Notes
- The OOB read does NOT panic (stays within mbuf backing store).
- No userspace info leak (the stale byte is used internally for firewall
  decision, never returned to userspace).
- No escalation path — this is a logic/misclassification bug, not a write
  primitive.
- FreeBSD ipfw2 uses the cached `f_id.flags` approach (avoids this bug).
