# DF-0752 — VERDICT

## Verdict
**REPRODUCED** — mbuf leak in `mpls_forward()` on the route-not-found path is
real and confirmed live on a kernel built with `options MPLS`. The fix
(`m_freem(m); mplsstat.mplss_cantforward++;` before the `return;`) is
**VALIDATED** by a built-and-booted single-fix kernel: the same flood that
leaks exactly +1 mbuf/packet on the unpatched kernel leaks ZERO mbufs on the
fixed kernel across 100000 packets.

## The bug (mechanism, with path:line)

`sys/netproto/mpls/mpls_input.c`, function `mpls_forward()` (lines 173–219):

```c
173: static void
174: mpls_forward(struct mbuf *m)
...
191:     if (cache_rt->ro_rt == NULL || smpls->smpls_label != label) {
...
199:         rtalloc(cache_rt);
200:         if (cache_rt->ro_rt == NULL) {
201:             /* route not found */
202:             return;          // <-- BUG: m is leaked; no m_freem(m)
203:         }
204:     }
```

When the MPLS forwarding route cache has no entry (or the cached route is for
a different label) and `rtalloc()` fails to find a route for the label
(`cache_rt->ro_rt == NULL`), the function returns at line 202 **without
calling `m_freem(m)`**. The mbuf passed in by the caller is orphaned — its
storage is never returned to the mbuf pool. The `mplss_cantforward` stat is
also never bumped (the finding notes this; the fix adds it for parity with the
`mplsforwarding=0` branch).

**This is the only drop path in the file that fails to free `m`.** Every
sibling error/drop path calls `m_freem(m)`:
- `mpls_input()` reserved-label path: `mpls_input.c:156` `m_freem(m); return;`
- `mpls_input()` `mplsforwarding=0` branch: `mpls_input.c:163-165`
  `mplsstat.mplss_cantforward++; m_freem(m); return;`  ← the template for our fix
- `mpls_input()` end-of-switch invalid label: `mpls_input.c:169-170`
  `mplsstat.mplss_invalid++; m_freem(m);`
- `mpls_forward()` `bad:` label (output error): `mpls_input.c:217-218` `m_freem(m);`

The route-not-found return at 202 is the lone exception — an oversight.

## Reachability

The path is reached for **every** MPLS frame whose label has no matching MPLS
route, when MPLS forwarding is enabled. Control flow:

1. An Ethernet frame with ethertype `ETHERTYPE_MPLS` (0x8847) is received.
   `ether_demux()` (`sys/net/if_ethersubr.c:1618-1624`, `#ifdef MPLS`) sets
   `M_MPLSLABELED` and routes it to `NETISR_MPLS`.
2. `mpls_input_handler()` → `mpls_input()` (`mpls_input.c:76-85, 87-171`).
   For labels > 15 (not reserved) with `mplsforwarding != 0` (default = 1,
   `mpls_input.c:51`), it calls `mpls_forward(m)` and returns
   (`mpls_input.c:159-161`).
3. `mpls_forward()` does `rtalloc(cache_rt)` for the label
   (`mpls_input.c:199`). If no MPLS route exists → leak at `return;` (202).

**`options MPLS` is required** — it is **NOT** in the shipped
`X86_64_GENERIC` config (`sys/config/X86_64_GENERIC` has no `options MPLS`;
the four `netproto/mpls/*.c` files are `optional mpls` in `sys/conf/files`).
So on the **default GENERIC kernel the bug is unreachable** (no MPLS code
compiled in, no MPLS demux in ether_input). The finding's Medium severity and
its stated precondition ("MPLS forwarding must be enabled") reflect this:
the bug is real and exploitable on any deployment that opts into MPLS, but
cannot be triggered against a stock GENERIC box.

## Reproduction (live, on an `options MPLS` kernel)

To make the path reachable I built `X86_64_GENERIC` with `options MPLS` added
(the documented precondition of the finding), booted it, and delivered MPLS
frames into the kernel via a `tap(4)` interface from a root-run injector
(`mpls_flood.c`) that simulates the on-link attacker who can put arbitrary
Ethernet frames on the wire. The MPLS route table was left empty so every
non-reserved label hits the route-not-found path.

**Unpatched MPLS kernel** (`6.5-DEVELOPMENT #1` built 2026-07-09 09:03,
sha256 `b31400ab…`):

```
1000-frame flood :  mbufs in use  7   -> 1007   (delta +1000, exactly 1/packet)
20000-frame flood:  mbufs in use  1009-> 21007  (delta +20000, exactly 1/packet)
```

The mbuf pool cap is 146632, so ~7 floods of 20000 frames exhaust it. This is
the remote memory-exhaustion DoS the finding describes.

## Exploit chain / escalation

None. This is a pure resource-exhaustion (mbuf leak) primitive — no memory
*corruption*, no type confusion, no write primitive. Each leaked mbuf is an
orphaned allocation that is never freed; the attacker cannot read or shape its
contents. The realistic impact ceiling is **denial of service**: sustained
flooding exhausts the mbuf pool and network processing stalls for all users.
There is no path to `uid=0` from a pure leak. (Characterized and moved to fix.)

## Fix

`fix.diff` adds the two lines that close the path, mirroring the
`mplsforwarding=0` sibling at lines 162–166 exactly:

```diff
--- a/sys/netproto/mpls/mpls_input.c
+++ b/sys/netproto/mpls/mpls_input.c
@@ -199,6 +199,8 @@
 		rtalloc(cache_rt);
 		if (cache_rt->ro_rt == NULL) {
 			/* route not found */
+			mplsstat.mplss_cantforward++;
+			m_freem(m);
 			return;
 		}
```

`m_freem(m)` returns the mbuf to the pool; the `mplss_cantforward++` brings
the stat into line with the other "cannot forward" drop paths so the leak
becomes observable in `netstat`-style counters.

## Fix validation (Phase 8) — single-fix kernel built, booted, and tested

Applied **only** `fix.diff` to `mpls_input.c` (MPLS config retained so the
path stays reachable), rebuilt with `make -j6 nativekernel KERNCONF=X86_64_GENERIC`
(warm obj), installed `/boot/kernel/kernel` from the freshly-built
`kernel.stripped` (sha256 `8d5bb36d…`, verified byte-for-byte), rebooted.

**Fixed MPLS kernel** (`6.5-DEVELOPMENT #1` built 2026-07-09 09:31):

```
20000-frame flood :  mbufs in use  8 -> 7   (delta -1, noise; NO leak)
40000-frame flood :  mbufs in use  8 -> 7   (delta -1, noise; NO leak)
40000-frame flood :  mbufs in use  7 -> 7   (delta  0; NO leak)
                                                       (100000 frames total, zero growth)
```

The before/after contrast is unambiguous: the unpatched kernel leaks exactly
1 mbuf/packet (20000 → +20000), the fixed kernel leaks 0 across 100000
packets. `fix_status: fixed`.

## Caveats

- **Default GENERIC is not affected** (MPLS not compiled in). The leak is
  reachable only on kernels built with `options MPLS`. This matches the
  finding's stated precondition and its Medium severity.
- The fix kernel was built with `options MPLS` added to `X86_64_GENERIC`
  (necessary for reachability); the only source diff between the unpatched
  baseline kernel and the fixed kernel is `fix.diff` (2 lines in
  `mpls_input.c`), isolating the fix as the variable.

## Files in this evidence pack

| file | purpose |
|------|---------|
| `mpls_flood.c`         | injector: writes N MPLS frames (unroutable label) to /dev/tap0 |
| `build.sh / run.sh`    | exact build/run |
| `fix.diff`             | git-apply-able fix (m_freem + mplss_cantforward) |
| `baseline_run.txt`     | unpatched-MPLS-kernel flood: +20000 mbufs (the leak) |
| `leak_sample.txt`      | all netstat -m before/after samples, both kernels |
| `fix_run.txt`          | fixed-kernel flood: 0 mbuf growth (20000 frames) |
| `fix_run2.txt`         | fixed-kernel flood: 0 mbuf growth (80000 more frames) |
| `fix_build.log`        | full untrimmed kernel build log (MPLS+fix, rc=0) |
| `fix_build_baseline_mpls.log` | full untrimmed kernel build log (MPLS unpatched, rc=0) |
| `build.log`            | injector build output |
| `env.txt`              | uname, kern.version, sha256, mpls symbols, route table |
| `README.md / VERDICT.md / manifest.json` | this narrative + catalog |
