# DF-0739 — ip_fw3_ctl_set_get heap over-read / kernel memory disclosure

## Verdict: REPRODUCED (root-reachable heap over-read / kernel pointer + string leak). FIX VALIDATED.

## The bug

`sys/net/ipfw3/ip_fw3_set.c:206-215`

```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);   /* <-- BUG */
        return 0;
}
```

`ctx->sets` is a **single `uint32_t`** and is the **LAST field** of
`struct ipfw3_context` (`sys/net/ipfw3/ip_fw3.h:482-490`):

```c
struct ipfw3_context {
        struct ip_fw                  *rules;
        struct ip_fw                  *default_rule;
        struct ipfw3_state_context    *state_ctx;
        struct ipfw3_table_context    *table_ctx;
        uint32_t                       sets;     /* offset 32; struct ends at 36 */
};
```

`sopt->sopt_valsize` is **attacker-controlled** (capped at `SOMAXOPT_SIZE =
65536` for non-root, `SOMAXOPT_SIZE0 = 32 MiB` for root —
`sys/kern/uipc_syscalls.c:1319-1330`, `sys/sys/socket.h:356-357`). Any
`valsize > 4` makes the `bcopy` read **past the `fw3_ctx[cpu]` allocation**
into adjacent kernel heap, and those bytes are returned verbatim to userspace
by `sys_getsockopt`'s `copyout(sopt->sopt_val, uap->val, sopt->sopt_valsize)`
(`sys/kern/uipc_syscalls.c:1349`).

There is **no bounds check** anywhere on this path — confirmed by reading
`ip_fw3_ctl_set_get`, `ip_fw3_ctl_set_sockopt`, `ip_fw3_ctl`, `ip_fw3_ctl_x`,
`ip_fw3_sockopt`, and `rip_ctloutput`.

## Trigger path (reachability)

1. `ipfw3.ko` must be loaded (`kldload ipfw3` — root-only). This is the
   acceptable operational precondition (a firewall admin loads ipfw3).
2. The `IP_FW_X` (`=49`, `sys/netinet/in.h:389`) socket option is handled
   **only** by `rip_ctloutput` (`sys/netinet/raw_ip.c:334` GET / `:385` SET),
   which is the `pr_ctloutput` for every `SOCK_RAW` inet protocol
   (`sys/netinet/in_proto.c` — all `rip_ctloutput` entries are `SOCK_RAW`).
3. Creating a raw IP socket requires `caps_priv_check(SYSCAP_NONET_RAW)`
   (`sys/netinet/raw_ip.c:473`), i.e. **root**. Confirmed empirically:
   `maxx` (uid 1001) gets `EPERM` on `socket(AF_INET, SOCK_RAW, …)`, and a
   `SOCK_DGRAM` socket gets `ENOPROTOOPT` (errno 42) on `IP_FW_X`.

**=> The live trigger is root-reachable.** This is a root→kernel heap
information leak. It is *not* an unprivileged→kernel escalation. The realistic
impact ceiling is: a process holding `SYSCAP_NONET_RAW` (or uid 0) on a host
where ipfw3 is loaded can disclose up to 64 KiB (32 MiB for root) of kernel
heap per call, including kernel pointers and the contents of adjacent slab
objects — bypassing any securelevel `>0` kernel-memory read restriction. With
sufficient calls / heap grooming this is a KASLR-defeat and a building block
for further exploitation. No memory-corruption primitive is obtained, so no
uid=0 escalation chain is derivable from this finding alone.

## Data flow (getsockopt)

```
user: getsockopt(s, IPPROTO_IP, IP_FW_X=49, buf[=x_header{opcode=IP_FW_SET_GET=95}+room], &len)
sys_getsockopt                      sys/kern/uipc_syscalls.c:1303
  sopt.sopt_valsize = len           (1319)  capped SOMAXOPT_SIZE[0]
  sopt.sopt_val = kmalloc(len,M_TEMP)        (1332)
  copyin(uap->val, sopt.sopt_val, len)       (1336)
  kern_getsockopt -> sogetopt -> rip_ctloutput
rip_ctloutput  case IP_FW_X (SOPT_GET)       sys/netinet/raw_ip.c:334
  -> ip_fw3_sockopt                          sys/net/ipfw3/ip_fw3_glue.c:51
     -> ip_fw_ctl_x_ptr = ip_fw3_ctl_x       sys/net/ipfw3/ip_fw3.c:1038
        sopt_valsize -= sizeof(ip_fw_x_header) /*4*/   (1044)
        bcopy(++x_header, sopt_val, sopt_valsize)      (1045)  /* shift past hdr */
        sopt_name = IP_FW_SET_GET                      (1043)
        -> ip_fw3_ctl case IP_FW_SET_GET               (1070)
           -> ip_fw3_ctl_set_sockopt                   (256)
              -> ip_fw3_ctl_set_get(sopt)              (206)
                 bcopy(&ctx->sets, sopt_val, sopt_valsize)  *** OVER-READ ***
  copyout(sopt_val, uap->val, sopt_valsize)            (1349)  *** LEAK TO USER ***
```

## Reproduction (observed on the unpatched `#0` kernel)

`./df0739 128` (root, ipfw3 loaded):

```
getsockopt returned 128 bytes (requested 132 payload after 4-byte x_header)
ctx->sets (first 4 bytes, the only legitimate field):
  0000: 00 00 00 00
Over-read past ctx->sets: 124 bytes (heap residue):
  0000: 00 00 00 00 00 00 00 00 00 00 00 00 80 26 0a 4f
  0010: 00 f8 ff ff 30 09 60 82 ff ff ff ff 00 00 00 00
  0030: 00 00 00 00 20 21 0a 4f 00 f8 ff ff 20 0b 60 82
  0040: ff ff ff ff 00 00 00 00 00 00 00 00 02 00 00 00
  ...
non-zero bytes in over-read region: 60 / 124
candidate 64-bit kernel pointers (0xffff...): 12
```

Decoded leaked 64-bit kernel pointers include `0xffffff80_4f0a2680`,
`0xffffff82_82600930`, `0xffffff80_4f0a2120`, `0xffffff82_82600b20` —
genuine KVA addresses. Across runs the over-read also disclosed the contents
of adjacent slab objects holding process/path strings: `csh`, `-c`,
`/usr/libexec/sftp-server`, `/usr/libexec/getty`, `ttyv0`, `ttyv3`, `ttyv5`.
The leak content varies with slab layout (different bytes across
reset/reload), confirming it is live kernel heap, not a fixed buffer.

Unprivileged reachability (maxx, uid 1001) — **confirmed not reachable**:
```
socket(SOCK_RAW) failed: Operation not permitted (need SYSCAP_NONET_RAW / root)
DGRAM getsockopt(IP_FW_X) rc=-1 errno=42 Protocol not available
```

## Exploit chain

None — this is a pure heap **over-read / information disclosure**, not a
write/corruption primitive. No uid=0 escalation is derivable. Impact ceiling:
kernel heap pointer + string disclosure (KASLR-defeat / securelevel
read-bypass building block), up to 64 KiB per call (non-root raw) / 32 MiB
(root). The leak is read-only; the realistic worst case is information
disclosure that aids a *separate* write-class bug.

## Fix (fix.diff — VALIDATED)

Clamp the copy to the 4 valid bytes of `ctx->sets` and set `sopt_valsize`
accordingly so `copyout` only returns those 4 bytes:

```c
 if (sopt->sopt_valsize < sizeof(ctx->sets))
         return (EINVAL);
 sopt->sopt_valsize = sizeof(ctx->sets);
 bcopy(&ctx->sets, sopt->sopt_val, sizeof(ctx->sets));
```

This matches the finding proposal (bound the bcopy to `sizeof(uint32_t)`).

## Phase 8 — fix validation (module-scoped, single file in `ipfw3.ko`)

The fix lives entirely in `sys/net/ipfw3/ip_fw3_set.c`, which compiles into
the loadable `ipfw3.ko` (not the base kernel). So a single-module rebuild +
reload is the correct validation, faster than a full kernel rebuild.

- **Baseline** (`/boot/kernel/ipfw3.ko` sha256 `ca6ccfb9…`, unpatched `#0`
  kernel 6.5-DEVELOPMENT): `./df0739 128` returns **128 bytes** — 124-byte
  heap over-read with **12 kernel pointers** + adjacent-slab strings.
  `./df0739 1024` returns **1024 bytes** — 1020-byte over-read. (run.log,
  leak_sample.txt)
- Applied `fix.diff` to `/usr/src/sys/net/ipfw3/ip_fw3_set.c`
  (`patch -p1` → `Hunk #1 succeeded at 210`).
- Built the module: `cd /usr/src/sys/net/ipfw3 && make obj && make
  KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC` → `rc=0`
  (`fix_build.log`).
- Installed: `cp /usr/obj/usr/src/sys/net/ipfw3/ipfw3.ko /boot/kernel/ipfw3.ko`
  (sha256 `241f263a…`). `kldload ipfw3` loads the fixed module.
- **Patched re-run**: `./df0739 128` and `./df0739 1024` both return
  **exactly 4 bytes** (`ctx->sets`), `len <= 4: no over-read observed`,
  deterministically across 4 runs. (`fix_run.log`)

**Result: `fixed`.** The over-read is gone on the patched module and present
on the unpatched baseline — clean before/after.

## PoC changes

The finding's `poc_path` was registered but the folder/markdown were not yet
materialized on disk, so the trigger PoC (`df0739.c`), `build.sh`, `run.sh`,
`fix.diff`, and all logs were authored fresh by this verification run. The
PoC exercises the exact cited path: `getsockopt(IPPROTO_IP, IP_FW_X=49)` with
an `ip_fw_x_header{opcode=IP_FW_SET_GET=95}` and an oversized `valsize`,
hex-dumps the over-read region, and counts leaked kernel pointers.

## Notes

- ipfw3 is loaded by default with `default to deny`, which cuts ssh off the
  moment it loads. The run.sh sets `net.filters_default_to_accept=1` first so
  the firewall allows ssh while the module is loaded. This sysctl is read by
  ipfw3 at load time (`sys/net/ipfw3/ip_fw3.c:1425`).
- Reachability for an *unprivileged* user was specifically tested and
  **denied** (`SYSCAP_NONET_RAW` required). Honest impact: root-reachable
  heap info leak, not unpriv→root. The Medium severity in the finding is
  appropriate for an info-leak / KASLR-defeat building block reachable once
  ipfw3 is loaded.
