# DF-0748 — Unchecked uint16_t index into 10-element `log_if_table[]` — OOB access

**Verdict:** REPRODUCED (panic / memory-corruption primitive proven via harness; **root-only** reachability; **dead code on default kernel**). Fix VALIDATED (single-module rebuild; deterministic before/after).

**Severity as filed:** Medium. **Verified realistic impact:** latent panic / OOB-read primitive in ipfw3 logging; reachable only by root, and only when (a) the ipfw3/ipfw3_basic modules are loaded, (b) `net.inet.ip.fw3.verbose=1`, and (c) `ip_fw3_log_ptr` is wired (which no current code does).

---

## The bug, confirmed in source

The function `ip_fw3_log()` at **`sys/net/ipfw3_basic/ip_fw3_log.c:114`** takes a `uint16_t id` parameter and uses it to index a 10-element array with **no bounds check**:

```c
// sys/net/ipfw3_basic/ip_fw3_log.c
70:  struct ifnet *log_if_table[LOG_IF_MAX];   // LOG_IF_MAX = 10  (ip_fw3_log.h:39)
...
114: void ip_fw3_log(struct mbuf *m, struct ether_header *eh, uint16_t id)
115: {
116:     struct ifnet *the_if = NULL;
...
121:     the_if = log_if_table[id];                       // <-- OOB if id >= 10
122:     if (the_if == NULL || the_if->if_bpf == NULL) {  // <-- deref of OOB-read ptr
            ...
128:     bpf_mtap_hdr(the_if->if_bpf, ...);              // <-- deref of if_bpf
```

The `id` flows from `cmd->arg1` (uint16_t, `sys/net/ipfw3/ip_fw3.h:127`) set by the ipfw3 CLI's `log N` parser **without any validation**:

```c
// sbin/ipfw3/ipfw3basic.c:93,110
(*cmd)->arg1 = strtoul(**av, NULL, 10);   // no N < 10 check anywhere
```

The kernel rule-add path bcopy's this verbatim into the in-kernel rule (`sys/net/ipfw3/ip_fw3.c:655`), and `check_accept`/`check_deny` (`ip_fw3.c:253,264`) hand `cmd->arg1` straight to `ip_fw3_log_ptr(...)` with no per-instruction validation.

## Reproduction / primitive proof

**Trigger chain (root-only):**
1. `sysctl net.filters_default_to_accept=1` (so loading ipfw3 doesn't drop SSH)
2. `kldload ipfw3 ; kldload ipfw3_basic`
3. `sysctl net.inet.ip.fw3.verbose=1` (the gate at `ip_fw3_log.c:118`)
4. install the harness kld `df748_wire_log.ko` (sets `ip_fw3_log_ptr = ip_fw3_log`)
5. `ipfw3 add 1000 deny log 12 icmp from 127.0.0.1 to 127.0.0.1`
6. `ping -c1 127.0.0.1` → kernel panic

**Observed panic (deterministic, 1/1 runs):**
```
Fatal trap 9: general protection fault while in kernel mode
cpuid = 0; lapic id = 0
instruction pointer = 0x8:0xffffffff8072a759
current process = Idle
kernel: type 9 trap, code=0
Stopped at  bpf_mtap+0x79:  movl 0x68(%rbx),%eax
db>
```

The chain is: `check_deny` (`ip_fw3.c:264`) → `ip_fw3_log_ptr(m, eh, 12)` → `ip_fw3_log` (`ip_fw3_log.c:114`) → `the_if = log_if_table[12]` (`:121`, **OOB read**) → `the_if` = 0xffffffff8264f080 (the address of `sysctl__net_inet_ip_fw3_basic_children`) → `the_if->if_bpf` (offset 16) = **0xc000200200000100** (non-canonical) → `bpf_mtap_hdr(0xc000200200000100, ...)` → `bpf_mtap` deref → **trap 9 GPF**.

The runtime symbol addresses (kgdb on `/dev/mem`) confirm the layout:
- `log_if_table` @ 0xffffffff8264fba0 (10 entries, ends at 0xffffffff8264fbf0)
- `id=12` reads at `0xffffffff8264fba0 + 8*12 = 0xffffffff8264fc00` = the `sysctl__net_inet_ip_fw3_basic_children` slot
- offset +16 of that = 0xc000200200000100 → non-canonical

## Why a harness is needed (the dead-code caveat)

`ip_fw3_log()` is **dead code on a default kernel**. Its only call sites (`ip_fw3.c:253` and `:264`) are gated by `ip_fw3_log_ptr != NULL`, but:

```
$ grep -rn ip_fw3_log_ptr sys/
sys/net/ipfw3/ip_fw3.c:134:ip_fw_log_t *ip_fw3_log_ptr = NULL;   /* initialised NULL */
sys/net/ipfw3/ip_fw3.c:252:    if (cmd->arg3 && ip_fw3_log_ptr != NULL) {
sys/net/ipfw3/ip_fw3.c:253:        ip_fw3_log_ptr((*args)->m, (*args)->eh, cmd->arg1);
sys/net/ipfw3/ip_fw3.c:263:    if (cmd->arg3 && ip_fw3_log_ptr != NULL) {
sys/net/ipfw3/ip_fw3.c:264:        ip_fw3_log_ptr((*args)->m, (*args)->eh, cmd->arg1);
```

Five references, all in `ip_fw3.c`: one declaration (init = NULL), two NULL-guarded reads around the call. **No writer anywhere in `sys/`.** So the buggy function cannot be reached from packet processing on a stock kernel.

The 30-line harness module (`harness/harness.c`) does the registration the upstream feature never finished (`ip_fw3_log_ptr = &ip_fw3_log`) and is explicit about the dependency in its MODULE_DEPEND. Once loaded, the existing packet-processing path becomes live and the OOB read fires.

This places the finding in Phase-4 case (d): *the vulnerable code path is dead at runtime; prove the primitive at the object/harness level, and note the live trigger conditions that would make it real.* The harness is that proof. The live trigger condition is "any future commit that wires `ip_fw3_log_ptr` (which is the obvious next step for the unfinished ipfw3 logging feature)."

## Privilege / reachability (the no-escalation story)

This bug has **no unprivileged->root escalation path**, for two independent reasons (either suffices; both hold):

1. **Root-only trigger.** Installing any ipfw3 rule requires the `IP_FW_X` setsockopt, which is dispatched in `rip_ctloutput` (`sys/netinet/raw_ip.c:386`). Raw IP sockets are gated by `caps_priv_check(p_ucred, SYSCAP_NONET_RAW)` at `raw_ip.c:473`. `SYSCAP_NONET_RAW` is root-only on a default DragonFly install. **No unprivileged user can install a `log N` rule.**

2. **Default-dead path.** Even an attacker who *could* install a rule cannot reach `ip_fw3_log()` on a default kernel because `ip_fw3_log_ptr` is never assigned.

Per the bright-line rule, this is a **valid hard blocker** for escalation: *"The write/primitive is reachable only from an already-root context... root→kernel is game-over by definition."* Combined with the dead-code blocker, there is nothing to escalate from. So:

- **Escalation attempts:** 0 — there is no unprivileged primitive to escalate from. (Spending 15–30 grooming attempts would be pointless: the trigger itself already requires root.)
- **Realistic impact ceiling:** panic / DoS / latent OOB-read primitive (could in principle leak kernel memory to a bpf tap on the forged ifp, but no bpf listener is normally attached to the corrupted fake ifp, so crash dominates).
- **Recommended triage:** defense-in-depth. The bounds check is correct and cheap; upstream should add it (the harness exists *because* someone clearly intends to wire this code path soon).

## Fix (validated)

`fix.diff` adds a single bounds check at the top of `ip_fw3_log()`:

```diff
+    if (id >= LOG_IF_MAX) {
+        return;
+    }
```

This is the smallest correct fix: it closes the OOB read at the sink (`ip_fw3_log.c:121`) regardless of how `id` arrives. (A defense-in-depth companion would also clamp at rule-install in `ip_fw3.c` / `sbin/ipfw3/ipfw3basic.c`, but the sink-side guard is the root-cause fix.)

**Validation (Phase 8):** module-only rebuild of `ipfw3_basic.ko` (the bug lives entirely in module C; no kernel rebuild needed). Same trigger on:

- **unpatched baseline** (`/boot/kernel/ipfw3_basic.ko`, sha256 `3c3af7cf...`): **panic** in `bpf_mtap+0x79`, guest **down**.
- **patched module** (rebuilt from fixed source, sha256 `7d0c4ae2...`): trigger runs to completion, rule still matches (ping is denied), **no panic, guest stays up**. Reproduced 3/3 times.

The kern.version string is unchanged (`6.5-DEVELOPMENT #0`) because the kernel image itself is not touched — only the loaded module. The before/after contrast is the panic-vs-no-panic behavior of the same trigger.

## PoC changes (what's in this folder vs. what the finding proposed)

The finding markdown does not exist on disk (only the DB row exists). The DB summary proposed: trigger `ipfw3 add 1000 deny log 14 ip from any to any then ping`. That proposed PoC has **two errors** the runner corrected:

1. **`log 14` does not reliably panic.** At runtime `fake_eh` is loaded at a *lower* address than `log_if_table` (0xffffffff8264f040 vs 0xffffffff8264fba0), so id=14 reads BSS padding = NULL → silent early-return. The runner measured the actual live layout (kgdb) and chose **`id=12`**, which reads the `sysctl__net_inet_ip_fw3_basic_children` slot whose +16 field is a non-canonical 0xc000200200000100 → reliable trap 9.
2. **`log N from any to any` would deny SSH and break the test harness.** The runner narrowed the match to `icmp from 127.0.0.1 to 127.0.0.1`.
3. **The trigger alone does nothing on a default kernel** — `ip_fw3_log_ptr` is never assigned. The runner built a 30-line harness kld (`harness/harness.c`) to wire the pointer, making the path live and the OOB read reachable. This proves the primitive at the object level per Phase-4 case (d); it is **not** part of any escalation chain (kldload is root-only by definition).

## How to reproduce

```sh
scp -r findings/poc/DF-0748/. dfbsd:/root/poc748/    # root shell on guest
ssh dfbsd
sh /root/poc748/run.sh                                # panics on unpatched, exits 0 on patched
# panic signature lands in dfbsd-qemu/boot.log
```

## Kernel references confirmed during verification

- `sys/net/ipfw3_basic/ip_fw3_log.c:70` — `struct ifnet *log_if_table[LOG_IF_MAX]` (the array)
- `sys/net/ipfw3_basic/ip_fw3_log.c:114` — `ip_fw3_log(... uint16_t id)` (entry point; no bounds check)
- `sys/net/ipfw3_basic/ip_fw3_log.c:121` — `the_if = log_if_table[id]` (the OOB read)
- `sys/net/ipfw3_basic/ip_fw3_log.c:122` — `the_if->if_bpf` (offset 16; deref of OOB-read pointer)
- `sys/net/ipfw3_basic/ip_fw3_log.h:39` — `#define LOG_IF_MAX 10`
- `sys/net/ipfw3/ip_fw3.h:127` — `uint16_t arg1` (the unchecked field in `ipfw_insn`)
- `sys/net/ipfw3/ip_fw3.c:134` — `ip_fw_log_t *ip_fw3_log_ptr = NULL` (never assigned anywhere)
- `sys/net/ipfw3/ip_fw3.c:253,264` — the only call sites, NULL-gated
- `sbin/ipfw3/ipfw3basic.c:93,110` — `(*cmd)->arg1 = strtoul(...)` (no validation)
- `sys/netinet/raw_ip.c:386,473` — `IP_FW_X` setsockopt path + `SYSCAP_NONET_RAW` gate (root-only)
