# DF-0473 — Verdict

**Verdict: REPRODUCED (OOB indirect function call -> kernel panic). FIX VALIDATED.**

## The bug

`ip_fw3_chk` (`sys/net/ipfw3/ip_fw3.c:506-507`) evaluates a firewall rule's
instructions and, for each, performs an indirect call:

```c
(filter_funcs[cmd->module][cmd->opcode])
    (&cmd_ctl, &cmd_val, &args, &f, cmd, ip_len);
```

`filter_funcs` is declared at `ip_fw3.c:163` as

```c
filter_func filter_funcs[MAX_MODULE][MAX_OPCODE_PER_MODULE];
```

with `MAX_MODULE = 10` and `MAX_OPCODE_PER_MODULE = 100` (`ip_fw3.h:94`) — a
**1000-entry** array. `cmd->module` and `cmd->opcode` are `uint8_t` (range
0..255) taken verbatim from the rule (`ipfw_insn`, `ip_fw3.h:124-132`) and are
**never bounds-checked** anywhere:

- the rule-add path `ip_fw3_ctl_add_rule` (`ip_fw3.c:951`) validates only the
  total `sopt_valsize` (`[sizeof(ioc_rule)-sizeof(ipfw_insn) .. 1020]`,
  `:956-957`), never the per-instruction `module`/`opcode`;
- the eval loop (`:493-507`) indexes the array directly.

A rule whose instruction carries `module >= 10` or `opcode >= 100` therefore
indexes **past the 1000-entry array**, reads an arbitrary kernel pointer, and
**calls it**. With `module=0x80 opcode=0x80` the index is
`0x80*100 + 0x80 = 12928`, i.e. **12828 entries** past the array — deep into
neighbouring kernel data (the `fw3_modules`/`fw3_ctx`/`fw3_sync_ctx` globals
and beyond).

## Distinctness from DF-0472

DF-0472 was the missing **`cmd_len`** validation: it fed garbage into the
rule via a `cmd_len=255` over-read. DF-0473 is a **separate, independent**
bug: the OOB `module`/`opcode` index use. This PoC proves it by installing a
**correctly-sized** rule (`cmd_len=2`, one 8-byte instruction, `act_ofs=0`,
full data supplied) whose single instruction explicitly carries
`module=0x80 opcode=0x80`. This rule passes DF-0472's `cmd_len` validation
(`size=48 ∈ [32,1020]`, `cmd_len=2`, `act_ofs=0 < cmd_len`, full data
present) yet still triggers the OOB call. DF-0473 therefore survives a
complete fix for DF-0472 and must be fixed independently at the call site
(and/or the install path).

## Mechanism (every hop cited)

- **Trigger (root):** `socket(AF_INET, SOCK_RAW, IPPROTO_RAW)` →
  `setsockopt(IPPROTO_IP, IP_FW_X=49, [x_hdr.opcode=IP_FW_ADD=50][ioc_rule], 48)`
  (`in.h:389`). `raw_ip.c` `rip_ctloutput` → `ip_fw3_sockopt`
  (`ip_fw3_glue.c`) → `ip_fw3_ctl_x` (`ip_fw3.c:1038`) strips the 4-byte
  `x_header` → `ip_fw3_ctl` → `ip_fw3_ctl_sockopt` (`:1138`) case `IP_FW_ADD`
  → `ip_fw3_ctl_add_rule` (`:951`).
- **No per-instruction validation (`:956-979`):** only `sopt_valsize` and
  (with DF-0472's fix) `cmd_len`/`act_ofs`/`size` are checked; `module`/
  `opcode` of every instruction are copied verbatim into the rule by
  `add_rule_dispatch` (`:655`, `bcopy(ioc_rule->cmd, rule->cmd, cmd_len*4)`).
- **OOB indirect call (`:506`):** when the firewall is enabled and a packet
  is evaluated, `ip_fw3_chk` walks the rule and calls
  `(filter_funcs[cmd->module][cmd->opcode])(...)`. Attacker `module=0x80,
  opcode=0x80` indexes entry 12928 of the 1000-entry array → wild pointer
  call → kernel panic.

## Evidence

### OOB-call panic (trap 9)

`./oobcall` on the unpatched `#0` module installs the OOB-index rule
(`rc=0`), enables the firewall, and the guest panics on the next evaluated
packet (full signature in `panic.txt`):

```
Fatal trap 9: general protection fault while in kernel mode
cpuid = 0; lapic id = 0
instruction pointer     = 0x8:0xffffffff826001a4
current process         = Idle
Stopped at      ip_fw3_chk+0x1a4:       ret
```

RIP `0xffffffff826001a4` = `ipfw3.ko` base (`0xffffffff82600000`) + `0x1a4`,
symbolised by ddb as `ip_fw3_chk+0x1a4` — the
`filter_funcs[module][opcode]` indirect-call site (`:506`). The wild call ran
a few instructions off the corrupted pointer and faulted on `ret`.

## Impact (honest)

- **Root-only trigger.** The rule-add path needs a raw socket (root) and the
  `ipfw3` KLD module loaded. No unprivileged-user privilege boundary is
  crossed; the value is the **kernel memory-corruption primitive**
  (CWE-129 OOB indirect call / CWE-787 wild call), not LPE. Relevance:
  compromised root process, a setuid ipfw3 front-end, jail escape.
- **Primitive characterisation.** On this guest SMEP/SMAP/KASLR are OFF, so
  a heap-grooming chain that lands a chosen value at
  `filter_funcs[0x80][0x80]` would redirect the call to userspace shellcode
  (`commit_creds(prepare_kernel_cred(0))`) for root→kernel code execution;
  demonstrated here at the panic (DoS) level. Because the trigger is already
  root, an LPE chain is moot — the deliverable is the memory-corruption
  primitive itself.

## The fix (`fix.diff`) — two defense-in-depth hunks

**Hunk 1 (root cause, what the finding recommends): call-site bounds + NULL
check at `ip_fw3.c:506`.** Before the indirect call, verify
`cmd->module < MAX_MODULE && cmd->opcode < MAX_OPCODE_PER_MODULE` and that
`filter_funcs[cmd->module][cmd->opcode] != NULL`; if any fails, set
`cmd_val = 0` and `goto next_cmd` (skip the instruction, treat as non-match).
This is the last line of defence: it catches OOB/unregistered opcodes from
*any* source (a crafted rule, the DF-0472 over-read path, future bugs, or
corrupted memory).

**Hunk 2 (defense-in-depth, front door): install-time range validation in
`ip_fw3_ctl_add_rule` (`ip_fw3.c:979`).** Walk every instruction of the
candidate rule using the same `F_LEN()` stepping as `ip_fw3_chk` and
`return EINVAL` if any instruction has `module >= MAX_MODULE` or
`opcode >= MAX_OPCODE_PER_MODULE`. This rejects the malicious rule before it
is ever installed.

Both hunks are minimal and targeted; a legitimate rule (`module=0` BASIC,
`opcode=0` ACCEPT) is still accepted (verified: `rc=0`, no regression).

This fix **supplements** (does not conflict with) DF-0472's `cmd_len` fix —
the two address orthogonal defects in the same two functions.

## Fix validation (Phase 8)

`ipfw3` is a loadable module, so the fix was validated by rebuilding only
`ipfw3.ko` (`make KERNBUILDDIR=.../X86_64_GENERIC` in `sys/net/ipfw3`,
`-Werror`, `rc=0`) and hot-swapping it (`kldunload`/`cp`/`kldload`); no
kernel rebuild/reboot required.

| test                                          | unpatched `#0` module                      | patched module                                |
|-----------------------------------------------|--------------------------------------------|-----------------------------------------------|
| OOB rule add (`module=0x80 opcode=0x80`)      | `rc=0` (rule ACCEPTED — **the bug**)       | `rc=-1 EINVAL` (hunk #2 rejects)              |
| full `oobcall` PoC (install + enable + pkt)   | **trap 9 panic** at `ip_fw3_chk+0x1a4`     | add rejected; firewall never enabled; **no panic**, guest UP |
| legit rule add (`module=0 opcode=0` ACCEPT)   | `rc=0`                                     | `rc=0` (**no regression**)                    |
| in-range/unregistered opcode (`module=0 opcode=50`, NULL `filter_func`) — exercises hunk #1 | would NULL-deref panic at `:506` | call-site NULL check skips; **no panic**, guest UP |
| `ipfw3.ko` build                              | n/a                                        | `-Werror`, `rc=0`                             |

Clean before/after: the bad behaviour (OOB rule accepted + panic) is present
on the unpatched `#0` module and **gone** on the single-fix module, with
legitimate rules unaffected and both hunks independently exercised.
**`fix_status = fixed`.**

## Reproduce

```
cd findings/poc/DF-0473
./build.sh            # cc -Wall -o oobcall oobcall.c (+ add_oob, add_legit, nullcall)
# as root, on the guest:
kldload ipfw3
sysctl net.inet.ip.fw3.enable=0
./oobcall             # UNPATCHED: kernel panic at ip_fw3_chk+0x1a4
# after applying fix.diff + hot-swapping ipfw3.ko:
./oobcall             # PATCHED: add rejected EINVAL, no panic
./add_legit           # PATCHED: legit rule still accepted (rc=0)
```
