# DF-0471 — `ip_fw3_ctl_x` size_t underflow → unbounded bcopy

## Bug
`sys/net/ipfw3/ip_fw3.c:1038-1047` — `ip_fw3_ctl_x()` does
`sopt->sopt_valsize -= sizeof(ip_fw_x_header)` (4) with **no bounds check**.
`sopt_valsize` is `size_t` (unsigned). A `setsockopt(IPPROTO_IP, IP_FW_X, buf, n)`
with `n ∈ {1,2,3}` passes the generic setsockopt plumbing (which only rejects
`n==0` and `n > SOMAXOPT_SIZE`) and arrives here with `sopt_valsize < 4`, so the
subtract wraps to ~`SIZE_MAX` and the next line's
`bcopy(++x_header, sopt->sopt_val, sopt_valsize)` runs an unbounded copy through
kernel heap → memory corruption → guest wedges in DDB.

## Threat model (read this)
- **Root-only trigger.** The path needs a raw IP socket
  (`socket(AF_INET, SOCK_RAW, IPPROTO_RAW)`) → `raw_ip.c:385` `IP_FW_X` →
  `ip_fw3_glue.c:51` → `ip_fw3_ctl_x`. Raw sockets require root. **No
  unprivileged→root escalation.** Relevance: compromised-root process, setuid
  ipfw3 front-end, jail escape.
- **Module not in GENERIC.** `ipfw3` is `optional ipfirewall3`; must be
  `kldload`ed.
- Impact: kernel memory-corruption → **hard DoS** (ddb wedge). Not an LPE.

## Reproduce
```sh
# as root, on the audit guest (vm.sh up)
sysctl net.filters_default_to_accept=1     # keep ssh alive after kldload
kldload ipfw3
cd findings/poc/DF-0471
./build.sh   # cc -Wall -o uflow uflow.c
./run.sh     # ./uflow  -> on the BUGGY kernel the guest wedges in DDB
```
Expected on the **buggy** `#0` module: `uflow` prints the setsockopt banner,
then the guest becomes unresponsive; serial console shows
`Stopped at systimer_add+0x179 ... db>`. `vm.sh status` ⇒ down.

Expected on the **fixed** module: `uflow` exits 0 with
`[+] kernel correctly rejected short payload (EINVAL)` and the guest stays up.
A 4-byte `IP_FW_X` payload is still accepted (no regression).

## Fix
`fix.diff` — one guard in `ip_fw3_ctl_x` before the unsigned subtraction:
`if (sopt->sopt_valsize < sizeof(ip_fw_x_header)) return EINVAL;`
Rebuild only `ipfw3.ko` (`make KERNBUILDDIR=.../X86_64_GENERIC` in
`sys/net/ipfw3`) and hot-swap.

## Files
- `uflow.c`        — minimal trigger (2-byte IP_FW_X → size_t underflow bcopy)
- `build.sh`/`run.sh` — exact build/run
- `run.log`        — decisive run on unpatched #0 (stops mid-setsockopt → wedge)
- `panic.txt`      — ddb trap from boot.log
- `fix.diff`       — git-apply-able one-hunk fix
- `fix_build.log`  — patched module build (-Werror, rc=0)
- `fix_run.log`    — patched module: EINVAL + regression (4-byte accepted)
- `env.txt`        — guest uname/cc/kldstat/sysctl
- `VERDICT.md`     — full narrative + fix before/after
- `manifest.json`  — machine-readable catalog
