# DF-2575 — VERDICT

## Verdict: REPRODUCED — kernel panic (UAF); fix VALIDATED.

## Mechanism (trigger → primitive → effect)

### Trigger
1. Load `ipfw3` + `ipfw3_basic` + `dummynet3` (the ipfw3-native dummynet
   that registers the `pipe` action opcode with ipfw3).
2. Set `net.inet.ip.fw3.one_pass=0` — **REQUIRED**: makes re-injected
   dummynet packets re-traverse the rule chain, dereferencing `args.rule`.
3. Configure pipe 1 with multi-second delay so packets SIT in the queue
   holding `dn_priv = &rule`.
4. Add a pipe rule scoped to ICMP on lo0 (safe: doesn't affect ssh on
   vtnet0): `ipfw3 add 100 pipe 1 icmp from 127.0.0.1 to 127.0.0.1`.
5. Flood pings to 127.0.0.1 → packets accumulate in pipe 1's queue, each
   tagged with `dn_priv = pointer to rule 100`.
6. Delete rule 100 while packets are queued → `delete_rule_dispatch` runs
   on all CPUs → `kfree(rule, M_IPFW3)` with dangling `dn_priv`.
7. After the pipe delay expires, dummynet re-injects the queued packets.

### Primitive: use-after-free (read + write on freed M_IPFW3 slab)

**Store site** — `sys/net/ipfw3/ip_fw3.c:630`:
```c
pkt->dn_priv = fwa->rule;     /* raw pointer, no refcount, dn_unref_priv=NULL */
```

**Free site** — `sys/net/ipfw3/ip_fw3.c:765` (`ip_fw3_delete_rule`):
```c
kfree(rule, M_IPFW3);         /* no sweep of in-flight dummynet tags */
```

**Deref site** — `sys/net/ipfw3/ip_fw3.c:435` (`ip_fw3_chk`, gated by
`one_pass==0 && flushing==0`):
```c
f = args->rule->next_rule;    /* UAF: args->rule loaded from dangling dn_priv */
if (f == NULL)
    f = lookup_next_rule(args->rule);  /* also derefs freed rule */
```

Then the rule-scan loop uses `f` → reads `f->rulenum`, `f->cmd_len`, etc.
On the "done" path: `f->pcnt++; f->bcnt += ip_len; f->timestamp = time_second;`
→ UAF **write** to the freed slab (if the scan reaches a matching rule).

### Effect
Under INVARIANTS slab poisoning (`debug.use_weird_array=1`), the freed
M_IPFW3 chunk is filled with `0xdeadc0de`. The re-injected packet's
`args.rule->next_rule` reads `0xdeadc0dedeadc0de` (non-canonical address)
→ the scan loop dereferences it → **general protection fault (trap 9)**.

**Panic signature** (from `dfbsd-qemu/boot.log`):
```
Fatal trap 9: general protection fault while in kernel mode
Stopped at      ip_fw3_chk+0x100:       movzbl  0x16(%rax),%ecx
```
`movzbl 0x16(%rax),%ecx` reads offset 0x16 (=22) from `rax` = the poisoned
rule pointer. Offset 22 in `struct ip_fw` is the `set` field.

Even **without** poisoning (default `use_weird_array=0`), the UAF is
present and silently corrupts: the freed slab retains stale rule data,
and "+++ ipfw: ouch!, skip past end of rules, denying packet" messages in
the boot log prove the freed memory is being traversed as a live rule
chain.

## Exploit chain

This is a **root-only** memory corruption. The entire trigger path
requires root:
- `kldload` → root
- `sysctl net.inet.ip.fw3.one_pass` → root
- `ipfw3 add/delete` → raw socket → `caps_priv_check(SYSCAP_NONET_RAW)` → root

Per the audit's bright-line rule, root→kernel is game-over by definition
(root can `kldload` arbitrary kernel code). Therefore **uid=0 escalation
from an unprivileged user is NOT possible** on this path. This is a valid
hard blocker for the escalation chain.

The realistic impact ceiling is:
- **Root-triggerable kernel panic / DoS** (confirmed).
- **Silent heap corruption** (the freed M_IPFW3 slab is read as a live
  rule chain even without poisoning — a root attacker could groom the slab
  to shape the stale data and achieve arbitrary read/write, though root
  already has kldload for that).
- **Defence-in-depth gap**: the dummynet `dn_unref_priv` callback mechanism
  exists precisely for this purpose and is used correctly by the classic
  ipfw (`sys/net/ipfw/ip_fw2.c:4448-4450`); ipfw3 simply omitted it.

## PoC changes

Authored the PoC from scratch (the PoC dir was empty):
- `poc.c` — documentation of the bug and privilege analysis.
- `poc.sh` — the trigger script (pipe config + one_pass=0 + ICMP flood +
  rule delete + wait for re-injection UAF).
- `run.sh` — setup (module load + sysctl) wrapper.
- `build.sh` — no-op (shell-based PoC).

Key design decisions:
- **Scoped to ICMP on lo0** (`from 127.0.0.1 to 127.0.0.1`) so the pipe
  rule doesn't capture ssh traffic on vtnet0.
- **`debug.use_weird_array=1`** to make the UAF crash deterministically
  (poisons freed slab with 0xdeadc0de → non-canonical deref → GP fault).
  Without this, the UAF is silent (reads stale-but-plausible data).
- **`one_pass=0`** is REQUIRED — with the default `one_pass=1`, the
  re-injected packet returns `IP_FW_PASS` before dereferencing
  `args.rule`, so no UAF occurs.

## Fix (fix.diff)

The fix implements the same refcounting pattern used by the classic ipfw
(`sys/net/ipfw/ip_fw2.c`):

1. **`ip_fw3.h`**: add `uint32_t refcnt` to `struct ip_fw`, filling the
   existing 4-byte alignment padding between `timestamp` and `sibling`.
   `sizeof(struct ip_fw)` is unchanged (60 bytes) — no ABI impact.

2. **`ip_fw3.c` `add_rule_dispatch`**: `rule->refcnt = 1` (the chain
   holds the initial reference).

3. **`ip_fw3.c` `ip_fw3_dummynet_io`**: take a reference before storing
   the pointer: `atomic_add_int(&fwa->rule->refcnt, 1)` and set
   `pkt->dn_unref_priv = ip_fw3_unref_dn_priv`.

4. **`ip_fw3.c` `ip_fw3_unref_dn_priv`** (new static function): the
   dummynet unref callback — `atomic_fetchadd_int(&rule->refcnt, -1)`;
   if it was the last reference, `kfree(rule, M_IPFW3)`.

5. **`ip_fw3.c` `ip_fw3_delete_rule` / `flush_rule_dispatch`**:
   refcount-aware free — decrement the chain reference; only `kfree`
   when `refcnt` reaches 0 (i.e., no in-flight dummynet packets hold a
   reference).

The fix compiles cleanly as an ipfw3 KLD module (`-Werror`) and was
validated by loading the fixed `ipfw3.ko` on the unpatched #0 kernel and
running the same PoC that panics the unpatched module: **3 consecutive
runs, no panic, guest stays up**.

## Before/after contrast

| Kernel / module             | PoC result                                              |
|-----------------------------|---------------------------------------------------------|
| #0 unpatched + stock ipfw3  | **Fatal trap 9** in `ip_fw3_chk+0x100` (UAF → GP fault)|
| #0 unpatched + fixed ipfw3  | **No panic**; script exits 0; guest up (3/3 runs)       |
