# DF-2575 — ipfw3 dummynet dn_priv use-after-free

**Severity:** High
**File:** `sys/net/ipfw3/ip_fw3.c:630` (`ip_fw3_dummynet_io`)
**Verdict:** REPRODUCED — kernel panic (UAF); fix VALIDATED.

## The bug

When a packet matches a dummynet pipe/queue rule, `ip_fw3_dummynet_io`
stores a raw, **unrefcounted** pointer to the matching `struct ip_fw` rule
into the dummynet packet tag:

```c
/* sys/net/ipfw3/ip_fw3.c:630 */
pkt->dn_priv = fwa->rule;     /* raw pointer — no refcount taken */
/* dn_unref_priv left NULL (struct bzero'd at line 604) */
```

`ip_fw3_delete_rule()` (line 757) unlinks and kfrees the rule slab
(`M_IPFW3`) on every CPU via `delete_rule_dispatch()` **without sweeping**
dummynet tags that still hold the dangling `dn_priv` pointer.

When the dummynet pipe delay expires and re-injects the queued packet,
`ip_fw3_check_in/out()` (lines 1191 / 1266) loads the stale pointer:

```c
args.rule = ((struct dn_pkt *)m_tag_data(mtag))->dn_priv;  /* UAF load */
```

When `net.inet.ip.fw3.one_pass == 0` (ip_fw3.c:428), `ip_fw3_chk()`
**dereferences** it:

```c
f = args->rule->next_rule;     /* UAF DEREF — freed M_IPFW3 slab */
```

Under INVARIANTS slab poisoning (`debug.use_weird_array=1`), the freed
chunk is filled with `0xdeadc0de` → `next_rule` becomes a non-canonical
address → `f = 0xdeadc0de...` → the rule-scan loop dereferences `f` →
**general protection fault (trap 9)** in `ip_fw3_chk+0x100`.

Even without poisoning (default `use_weird_array=0`), the UAF is silently
exploitable: the freed slab retains stale-but-plausible rule data, and
the "+++ ipfw: ouch!" messages in the boot log prove the freed memory is
being traversed as a live rule chain (silent data corruption).

## Privilege gate (root-only)

The entire path is **root-only**:
- `kldload ipfw3 / ipfw3_basic / dummynet3` → root
- `sysctl net.inet.ip.fw3.one_pass` → root (`CTLFLAG_RW`)
- `ipfw3 add / ipfw3 delete` (raw-socket `setsockopt(IPPROTO_IP, IP_FW_X)`)
  → `rip_ctloutput` → `caps_priv_check(SYSCAP_NONET_RAW)` → root

So this is a **root→kernel memory-corruption** bug. Root→kernel is
game-over by definition (root can `kldload` arbitrary code), so uid=0
escalation from an unprivileged user is **not possible** on this path.
Impact = root-triggerable kernel panic / DoS, plus a defence-in-depth
hardening gap (the `dn_unref_priv` callback should be used — the classic
ipfw `ip_fw2.c` already does this correctly).

## Reproduce

### Build
```
./build.sh    # shell-only PoC, no compilation needed
```

### Setup (as root on the guest)
```
sysctl -w net.filters_default_to_accept=1   # BEFORE loading ipfw3
kldload ipfw3
kldload ipfw3_basic
kldload dummynet3
sysctl -w debug.use_weird_array=1           # INVARIANTS slab poisoning
```

### Trigger (as root)
```
sh /root/poc/DF-2575/poc.sh 100 3000 500
```

The trigger:
1. Configures pipe 1 with 3000 ms delay (packets sit in queue).
2. Sets `one_pass=0` (REQUIRED for the UAF deref path).
3. Adds rule 100: `pipe 1 icmp from 127.0.0.1 to 127.0.0.1` (lo0 only —
   does NOT affect ssh on vtnet0).
4. Floods pings to 127.0.0.1 → packets accumulate in pipe 1 tagged with
   `dn_priv = &rule_100`.
5. Deletes rule 100 while packets are queued → `kfree(rule)` with dangling
   `dn_priv`.
6. After 3 s delay, dummynet re-injects → UAF deref → panic.

### Expected
- **Unpatched** `6.5-DEVELOPMENT #0`: Fatal trap 9 (GP fault) in
  `ip_fw3_chk+0x100: movzbl 0x16(%rax),%ecx`.
- **Patched** (fix.diff / ipfw3_fixed.ko): no panic; script exits cleanly;
  guest stays up. Validated 3 consecutive runs.

## Fix

See `fix.diff`. The fix adds a `uint32_t refcnt` field to `struct ip_fw`
(filling existing alignment padding — no ABI change) and implements the
same refcounting pattern as the classic ipfw (`sys/net/ipfw/ip_fw2.c`):

1. `add_rule_dispatch`: `rule->refcnt = 1` (chain holds one reference).
2. `ip_fw3_dummynet_io`: `atomic_add_int(&rule->refcnt, 1)` +
   `pkt->dn_unref_priv = ip_fw3_unref_dn_priv`.
3. `ip_fw3_delete_rule` / `flush_rule_dispatch`: refcount-aware free
   (kfree only when `refcnt` reaches 0).
4. `ip_fw3_unref_dn_priv` callback: called by dummynet when a tagged
   packet is freed/re-dispatched → decrements refcnt → kfree if last ref.

See `VERDICT.md` for the full analysis and `fix_run.log` for the
before/after contrast.
