# DF-0703 — Mbuf leak in `edge_socket_handler` (every received datagram leaks its mbuf chain forever)

> Same ipfw3_sync dead-code caveat as DF-0700 — see that VERDICT for the full
> unreachability proof. The "remote unauthenticated attacker" impact claimed in the
> finding summary presupposes the edge socket is bound and the handler kthread is
> running; **neither happens on this kernel** because the start path is dead code.

## Verdict

**NOT REPRODUCED — LATENT dead code.** Real source bug (unconditional per-datagram
mbuf leak); unreachable on this kernel because the kthread that runs the leaky loop
is never started. Fix is defense-in-depth.

## The bug (source-level trace)

`ip_fw3_sync_edge_socket_handler()` (`sys/net/ipfw3_basic/ip_fw3_sync.c:185-230`) is
the body of the "edge" sync kthread. It receives UDP datagrams and dispatches on
their type:

```c
while (fw3_sync_ctx.running & 1) {
    sbinit(&sio, 1000000000);                          /* :204 — zeroes sio.sb_mb */
    error = so_pru_soreceive(so, NULL, NULL, &sio, NULL, &flags);  /* :205 */
    if (error) break;
    m = sio.sb_mb;                                      /* :208 — take the chain */
    type = (int *)m->m_data;
    if (*type == SYNC_TYPE_SEND_TEST) { ... }           /* :210-213 */
    else if (*type == SYNC_TYPE_SEND_STATE) { ... }     /* :214-219 */
    else if (*type == SYNC_TYPE_SEND_NAT) { ... }       /* :220-222 */
    else { kprintf("Error ignore\n"); }                 /* :223-225 */
    /* <-- NO m_freem(m). The received mbuf chain is never freed. */
}
```

`soreceive` fills `sio.sb_mb` with a freshly-allocated mbuf chain for each datagram.
The handler reads `m = sio.sb_mb` and uses `m->m_data`, but **never calls
`m_freem(m)`**. On the next iteration, `sbinit(&sio, ...)` (`:204`) re-zeroes
`sio.sb_mb` (sets it to NULL) **without freeing** the previous chain — so every
received datagram leaks one mbuf chain permanently. (There is also a per-datagram
`kprintf` at `:213`/`:222` that floods the console — a log-amplification side issue.)

The edge socket is bound to `INADDR_ANY` (`:247`), so **any** UDP source can deliver
datagrams to it — hence the finding's "remote unauthenticated" framing. A remote
flooder would exhaust the mbuf zone → network stall / panic (the mbuf zone is
finite and shared by all networking).

## Why unreachable on this kernel

The handler is a kthread started by `ip_fw3_ctl_sync_edge_start()` (`:232-266`):

```c
error = kthread_create(ip_fw3_sync_edge_socket_handler, NULL,
        &fw3_sync_ctx.edge_td, "sync_edge_thread");     /* :260-261 */
```

`edge_start` is a sockopt handler → only reachable through `ip_fw3_ctl_sync_sockopt`
→ only dispatched via `ip_fw3_ctl_sync_ptr` (`ip_fw3.c:133`), which is **never
assigned anywhere in the tree** (grep-verified — same dead dispatcher as DF-0700).
So `edge_start` is never called, the kthread is never created, the edge socket is
never bound, and the leaky loop never runs. There is nothing for a remote attacker
to send datagrams to.

Plus the base-kernel ipfw3 stub returns `ENOPROTOOPT` until `kldload ipfw3`, and
the modules aren't loaded by default. Empirically confirmed by
`ipfw3_sync_deadpath` (`run.log`): the sockopt that would start the edge returns
`ENOPROTOOPT`.

## Fix (defense-in-depth)

Free the received mbuf chain at the end of each loop iteration. See `fix.diff`:

```c
    else { kprintf("Error ignore\n"); }
    if (m != NULL)                 /* +DF-0703 */
        m_freem(m);                /* free each received datagram's chain */
}
```

## Fix validation

- `fix.diff` applies cleanly.
- Compile-validated with DF-0701/0702, `make -j6 nativekernel` → **rc=0**.
- `fix_status: not_testable` — dead code; validated applies + compiles.

## Files

| file | desc |
|---|---|
| `ipfw3_sync_deadpath.c` | empirical dead-path demo |
| `fix.diff`              | m_freem(m) each iteration |
| `run.log`               | dead-path demo output (ENOPROTOOPT) |
| `env.txt`               | guest environment |

## Kernel references (confirmed)

- `sys/net/ipfw3_basic/ip_fw3_sync.c:185-230` — `edge_socket_handler`
- `sys/net/ipfw3_basic/ip_fw3_sync.c:204` — `sbinit` zeroes `sio.sb_mb` without freeing
- `sys/net/ipfw3_basic/ip_fw3_sync.c:205` — `so_pru_soreceive` fills a new chain
- `sys/net/ipfw3_basic/ip_fw3_sync.c:208` — `m = sio.sb_mb` (taken, never freed)
- `sys/net/ipfw3_basic/ip_fw3_sync.c:247` — edge binds `INADDR_ANY` (would be remote-reachable *if* started)
- `sys/net/ipfw3_basic/ip_fw3_sync.c:260-261` — kthread create in `edge_start`
- `sys/net/ipfw3/ip_fw3.c:133` — `ip_fw3_ctl_sync_ptr = NULL` (dead dispatcher; edge_start unreachable)
