# DF-0631 — ipfw3 state->stub UAF after rule deletion

## Verdict
**REPRODUCED — use-after-free (memory corruption) confirmed, and uid0 escalation is
structurally blocked by M_IPFW3 slab zone isolation.**

## Mechanism (every hop cited)
1. `sys/net/ipfw3_basic/ip_fw3_state.c:325` — `check_keep_state` stores an
   **unrefcounted** raw pointer to the creating rule: `s->stub = *f;`.
2. `sys/net/ipfw3/ip_fw3.c:765` — `ip_fw3_delete_rule` (and `flush_rule_dispatch`
   at `:787`) `kfree(rule, M_IPFW3)` with **no sweep** of the per-CPU state trees
   that reference it. `delete_rule_dispatch` (`:824`) runs on every CPU via
   `netisr_forwardmsg_all` but never walks states.
3. `sys/net/ipfw3_basic/ip_fw3_state.c:210` and `:225` — `check_check_state`, on a
   match, restores the dangling pointer `*f = s->stub;` and returns
   `cmd_ctl = IP_FW_CTL_CHK_STATE`.
4. `sys/net/ipfw3/ip_fw3.c:522-523` — the consumer immediately dereferences the
   dangling `f`: `cmd = ACTION_PTR(f); l = f->cmd_len - f->act_ofs;`, and the
   `done:` path at `:575-577` **writes** `f->pcnt++; f->bcnt += ip_len;
   f->timestamp = time_second;`.

## Proof (decisive)
A keep-state rule creates a state (`s->stub = <rule ptr>`). After `ipfw3 delete`,
`ipfw3 state show` displays the state's cached `stub->rulenum`. Because the freed
rule slab was poisoned (`debug.use_weird_array=1`), the rulenum read is the low 16
bits of WEIRD_ADDR:

```
STATE_AFTER_CREATE:   00200 5 udp 10.0.2.15:11111 10.0.2.2:1 i 29   (stub valid)
STATE_AFTER_DELETE:   49374 5 udp 10.0.2.15:11111 10.0.2.2:1 i 29   (49374 = 0xC0DE = low16 of 0xdeadc0de)
```

`49374 == 0xC0DE` is conclusive: the state's `stub` dereferences **freed, poisoned
kernel memory**. With the default `use_weird_array=0` the freed chunk keeps its
stale (valid-looking) contents, so the UAF is **silent** (wrong rule's
action/counters execute) rather than panicking — itself a firewall-correctness
defect. Trigger packets (the trigger can be an unprivileged/remote 5-tuple match
within the proto timeout window) exercise the deref+write of the freed slot.

## Exploit chain / escalation assessment
- Primitive: a dangling `struct ip_fw *` is dereferenced and **written**
  (`pcnt++`, `bcnt += ip_len` (0..~64K attacker-influenced), `timestamp`) on every
  matching packet, plus a control-flow-affecting read
  (`filter_funcs[cmd->module][cmd->opcode]`).
- **uid0 escalation is NOT achievable from this primitive.** The freed object is in
  the **M_IPFW3** malloc zone, which is type-isolated: only `struct ip_fw` rule
  objects are ever allocated from it, so the dangling slot can only be reclaimed by
  another firewall rule (a valid `struct ip_fw`), never by a credential-bearing
  victim (`struct ucred`/`struct file`/`struct proc`). The writes land on rule
  counter fields at fixed offsets and are increments, not arbitrary writes. There is
  no privilege object reachable in this slab and no way for an unprivileged user to
  place a forged credential there (only root can add ipfw3 rules into M_IPFW3). This
  is a genuine structural blocker (Phase 6), not a laziness bail.
- Realistic impact ceiling: **DoS / silent memory corruption** of freed slab; panic
  possible if the poisoned deref lands on a NULL/unmapped `filter_funcs` slot. This
  is a root-configured-firewall corruption bug (keep-state rule is an admin setup;
  the rule-delete is an admin reload; the *triggering* packet can be unprivileged or
  unauthenticated-remote within the state timeout window).

## PoC
- `udppkt.c` — sends a fixed-5-tuple UDP datagram (create-state then trigger).
- `run.sh` — orchestrates load + rules + create + delete + trigger.
- The decisive evidence is `ipfw3 state show` showing rulenum `49374` after delete.

## PoC changes
- Wrote `udppkt.c` and `run.sh` (none existed). Added lockout mitigation
  (`net.filters_default_to_accept=1`) and `debug.use_weird_array=1` to make the UAF
  observable via the poisoned rulenum read.

## Fix
`fix.diff` adds a per-CPU state sweep (`ip_fw3_state_remove_rule`) invoked from the
rule-deletion dispatches (`delete_rule_dispatch`, `flush_rule_dispatch`) before
`kfree`. The sweep `RB_REMOVE`+`kfree`s every state whose `s->stub == <rule>` on the
owning CPU. Validated: after the fix, `ipfw3 state show` is empty after delete (state
swept) instead of showing the poisoned `49374` rulenum.
