# DF-0669 — VERDICT

## Verdict: REPRODUCED (kernel panic, root-only trigger)

The NULL-deref in `rn_flush(table_ctx->node, ...)` on a never-created
ipfw3 table is **real and reproduces deterministically** on the default
GENERIC kernel (#0 master DEV build, INVARIANTS ON). The trigger is
**root-only** (`SOCK_RAW` required to reach `rip_ctloutput`'s `IP_FW_X`
case), matching the finding's `PR:H` CVSS vector — a root-loaded ipfw3
module can be panicked by a root setsockopt. With ipfw3 loaded at boot
(a realistic firewall config), any process holding a raw socket (root
in the default jail-less configuration) can panic the kernel.

## Mechanism (cited path:line)

1. **`sys/net/ipfw3/ip_fw3.c:1038-1046` `ip_fw3_ctl_x`** strips the
   4-byte `ip_fw_x_header` from the setsockopt value via
   `sopt_valsize -= sizeof(ip_fw_x_header); bcopy(++x_header, sopt_val, ...)`
   and dispatches on `x_header->opcode`. No `sopt_valsize` validation.
2. **`sys/netinet/raw_ip.c:385-386`** routes `IP_FW_X` setsockopt on a
   `SOCK_RAW` socket to `ip_fw3_sockopt` → `ip_fw3_ctl_x`. (Only raw
   sockets handle `IP_FW_X` — `SOCK_DGRAM` returns `ENOPROTOOPT`.)
3. **`sys/net/ipfw3_basic/ip_fw3_table.c:566-572`
   `ip_fw3_table_init_dispatch`** allocates the per-CPU table context
   with `M_ZERO`, so every `table_ctx->node` is `NULL` until
   `IP_FW_TABLE_CREATE` calls `rn_inithead` on it.
4. **`sys/net/ipfw3_basic/ip_fw3_table.c:118-134`
   `table_delete_dispatch`** does **no NULL check** before
   `rn_flush(table_ctx->node, flush_table_entry)` at line 130.
5. **`sys/net/radix.c:1341` `rn_flush`** derefs `head->rnh_walktree`.
   With `head == NULL` this is a NULL+offset deref → page fault.
   (`table_flush_dispatch:248` has the same bug; `ip_fw3_table_fini_dispatch:583`
   loops all 32 tables through `rn_flush`, so kldunloading `ipfw3_basic`
   after normal use — when most slots are still NULL — panics
   deterministically too.)

## Reproduction

PoC: `df0669_ipfw3_null.c` — issues
`setsockopt(SOCK_RAW, IPPROTO_IP, IP_FW_X, {x_header.opcode=IP_FW_TABLE_DELETE, ioc_table.id=0}, valsize=48)`
on a fresh-loaded ipfw3 with no tables created.

Run as root via `test.sh` (loads ipfw3+ipfw3_basic, then runs the PoC;
output and panic land in `dfbsd-qemu/boot.log` via `/dev/console`
because ipfw3's default-deny severs networking the moment it loads):

```
Fatal trap 12: page fault while in kernel mode
cpuid = 0; lapic id = 0
fault virtual address	= 0x28
fault code		= supervisor read data, page not present
instruction pointer	= 0x8:0xffffffff8074946d
stack pointer	        = 0x10:0xfffff8008d1f8960
frame pointer	        = 0x10:0xfffff8008d1f8980
current process		= Idle
panic: page fault
```

The fault virtual address `0x28` is the byte offset of
`rnh_walktree` within `struct radix_node_head`, confirming the panic
is at `head->rnh_walktree` with `head == NULL` (i.e. inside
`rn_flush(NULL)` from `table_delete_dispatch:130`).

The full untrimmed panic signature is in `panic.txt`.

## Privilege / threat model

- **Trigger requires root** (raw socket). With ipfw3 enabled at boot by
  the administrator (a realistic firewall config), any local process
  with `PRIV_NETCTRL`/root can DoS the kernel.
- **No memory corruption** — pure NULL deref. Impact is reliable kernel
  panic (DoS), not escalation. The CVSS vector `AV:L/AC:L/PR:H/...A:H`
  matches: it is a root-only local DoS.

## Recommended fix

`fix.diff` adds `if (table_ctx->node != NULL)` guards in
`table_delete_dispatch`, `table_flush_dispatch`, and the loop in
`ip_fw3_table_fini_dispatch`. The finding's proposed fix matches this.
**Matches the finding proposal.**
