# DF-3468 — `mpls_forward()` route-miss mbuf/cluster leak — PoC & evidence pack

## TL;DR

Every MPLS packet whose label has **no matching AF_MPLS route** — the default
state of any MPLS-enabled kernel, since none exist until an admin adds them —
is **permanently leaked** by `mpls_forward()` (`sys/netproto/mpls/mpls_input.c:200-203`):
the route-miss `return;` drops the function without `m_freem(m)`, although the
caller (`mpls_input()` at `:159-161`) transferred ownership with
`mpls_forward(m); return;`. An unauthenticated **on-link** attacker needs only
~33k small MPLS frames to exhaust the cluster pool (nmbclusters=33296,
~2 KB each) → ENOBUFS network-wide DoS on `options MPLS` kernels.

**Verdict: REPRODUCED (+1 leaked cluster per frame, permanent). Fix VALIDATED
(zero growth after patch).**

## Reachability (empirically pinned down)

- MPLS is `optional mpls` (`sys/conf/files:1865-1868`); NOT in X86_64_GENERIC.
- **kldload mpls.ko on a stock kernel is NOT sufficient**: the
  `case ETHERTYPE_MPLS` dispatch in `ether_demux_oncpu`/`ether_characterize`
  (`sys/net/if_ethersubr.c:1146-1150,1619-1622`) is `#ifdef MPLS`-compiled-out
  of the kernel; a loaded module cannot receive the frames (measured: 200
  frames, counts flat). Attack surface = kernels built with `options MPLS`
  (or a distribution kernel built with it).

## Files

| file | purpose |
|---|---|
| `mpls_leak.c` | trigger: injects N MPLS frames (label=1000, **S=1** so the known DF-0751 label-0/2 loop is NOT hit) via bpf write + `BIOCSFEEDBACK` on vtnet0; doubles the ether header because DF's `bpf_movein(DLT_EN10MB)` strips the leading 14 bytes into the output sockaddr and the feedback `m_dup` starts at the stripped mbuf |
| `build.sh` | guest-side build steps (trigger / MPLS kernel / patched kernel) |
| `run.sh` | guest-side measurement: baseline → N frames → +3s → +28s → control frames with unknown ethertype |
| `run.log` | decisive baseline run (500 frames → +500 clusters, permanent; control → +0) |
| `run.patched.log` | patched-kernel run (500+500 frames → +0 clusters) |
| `kernel_build.log` | full `make nativekernel KERNCONF=MPLSAUDIT` output |
| `build.log` | trigger rebuild on guest |
| `env.txt` | guest uname (MPLSAUDIT kernel), cc 8.3, kldstat, nmbclusters |
| `fix.diff` | `git apply`-able one-hunk fix: `m_freem(m)` before the route-miss `return` |

## Reproduce (inside guest, root)

```sh
# 1. kernel with MPLS (stock kernel cannot deliver ETHERTYPE_MPLS)
cp /usr/src/sys/config/X86_64_GENERIC /usr/src/sys/config/MPLSAUDIT
echo "options 	MPLS" >> /usr/src/sys/config/MPLSAUDIT
cd /usr/src && make -j6 nativekernel KERNCONF=MPLSAUDIT && make installkernel KERNCONF=MPLSAUDIT && reboot

# 2. trigger
cc -O2 -o /root/mpls_leak /root/mpls_leak.c

# 3. measure  (expect: clusters 256 -> 756 after 500 frames, still 756 at +28s;
#             control 500 unknown-ethertype frames -> no change)
netstat -m | head -2
/root/mpls_leak vtnet0 500
sleep 3  && netstat -m | head -2
sleep 25 && netstat -m | head -2
/root/mpls_leak vtnet0 500 88b7 && sleep 3 && netstat -m | head -2

# 4. fix validation
cd /usr/src && patch -p1 < /root/DF-3468.fix.diff
make -j6 nativekernel KERNCONF=MPLSAUDIT && make installkernel KERNCONF=MPLSAUDIT && reboot
# re-run step 3: counts stay flat
```
