# DF-0754 — `mpls_output()` by-value bug: manifestations 1 & 2 (mpls_output_process / ip_output)

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

**Impact:** `panic` / memory corruption (double-free, UAF, mbuf leak). The bug
is a genuine code defect: `mpls_output()` takes `struct mbuf *m` **by value**
(`mpls_output.c:50`), so when `mpls_push`/`mpls_swap`/`mpls_pop` rebind the
local `m` (via `M_PREPEND`/`m_pullup`), neither `mpls_output_process`
(the direct caller at `:143`) nor `ip_output` (the upstream caller at `:695`)
ever sees the new head. Two consequences — **manifestations 1 and 2** of this
finding — are confirmed by a deterministic userspace harness:

- **Manifestation 1** — `mpls_output_process()` error-path **double-free / leak**
  (`mpls_output.c:143-146`): when `mpls_output()` returns an error after a
  realloc, `mpls_output_process:145` does `m_freem(m)` on the stale pointer.
- **Manifestation 2** — `ip_output()` success-path **stale-mbuf-to-driver**
  (`ip_output.c:698` / `:742`): on the success path, the stale `m` is handed
  to `ifp->if_output()` — either a demoted/leaked old head (PUSH) or freed
  memory (SWAP/POP via `m_pullup`).

**Manifestation 3** (`mpls_forward` in `mpls_input.c:208-218`) is the sibling
finding **DF-0753**, already verified separately with the same root-cause fix.

**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 — a **valid hard blocker** for escalation. The
realistic impact is **panic / DoS** on the default kernel. (Same assessment as
DF-0753; see "Exploitation assessment" below.)

## 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`). **OOM path**
(`:1510`): `m_freem(m); return NULL` — the old `m` is **freed**. **Success
path**: allocates a new head `mn`, `M_MOVE_PKTHDR(mn, m)`, chains old `m` as
`mn->m_next`, returns `mn`. In both cases the local `*m` (in `mpls_push`,
which correctly takes `struct mbuf **`) is updated — but only `mpls_output`'s
local copy. **`mpls_output_process`'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. 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
at `:100`). **The caller's `m` is a dangling pointer to freed memory → UAF.**

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

### Manifestation 1: `mpls_output_process` error-path double-free / leak

`sys/netproto/mpls/mpls_output.c:134-150`:
```c
boolean_t
mpls_output_process(struct mbuf *m, struct rtentry *rt)   // m BY VALUE
{
    int error;
    if (!(rt->rt_flags & RTF_MPLSOPS))            // :140
        return TRUE;
    error = mpls_output(m, rt);                    // :143 — m by VALUE
    if (error) {
        m_freem(m);                                // :145 — DOUBLE-FREE / leak
        return FALSE;
    }
    return TRUE;
}
```

- **M1a** — PUSH + `m_prepend` OOM: `m_prepend` does `m_freem(m); return NULL`
  (`uipc_mbuf.c:1510`). `mpls_push` returns ENOBUFS, `mpls_output` returns
  ENOBUFS. **`:145 m_freem(m)` frees the already-freed `m` → DOUBLE-FREE.**
- **M1b** — SWAP/POP + `m_pullup` OOM: `m_pullup` does `m_freem(n); return NULL`.
  `mpls_swap`/`mpls_pop` returns ENOBUFS, `mpls_output` returns ENOBUFS.
  **`:145 m_freem(m)` frees the already-freed `m` → DOUBLE-FREE.**
- **M1c** — PUSH succeeds (new head `mn` created, old `m` chained), then a
  subsequent op errors (e.g. unknown op → ENOTSUP at `:124`). **`:145
  m_freem(stale m)` frees only the old chain; the new head `mn` is unreachable
  from the stale `m` (`mn->m_next = m`, not `m->m_next = mn`) and is LEAKED.**

### Manifestation 2: `ip_output` success-path stale-mbuf-to-driver

`sys/netinet/ip_output.c:694-700` (and the fragmented path at `:738-744`):
```c
#ifdef MPLS
        if (!mpls_output_process(m, ro->ro_rt))    // :695 — m by VALUE
            goto done;
#endif
        error = ifp->if_output(ifp, m, (struct sockaddr *)dst,   // :698 — STALE m
                               ro->ro_rt);
```

- **M2a** — PUSH realloc succeeded: `m` is the OLD (demoted) head, chained
  under the leaked new head `mn`. The driver receives a **stale mbuf missing
  the freshly-pushed MPLS label** → garbage on wire. New head `mn` is LEAKED.
- **M2b** — SWAP/POP `m_pullup` realloc succeeded: `m` points to **freed
  memory** → **UAF** when `if_output` dereferences `m->m_len`/`m_data`/etc.

## Reproduction — deterministic harness (primary proof)

`harness.c` transcribes `mpls_output`/`mpls_push`/`mpls_swap`/`mpls_pop`/
`mpls_output_process` + the `ip_output` MPLS-dispatch + `m_prepend`/`m_pullup`
verbatim from the kernel, with userspace mbuf stand-ins, a **poisoned
allocator** (freed memory marked `0xdeadc0de`, matching INVARIANTS WEIRD_ADDR),
and an **OOM injection knob** (`fail_after`) to drive the `m_prepend`/`m_pullup`
NULL-return paths deterministically.

### Results (unpatched harness — `harness.c`):

| Scenario | Condition | double_free | uaf | leak | Verdict |
|---|---|---|---|---|---|
| **M1a** | PUSH + m_prepend OOM | **1** | 0 | 0 | **DOUBLE-FREE CONFIRMED** (`:145`) |
| **M1b** | SWAP + m_pullup OOM | **2** | 0 | 0 | **DOUBLE-FREE CONFIRMED** (`:145`) |
| **M1c** | PUSH ok, then unknown-op ENOTSUP | 0 | 0 | **1** | **NEW-HEAD LEAK CONFIRMED** |
| **M2a** | PUSH realloc SUCCESS | 0 | 0 | **1** | **STALE-TO-DRIVER + LEAK** (if_output gets old head label, not pushed head) |
| **M2b** | SWAP m_pullup realloc SUCCESS | 0 | **1** | 1 | **UAF CONFIRMED** (driver derefs freed m) |
| CONTROL | PUSH, leading_space=14 (normal ether) | 0 | 0 | 0 | no realloc → no bug (control) |

All five manifestation scenarios fire deterministically. The CONTROL scenario
proves the bug is realloc-dependent: standard ethernet frames (14-byte headroom
from `ether_input`) never trigger `m_prepend` because max PUSH = 3 × 4 = 12 < 14.

### Fixed harness — `harness_fixed.c`:

Applies the fix: `mpls_output`/`mpls_swap`/`mpls_pop`/`mpls_output_process` take
`struct mbuf **mp` and write `*mp = m` after every rebind (plus an `out:` label
that always propagates the head before return). `ip_output` passes `&m`.

| Scenario | double_free | uaf | leak | if_output gets | Verdict |
|---|---|---|---|---|---|
| M1a | 0 | 0 | 0 | (no if_output; *mp=NULL freed once) | **PASS** |
| M1b | 0 | 0 | 0 | (no if_output; *mp=NULL freed once) | **PASS** |
| M1c | 0 | 0 | 0 | new head (correctly forwarded) | **PASS** |
| M2a | 0 | 0 | 0 | new pushed head label (CORRECT) | **PASS** |
| M2b | 0 | 0 | 0 | new head (LIVE, not freed) | **PASS** |
| CONTROL | 0 | 0 | 0 | same (unchanged) | **PASS** |

**ALL scenarios pass — the fix eliminates both manifestations.**

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

Same valid hard blockers as DF-0753 (identical primitive class — mbuf
double-free/UAF from the dedicated `mbuf_zone` slab):

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). On `noinv` (non-default), silent corruption
   within `mbuf_zone` is possible but the victim is always another mbuf.

2. **mbufs are allocated from a dedicated slab zone (`mbuf_zone`), not the
   general `kmalloc` pool.** Cross-zone grooming (placing a `struct ucred` or
   function-pointer-bearing object adjacent to the freed mbuf) is not possible
   without a separate zone-confusion bug. The victim object is always another
   mbuf, which does not carry a function pointer, `ucred *`, or `uid` field.

**Realistic impact on the default GENERIC kernel: panic / DoS.** No escalation
chain was developed because both valid hard blockers apply.

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

**Identical to the DF-0753 root-cause fix** (same root cause → same fix). Change
`mpls_output` to take `struct mbuf **mp` and propagate the new head through
`*mp` at every rebind (plus an `out:` label for the error paths). Apply the same
`struct mbuf **` change to `mpls_swap`, `mpls_pop`, and `mpls_output_process`.
Update all callers (`mpls_forward` at `mpls_input.c:208`; `ip_output` at
`:695`/`:739`).

### Files changed (5 files, 12 hunks):
- `sys/netproto/mpls/mpls_var.h:56-60` — update prototypes
- `sys/netproto/mpls/mpls_output.c:43-228` — `mpls_output`/`mpls_swap`/`mpls_pop`/
  `mpls_output_process` take `struct mbuf **mp`; `out:` label + `*mp = m`
- `sys/netproto/mpls/mpls_input.c:208` — `mpls_output(&m, ...)`
- `sys/netinet/ip_output.c:695,739` — `mpls_output_process(&m, ...)`

This single fix closes **DF-0753** (manifestation 3) **AND DF-0754**
(manifestations 1 & 2).

### Fix validation (Phase 8):

| | unpatched | patched |
|---|---|---|
| **harness M1a** (PUSH+OOM) | **DOUBLE-FREE** (df=1) | **PASS** (df=0) |
| **harness M1b** (SWAP+OOM) | **DOUBLE-FREE** (df=2) | **PASS** (df=0) |
| **harness M1c** (PUSH+ENOTSUP) | **LEAK** (live=1) | **PASS** (live=0) |
| **harness M2a** (PUSH success) | **STALE+LEAK** | **PASS** (correct head) |
| **harness M2b** (SWAP success) | **UAF** (uaf=1) | **PASS** (uaf=0) |
| **mpls.ko module build** | (baseline) | **BUILD=0** (-Werror, clean) |
| **patched mpls.ko sha256** | — | `72fb3810...` (byte-identical to DF-0753's) |

The fix compiles cleanly under `-Werror`, and the fixed harness proves both
manifestations are structurally eliminated. The patched module sha256 matches
DF-0753's exactly (same source, same fix → deterministic build).

## Cross-reference

- **DF-0753** — manifestation 3 (`mpls_forward`/`mpls_input.c:208-218`). Same
  root cause, same fix. Verified immediately prior on this guest.
- **DF-0751** — separate MPLS loop bug (different root cause).
