# DF-0753 — Stale mbuf pointer after mpls_output may reallocate — PoC & evidence

## TL;DR

`mpls_output(struct mbuf *m, ...)` takes `m` **by value**. When `mpls_push`
(via `M_PREPEND`/`m_prepend`) or `mpls_swap`/`mpls_pop` (via `m_pullup`)
reallocate the head mbuf, the rebind only updates `mpls_output`'s local copy.
The caller `mpls_forward` (`mpls_input.c:208`) still holds the **old pointer**:
- PUSH: old m is alive but no longer the chain head → `if_output` (:211)
  receives a stale/wrong mbuf; on `if_output` error, `m_freem(m)` (:218)
  double-frees.
- SWAP/POP: `m_pullup` **frees** the old m → caller's `m` is a dangling
  pointer → true UAF + double-free.

**Verdict: REPRODUCED (deterministic harness). Fix VALIDATED.**

**Impact:** panic / memory corruption (UAF + double-free of mbuf in mbuf_zone).
On GENERIC (INVARIANTS ON) the slab allocator catches the double-free → panic
(DoS). Escalation to `uid=0` is blocked by two valid hard blockers: INVARIANTS
catches the double-free before grooming lands, and mbufs are in a dedicated
slab zone (no cross-object victim for privilege escalation).

## Files

| file | purpose |
|---|---|
| `mpls_stale_harness.c` | **Primary deterministic proof.** Transcribes `mpls_forward`/`mpls_output`/`mpls_push`/`mpls_swap`/`mpls_pop`/`m_prepend`/`m_pullup` verbatim with a poisoned allocator. Proves DOUBLE-FREE + UAF in scenarios A2/B. |
| `mpls_stale_harness_fixed.c` | **Fix proof.** Same code with `struct mbuf **mp` fix applied. All scenarios PASS. |
| `mpls_trigger.c` | Live trigger (bpf BIOCSFEEDBACK on vtnet0). Confirms 14-byte headroom prevents realloc on standard frames. |
| `Makefile` | KLD module Makefile for `mpls.ko`. |
| `fix.diff` | Standalone `git apply`-able fix: `mpls_output(struct mbuf **mp, ...)`. |
| `VERDICT.md` | Full narrative with path:line cites, mechanism, before/after. |
| `build.log` / `run.log` | Harness build + decisive run (full untrimmed). |
| `harness_unpatched.log` / `harness_fixed.log` | Before/after harness contrast. |
| `module_build.log` / `fix_build.log` / `fix_run.log` | Module build + fix validation. |
| `env.txt` | Guest uname, cc version, kldstat. |
| `manifest.json` | Machine-readable artifact catalog. |

## Reproduce

```sh
./build.sh                # cc -O2 -Wall -o mpls_stale_harness mpls_stale_harness.c
./run.sh                  # runs unpatched + fixed harness, shows contrast
```

Expected: UNPATCHED shows `*** DOUBLE-FREE CONFIRMED ***` in scenarios A2/B;
FIXED shows `ALL scenarios PASS`.

### Live trigger (requires root, needs mpls.ko)

```sh
cd /usr/src/sys/netproto/mpls && make && cp mpls.ko /boot/kernel/
kldload mpls.ko
./mpls_trigger vtnet0     # injects 1 MPLS frame; no panic (14-byte headroom)
```

### Fix validation

```sh
cd /usr/src && git apply fix.diff
cd sys/netproto/mpls && make && cp mpls.ko /boot/kernel/
# cold boot (kldunload mpls crashes — separate domain-teardown bug)
kldload mpls.ko && ./mpls_trigger vtnet0   # no panic, no regression
```
