# DF-0747 — VERDICT

## Verdict: REPRODUCED (Low severity; OOB read + firewall misclassification; fix VALIDATED)

## Mechanism

### Trigger → primitive → effect

1. **Trigger:** A non-first IPv4 fragment of a TCP datagram arrives at `ip_input()`.
   `pfil_run_hooks(&inet_pfil_hook, ...)` runs at `ip_input.c:631` **before**
   `ip_reass()` at `ip_input.c:858`, so individual fragments reach ipfw3 before
   reassembly.

2. **ipfw3 dispatcher** (`sys/net/ipfw3/ip_fw3.c:317-411`):
   - Line 368: `proto = args->f_id.proto = ip->ip_p;` — set **unconditionally**,
     regardless of fragment offset. So `f_id.proto == IPPROTO_TCP` for TCP fragments.
   - Line 371: `offset = ntohs(ip->ip_off) & IP_OFFMASK;` — `offset > 0` for non-first.
   - Line 384: `if (offset == 0)` — the `PULLUP_TO(hlen + sizeof(tcphdr))` at line 387
     is **skipped** for non-first fragments. `args->f_id.flags` (line 391) is never set.

3. **check_established** (`sys/net/ipfw3_layer4/ip_fw3_layer4.c:173-191`):
   - Line 182: `if (fid->proto == IPPROTO_TCP)` — **true** for TCP fragments (set at
     ip_fw3.c:368 unconditionally).
   - Line 184: `L3HDR(struct tcphdr, ip)->th_flags` — `L3HDR` is defined at
     `ip_fw3.h:524` as `(T *)((uint32_t *)(ip) + (ip)->ip_hl)`, i.e. `ip + ip_hl*4`.
     For a standard 20-byte header, `th_flags` is at `ip + 20 + 13 = ip + 33`.
   - For a non-first fragment with 8-byte payload (RFC 791 minimum), the valid data
     ends at byte 27 (`ip_len = 28`). Reading at byte 33 is a **5-byte over-read**
     past valid `m_len` into the mbuf backing store.
   - For fragments with ≥ 14 bytes of payload, byte 33 is within the attacker-
     controlled payload. The attacker controls the value read as `th_flags`.

4. **Effect:** The firewall decision (established match) is driven by stale or
   attacker-controlled bytes. An attacker sending crafted non-first TCP fragments
   can deliberately cause a fragment to match or fail the established check by
   controlling byte 13 of the fragment payload.

### Why no panic / no escalation
- The OOB read stays within the mbuf's allocated backing store (inline mbuf data
  area or cluster), so no page fault / panic.
- The stale byte is used internally for the firewall decision — never returned to
  userspace, so no info leak.
- This is a logic/misclassification bug, not a write primitive. No escalation path.

## Reproduction evidence

### Baseline (unpatched module, kernel #0)

Rule set: `100 allow tcp established`, `65534 allow ip from any to any`.

The trigger sends 4 crafted non-first TCP fragments (offset=8):
- Case 1: 24B payload, byte[13]=0x10 (ACK) → should match established
- Case 2: 24B payload, byte[13]=0x02 (SYN) → should NOT match
- Case 3:  8B payload, byte[13] OOB read → stale byte
- Case 4: 14B payload, byte[13]=0x10 (ACK) → should match

Counters delta (after - before), over 2 independent runs:

| Run | Rule 100 Δ (established) | Rule 65534 Δ (fallthrough) |
|-----|--------------------------|----------------------------|
| 1   | +16 pkts / +1928 B       | **+2 pkts / +88 B**       |
| 2   | +14 pkts / +1732 B       | **+2 pkts / +88 B**       |

Rule 65534 delta = exactly 2 × 44 bytes = Case 2 fragment (44B) evaluated on
both PFIL_IN and PFIL_OUT. This proves check_established read the attacker-
controlled byte 0x02 (SYN) at fragment position 13 and classified the fragment
as non-established.

### Positional confirmation (test2)

Additional test varying the byte at position 13:
- ACK@byte13 → MATCH (rule 100)
- SYN@byte13 → NOT match (rule 65534) — **4 pkts / 176 B** (2 fragments × 2 dirs)
- 0xAA@byte13: (0xAA & 0x16) = 0x02 = SYN → NOT match (rule 65534)

Result: rule 65534 delta = 4 pkts / 176 B = exactly the SYN@13 + 0xAA@13 fragments.
This confirms byte 13 is the exact position read.

### Fixed module (ipfw3_layer4.ko replaced, same kernel #0)

With the fix, `check_established` uses `fid->flags` (0 for non-first fragments)
instead of reading the mbuf. All fragments deterministically match established.

| Test          | Rule 100 Δ | Rule 65534 Δ |
|---------------|-----------|--------------|
| Baseline      | +14/+16   | **+2 pkts / 88 B** |
| Fixed module  | +16       | **0 pkts / 0 B**   |

The 2-packet fallthrough (Case 2 SYN@byte13) is **gone** with the fix. The
misclassification is eliminated.

## PoC changes

Created from scratch (no prior PoC existed on disk):
- `trigger.c` — sends 4 crafted non-first TCP fragments via raw socket (IP_HDRINCL)
  to 127.0.0.1, controlling byte 13 of the fragment payload (the th_flags position)
- `setup.sh` — loads ipfw3 modules with default-accept, installs rules
- `build.sh`, `run.sh` — convenience scripts

## Fix

`fix.diff` changes `check_established` to use the cached `fid->flags` instead of
re-reading `L3HDR(struct tcphdr, ip)->th_flags` from the mbuf. The `fid->flags`
field is populated only for first/unfragmented TCP segments at `ip_fw3.c:391`
(inside the `offset == 0` branch). For non-first fragments, it stays 0, giving a
deterministic and safe firewall decision. The now-unused `struct mbuf *m` and
`struct ip *ip` local variables are removed.

This matches the FreeBSD ipfw2 approach (using cached `f_id.flags`).

### Fix validation
- **Method:** Built the fixed `ipfw3_layer4.ko` module from patched source,
  replaced `/boot/kernel/ipfw3_layer4.ko`, rebooted, re-ran trigger.
- **Kernel:** Still #0 (the bug is in a loadable module, not the kernel binary).
- **Module hash:** `aec57daea3003e1342c79e336fc0f7d7bdb3496620092f67da62c0d5ef66e001`
- **Result:** Rule 65534 delta went from **2 pkts / 88 B** (buggy) to **0 pkts / 0 B**
  (fixed). The attacker-controlled misclassification is eliminated.
