# DF-0580 — ieee80211_defrag use-after-free via DragonFly m_cat

## Verdict
**NOT TESTABLE at runtime (no Wi-Fi hardware) — bug CONFIRMED at source
(certain).** Fix authored in `fix.diff`; compiled cleanly as part of a combined
`nativekernel` build (wlan is compiled into the kernel) carrying all four
verified findings' fixes.

## Mechanism (source trace, every hop cited)

`ieee80211_defrag()` (`sys/netproto/802_11/wlan/ieee80211_input.c:178-268`)
reassembles 802.11 fragments. It captures the *incoming* fragment's header
pointer up front:

- `wh = mtod(m, struct ieee80211_frame *)` (`:182`) — `m` is the new fragment.

On a subsequent (concatenatable) fragment it takes the DragonFly-specific
branch:

```c
249: 	} else {				/* concatenate */
250: 		m_adj(m, hdrspace);		/* strip header */
251: #if defined(__DragonFly__)
252: 		m_cat(mfrag, m);
253: 		/* NB: m_cat doesn't update the packet header */
254: 		mfrag->m_pkthdr.len += m->m_pkthdr.len;   /* <- UAF read */
255: #else
256: 		m_catpkt(mfrag, m);
257: #endif
258: 		/* track last seqnum and fragno */
259: 		lwh = mtod(mfrag, struct ieee80211_frame *);
260: 		*(uint16_t *) lwh->i_seq = *(uint16_t *) wh->i_seq;  /* <- UAF read */
261: 	}
```

**DragonFly's `m_cat()` (`sys/kern/uipc_mbuf.c:1840-1856`) diverges from
FreeBSD's `m_catpkt()`.** It rebinds its first arg to the *last* mbuf of the
destination chain (`m = m_last(m)`, `:1843`) and then, **for each mbuf of the
source chain whose data fits in the trailing space of that last mbuf, it
`bcopy`s the data in and `m_free()`s the source mbuf** (`:1845-1854`):

```c
1841: m_cat(struct mbuf *m, struct mbuf *n)
1842: {
1843: 	m = m_last(m);
1844: 	while (n) {
1845: 		if (m->m_flags & M_EXT ||
1846: 		    m->m_data + m->m_len + n->m_len >= &m->m_dat[MLEN]) {
1847: 			/* just join the two chains */
1848: 			m->m_next = n; return;
1849: 		}
1850: 		/* splat the data from one into the other */
1852: 		bcopy(mtod(n, caddr_t), mtod(m, caddr_t) + m->m_len, n->m_len);
1853: 		m->m_len += n->m_len;
1854: 		n = m_free(n);          /* FREES the source mbuf, returns m_next */
1855: 	}
1856: }
```

`m_free()` (`sys/kern/uipc_mbuf.c:1310`) returns the freed mbuf's `m_next`
and returns the mbuf to the objcache (`:1338`). So when a **small** 2nd
fragment's data fits in `mfrag`'s trailing space, `m_cat()` **frees the head
mbuf of the fragment chain `m`** (the very pointer captured as `wh`).

After `m_cat()` returns, `ieee80211_defrag` then dereferences the freed mbuf:
- `:254`  `mfrag->m_pkthdr.len += m->m_pkthdr.len;` — **UAF read** of the freed
  fragment's pkthdr.len (corrupts the reassembled length → downstream OOB);
- `:260`  `*(uint16_t *)lwh->i_seq = *(uint16_t *)wh->i_seq;` — `wh` was
  `mtod(m,...)` and `m` is freed → **UAF read** of the stale fragment, then a
  **write** of that stale value into the reassembled header's seq field.

The FreeBSD branch uses `m_catpkt()` (pure append, never frees the source), so
this is a **DragonFly-specific regression** introduced when the code was
`#if defined(__DragonFly__)`-forked. Trigger: a remote unauthenticated station
sends a small unicast 2nd fragment whose payload fits in `mfrag`'s last-mbuf
trailing space.

INVARIANTS is ON in `X86_64_GENERIC`; depending on objcache reuse this can
surface as a slab-magic/`M_TRAILINGSPACE` KASSERT panic, or as silent length
corruption feeding an OOB later. Either is a remote Wi-Fi DoS / memory
corruption.

## Why not reproduced live (the realistic constraint)
The QEMU/KVM audit guest has **no Wi-Fi hardware**. Although `ieee80211_defrag`
IS present in `/boot/kernel/kernel` (nm confirms the symbol), the receive path
`ieee80211_input -> ieee80211_defrag` is only reached by a real 802.11 driver
handing a received frame to net80211. No ath/run/ral/iwm/… driver attaches on
QEMU (only virtio/e1000 wired NICs), so no frame ever reaches the defrag path.
There is no in-tree virtual Wi-Fi injector, so the path is unreachable here.

## Fix (`fix.diff`)
Capture both values still needed from the about-to-be-freed fragment — its
`m_pkthdr.len` and its `i_seq` word — **before** `m_cat()`, and use the saved
copies afterward. This is the minimal targeted fix: it makes the post-`m_cat`
code never touch the freed source mbuf, on both the DragonFly `m_cat` and
FreeBSD `m_catpkt` paths.

## PoC changes
`findings/poc/DF-0580/` was empty on arrival. This runner authored `fix.diff`,
`build.sh`, `run.sh`, `README.md`, `VERDICT.md`, `manifest.json`, `env.txt`.
