# DF-0471 — Verdict

**Verdict: REPRODUCED (kernel memory-corruption → hard DoS / ddb wedge). FIX VALIDATED.**

## Summary

`ip_fw3_ctl_x()` (`sys/net/ipfw3/ip_fw3.c:1038-1047`) strips the 4-byte
`ip_fw_x_header` from a `setsockopt(IPPROTO_IP, IP_FW_X, ...)` payload with no
bounds check:

```c
int
ip_fw3_ctl_x(struct sockopt *sopt)
{
    ip_fw_x_header *x_header;
    x_header = (ip_fw_x_header *)(sopt->sopt_val);
    sopt->sopt_name = x_header->opcode;            /* :1043 */
    sopt->sopt_valsize -= sizeof(ip_fw_x_header);  /* :1044  NO check */
    bcopy(++x_header, sopt->sopt_val, sopt->sopt_valsize); /* :1045 */
    return ip_fw3_ctl(sopt);
}
```

`sopt->sopt_valsize` is `size_t` (unsigned). The setsockopt plumbing
(`sys/kern/uipc_syscalls.c:1250` `sopt.sopt_valsize = uap->valsize`;
`kern_setsockopt` at `:1221` rejects only `valsize==0` and `:1223` only
`valsize > SOMAXOPT_SIZE`) lets `valsize ∈ {1,2,3}` through. With e.g.
`valsize=2`, line 1044 computes `2 - 4 = 0xfffffffffffffffe` and line 1045 issues
`bcopy(sopt_val+4, sopt_val, 0xfffffffffffffffe)` — an unbounded forward copy
that walks straight off the option buffer through kernel heap, corrupting
everything in its path. The guest wedges: on the next timer interrupt the
corrupted heap trips the kernel into DDB.

## Reachability / threat model (honest)

- **Trigger is root-only.** The control path needs a raw IP socket
  (`socket(AF_INET, SOCK_RAW, IPPROTO_RAW)`) to reach `rip_ctloutput`
  (`sys/netinet/raw_ip.c:385` SOPT_SET `IP_FW_X` → `ip_fw3_sockopt`
  `sys/net/ipfw3/ip_fw3_glue.c:51` → `ip_fw3_ctl_x`), and raw sockets require
  root. **No unprivileged privilege boundary is crossed.** Relevance: a
  compromised-root process, a setuid ipfw3 front-end, or a jail escape.
- **Module not in GENERIC.** `ipfw3` is `optional ipfirewall3`
  (`sys/conf/files:1818`); the default `X86_64_GENERIC` does not compile it.
  It must be `kldload`ed. (The 2-line glue `ip_fw3_glue.c` is `optional inet`
  and IS in GENERIC, but only forwards to the module.)

## Mechanism (every hop cited)

1. **Attacker (root):** `setsockopt(s, IPPROTO_IP, IP_FW_X=49, buf[2], 2)`.
2. `sys_setsockopt` (`sys/kern/uipc_syscalls.c:1242`): `sopt_valsize = 2`;
   `sopt_val = kmalloc(2)`; `copyin(buf, sopt_val, 2)`.
3. `kern_setsockopt` (`:1213`): valsize `0 < 2 <= SOMAXOPT_SIZE` → accepted.
4. `sosetopt` → `rip_ctloutput` (`sys/netinet/raw_ip.c:385`): case `IP_FW_X`
   → `ip_fw3_sockopt` (`ip_fw3_glue.c:51`).
5. `ip_fw3_ctl_x` (`ip_fw3.c:1038`):
   - `:1042` `x_header = sopt_val` (cast; points into the 2-byte buffer)
   - `:1043` `sopt_name = x_header->opcode` reads a `uint16_t` (2 bytes) — in
     bounds for valsize=2.
   - `:1044` `sopt_valsize -= 4` → `2 - 4 = 0xfffffffffffffffe` (size_t wrap).
   - `:1045` `bcopy(sopt_val+4, sopt_val, 0xfffffffffffffffe)` — unbounded copy.

## Evidence

### Reproduction (unpatched `#0` kernel)

`./run.sh` (load `ipfw3` with `filters_default_to_accept=1` so ssh survives,
then run `./uflow`). The PoC opens a raw socket and issues
`setsockopt(IPPROTO_IP, IP_FW_X, buf[2], 2)`. Output stops mid-call; the guest
becomes unresponsive. Serial console (`dfbsd-qemu/boot.log`):

```
ipfw3 initialized, default to accept
Stopped at      systimer_add+0x179:     cmpq    %r13,0x18(%rsi)
db>
```

The unbounded bcopy corrupted kernel heap (including data used by the periodic
systimer); on the next tick the kernel faulted into DDB at `systimer_add+0x179`.
`vm.sh status` ⇒ **down** (guest wedged in the debugger). Full run transcript in
`run.log`; the ddb trap in `panic.txt`.

### Impact ceiling (honest — NOT an LPE)

- **Root-only trigger** → no privilege boundary crossed (valid hard blocker for
  an escalation chain).
- **Write content is not attacker-controlled** — the bcopy copies whatever
  follows `sopt_val` in kernel heap, with an unbounded size that faults/hangs
  rather than landing a precise controlled value. (Valid hard blocker.)
- Net demonstrated impact: **kernel memory corruption → hard DoS** (wedge in
  ddb). The value is the memory-corruption primitive reachable from a root /
  compromised-root context (CWE-787 OOB write via unsigned wrap), not an
  unprivileged escalation.

## The fix (`fix.diff`)

One hunk — reject short payloads before the unsigned subtraction, in
`ip_fw3_ctl_x`:

```c
if (sopt->sopt_valsize < sizeof(ip_fw_x_header))
    return EINVAL;
```

`sizeof(ip_fw_x_header) == 4` (`uint16 opcode` + `uint16 pad`, `ip_fw3.h:366`).
This is the minimal targeted fix at the root cause. It supersedes the finding
markdown's proposal (which suggested the same guard) with a tested, line-accurate
implementation.

## Fix validation (Phase 8)

Because `ipfw3` is a loadable module, 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 (`cp /boot/kernel/ipfw3.ko`, `kldload`), no kernel
rebuild/reboot required.

| test                          | unpatched `#0` module                 | patched module                              |
|-------------------------------|---------------------------------------|---------------------------------------------|
| `./uflow` (2-byte `IP_FW_X`)  | guest **WEDGES** in ddb at `systimer_add+0x179` | `setsockopt` ⇒ `errno=22 (EINVAL)`, guest stays up |
| regression: 4-byte `IP_FW_X`  | accepted (reaches `ip_fw3_ctl`)       | accepted `rc=0` (gets past the new check, no regression) |
| guest after test              | **down** (wedged)                     | **up**, responsive                          |

Clean before/after: the bad behaviour (unbounded bcopy → ddb wedge) is present on
the unpatched `#0` module and **gone** on the single-fix module. A legitimate
≥4-byte `IP_FW_X` payload is still accepted (`rc=0`). `fix_status = fixed`.
