# DF-0981 — VERDICT

## Verdict

**REPRODUCED (code-level harness) — heap OOB write confirmed; live trigger
UNREACHABLE on this guest (no run(4) USB hardware).  Bug is REAL and the fix
is VALIDATED (module compiles under `-Werror`; harness before/after shows the
overflow is eliminated).**

## The bug (line-by-line trace)

In `sys/bus/u4b/wlan/if_run.c`, `run_bulk_rx_callback()` reassembles
device-aggregated 802.11 frames out of a bulk RX URB.  The per-frame DMA length
`dmalen` is **device-controlled**:

```c
/* if_run.c:2989 */
dmalen = le32toh(*mtod(m, uint32_t *)) & 0xffff;
```

The only bounds placed on `dmalen` are (if_run.c:2991-3001):

```c
if ((dmalen >= (uint32_t)-8) || (dmalen == 0) || ((dmalen & 3) != 0)) { break; }
if ((dmalen + 8) > (uint32_t)xferlen) { break; }
```

i.e. `dmalen` may be as large as `xferlen - 8`, and `xferlen` itself may be up
to `RUN_MAX_RXSZ`:

```c
/* if_runvar.h:26-27 */
#define RUN_MAX_RXSZ  MIN(4096, MJUMPAGESIZE)   /* == 4096 */
```

The URB buffer (`sc->rx_m`) is correctly allocated as a 4096-byte jumbo
cluster (if_run.c:2933, `m_getjcl(..., MJUMPAGESIZE)`), so the SOURCE side is
fine.  But when the callback detects aggregation
(`(xferlen -= dmalen + 8) > 8`, if_run.c:3003), it copies each per-frame slice
into a **newly allocated 2048-byte cluster**:

```c
/* if_run.c:3013 (VULNERABLE) */
m0 = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR);          /* MCLBYTES == 2048 */
...
/* if_run.c:3023-3024 (THE OOB WRITE) */
m_copydata(m, 4 /* skip 32-bit DMA-len header */,
    dmalen + sizeof(struct rt2870_rxd), mtod(m0, void *));
```

`m_copydata()` (sys/kern/uipc_mbuf.c:1671-1696) is a **blind `bcopy()` of
`len` bytes into the supplied destination with no destination-bound check** —
it trusts the caller to size the buffer.  With `dmalen` up to ~4079 and
`sizeof(struct rt2870_rxd) == 4` (if_runreg.h:834, a single `uint32_t __packed`),
the copy writes up to **~4083 bytes into a 2048-byte cluster**, overflowing the
kernel heap by up to **~2035 bytes**.  The overflow content is fully
attacker-controlled (it is the USB URB payload).

`run_rx_frame()` then sets `m->m_pkthdr.len = m->m_len = len` from the device
`rxwi->len` (if_run.c:2845), and net80211 reads attacker-controlled bytes from
beyond the 2048-byte cluster into upper-layer protocol parsing.

## Why it's a heap overflow

- Destination: `m_getcl()` ⇒ `MCLBYTES == 2048` (sys/sys/param.h:497).
- Copy length: `dmalen + 4`, bounded only by `xferlen - 8 + 4 ≤ 4092`.
- Overflow: `min(4092, dmalen+4) - 2048 ≥ 0` for any `dmalen ≥ 2044` that is a
  multiple of 4 and leaves `xferlen - dmalen - 8 > 8` (i.e. `dmalen ≤ 4079`).

So for every `dmalen ∈ {2044, 2048, …, 4076}` (multiples of 4), the copy
overflows.  The finding's `dmalen = 3000` example overflows by **956 bytes**
(3004 − 2048), exactly reproduced by the harness.

## Reachability / threat model

- **Primary (the realistic one):** a malicious USB peripheral enumerating as a
  `run_devs[]` VID/PID (e.g. `USB_VP(0x148f, 0x2770)`).  DragonFlyBSD
  plug-and-plays `run(4)` with no user interaction and no privilege, so
  hot-plugging the crafted device triggers this on any USB-capable host.
- **Secondary (wireless):** a hostile AP / RF injector delivering a large HT
  A-MSDU whose per-frame DMA length exceeds ~2044 bytes; the RT2860/RT2870 MAC
  accepts MPDUs up to `MAX_LEN_CFG = 0x2fff = 12287` (if_run.c:6027) and the
  firmware faithfully passes a large `dmalen` to the host.

## Reproduction on this guest

**The live path is NOT reachable on this guest** — there is no USB controller
exposed to the KVM guest and no `run(4)` device:

```
$ usbconfig list
No device match or lack of permissions.
$ kldstat | grep run
(empty — module not loaded)
$ ifconfig -a
vtnet0: ... (virtio)
lo0: ...
```

This is a **valid hard blocker** for a live `uid=0` chain: the corrupting
`m_copydata` write never executes without the hardware, so no escalation is
possible to demonstrate on this guest.  The realistic escalation surface is a
USB-equipped DragonFlyBSD host with a `run(4)` adapter present (default config
on such hardware).  On that host, with no SMAP/SMEP/KASLR, a 2 KB
attacker-controlled heap overflow from an unprivileged physical-access vector
("plug in a USB stick") is a credible LPE.

Per the run instructions, the proof is a **deterministic code-level harness**
replicating the exact logic.  `run_aggr_oob.c` does this; it shows:

```
[BEFORE] m_getcl() (cluster=2048), no extra bound:
  *** HEAP OOB WRITE CONFIRMED: 956 bytes past the 2048-byte cluster ***
  -> matches if_run.c:3023-3024 m_copydata() overflow
```

## Exploit chain

**Not pursuable on this guest: valid hard blocker — no run(4) device, so the
write primitive never fires in-kernel here.**  Characterized at the code level:

- **Class:** kernel heap buffer overflow (CWE-122).
- **Write size:** up to ~2035 bytes, fully attacker-content-controlled (USB
  URB payload).
- **Destination slab:** an mbuf cluster from `m_getcl()` — the `mbufcluster`
  objcache; the overflow lands on whatever allocator-adjacent kernel object
  follows the cluster (other mbufs/clusters, malloc'd objects).
- **Conversion on a USB-equipped host (NOT demonstrable here):** classic
  heap-grooming — fill the mbuf-cluster slab, punch a hole, trigger the
  overflow to corrupt an adjacent victim object (e.g. another `struct mbuf`'s
  `m_next`/`m_data`, or a neighboring kmalloc object holding a function
  pointer / `ucred *`), then drive the victim through normal network/proc
  syscalls.  No SMAP/SMEP/KASLR on this guest family means a hijacked kernel
  function pointer can jump straight to userspace shellcode that calls
  `commit_creds(prepare_kernel_cred(NULL))`.  The blocker is purely that the
  trigger hardware is absent, not that the primitive is benign.

## PoC changes

The PoC directory was **empty** on arrival (no README, no source, no fix.diff).
I authored the full evidence pack from scratch:

- `run_aggr_oob.c` — the harness (faithful reimplementation of the
  `run_bulk_rx_callback` aggregation loop + `m_copydata` blind-copy semantics,
  with guard-padded destination buffers so the OOB is directly observable).
- `build.sh`, `run.sh` — exact repro commands.
- `fix.diff` — the verified fix (2 hunks).
- `VERDICT.md`, `README.md`, `manifest.json`, full logs.

## The fix

`fix.diff` (git-apply-able, applies cleanly with `git apply -p1` and
`patch -p1`) makes two changes to `sys/bus/u4b/wlan/if_run.c`:

1. **Allocation fix (root cause):** `m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR)` →
   `m_getjcl(M_NOWAIT, MT_DATA, M_PKTHDR, MJUMPAGESIZE)` at the aggregated
   branch, so the destination cluster is at least RUN_MAX_RXSZ (4096) bytes —
   matching how `sc->rx_m` is already allocated at if_run.c:2933.
2. **Defense-in-depth bound:** reject `dmalen + sizeof(struct rt2870_rxd) >
   MCLBYTES` before the copy, so even a future change to RUN_MAX_RXSZ cannot
   re-introduce the mismatch.

This **supersedes** the finding markdown's proposal: the markdown proposed
only change #1 (`m_getjcl(MJUMPAGESIZE)`).  I add #2 (explicit bound) as
defense-in-depth and also note the markdown's optional secondary hardening
(tighten the `len > dmalen` check in `run_rx_frame`) is left for separate
treatment.

## Fix validation

Because `run` is a loadable module (not in X86_64_GENERIC) and the guest has
no hardware, validation is **module compile + harness before/after** (per the
run instructions):

- **Baseline (unpatched) module builds:** `if_run.ko` compiles with
  `cc ... -Werror` and `rc=0`. (`build.log`)
- **Patched module builds:** `if_run.ko` compiles identically with `-Werror`
  and `rc=0` (76392 bytes). (`fix_build.log`)
- **Harness before/after:** the vulnerable path overflows by 956 bytes; the
  patched logic (both the `m_getjcl` reallocation AND the explicit bound)
  eliminates the overflow. (`run.log`, `fix_run.log`)

A full `nativekernel` rebuild would NOT recompile `if_run.c` (module-only), so
it adds no signal beyond the module build and was not performed.

## Impact

- **On a USB-equipped DragonFlyBSD host with run(4):** kernel heap OOB write
  of up to ~2035 attacker-controlled bytes from an unprivileged
  physical-access vector (malicious USB peripheral) or a hostile wireless
  peer.  Credible LPE / kernel RCE; reliable kernel panic (DoS) at minimum.
  Severity **High** is correct (CVSS AV:A/physical-proximity or AV:P/USB, but
  the finding's AV:A/AC:L wireless framing is also valid).
- **On this audit guest:** unreachable (no hardware) — DoS/corruption impact
  cannot be triggered; documented as a real-but-hardware-gated bug.
