# DF-2576 — ip_fw3_ctl_zero_entry KKASSERT(start_rule==NULL) panic — VERDICT

## Verdict: REPRODUCED (deterministic KKASSERT panic on default INVARIANTS kernel); fix VALIDATED

- **status:** reproduced
- **reproduced:** 1
- **impact:** panic (root-reachable deterministic DoS; fires on default X86_64_GENERIC which ships `options INVARIANTS`)
- **confidence:** certain
- **fix_status:** fixed

## The bug (confirmed in source, line-by-line)

`sys/net/ipfw3/ip_fw3.c` `ip_fw3_ctl_zero_entry()` (:904–944):

```c
909:    struct ipfw3_context *ctx = fw3_ctx[mycpuid];
...
917:    if (rulenum == 0) {
...
920:    } else {
921:        struct ip_fw *rule;
926:        for (rule = ctx->rules; rule; rule = rule->next) {
927:            if (rule->rulenum == rulenum)
928:                break;
929:        }
930:        if (rule == NULL)
931:            return (EINVAL);
932:        zmsg.start_rule = rule;          /* <-- set, never used by dispatch */
...
938:    netisr_domsg(nmsg, 0);              /* runs ip_fw3_zero_entry_dispatch on all CPUs */
939:    KKASSERT(zmsg.start_rule == NULL);  /* <-- PANIC: dispatch never clears it */
```

`ip_fw3_zero_entry_dispatch()` (:876–894) only touches `zmsg->rulenum` and
`zmsg->log_only`; it **never** writes `zmsg->start_rule`. So after
`netisr_domsg()` returns, `zmsg.start_rule` is still the non-NULL `rule`
pointer set at :932, and the unconditional `KKASSERT(zmsg.start_rule == NULL)`
at :939 fires on every INVARIANTS-enabled kernel. `netmsg_zent.start_rule`
(:125) is a **dead field** — set at :932, never read anywhere — paired with an
incorrect assertion.

`KKASSERT` expands to an inline `panic()` (it is INVARIANTS-gated and
compiled into the default `X86_64_GENERIC` kernel, so it fires on the stock
kernel — verified by the panic observed).

## Reachability

`setsockopt(SOCK_RAW, IPPROTO_IP, IP_FW_X=49, {opcode=IP_FW_ZERO=53, rulenum})`
→ `rip_ctloutput` → `ip_fw3_sockopt` → `ip_fw3_ctl` (`case IP_FW_X` :1058) →
`ip_fw3_ctl_x` strips the 4-byte `ip_fw_x_header`, sets `sopt_name=53`
(:1043-1044) → `ip_fw3_ctl` (`case IP_FW_ZERO` :1066) →
`ip_fw3_ctl_sockopt` (`case IP_FW_ZERO` :1158) → reads the 4-byte `rulenum`
(:1162) → `ip_fw3_ctl_zero_entry(rulenum, log_only)` (:1168).

The **default rule** (rulenum `IPFW_DEFAULT_RULE = 65535`, `ip_fw3.h:87`) is
**always** present in `ctx->rules` (`ctx_init_dispatch`, :1432-1433, runs on
every CPU at `kldload`), so zeroing rule 65535 needs no prior rule to be
added — the bug is triggerable immediately after `kldload ipfw3 ipfw3_basic`.

Privilege: a raw IP socket requires `caps_priv_check(SYSCAP_NONET_RAW)`
(root), and `kldload` requires root. This is therefore a **root-reachable
deterministic DoS** — but on a system where ipfw3 is the active firewall
(a normal production config), any root-driven `ipfw3 zero N` /
`ipfw3 resetlog N` (N != 0, matching an existing rule) panics the kernel.

## Reproduction (unpatched #0 baseline)

`run.sh` sets `net.filters_default_to_accept=1` **before** `kldload` (the
sysctl is a base-kernel tunable in `sys/net/pfil.c:83-87`, read at MOD_LOAD
in `ctx_init_dispatch` :1425, so setting it first keeps ssh alive), loads
`ipfw3` + `ipfw3_basic`, then issues the zero opcode for rule 65535.

Result on the unpatched `6.5-DEVELOPMENT #0` kernel (`boot.log`):
```
panic: assertion "zmsg.start_rule == NULL" failed in ip_fw3_ctl_zero_entry at /usr/src/sys/net/ipfw3/ip_fw3.c:939
ip_fw3_ctl_zero_entry() at ip_fw3_ctl_zero_entry+0x1c9 0xffffffff82601d39
ip_fw3_ctl_sockopt() at ip_fw3_ctl_sockopt+0xe2 0xffffffff826020c2
Stopped at      Debugger+0x7c:  movb    $0,0xbdaf09(%rip)
```
Exact match to the cited line 939, through the cited call chain. Guest dead.

## Fix

`fix.diff` removes the incorrect KKASSERT (the field is dead and the
assertion can never hold):

```diff
 	netisr_domsg(nmsg, 0);
-	KKASSERT(zmsg.start_rule == NULL);
 
 	if (sysctl_var_fw3_verbose)
```

## Fix validation

ipfw3 is a loadable module (`/boot/kernel/ipfw3.ko`). The fix was validated
by rebuilding just `ipfw3.ko`, reinstalling it, reloading, and re-running the
*same* PoC:

- **Apply:** `patch -p1 < fix.diff` → `Hunk #1 succeeded at 936`.
- **Build:** `cd sys/net/ipfw3 && make` → `rc=0` (`-Werror` clean; see `fix_build.log`).
- **Install:** `cp ipfw3.ko /boot/kernel/ipfw3.ko`.
- **Re-run (rulenum 65535 ×3, plus rulenum 0 ×1):** every call returns
  `setsockopt rc=0`, **no panic**, guest stays up.

Before/after contrast:
| kernel               | same PoC (zero rule 65535)                                                   |
|----------------------|------------------------------------------------------------------------------|
| unpatched `#0`       | `panic: assertion "zmsg.start_rule == NULL" failed ... ip_fw3.c:939`, guest **dead** |
| patched `ipfw3.ko`   | `setsockopt rc=0`, no panic, guest **up** (deterministic over 4 runs)        |

The fix closes the bug.

## Threat model

DoS only (a kernel assertion panic, no attacker-controlled write). Root-only
reachability via raw socket + kldload. There is no memory-corruption
primitive, hence no escalation chain.

## PoC changes / artifacts

- `poc.c` — setsockopt(IPPROTO_IP, IP_FW_X=49, {opcode=53, rulenum}) PoC; the
  `ip_fw_x_header` pattern mirrors DF-2580.
- `run.sh` — sets `net.filters_default_to_accept=1` *before* `kldload`
  (base-kernel tunable; read at MOD_LOAD) to avoid the default-deny rule
  locking out ssh, then loads ipfw3(+basic) and runs the PoC.
- `build.sh`, `build.log`, `run.log`, `fix_build.log`, `fix_run.log`,
  `panic.txt`, `env.txt`, `fix.diff`, `manifest.json`.
