# DF-0700 — User-controlled edge count not bounded by MAX_EDGES → BSS OOB array write on `centre_socks[]`

> **Note on the run brief:** the task brief labels DF-0700–DF-0703 as "Bluetooth
> BNEP." They are not — the cited file `sys/net/ipfw3_basic/ip_fw3_sync.c` is the
> **ipfw3 firewall state-synchronization** sub-component (nothing to do with
> Bluetooth). The analysis below is for the actual ipfw3_sync code.

## Verdict

**NOT REPRODUCED — LATENT dead code.** The bug is **real and unambiguous at the
source level**, but the vulnerable function is **unreachable from userspace** on
this kernel because the sockopt dispatcher that would call it is never wired up
(`ip_fw3_ctl_sync_ptr` is `NULL` and never assigned anywhere in the tree). This is
a valid hard blocker (dead/unreachable at runtime); the fix is defense-in-depth
for when the code is eventually wired up.

## The bug (source-level trace)

`ip_fw3_ctl_sync_centre_conf()` (`sys/net/ipfw3_basic/ip_fw3_sync.c:137-154`) takes
`ioc_centre->count` straight from the user sockopt value with **no bounds check
against `MAX_EDGES` (10)**:

```c
ip_fw3_ctl_sync_centre_conf(struct sockopt *sopt)
{
    struct ipfw3_ioc_sync_centre *ioc_centre;
    int size;
    ioc_centre = sopt->sopt_val;
    size = ioc_centre->count * LEN_SYNC_EDGE;          /* :143 — no bound */
    ...
    fw3_sync_ctx.count = ioc_centre->count;            /* :150 — stored unvalidated */
    bcopy(ioc_centre->edges, fw3_sync_ctx.edges, ...);  /* :151 */
}
```

`fw3_sync_ctx.count` then drives array indexing over
`fw3_sync_ctx.centre_socks[MAX_EDGES]` (`ip_fw3_sync.h:77`, a `struct socket *[10]`
that is the **last** member of `struct ipfw3_sync_context`), in five places, all
with `for (i = 0; i < fw3_sync_ctx.count; i++)` and no per-index bound:

| site | what it writes past `centre_socks[10]` | line |
|---|---|---|
| `ip_fw3_ctl_sync_centre_start` | `socreate(..., &centre_socks[i], ...)` — a **kernel heap pointer** (a new `struct socket *`) | `ip_fw3_sync.c:279-286` |
| `ip_fw3_ctl_sync_centre_test`  | reads `centre_socks[i]` to sosend | `:339-345` |
| `ip_fw3_ctl_sync_centre_stop`  | `soclose(centre_socks[i])` | `:367-369` |
| `ip_fw3_sync_send_state`       | `so_pru_sosend(centre_socks[i], ...)` | `:458-464` |
| `ip_fw3_sync_modevent` (MOD_UNLOAD) | `soclose(centre_socks[i])` | `:487-489` |

With `count > 10`, `centre_start` writes `socreate`'s socket-pointer result past the
end of `centre_socks[]` — i.e. past the end of the global `fw3_sync_ctx` BSS symbol
into whatever follows it in BSS. That is an attacker-controlled-count, fixed-stride
OOB write of a kernel heap pointer (CVSS C:N/I:H/A:H). The attacker can shape `count`
to target a specific BSS offset.

## Why it is unreachable on this kernel (dead-code proof)

The whole `centre_*` sockopt-handler family is only callable through the dispatcher
`ip_fw3_ctl_sync_sockopt()` (`ip_fw3_sync.c:390-435`). That dispatcher is itself only
reachable through the function pointer `ip_fw3_ctl_sync_ptr`, dispatched in
`ip_fw3_ctl()`:

```c
case IP_FW_SYNC_CENTRE_CONF:                       /* sys/net/ipfw3/ip_fw3.c:1120 */
...
case IP_FW_SYNC_CENTRE_CLEAR:                      /* :1124 */
    if (ip_fw3_ctl_sync_ptr != NULL) {             /* :1125 */
        error = ip_fw3_ctl_sync_ptr(sopt);         /* :1126 */
    }
    break;
```

**`ip_fw3_ctl_sync_ptr` is initialized to `NULL` (`ip_fw3.c:133`) and is never
assigned anywhere in the entire `sys/` tree** (grep-verified:

```
$ grep -rn 'ip_fw3_ctl_sync_ptr' sys/
sys/net/ipfw3/ip_fw3.c:133:  ip_fw_ctl_t *ip_fw3_ctl_sync_ptr = NULL;
sys/net/ipfw3/ip_fw3.c:1125:         if (ip_fw3_ctl_sync_ptr != NULL) {
sys/net/ipfw3/ip_fw3.c:1126:         error = ip_fw3_ctl_sync_ptr(sopt);
```

— only the NULL init and the NULL-check; no assignment). So the dispatch is always
skipped; every `IP_FW_SYNC_*` sockopt is a silent no-op. `centre_conf` (and thus
the OOB) is never called.

There is also a second layer of unreachability: the base kernel ships only the ipfw3
**stub** `ip_fw3_glue.c` (`ip_fw3_sockopt` returns `ENOPROTOOPT` until
`kldload ipfw3`). The full `ip_fw3.c` (with `ip_fw3_ctl`) and `ip_fw3_sync.c` (the
buggy file) live in loadable modules that are **not loaded by default**
(`kldstat | grep ipfw` is empty). So even reaching the `IP_FW_SYNC_*` cases needs a
root `kldload`.

**Empirical confirmation** (`ipfw3_sync_deadpath`):
```
setsockopt(IPPROTO_IP, IP_FW_X, {SYNC_CENTRE_CONF, count=9999}) = -1
  errno=42 (Protocol not available)          # ENOPROTOOPT — module not loaded
```
Issuing the sockopt that would trigger the bug returns `ENOPROTOOPT` on the default
kernel; the SYNC_* dispatch is never entered. (And even after `kldload ipfw3`,
`ip_fw3_ctl_sync_ptr` stays NULL, so the dispatch is still skipped.)

To make the bug live would require a **source change** assigning
`ip_fw3_ctl_sync_ptr = ip_fw3_ctl_sync_sockopt` in the module's MOD_LOAD — not an
attacker action. This is valid hard blocker (d): dead/unreachable at runtime on this
guest, with no harness exercisable by an unprivileged user (a harness needs
`kldload`, which is root → root:kernel, not unpriv→root).

## Fix (defense-in-depth)

Validate `ioc_centre->count` against `MAX_EDGES` in `centre_conf`, so the OOB
array indexing in `centre_start/test/stop/send_state/MOD_UNLOAD` becomes impossible
whenever the code is wired up. See `fix.diff`:

```c
ioc_centre = sopt->sopt_val;
if (ioc_centre->count < 0 || ioc_centre->count > MAX_EDGES)   /* +DF-0700 */
    return EINVAL;
size = ioc_centre->count * LEN_SYNC_EDGE;
```

The **root** fix is for the maintainer to decide: either wire up
`ip_fw3_ctl_sync_ptr` (and then this bounds check is mandatory) or delete the dead
sync code. Either way, this `fix.diff` makes the function safe.

## Fix validation

- `fix.diff` applies cleanly to the original source (all hunks succeed).
- Compile-validated: applied alongside DF-0701/0702/0703's fixes (DF-0701 supersedes
  this check for the combined build), `make -j6 nativekernel KERNCONF=X86_64_GENERIC`
  → **rc=0**, `ip_fw3_sync.c` compiles into `ipfw3_basic.ko` with `-Werror`.
- `fix_status: not_testable` — the buggy path is dead code; the fix cannot be
  exercised from userspace. Validated applies + compiles + traced that it closes the
  unbounded-index path.

## Files

| file | desc |
|---|---|
| `ipfw3_sync_deadpath.c` | empirical dead-path demo (issues the sockopt, shows ENOPROTOOPT) |
| `fix.diff`              | MAX_EDGES bounds check in centre_conf |
| `run.log`               | dead-path demo output (ENOPROTOOPT) |
| `env.txt`               | guest environment |

## Kernel references (confirmed)

- `sys/net/ipfw3_basic/ip_fw3_sync.c:137-154` — `ip_fw3_ctl_sync_centre_conf` (no bounds check)
- `sys/net/ipfw3_basic/ip_fw3_sync.c:150` — `fw3_sync_ctx.count = ioc_centre->count` (stored unvalidated)
- `sys/net/ipfw3_basic/ip_fw3_sync.c:279-286` — `centre_start` OOB `socreate` into `centre_socks[i]`
- `sys/net/ipfw3_basic/ip_fw3_sync.c:339-345` — `centre_test` OOB read of `centre_socks[i]`
- `sys/net/ipfw3_basic/ip_fw3_sync.c:367-369` — `centre_stop` OOB `soclose`
- `sys/net/ipfw3_basic/ip_fw3_sync.c:458-464` — `send_state` OOB `so_pru_sosend`
- `sys/net/ipfw3_basic/ip_fw3_sync.c:487-489` — `MOD_UNLOAD` OOB `soclose`
- `sys/net/ipfw3_basic/ip_fw3_sync.h:77` — `centre_socks[MAX_EDGES]` is last field of the struct
- `sys/net/ipfw3/ip_fw3.c:133` — `ip_fw3_ctl_sync_ptr = NULL` (never assigned — root cause of dead path)
- `sys/net/ipfw3/ip_fw3.c:1125-1126` — dead dispatch
- `sys/net/ipfw3/ip_fw3_glue.c:51-62` — base-kernel stub returns ENOPROTOOPT until kldload
