# DF-0472 — `ip_fw3_ctl_add_rule` missing `cmd_len` validation

**Heap over-read (CWE-125) + kernel heap info leak (CWE-200) + OOB function-pointer call (CWE-787)**

`sys/net/ipfw3/ip_fw3.c:951` (`ip_fw3_ctl_add_rule`), `:655` (`add_rule_dispatch`),
`:1026` (`ip_fw3_ctl_get_rules`), `:506` (`ip_fw3_chk`).

## Status

**REPRODUCED** (both the info leak and the OOB-call panic), **FIX VALIDATED**.

Reachability is **root-only**: the ipfw3 control path
(`rip_ctloutput` → `ip_fw3_sockopt` → `ip_fw3_ctl_x` → `ip_fw3_ctl_add_rule`) is
reached through an `AF_INET SOCK_RAW` socket, which requires root. `ipfw3` is a
loadable module (`/boot/kernel/ipfw3.ko`), not compiled into `X86_64_GENERIC`,
so it must first be `kldload`ed. This is a root→kernel primitive — relevant to a
compromised root process, a setuid ipfw3 front-end, or a jail-escape context that
can open a raw socket and load the module. It is **not** an unprivileged LPE.

## The bug

`ip_fw3_ctl_add_rule` validates only that the *total* `sopt_valsize` is in
`[sizeof(ioc_rule)-sizeof(ipfw_insn), 1020]` and then `krealloc()`s the buffer to
1020 bytes (`sizeof(uint32_t)*IPFW_RULE_SIZE_MAX`, **no `M_ZERO`**). It never
validates `ioc_rule->cmd_len` against the data actually supplied. A caller sends
52 bytes (4-byte `IP_FW_X` header + a 48-byte `ioc_rule`) but sets
`cmd_len = IPFW_RULE_SIZE_MAX = 255`.

`add_rule_dispatch` then does:

```c
rule->cmd_len = ioc_rule->cmd_len;                                  /* 255        */
bcopy(ioc_rule->cmd, rule->cmd, rule->cmd_len * 4);                 /* 255*4=1020 */
```

`ioc_rule->cmd` lives at offset 36 of a 1020-byte (kmalloc-1024 slab) buffer, so
this `bcopy` reads bytes `[36, 1056)`:

| region              | bytes        | source                                       |
|---------------------|--------------|----------------------------------------------|
| our 1 real cmd      | `[36, 44)`   | attacker data                                |
| krealloc tail       | `[44, 1024)` | ~976 bytes **uninitialized heap** (slab residue) |
| over-read           | `[1024,1056)`| 32 bytes **past the slab object** (neighbour heap) |

That garbage becomes the rule. `ip_fw3_ctl_get_rules` copies it straight back to
userland via `IP_FW_GET` → **kernel heap info leak**. `ip_fw3_chk` then iterates
the garbage and at `:506` does

```c
(filter_funcs[cmd->module][cmd->opcode])(...);
```

`filter_funcs` is `[MAX_MODULE=10][MAX_OPCODE_PER_MODULE=100]`; attacker-chosen
`module`/`opcode` index out of bounds → **OOB indirect function call**.

## Files

| file             | purpose                                                          |
|------------------|------------------------------------------------------------------|
| `leak.c`         | info-leak / heap-over-read trigger (`IP_FW_X` ADD with `cmd_len=255`, then GET) |
| `panic.c`        | OOB-call trigger (installs a rule with `cmd[0].module=0x80 opcode=0x80`, enables firewall, sends a packet) |
| `build.sh`       | `cc -Wall -o leak leak.c; cc -Wall -o panic panic.c`            |
| `run.sh`         | loads `ipfw3`, disables packet filtering, runs `./leak`          |
| `panic.sh`       | runs `./panic` (crashes the guest)                              |
| `leak_sample.txt`| captured leaked bytes across runs (path strings + kernel pointer)|
| `panic.txt`      | fatal-trap-9 signature from `dfbsd-qemu/boot.log`               |
| `run.log`        | full decisive leak run (unpatched)                              |
| `fix_build.log`  | module build log for the patched `ipfw3.ko`                     |
| `fix_run.log`    | leak + panic PoC on the patched module (EINVAL, no panic)       |
| `env.txt`        | guest uname / cc / sysctl state                                  |
| `fix.diff`       | `git apply`-able one-hunk fix                                    |
| `VERDICT.md`     | full narrative + fix before/after                               |

## Reproduce (on the audit guest)

```sh
# as root (ssh dfbsd), guest already booted on the with-src (#0) kernel:
cd /root/poc/DF-0472 && cc -Wall -o leak leak.c && cc -Wall -o panic panic.c
sysctl net.filters_default_to_accept=1   # keep ssh alive if firewall hooks
kldload ipfw3
sysctl net.inet.ip.fw3.enable=0          # do NOT evaluate rules on live traffic
./leak                                   # expect: rc=0, non-zero "leaked" bytes
# OOB-call panic (crashes guest — capture in dfbsd-qemu/boot.log):
# ./panic
```

### Expected (bug present)

`./leak` prints `setsockopt(IP_FW_ADD) rc=0` and a hex dump whose tail contains
**leaked kernel heap** — observed: ASCII path strings (`"/root/poc/DF-0472/leak"`,
`"/home/m/r/root/poc/DF-0472/root"`) and a **kernel pointer**
`0xfffff8008db3b000`. The byte count/offsets vary run-to-run (genuine residue).

`./panic` enables the firewall and sends one UDP packet; the guest panics:

```
Fatal trap 9: general protection fault while in kernel mode
instruction pointer = 0x8:0xffffffff826001a4   (ipfw3.ko+0x1a4)
Stopped at  ip_fw3_chk+0x1a4:  ret
```

### Expected (after fix.diff)

`./leak` and `./panic` both get `setsockopt(IP_FW_ADD) rc=-1 errno=22 (EINVAL)`;
no rule is installed, nothing is leaked, no panic. A legitimate rule
(`cmd_len=2`, full data) is still accepted (`rc=0`).
