# DF-0668 — ipfw3 table op OOB via unchecked `ioc_table->id`

## Verdict: REPRODUCED (panic in cited function + fix validated)

Every ipfw3 table dispatch handler indexes a per-CPU array
`ctx->table_ctx[IPFW_TABLES_MAX=32]` with a user-controlled `int id` and **no bounds
check**:

```c
/* sys/net/ipfw3_basic/ip_fw3_table.c:84-95 (table_create_dispatch) */
ioc_table = tbmsg->ioc_table;
int id = ioc_table->id;            /* from user, unchecked */
table_ctx = ctx->table_ctx;
table_ctx += id;                   /* OOB pointer arithmetic */
table_ctx->type = ioc_table->type; /* write */
table_ctx->count = 0;              /* write */
strlcpy(table_ctx->name, ...);     /* write */
rn_inithead(&table_ctx->mask, ...);/* deposits a kernel pointer at OOB */
rn_inithead(&table_ctx->node, ...);/* deposits a kernel pointer at OOB */
```

The identical unchecked `table_ctx += ioc_tbl->id` exists in `table_delete_dispatch`
(:127), `table_append_dispatch` (:149), `table_remove_dispatch` (:205),
`table_flush_dispatch` (:244), `table_rename_dispatch` (:265). `ctx->table_ctx` is
`kmalloc(32*sizeof(struct ipfw3_table_context)=1792, M_IPFW3_TABLE)`
(`ip_fw3_table.c:570`); any `id >= 32` walks past it. `IPFW_TABLES_MAX=32` is defined
but **never used to validate `id`** anywhere.

## Reachability — ROOT-ONLY (valid hard blocker for uid=0)

The sink is reached via `setsockopt(IPPROTO_IP, IP_FW_X, ...)` on a **raw IP socket**:

```
setsockopt(IPPROTO_IP, IP_FW_X=49, [x_header{opcode=IP_FW_TABLE_CREATE=73}]
                                    [struct ipfw_ioc_table{id,type,...}])
  -> raw_ip.c rip_ctloutput -> ip_fw3_sockopt -> ip_fw3_ctl (IP_FW_X)
  -> ip_fw3_ctl_x (strip x_header, sopt_name=73) -> ip_fw3_ctl (IP_FW_TABLE_CREATE)
  -> ip_fw3_ctl_table_ptr -> ip_fw3_ctl_table_create -> table_create_dispatch [per CPU]
```

Creating a raw IP socket requires `caps_priv_check(SYSCAP_NONET_RAW)`
(`sys/netinet/raw_ip.c:473`). An unprivileged user gets **EPERM**:

```
$ ./ipfw3_table_oob 33            # as maxx (uid 1001)
socket(SOCK_RAW) failed: Operation not permitted (expected for unpriv)
```

So the write is reachable **only from an already-root context**. This is a root→kernel
hardening gap (a root user with ipfw3 access can corrupt arbitrary kernel memory), not
an unprivileged local privilege escalation. **There is no unprivileged→uid=0 chain**
because the privilege boundary (raw-socket capability) is not crossed.

## Primitive characterization (measured on this guest)

| property | value |
|---|---|
| allocation overflowed | `kmalloc(1792, M_IPFW3_TABLE)` per CPU (ctx->table_ctx) |
| write offset | `id * sizeof(struct ipfw3_table_context)` = `id * 56` bytes (fully attacker-controlled via `id`) |
| fields written | `type` (attacker int), `count` (0), `name[32]` (attacker bytes), + 2 radix_node_head kernel pointers (rn_inithead) |
| small OOB (id=33..N) | silent corruption of adjacent M_IPFW3_TABLE slab / kmalloc arena |
| large OOB (id=0x10000000) | page fault, panic in `table_create_dispatch+0x45` (`movl $0,0x30(%rbx)` = `table_ctx->count = 0`) |

Decisive baseline evidence (`id=0x10000000`, root):
```
Fatal trap 12: page fault while in kernel mode
Stopped at      table_create_dispatch+0x45:     movl    $0,0x30(%rbx)
```

## Escalation ceiling

None for an unprivileged user (root-only reachability — see above). For a root caller
the primitive is a fully-controlled kernel heap write (chosen 56-byte-stride offset +
controlled type/name bytes + kernel pointer deposits), trivially gameable for
root→kernel code execution, but root→kernel is already game-over. The realistic
finding value is a **privilege-boundary / robustness bug**: ipfw3 must never trust
`ioc_table->id`, and any future unprivileged ipfw3 control path (e.g. a delegated
socket, jail, or setuid helper) would turn this into local kernel RCE.

## PoC changes

- Wrote `ipfw3_table_oob.c`: opens a raw IP socket, issues
  `setsockopt(IPPROTO_IP, IP_FW_X, {x_header opcode=TABLE_CREATE} + {ioc_table id,type})`.
  `id` is argv[1] (default 0x4000). Build/run via `build.sh`/`run.sh` (run.sh loads
  ipfw3 with `net.filters_default_to_accept=1` so ssh survives the firewall).

## Fix (fix.diff)

Add an `id < 0 || id >= IPFW_TABLES_MAX` bounds check (return EINVAL, forward the
netmsg and return) to **all six** table dispatch functions (create/delete/append/
remove/flush/rename). Validated on a freshly-built `ipfw3_basic.ko`:

- baseline: `id=16384` → `setsockopt` returns **0** (OOB accepted/silent); `id=0x10000000` → **panic** in `table_create_dispatch`.
- fixed:    `id=16384` and `id=0x10000000` → **EINVAL** (no panic); valid `id=5` → **0** (legit tables still work).

The finding markdown had no per-finding proposal to supersede; this fix targets the
root cause (missing bounds check at every `table_ctx += id`).

## Kernel references (verified)
- `sys/net/ipfw3_basic/ip_fw3_table.c:92-95` — create: `id = ioc_table->id; table_ctx += id`
- `sys/net/ipfw3_basic/ip_fw3_table.c:127,149,205,244,265` — delete/append/remove/flush/rename: `table_ctx += ioc_tbl->id`
- `sys/net/ipfw3_basic/ip_fw3_table.c:570` — `ctx->table_ctx = kmalloc(... * IPFW_TABLES_MAX, ...)`
- `sys/net/ipfw3_basic/ip_fw3_table.h:39` — `#define IPFW_TABLES_MAX 32`
- `sys/netinet/raw_ip.c:473` — `caps_priv_check(... SYSCAP_NONET_RAW)` (root-only)
- `sys/netinet/in.h:389` — `#define IP_FW_X 49`
- `sys/net/ipfw3/ip_fw3.h:397` — `#define IP_FW_TABLE_CREATE 73`
