# DF-0749 — Verdict

**Verdict: REPRODUCED (panic / DoS, root-reachable).** Fix VALIDATED on a
built-and-booted single-fix kernel.

## The bug (confirmed by source trace + live panic)

`check_mac_from_lookup` (`sys/net/ipfw3_layer2/ip_fw3_layer2.c:109-132`) and
`check_mac_to_lookup` (`sys/net/ipfw3_layer2/ip_fw3_layer2.c:152-175`) index
the per-CPU `ctx->table_ctx[]` array (32 entries, `IPFW_TABLES_MAX`,
`sys/net/ipfw3_basic/ip_fw3_table.h:39`; allocated at
`sys/net/ipfw3_basic/ip_fw3_table.c:570`) with `cmd->arg1` and **no bounds
check**:

```c
table_ctx = ctx->table_ctx;
table_ctx += cmd->arg1;        /* line 119 / 162: NO check that arg1 < 32 */
rnh = table_ctx->node;          /* line 120 / 163: OOB load */
...
if ((*args)->eh != NULL) { ... /* eh-gate fires AFTER the OOB load */ }
```

`cmd->arg1` is a `uint16_t` populated from userland by the ipfw3 rule parser
with **no validation**:

```c
/* lib/libipfw3/layer2/ipfw3_layer2.c:153 */
(*cmd)->arg1 = strtoul(**av, NULL, 10);
```

So `ipfw3 add ... mac-from table 65535` (or `mac-to table 65535`) plants
`arg1 = 65535` directly into the rule. The matcher then runs on **every** IP
packet traversing the rule and performs `table_ctx += 65535` (offset
65535 × sizeof(struct ipfw3_table_context) = 65535 × 56 = 3,669,960 bytes
≈ 3.5 MB past the array), followed by an 8-byte load at that OOB address.

**Crucially, the OOB load at line 120/163 executes BEFORE the
`(*args)->eh != NULL` gate at line 124/167**, so the gate does not protect
against it — contradicting any "eh is always NULL so it's dormant" hand-wave.
The load itself is the bug; it just happens to be benign only when the OOB
address is mapped. With `arg1=65535` the OOB offset is unmapped → page fault.

## Reproduction

Twice, on a fresh `vm.sh reset with-src` (`6.5-DEVELOPMENT #0`,
unpatched audit-source kernel), as root:

```sh
sysctl net.filters_default_to_accept=1   # keep ssh alive
kldload ipfw3; kldload ipfw3_basic; kldload ipfw3_layer2
ipfw3 flush
ipfw3 add 1000 allow ip from any to any mac-from table 65535
ping -c 1 127.0.0.1
```

Result (both runs, identical signature modulo fault VA / cpuid):

```
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff80118b1f148   (run 1) / 0xfffff80118ad0a48   (run 2)
fault code            = supervisor read data, page not present
instruction pointer   = 0x8:0xffffffff826de167   (in ipfw3_layer2.ko)
Stopped at      check_mac_from_lookup+0x37:     movq    (%rax),%rbx
```

`check_mac_from_lookup+0x37` disassembles to the `movq (%rax), %rbx` that
implements `rnh = table_ctx->node` (line 120). `%rax` holds the OOB address;
the supervisor read of an unmapped page vector-12's → kernel panic.

## Reachability / Phase-6 escalation analysis (HARD BLOCKER — valid)

The primitive is an **OOB read** (load of `table_ctx->node` into a local
`rnh`). Reaching it requires **all** of:

1. `kldload ipfw3` + `kldload ipfw3_basic` + `kldload ipfw3_layer2` — **root
   only** (`securelevel`/kld privilege check).
2. `ipfw3 add ...` — issues `setsockopt(IPPROTO_IP, IP_FW_X, ...)` which is
   gated by `PRIV_ROOT` / `SYSCAP_NONET_RAW`.
3. A packet traversing the rule (any IP packet, including loopback).

This is **root → kernel**, which the bright-line rule explicitly lists as a
valid hard blocker: "reachable only from an already-root context (kldload /
wheel-only ioctl …), so there is no privilege boundary to cross
(root→kernel is game-over by definition)." There is **no unprivileged
path** to the matcher — neither `kldload` nor `IP_FW_X` is callable by
uid!=0.

Even setting reachability aside, the primitive itself is **read-only**:
the OOB value lands in a local `struct radix_node_head *rnh` which is only
dereferenced inside the `eh != NULL` branch (always false on the ipfw3
packet path), and is **never copied to userspace**. So there is no
attacker-observable info leak either; the only observable effect is the
page-fault panic (DoS).

Per Phase 6, **two independent valid hard blockers apply** (root-only
reachability AND read-only primitive). No escalation chain is attempted
because none is possible. The realistic impact ceiling is **kernel panic
(DoS) at root**.

## Fix

`findings/poc/DF-0749/fix.diff` — bounds-check `cmd->arg1` against
`IPFW_TABLES_MAX` **before** the `table_ctx += cmd->arg1` index, in both
`check_mac_from_lookup` and `check_mac_to_lookup`. Also adds the
`table_ctx->type == 0 || rnh == NULL` guard (matching the IPv4 sibling
`check_from_lookup` at `ip_fw3_basic.c:314` shape) as defense-in-depth
against using an uninitialized table. The bounds check is the load-bearing
fix; the type guard prevents a separate latent issue (using a table slot
that was never `create`d). Supersedes any pre-verification proposal by
placing the check at the earliest possible return point (before the OOB
index), not after.

## Fix validation (Phase 8)

Built a single-fix kernel + module on the guest:
`make -j6 nativekernel KERNCONF=X86_64_GENERIC` (warm obj, ~7 min,
`rc=0`, `-Werror` clean). Installed `kernel.stripped → /boot/kernel/kernel`
and `ipfw3_layer2.ko → /boot/kernel/ipfw3_layer2.ko`, rebooted.
`kern.version` bumped `#0` (Jul 2) → `#1` (Jul 9).

Re-ran the **identical** trigger on the patched kernel:

| kernel        | rule added | packets sent | guest after | boot.log                                    |
|---------------|-----------|--------------|-------------|---------------------------------------------|
| `#0` unpatched| yes       | 1 ping       | **down**    | `Fatal trap 12 … Stopped at check_mac_from_lookup+0x37` |
| `#1` patched  | yes       | many pings   | **up**      | only `ipfw3 module layer2 loaded`, no trap  |

The previously-observed page fault is **gone** on the patched kernel, and
legitimate in-bounds usage (`mac-from table 0`, `mac-from table 31`) still
works correctly. Fix status: **fixed**.

## Conclusion

- Status: **reproduced** (panic, deterministic, twice on fresh boot).
- Impact: **panic** (root-reachable DoS; primitive is read-only,
  unprivileged path does not exist).
- Severity as filed: Medium — agrees with the demonstrated impact ceiling.
- Fix: `fix.diff`, validated by built-and-booted single-fix kernel.

Same pattern as DF-0668 (`sys/net/ipfw3_basic/ip_fw3_table.c` dispatch
handlers); same root cause class (unvalidated `cmd->arg1`/`ioc_table->id`
used as array index). The IPv4 sibling `check_from_lookup` at
`ip_fw3_basic.c:311-320` has the **same** unbounded `table_ctx += cmd->arg1`
at line 312 and should receive the same fix (covered by DF-0668's scope).
