# DF-0477 — ip_fw3_ctl_get_modules bcopy heap overflow

## Verdict: REPRODUCED (heap overflow confirmed); fix VALIDATED (clean before/after)

## The bug
`sys/net/ipfw3/ip_fw3.c` `ip_fw3_ctl_get_modules()` (lines 985-987):

```c
bzero(sopt->sopt_val, sopt->sopt_valsize);
bcopy(module_str, sopt->sopt_val, strlen(module_str));   /* NO bounds check */
sopt->sopt_valsize = strlen(module_str);
```

`module_str` is built by `strcat`-ing every loaded ipfw3 submodule name into a
1024-byte stack buffer. `sopt->sopt_val` is a `kmalloc(sopt_valsize, M_TEMP)`
buffer whose size is the **caller-controlled** getsockopt buffer length. The
`bcopy` uses `strlen(module_str)` as the length with **no check that it is ≤
sopt_valsize**, so it writes past the kmalloc'd slab chunk.

## Reachability / privilege boundary
Reached via `getsockopt(IPPROTO_IP, IP_FW_X=49, {opcode=IP_FW_MODULE=67, ...})`
on a raw IP socket. The path: `rip_ctloutput` → `ip_fw3_sockopt` →
`ip_fw3_ctl_x` (strips the 4-byte `ip_fw_x_header`, reduces `sopt_valsize` by 4)
→ `ip_fw3_ctl` → `IP_FW_MODULE` → `ip_fw3_ctl_get_modules`.

**Privilege:** requires a raw IP socket (`rip_attach` →
`caps_priv_check(SYSCAP_NONET_ROOT)` = root, `sys/netinet/raw_ip.c:473`) AND
`kldload ipfw3` (root). This is a **root→kernel** path: there is **no
unprivileged→root escalation** (root already owns the kernel). It is a
root→kernel hardening gap with a real heap-overflow primitive.

## Reproduction (confirmed)
With `ipfw3` + submodules `basic,layer2,layer4,nat` loaded,
`module_str = "basic,layer2,layer4,nat"` (23 bytes). Issuing the getsockopt
with a 5-byte buffer (4-byte header + 1 payload byte → `kmalloc(5, M_TEMP)` →
Zone-16 chunk):

```
bufsize=5 iter 0: getsockopt rc=0 returned_len=23
  payload_slot=1 returned_len=23 => OVERFLOW (bcopy exceeded payload slot)
```

The kernel set `sopt_valsize = strlen(module_str) = 23` and `bcopy`'d 23 bytes
into a 16-byte slab chunk ⇒ **7-byte heap overflow** into the adjacent M_TEMP
Zone-16 chunk. The `returned_len=23` (≫ payload_slot=1) is the proof the
overflow fired.

On default GENERIC (INVARIANTS ON) this did **not** immediately panic: in a
tight loop the same two slab chunks are recycled (LIFO), so the corrupted
neighbour stays free and is never re-validated by `chunk_mark_allocated`. The
corruption is real; a panic is achievable with slab grooming (drain the free
list so the neighbour is a live, validated object) but is moot for impact since
the path is already root-only.

## Fix (validated)
`fix.diff` bounds the `bcopy` length by `sopt_valsize`:

```c
size_t mlen = strlen(module_str);
bzero(sopt->sopt_val, sopt->sopt_valsize);
if (mlen > sopt_valsize)
    mlen = sopt_valsize;
bcopy(module_str, sopt->sopt_val, mlen);
sopt->sopt_valsize = mlen;
```

### Before / after (clean contrast)
| kernel / module      | bufsize=5 `returned_len` | overflow? |
|----------------------|--------------------------|-----------|
| unpatched (#0)       | **23**                   | **YES** (7-byte heap overflow) |
| patched (#1)         | **1** (truncated "b")    | no        |

With a large buffer (256) the patched module still returns the full 23-byte
`module_str` ⇒ **no regression**, the fix only bounds the small-buffer case.

## Files
- `ipfw3_get_modules_overflow.c` — parameterized trigger (iters, bufsize)
- `run_detached.sh` — baseline driver (unpatched; output to /root/df0477.out)
- `run_fixed.sh` — patched driver (output to /root/df0477_fixed.out)
- `run.log` — unpatched baseline run (returned_len=23 ⇒ overflow)
- `run_patched.log` — patched run (returned_len=1 ⇒ no overflow)
- `fix.diff` — git-apply-able fix
- `build.sh` / `run.sh` — repro scripts
