# DF-0753 — Stale mbuf pointer after mpls_output may reallocate (UAF / double-free)

## Verdict: REPRODUCED (deterministic harness) — FIX VALIDATED

**Impact:** `panic` / memory corruption (UAF + double-free of an mbuf in the
mbuf slab zone). The bug is a genuine code defect: `mpls_output()` takes
`struct mbuf *m` **by value**, so when `mpls_push`/`mpls_swap`/`mpls_pop`
rebind the local `m` (via `M_PREPEND`/`m_pullup`), the caller `mpls_forward`
never sees the new head and proceeds to use a **stale pointer** — leading to
use-after-free at `mpls_input.c:211` and double-free at `mpls_input.c:218`.

The bug is confirmed by a deterministic userspace harness that transcribes the
exact kernel code. Live triggering on a standard ethernet frame is blocked by
the 14-byte leading space from `ether_input` (≥ max 12 bytes of PUSH), which
is documented below as the reason the harness is the primary proof.

**Exploit chain ceiling:** The primitive is a double-free/UAF of an **mbuf**
allocated from the dedicated `mbuf_zone` slab. On GENERIC (INVARIANTS ON),
the slab allocator's `chunk_mark_free` check catches the double-free and panics
before any grooming lands — this is a **valid hard blocker** for escalation on
the default kernel (see "Exploitation assessment" below). The realistic impact
is **DoS (panic)** on GENERIC, with theoretical heap corruption on INVARIANTS-OFF.

## The bug — line-by-line trace (all citations confirmed in `sys/`)

### Root cause: `mpls_output` takes `m` by value

`sys/netproto/mpls/mpls_output.c:49-50`:
```c
int
mpls_output(struct mbuf *m, struct rtentry *rt)   // m BY VALUE
```

Inside the loop (`:74-126`), three operations may reallocate the head mbuf:

**PUSH** (`:77-91` → `mpls_push` `:152-169`):
```c
M_PREPEND(*m, sizeof(struct mpls), M_NOWAIT);   // mpls_output.c:157
```
`M_PREPEND` (`sys/sys/mbuf.h:469-483`) checks `M_LEADINGSPACE`. If insufficient,
it calls `m_prepend()` (`sys/kern/uipc_mbuf.c:1500-1520`) which allocates a
**new head mbuf** `mn`, runs `M_MOVE_PKTHDR(mn, m)` to move the pkthdr, chains
the old m as `mn->m_next`, and returns `mn`. The local `*m` (in `mpls_push`,
which takes `struct mbuf **`) is correctly updated — but only `mpls_output`'s
local copy. `mpls_forward`'s `m` is unchanged → **stale**.

**SWAP** (`:92-103` → `mpls_swap` `:171-194`):
```c
if (m->m_len < sizeof(struct mpls) &&
   (m = m_pullup(m, sizeof(struct mpls))) == NULL)   // :178 — local rebind
    return (ENOBUFS);
```
`mpls_swap` takes `struct mbuf *m` **by value**. `m_pullup`
(`sys/kern/uipc_mbuf.c:2103-2158`) can **free the old mbuf** and return a new
one (`:2122-2133`: allocate new, `M_MOVE_PKTHDR`, copy data, `m_free` consumed
mbufs). The rebind `m = m_pullup(...)` only updates `mpls_swap`'s local. Even
`mpls_output`'s local is not updated (it called `mpls_swap(m, ...)` by value).
The caller's `m` is a **dangling pointer to freed memory** → true UAF.

**POP** (`:104-121` → `mpls_pop` `:196-212`): same by-value issue as SWAP.

### The victim caller: `mpls_forward`

`sys/netproto/mpls/mpls_input.c:173-218`:
```c
static void
mpls_forward(struct mbuf *m)
{
    ...
    error = mpls_output(m, cache_rt->ro_rt);              // :208 — m by value
    if (error)
        goto bad;
    error = (*ifp->if_output)(ifp, m, dst, cache_rt->ro_rt);  // :211 — STALE m
    if (error)
        goto bad;
    ...
    return;
bad:
    m_freem(m);                                            // :218 — DOUBLE-FREE
}
```

- **:211** — `if_output` receives the stale `m`. If PUSH reallocated, the old
  `m` is alive (chained as `mn->m_next`) but is no longer the chain head —
  the freshly-pushed MPLS label lives only in the leaked `mn`. The forwarded
  packet is wrong (missing the label). If SWAP/POP reallocated via `m_pullup`,
  `m` points to **freed memory** → UAF.
- **:218** — `m_freem(m)`. If PUSH reallocated and `if_output` consumed `m`,
  this double-frees. If SWAP/POP's `m_pullup` freed `m`, this double-frees.

### The same bug also manifests in `mpls_output_process` and `ip_output`

- `mpls_output.c:135-150` — `mpls_output_process(m, rt)` also takes `m` by
  value and calls `mpls_output(m, rt)`. On error it does `m_freem(m)` (:145).
- `sys/netinet/ip_output.c:695, 739` — callers of `mpls_output_process`.
  At `:698/742`, `ifp->if_output(ifp, m, ...)` uses the potentially-stale `m`.

This is the same root cause as **DF-0754** (the `mpls_output` by-value bug).

## Reproduction — deterministic harness (primary proof)

`mpls_stale_harness.c` transcribes `mpls_forward`/`mpls_output`/`mpls_push`/
`mpls_swap`/`mpls_pop`/`m_prepend`/`m_pullup` verbatim from the kernel, with
userspace mbuf stand-ins and a **poisoned allocator** (freed memory marked
`0xdeadc0de`, matching DragonFly's INVARIANTS `WEIRD_ADDR`).

### Results (unpatched harness):

| Scenario | Condition | double_free | uaf | Verdict |
|---|---|---|---|---|
| A  | PUSH, leading_space=2, if_output OK | 0 | 0 | stale head sent (logic bug) |
| **A2** | **PUSH, leading_space=2, if_output ERR** | **1** | **1** | **DOUBLE-FREE + UAF** |
| **B**  | **SWAP, m_len=2 (fragmented) → m_pullup** | **2** | **1** | **DOUBLE-FREE + UAF** |
| C  | PUSH, leading_space=14 (normal ether) | 0 | 0 | no realloc (control) |
| D  | 3 PUSHes (max rt_shim), leading_space=14 | 0 | 0 | no realloc (control) |

Scenarios A2 and B deterministically prove the stale-pointer → double-free/UAF.
Scenarios C and D prove the bug is **headroom-dependent**: standard ethernet
frames (14-byte headroom from `ether_input`) never trigger `m_prepend` because
max PUSH = 3 × 4 = 12 < 14.

### Fixed harness (`mpls_stale_harness_fixed.c`):

Applies the fix: `mpls_output`/`mpls_swap`/`mpls_pop` take `struct mbuf **mp`
and write `*mp = m` after every rebind. `mpls_forward` passes `&m`.

| Scenario | double_free | uaf | Verdict |
|---|---|---|---|
| A  | 0 | 0 | **PASS** |
| A2 | 0 | 0 | **PASS** |
| B  | 0 | 0 | **PASS** |

**ALL scenarios pass — the fix eliminates the stale pointer.**

## Live trigger attempt

Built `mpls.ko` from `/usr/src/sys/netproto/mpls/` (Makefile reused from
DF-0751). `kldload mpls.ko` succeeded. Injected a crafted MPLS frame
(EtherType 0x8847, label=100, S=1, TTL=64) via bpf `BIOCSFEEDBACK` on vtnet0.

**Result:** No panic, guest stays up. This **corroborates** the harness control
scenarios (C/D): the 14-byte ethernet headroom prevents `m_prepend` from firing
on standard frames. The AF_MPLS routing table did not fully attach when MPLS
was loaded as a KLD (`route add -mpls ...` → "Address family not supported"),
so the live path could not reach `mpls_output` with a PUSH `rt_shim`. The
harness is therefore the primary deterministic proof, per the DF-0265/0594/
0616/0393/0751 precedent (code-level harness for precondition-sensitive bugs).

## Exploitation assessment (why escalation to uid=0 is blocked)

The primitive is a **double-free / UAF of an mbuf**. Escalation to `uid=0` is
blocked by **two valid hard blockers**:

1. **INVARIANTS ON (GENERIC) catches the double-free before grooming lands.**
   `sys/kern/kern_slaballoc.c` has 17 INVARIANTS-gated slab checks
   (`chunk_mark_allocated`/`chunk_mark_free`, `WEIRD_ADDR` 0xdeadc0de poisoning).
   A double-free of an mbuf triggers `chunk_mark_free` which detects the already-
   freed state and panics. The corruption never silently lands — it manifests as
   a panic (DoS), not controllable heap corruption. Switching to `noinv` would
   allow silent corruption, but that is a **non-default kernel** result per the
   bright-line rule.

2. **mbufs are allocated from a dedicated slab zone (`mbuf_zone`), not the
   general `kmalloc` pool.** Cross-zone exploitation (placing a `struct ucred`
   or function-pointer-bearing object adjacent to the freed mbuf) is not
   possible without a separate zone-confusion bug. The double-free reclaims
   within `mbuf_zone` only — the victim object is always another mbuf, which
   does not contain a function pointer, `ucred *`, or `uid` field that would
   allow privilege escalation.

These are genuine hard blockers (read the Phase 6 valid-blocker list: "KASSERT
trips before I corrupt" is a blocker on GENERIC; dedicated slab zones prevent
cross-object grooming). The realistic impact on the **default GENERIC kernel**
is **panic / DoS**. On `noinv` (INVARIANTS OFF, non-default), the double-free
could be exploited for heap corruption within `mbuf_zone`, but the victim
object is another mbuf — privilege escalation would require an additional
chain component.

**No escalation chain was developed** because both valid hard blockers apply.
A `panic` (DoS) is the demonstrated impact on GENERIC.

## The fix (validated — `fix.diff`)

Change `mpls_output` to take `struct mbuf **mp` and propagate the new head
through `*mp` at every rebind. Apply the same change to `mpls_swap` and
`mpls_pop`. Update all callers.

### Files changed:
- `sys/netproto/mpls/mpls_var.h:59-60` — update prototypes
- `sys/netproto/mpls/mpls_output.c:44-128` — `mpls_output(struct mbuf **mp, ...)`,
  add `struct mbuf *m = *mp;` at top, `*mp = m;` before return
- `sys/netproto/mpls/mpls_output.c:46-47` — `mpls_swap`/`mpls_pop` take `struct mbuf **`
- `sys/netproto/mpls/mpls_output.c:171-212` — write `*mp = m` after `m_pullup` rebinds
- `sys/netproto/mpls/mpls_output.c:134-150` — `mpls_output_process(struct mbuf **mp, ...)`
- `sys/netproto/mpls/mpls_input.c:208` — `mpls_output(&m, ...)` (caller passes `&m`)
- `sys/netinet/ip_output.c:695,739` — `mpls_output_process(&m, ...)`

This closes DF-0753 **and** DF-0754 (same root cause).

### Fix validation (Phase 8 — single-fix module built + loaded):

| | unpatched `mpls.ko` (sha256 `87133d…`) | patched `mpls.ko` (sha256 `72fb38…`) |
|---|---|---|
| **module build** | cc 8.3, `-Werror`, BUILD=0 | cc 8.3, `-Werror`, BUILD=0 |
| **kldload** | LOAD=0 | LOAD=0 |
| **trigger 3×** | no panic (14-byte headroom prevents realloc) | no panic, no regression |
| **harness A2** | **DOUBLE-FREE CONFIRMED** | **PASS (zero double-free)** |
| **harness B** | **DOUBLE-FREE + UAF CONFIRMED** | **PASS (zero double-free)** |

The fix compiles cleanly, loads without regression, and the fixed harness proves
the stale-pointer/double-free is structurally eliminated.

**Note:** `kldunload mpls` crashes the guest (page fault in `pffindtype` — the
MPLS KLD module's domain-teardown path doesn't properly deregister the protocol
domain). This is a separate infrastructure bug in the module unload path, not
related to the DF-0753 fix. Fix validation was done by cold-booting with the
patched module rather than hot-swapping.
