# DF-2592 — kernel heap memory disclosure via `ip_fw3_ctl_set_get` unbounded `bcopy`

## Verdict
**REPRODUCED** on the unpatched `6.5-DEVELOPMENT #0` kernel (ipfw3.ko + ipfw3_basic.ko
loaded). **FIX VALIDATED** on the same `#0` kernel with a single-clamp rebuild of
`ipfw3.ko` (sha256 `f44f0a2a…`). The leak is real: an attacker-controlled
`sopt->sopt_valsize` drives an unbounded `bcopy` off the end of a 4-byte field,
disclosing stale kernel heap (text/data pointers) back to userspace on every call.

## The bug (confirmed line-by-line)
`ip_fw3_ctl_set_get()` in `sys/net/ipfw3/ip_fw3_set.c:207`:

```c
int
ip_fw3_ctl_set_get(struct sockopt *sopt)
{
    struct ipfw3_context *ctx;
    ctx = fw3_ctx[mycpuid];
    bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize);   /* line 213 */
    return 0;
}
```

`ctx->sets` is a **single `uint32_t`** (4 bytes) — the last field of
`struct ipfw3_context` (`sys/net/ipfw3/ip_fw3.h:482`, 5 fields = 36 bytes on amd64),
allocated with `kmalloc(LEN_FW3_CTX, M_IPFW3, M_WAITOK|M_ZERO)` (`ip_fw3.c:1410`,
lands in the kmalloc-64 bucket). `sopt->sopt_valsize` is the caller-supplied
getsockopt length, **never clamped** to `sizeof(ctx->sets)`. The `bcopy` therefore
reads `sopt_valsize - 4` bytes past the `sets` field — through the zeroed
intra-chunk padding, then into neighbouring slab chunks / adjacent pages — and
copies all of it into the reply buffer, which `sys_getsockopt` then `copyout`s to
the caller (`sys/kern/uipc_syscalls.c:1349`).

Reach path (getsockopt, all in-kernel):
```
getsockopt(raw_ip_sock, IPPROTO_IP, IP_FW_X=49, buf, &len)   /* len attacker-controlled */
  sys_getsockopt        (uipc_syscalls.c:1303)   sopt_valsize=len; sopt_val=kmalloc(len)
  -> sogetopt -> rip_ctloutput  (raw_ip.c:327 SOPT_GET / case IP_FW_X:335)
  -> ip_fw3_sockopt             (ip_fw3_glue.c:51)
  -> ip_fw3_ctl_x               (ip_fw3.c:1039)  strips 4-byte ip_fw_x_header{opcode=95},
                                                  sopt_valsize -= 4, shifts sopt_val
  -> ip_fw3_ctl                 (ip_fw3.c:1070 case IP_FW_SET_GET=95)
  -> ip_fw3_ctl_set_sockopt     (ip_fw3_set.c:256)
  -> ip_fw3_ctl_set_get         (ip_fw3_set.c:207)  *** unbounded bcopy -> OOB read ***
```
`sopt_valsize` is bounded only by `SOMAXOPT_SIZE=65536` (non-root) /
`SOMAXOPT_SIZE0=32MB` (root) at `uipc_syscalls.c:1324` — so the attacker chooses the
read length (minus the 4-byte `IP_FW_X` header).

## Reproduction (unpatched #0 kernel)
Run as root after `sysctl net.filters_default_to_accept=1; kldload ipfw3; kldload ipfw3_basic`
(the latter survives ssh while the firewall is up):

```
# /tmp/poc2592 4096
[*] getsockopt(IPPROTO_IP, IP_FW_X=49, opcode=IP_FW_SET_GET=95) readlen=4096
[+] raw socket opened fd=3
[*] getsockopt rc=0 errno=0 returned len=4092          <-- 4092 bytes back from a 4-byte field
[!] LEAK CONFIRMED: 477 nonzero bytes returned past the 4-byte sets field
offset 0x10: 80 98 0a 4f 00 f8 ff ff  -> 0xfffff8004f0a9880   (kernel direct-map pointer)
offset 0x18: 30 09 60 82 ff ff ff ff  -> 0xffffffff82600930   (inside ipfw3.ko text @ 0xffffffff82600000)
```
The leaked `0xffffffff82600930` is a kernel text pointer into the just-loaded
`ipfw3.ko` module (kldstat base `0xffffffff82600000`) — a definitive KASLR-defeat /
kernel-base disclosure. Identical residue across runs (4096/2048/8192) reflects a
deterministic heap layout on this boot; the values are genuine heap pointers, not a
static/cosmetic buffer.

## Privilege analysis (why this is a leak, not an escalation)
The only entry to `ip_fw3_ctl_*` is `rip_ctloutput`, the `pr_ctloutput` of the raw-IP
protocol. Creating such a socket runs `rip_attach` (`raw_ip.c:460`) which gates on
`caps_priv_check(ai->p_ucred, SYSCAP_NONET_RAW)` (`raw_ip.c:473`) — i.e. **root**
(or a jail with `allow_raw_sockets`, which still grants it only to jail-root). An
unprivileged user cannot enter the bug path. Per the Phase-6 bright-line rule,
root-only reachability is a **valid hard blocker** for *escalation*, but the finding
class here is **info leak**, not memory corruption: there is no chain to develop.
The realistic impact ceiling is a **root→kernel heap disclosure / KASLR-defeat
primitive** (a hardening gap; root can already read kernel memory via `/dev/mem`,
kmem, etc., so practical privilege gain is nil, but the uninitialised-kernel-memory
exposure is real and worth fixing).

## The fix
One clamp, at the root cause (`ip_fw3_set.c:213`):

```c
/* DF-2592: ctx->sets is a single uint32_t; never copy more than that,
 * otherwise we read (and disclose) stale kernel heap past the field. */
if (sopt->sopt_valsize > sizeof(ctx->sets))
    sopt->sopt_valsize = sizeof(ctx->sets);
bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize);
```
Standalone `git apply`-able diff: `fix.diff`. Supersedes the generic
"zero/M_ZERO the response" idea in the finding prompt — the real defect is an
**unbounded read length**, not an uninitialised stack struct, so clamping the length
(not zeroing the destination) is the correct minimal fix.

## Fix validation (rebuilt ipfw3.ko, reloaded, re-ran same PoC)
- **Before** (unpatched #0 kernel + #0 module, sha `ca6ccfb9…`):
  `getsockopt(len=4096)` → returns `len=4092`, **477 nonzero bytes** past the 4-byte
  `sets` field, including kernel text pointer `0xffffffff82600930`.
- **After** (#0 kernel + rebuilt single-fix `ipfw3.ko`, sha `f44f0a2a…`):
  `getsockopt(len=4096)` → returns `len=4`, **0 nonzero bytes** past offset 4
  (only the valid `ctx->sets` value is copied out). Repeated at readlen 4096/8192 —
  deterministic.
- Rebuilt via `cd /usr/src/sys/net/ipfw3 && make KERNBUILDDIR=…/X86_64_GENERIC`
  (module-only; `MAKE_RC=0`), installed to `/boot/kernel/ipfw3.ko`, clean reboot
  to load (kldunload of the active firewall triggers a *separate* `rn_flush` panic —
  see `panic.txt` — unrelated to DF-2592).

**fix_status = fixed** (clean before/after, deterministic across readlen and runs).

## Files
- `poc.c` — minimal leak trigger (raw socket + `getsockopt(IP_FW_X)` with opcode 95).
- `build.sh` / `run.sh` — exact build & run (run as root after loading ipfw3).
- `run.log` — baseline reproduction (3 readlens, leaked kernel pointers).
- `fix_run.log` — patched-module re-run (4 bytes back, 0 leaked).
- `fix_build.log` — module rebuild output (`MAKE_RC=0`).
- `leak_sample.txt` — raw leaked bytes across runs (kernel text/data pointers).
- `env.txt` — guest uname / cc / kld / sha / sysctl state.
- `panic.txt` — the *unrelated* `rn_flush` unload panic (recorded for completeness).
- `fix.diff` — the standalone git-apply-able clamp fix.
- `manifest.json` — artifact catalog.
