# DF-2580 — ipfw3 table id bounds-check missing

Minimal PoC for the missing `id` bounds check in `sys/net/ipfw3_basic/ip_fw3_table.c`.
Every ipfw3 table operation does `table_ctx += id` against `table_ctx[IPFW_TABLES_MAX=32]`
with no `id < 0 || id >= IPFW_TABLES_MAX` check, so an attacker-controlled signed `id`
gives a controlled kernel-heap OOB read/write at byte offset `id*56`.

## Reach
`setsockopt(raw_ip_socket, IPPROTO_IP, IP_FW_X=49, payload)` where `payload` is a
4-byte `ip_fw_x_header{uint16 opcode=73(CREATE), uint16 pad}` followed by a
`struct ipfw_ioc_table{int id; int type; int count; char name[32];}`. The opcode is
stripped by `ip_fw3_ctl_x` and the remaining struct is dispatched to
`table_create_dispatch`, which does the unbounded `table_ctx += id`.

**Privilege:** creating a raw IP socket requires `SYSCAP_NONET_RAW` (root, or a jail
with `allow_raw_sockets`). The unprivileged user gets `EPERM` at `socket()`. So this
PoC must be run as root.

## Build (on the guest, as root)
```
cc -o poc poc.c
```

## Run (as root, with ipfw3 + ipfw3_basic loaded)
```
# load the modules with default-to-accept so ssh survives
sysctl -w net.filters_default_to_accept=1
kldload ipfw3
kldload ipfw3_basic

./poc 73 100000 1     # opcode=CREATE id=100000 type=1 -> PANIC on unpatched
./poc 73 64 1         # silent OOB write (offset 3584)
./poc 73 -2 1         # silent OOB write (negative idx, offset -112)
./poc 73 5 1          # valid id (sanity)
```

## Expected
- **Unpatched** `6.5-DEVELOPMENT #0`: `id=100000` panics with
  `Fatal trap 12 … Stopped at table_create_dispatch+0x45: movl $0,0x30(%rbx)`.
  `id=64`/`-2` return success (silent OOB write). Valid `id=5` works.
- **Patched** (DF-2580 fix): every out-of-range id returns `EINVAL` (errno 22);
  valid id still works; no panic.

See `VERDICT.md` for the full analysis, `fix.diff` for the fix, `run.log` /
`fix_run.log` for the captured before/after, and `panic.txt` for the crash signature.
