# DF-3468 — VERDICT

## Status: REPRODUCED — impact `leak` (kernel memory exhaustion DoS) — FIX VALIDATED

## Root cause (line-accurate)

`sys/netproto/mpls/mpls_input.c`, `mpls_forward()`:

```
190:	smpls = (struct sockaddr_mpls *) &cache_rt->ro_dst;
191:	if (cache_rt->ro_rt == NULL || smpls->smpls_label != label) {
192:		if (cache_rt->ro_rt != NULL) {
193:			RTFREE(cache_rt->ro_rt);
194:			cache_rt->ro_rt = NULL;
195:		}
196:		smpls->smpls_family = AF_MPLS;
197:		smpls->smpls_len = sizeof(struct sockaddr_mpls);
198:		smpls->smpls_label = htonl(label);
199:		rtalloc(cache_rt);
200:		if (cache_rt->ro_rt == NULL) {
201:			/* route not found */
202:			return;			*** BUG: no m_freem(m) ***
203:		}
204:	}
```

Ownership flow: `mpls_input()` `:159-161` — `mpls_forward(m); return;` — hands
the mbuf over exclusively. Every other exit of `mpls_forward()` consumes the
mbuf (`if_output` on success, `m_freem(m)` in the `bad:` label at `:217-218`).
The route-miss return at `:202` is the one exception: the mbuf is neither
freed nor handed to anyone → **permanent leak of the mbuf + its cluster**.

Trigger condition (all attacker-controlled):
- packet arrives as EtherType 0x8847/0x8847-mcast on any interface of an
  `options MPLS` kernel (`if_ethersubr.c:1146-1150,1619-1622`);
- first label > 15 (`mpls_input.c:154` reserved-range check passes);
- `mplsforwarding` — always 1; the sysctl is commented out
  (`mpls_input.c:51-55`);
- no AF_MPLS route for the label — **the default state**, since the AF_MPLS
  routing table is empty until an administrator populates it
  (`mpls_proto.c:mpls_inithead` attaches an empty radix tree).

Attacker: unauthenticated on-link peer (MPLS frames are link-local; not
routed). No privileges, no response needed, one-way traffic.

## Verification narrative (what was run, what was seen)

1. **Compile-gate pinned empirically.** Stock X86_64_GENERIC kernel +
   kldload'd `mpls.ko` (built from `/usr/src/sys/netproto/mpls`): 200 frames
   injected, `netstat -m` perfectly flat — frames die in
   `ether_demux_oncpu`'s `default:` case because the MPLS case label is
   `#ifdef MPLS`-compiled-out. Conclusion recorded: the bug needs
   `options MPLS` kernels; a KLD alone does not expose it.
2. **MPLS kernel built** (`X86_64_GENERIC` + `options MPLS` = MPLSAUDIT
   config), installed, booted. `nm /boot/kernel/kernel` shows `mpls_input`.
3. **Injection obstacle solved.** First trigger run was flat — root cause:
   DF's `bpf_movein(DLT_EN10MB)` (`sys/net/bpf.c`) strips the leading 14
   bytes of the written frame into the output sockaddr; the `BIOCSFEEDBACK`
   copy is `m_dup`'d from the *stripped* mbuf and fed to
   `(*ifp->if_input)()` (`bpf.c:605-606`), so `ether_input` saw a
   header-less mbuf. Fix: write the real ether header **twice**; after the
   strip, the feedback copy begins with the second copy.
4. **Leak reproduced.** 100-frame warmup: clusters 256 → 356 (+100). Then
   500 frames: 356 → 856 (+500), **still 856 at +28 s** (no reclaim).
   Control: 500 frames with unknown EtherType 0x88B7 (drop path):
   **+0**. Exact per-frame accounting, permanent retention, clean
   differential. See `run.log`.
5. **Fix validated.** `fix.diff` applied to guest `/usr/src`; incremental
   `make nativekernel` rebuild; reboot into patched kernel; exact same PoC:
   500 + 500 frames → clusters **256 → 256 → 256**. Guest healthy
   throughout. See `run.patched.log`.

## Impact ceiling

Remote-unauth (on-link, `AV:A`) kernel memory exhaustion: each ~1.4 KB frame
permanently removes one mbuf+cluster (2 KB). nmbclusters = 33296 → ~33k
frames (~46 MB of traffic, well under one second on any LAN) exhausts the
cluster pool → ENOBUFS for all networking, dummynet/panic risk on
allocation-failure paths, reboot required to reclaim. Gated on non-default
kernel config (`options MPLS`) per the empirical gate above — hence Medium,
not High, under this project's rubric (remote DoS on *default* config).

`exploit_chain: none` — resource-exhaustion primitive only; no corruption,
no info disclosure, no escalation.

## Fix

`m_freem(m)` before the route-miss `return` — see `fix.diff` (validated).
Consider also a per-route-miss counter/stat for operator visibility.

## Guest hygiene

Guest was reset to the clean-source snapshot (`vm.sh reset with-src`) after
validation: stock `X86_64_GENERIC #0` kernel, pristine `/usr/src`
(no MPLSAUDIT config, no patch residue).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
