# DF-0714 — Verdict

## Verdict: REPRODUCED (code-confirmed, runtime-silent)

**Impact:** 1-byte OOB read (silent — no panic, no leak, no corruption).
**Severity:** Low (matches finding).
**Confidence:** certain.

## The Bug

**File:** `sys/netgraph7/tcpmss/ng_tcpmss.c:426`
**Function:** `correct_mss()` (lines 410–445)

The TCP option parser in `correct_mss()` iterates over the TCP options area of
a SYN packet. For non-EOL/NOP options, it reads the option-length byte
`*(opt+1)` at line 426 **without first checking that `olen >= 2`**. After
consuming NOPs (which advance `opt` by 1 and decrement `olen` by 1 each),
`olen` can reach 1 while the loop condition (`olen > 0`) still holds. When the
remaining byte is a TLV-kind option, `*(opt+1)` reads **1 byte past the TCP
options boundary**.

### Code trace (trigger: SYN with options `NOP NOP NOP MAXSEG-kind`)

```
Line 419: for (olen = hlen - sizeof(struct tcphdr), opt = (u_char *)(tc + 1);
Line 420:      olen > 0; olen -= optlen, opt += optlen)
```

Initial state: `olen = 4` (th_off=6 → tcphlen=24, options=4 bytes).

| Iter | *opt  | Branch         | optlen | olen after | opt after |
|------|-------|----------------|--------|------------|-----------|
| 1    | 0x01  | TCPOPT_NOP     | 1      | 3          | +1        |
| 2    | 0x01  | TCPOPT_NOP     | 1      | 2          | +1        |
| 3    | 0x01  | TCPOPT_NOP     | 1      | 1          | +1        |
| 4    | 0x02  | else (line 425)|        |            |           |

At iteration 4: `olen = 1`, `*opt = 0x02` (TCPOPT_MAXSEG, not EOL/NOP).

```
Line 425: else {
Line 426:     optlen = *(opt + 1);   // *** OOB READ ***
```

`opt` points to the **last byte** of the 4-byte options area. `opt + 1` is
**1 byte past the options boundary** — into the mbuf data buffer beyond the
pulled-up region (`pullup_len = iphlen + tcphlen = 44`).

### Why the OOB is silent

1. **No page fault:** The mbuf data buffer is `MHLEN` (~213 bytes), far larger
   than the 44-byte pullup region. The OOB byte at offset 44 is well within
   the allocated buffer.

2. **No corruption:** The OOB byte is used only as `optlen` and immediately
   bounds-checked:
   ```
   Line 427: if (optlen <= 0 || optlen > olen) break;
   ```
   Since `olen = 1`, any `optlen >= 2` breaks the loop. `optlen == 0` also
   breaks. `optlen == 1` passes but `*opt == TCPOPT_MAXSEG` → `optlen(1) !=
   TCPOLEN_MAXSEG(4)` → `continue` → `olen -= 1 → 0`, loop ends. In all cases,
   no data is modified.

3. **No exfiltration:** The OOB byte is never returned to userspace. It is
   used only internally as `optlen` for the bounds check.

## Reachability

`ng_tcpmss` is an **optional netgraph7 module** — not in the default kernel
config (`sys/conf/files`: `optional netgraph7_tcpmss`), not pre-built in
`/boot/kernel/`. An admin must:
1. Build and `kldload ng_tcpmss`.
2. Create a tcpmss netgraph node and wire it into the packet path (e.g.,
   between an interface and the IP stack via `ng_ether`).
3. Configure `maxMSS != 0` via `NGM_TCPMSS_CONFIG`.

This is a realistic admin action — tcpmss exists specifically as a PMTUD
workaround tool. Once configured, **any remote host** sending a crafted TCP SYN
triggers the OOB read. The trigger is unprivileged network traffic.

## Runtime PoC

The PoC (`trigger.c`) builds `ng_tcpmss.ko` from source, loads it, creates a
netgraph socket-to-socket topology (`sender → tcpmss:in → tcpmss:out →
receiver`), and sends two crafted SYN packets:

- **Test 1 (control):** Valid MAXSEG option (kind=2, len=4, MSS=1460). tcpmss
  correctly lowers MSS to 536 (`FixedPkts=1`). Confirms tcpmss works.
- **Test 2 (trigger):** Options `NOP NOP NOP MAXSEG-kind` (th_off=6, olen=4).
  Drives `olen` to 1, triggering the `*(opt+1)` OOB read at line 426. The
  packet is forwarded unmodified (`FixedPkts` stays 1), confirming
  `correct_mss` was called but the OOB read broke the loop before MAXSEG
  processing. No panic, no crash, no observable side effect.

**Note:** The PoC bypasses `libnetgraph` (which links against old
`<netgraph/ng_message.h>` headers, `NG_VERSION=2`) and uses raw `sendto`/
`recvfrom` with `<netgraph7/ng_message.h>` (`NG_VERSION=8`) to match the
rebuilt kernel modules. The pre-built modules in `/boot/kernel/` had
`abi_version=2` (from an older build); all three modules (netgraph, ng_socket,
ng_tcpmss) were rebuilt from the `/usr/src` source tree to get `abi_version=12`.

## PoC changes

The PoC folder was empty (never seeded). All files were created from scratch:
- `trigger.c` — netgraph socket-to-socket topology + crafted SYN packets.
- `build.sh`, `run.sh` — build/run scripts.
- `setup.sh` — module build/load helper (run as root).

## Fix

**`fix.diff`:** Add `if (olen < 2) break;` before `optlen = *(opt + 1);` at
line 426. This ensures the option-length byte is only read when at least 2
bytes remain in the options area.

```diff
--- a/sys/netgraph7/tcpmss/ng_tcpmss.c
+++ b/sys/netgraph7/tcpmss/ng_tcpmss.c
@@ -423,6 +423,8 @@
 		else if (*opt == TCPOPT_NOP)
 			optlen = 1;
 		else {
+			if (olen < 2)
+				break;
 			optlen = *(opt + 1);
```

This matches the finding's recommended fix: "if(olen<2) break before *(opt+1)."

## Fix Validation

- **Unpatched baseline:** `ng_tcpmss.ko` built from unmodified source. PoC
  Test 2 exercises `correct_mss` with `olen=1` (SYNPkts=2), and the OOB read
  at line 426 occurs (code trace). Runtime is silent (no panic).
- **Patched:** `ng_tcpmss.ko` built with `fix.diff` applied. PoC Test 2 still
  exercises `correct_mss` (SYNPkts=2), but the `if (olen < 2) break;` guard
  at line 426 prevents the OOB read — the loop breaks before `*(opt+1)`.
  Runtime output is identical (silent bug, no regression). Test 1 still
  correctly lowers MSS (FixedPkts=1).

The fix compiles, loads, and prevents the OOB read without regression. Since
the bug is a silent 1-byte read, the before/after runtime output is identical
— the fix is validated by code trace (the guard catches `olen < 2` before the
read) plus no functional regression.
