mbuf leak in mpls_forward on route-not-found β remote memory-exhaustion DoS
Summary
mpls_input.c:199-202 rtalloc(cache_rt) if(cache_rt->ro_rt==NULL){ /* route not found */ return; } β NO m_freem(m) before return. mbuf leaked. mpls_forward reached from mpls_input.c:159-161 default case label>15 mplsforwarding=1 (default :51). Each unroutable-label MPLS packet leaks one mbuf+cluster. Remote unauth on-link attacker floods MPLS frames label=1000 (no route) exhausts mbuf pool networking stalls. mplss_cantforward NOT incremented (leak occurs before stat bump). Fix: mplsstat.mplss_cantforward++; m_freem(m); before return.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0752 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| mpls_flood.c | trigger-source | injector: writes N MPLS frames (unroutable label) to /dev/tap0 | 2.8 KB | view raw |
| build.sh | build-script | exact cc build of the injector | 185 B | view raw |
| run.sh | run-script | tap0 setup + flood + netstat -m before/after | 1.1 KB | view raw |
| fix.diff | suggested-fix | m_freem(m) + mplss_cantforward++ on route-not-found path | 324 B | view raw |
| baseline_run.txt | run-log | unpatched MPLS kernel: 20000-frame flood leaks +20000 mbufs | 830 B | view raw |
| fix_run.txt | run-log | fixed kernel: 20000-frame flood, 0 mbuf growth | 797 B | view raw |
| fix_run2.txt | run-log | fixed kernel: 2x 40000-frame floods, 0 mbuf growth | 673 B | view raw |
| leak_sample.txt | leak-sample | all netstat -m before/after samples, both kernels | 2.0 KB | view raw |
| fix_build.log | build-log | full MPLS+fix kernel build (rc=0) | 5.6 MB | β download |
| fix_build_baseline_mpls.log | build-log | full MPLS-unpatched baseline kernel build (rc=0) | 5.6 MB | β download |
| build.log | build-log | injector build output | 92 B | view raw |
| env.txt | environment | uname, kern.version, kernel sha256, mpls symbols, route table | 914 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, fix, validation | 7.5 KB | β raw |
| README.md | readme | human reproduce instructions | 1.8 KB | β raw |
| manifest.json | manifest | this catalog | 3.4 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0752 β mbuf leak in mpls_forward on route-not-found
Claim
mpls_forward() in sys/netproto/mpls/mpls_input.c leaks the input mbuf on
the route-not-found path: when rtalloc(cache_rt) leaves cache_rt->ro_rt ==
NULL, the function returns at line 202 without calling m_freem(m) (and
without bumping mplss_cantforward). Every other drop path in this file frees
the mbuf (lines 156, 164, 170, 218). An on-link remote attacker who can deliver
MPLS-framed Ethernet packets (ethertype 0x8847) with a label that has no
matching MPLS route causes one mbuf+cluster leak per packet β memory-exhaustion
DoS. Requires options MPLS in the kernel config (non-default; the X86_64_GENERIC
shipped kernel does NOT compile MPLS in).
Reproduce
The bug is unreachable on default GENERIC (MPLS not compiled in). The harness
here therefore uses a kernel built with options MPLS (the documented
precondition of the finding) and delivers MPLS frames into the kernel via a
tap(4) interface from a root-run injector (simulating the on-link attacker).
./build.shβ build the injector (mpls_flood)- On an
options MPLSkernel:sudo ifconfig tap0 create; sudo ifconfig tap0 up sudo ./run.shβ flood 20000 unroutable-label MPLS frames into tap0, printingnetstat -mmbuf counts before/after.
Expected on the UNPATCHED MPLS kernel: mbufs-in-use grows by ~20000 (leak). Expected on the FIXED kernel: mbufs-in-use stays flat (m_freem now called).
Files
mpls_flood.cβ injector: writes N MPLS frames (label=1000, no route) to /dev/tap0build.sh / run.shβ exact build/runfix.diffβ addsm_freem(m); mplsstat.mplss_cantforward++;beforereturn;VERDICT.mdβ full narrativemanifest.jsonβ artifact catalog
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):
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:
- An Ethernet frame with ethertype
ETHERTYPE_MPLS(0x8847) is received.ether_demux()(sys/net/if_ethersubr.c:1618-1624,#ifdef MPLS) setsM_MPLSLABELEDand routes it toNETISR_MPLS. mpls_input_handler()βmpls_input()(mpls_input.c:76-85, 87-171). For labels > 15 (not reserved) withmplsforwarding != 0(default = 1,mpls_input.c:51), it callsmpls_forward(m)and returns (mpls_input.c:159-161).mpls_forward()doesrtalloc(cache_rt)for the label (mpls_input.c:199). If no MPLS route exists β leak atreturn;(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:
--- 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 MPLSadded toX86_64_GENERIC(necessary for reachability); the only source diff between the unpatched baseline kernel and the fixed kernel isfix.diff(2 lines inmpls_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 |
Fix verification
fixedVALIDATED. Applied only fix.diff to mpls_input.c, built a single-fix kernel with make -j6 nativekernel KERNCONF=X86_64_GENERIC (with options MPLS added to the config so the leak path stays reachable -- the finding's documented precondition; the ONLY source diff between baseline and fixed kernels is fix.diff's 2 lines), installed /boot/kernel/kernel from the freshly-built kernel.stripped (byte-for-byte verified, sha256 8d5bb36d), rebooted, re-ran the SAME flood. Baseline unpatched MPLS kernel: 20000-frame flood leaked exactly +20000 mbufs (1009->21007). Fixed kernel: 20000-frame flood left mbufs flat (8->7); a further 40000+40000=80000 frames also left mbufs flat at 7 (100000 frames total, zero growth). The fix closes the leak completely. (Note: on the default GENERIC kernel without options MPLS the bug is unreachable, so the leak cannot be demonstrated there; the MPLS-enabled config is the realistic per-finding threat model.)
baseline (MPLS, no fix, sha256 b31400ab): 20000-frame flood -> mbufs in use 1009 -> 21007 (+20000, 1/packet). fixed (MPLS+fix, sha256 8d5bb36d): 20000-frame flood -> mbufs 8 -> 7 (0); 40000-frame flood -> 7 -> 7 (0); another 40000-frame flood -> 7 -> 7 (0). 100000 frames post-fix, ZERO mbuf growth.
Confirmed kernel references
- sys/netproto/mpls/mpls_input.c:199
- sys/netproto/mpls/mpls_input.c:200
- sys/netproto/mpls/mpls_input.c:202
- sys/netproto/mpls/mpls_input.c:156
- sys/netproto/mpls/mpls_input.c:163
- sys/netproto/mpls/mpls_input.c:169
- sys/netproto/mpls/mpls_input.c:217
- sys/netproto/mpls/mpls_input.c:159
- sys/net/if_ethersubr.c:1618
- sys/conf/files:1866
Detail
Exploit chain
none -- pure resource-exhaustion (mbuf leak) primitive. No memory corruption, type confusion, or write primitive; each leaked mbuf is an orphaned allocation whose contents the attacker cannot read or shape. Impact ceiling is denial of service: sustained flooding exhausts the 146632-entry mbuf pool and network processing stalls for all users. Characterized (1 mbuf leaked per unroutable-label MPLS packet, deterministically) and moved to fix.
Evidence (decisive lines)
UNPATCHED MPLS kernel (sha256 b31400ab), 20000-frame flood: 'mbufs in use' 1009 -> 21007 (delta +20000, exactly 1/packet); 1000-frame flood: 7 -> 1007 (delta +1000). mbuf pool cap 146632. FIXED MPLS kernel (sha256 8d5bb36d), same flood: 20000 frames -> 8 to 7 (delta -1, noise); +40000 frames -> 7 to 7; +40000 frames -> 7 to 7 (100000 frames total, ZERO growth). boot.log clean, no panic. Full samples in baseline_run.txt / fix_run.txt / fix_run2.txt / leak_sample.txt.
PoC changes
Created the evidence pack from scratch (no prior PoC existed). Wrote mpls_flood.c (a tap(4) injector that writes N Ethernet frames ethertype 0x8847 with an unroutable MPLS label into /dev/tap0, simulating the on-link remote attacker), build.sh, run.sh (tap0 setup + flood + netstat -m before/after), fix.diff (m_freem + mplss_cantforward), VERDICT.md, manifest.json, leak_sample.txt, env.txt. Compiled the injector with the guest cc 8.3.
Verified recommended fix
In sys/netproto/mpls/mpls_input.c mpls_forward(), at the route-not-found block (lines 200-203), add mplsstat.mplss_cantforward++; m_freem(m); before the return; -- mirroring the mplsforwarding=0 sibling at lines 162-166 exactly. This frees the orphaned mbuf and makes the leak observable in stats. The git-apply-able diff is in findings/poc/DF-0752/fix.diff; it supersedes/matches the finding markdown's proposal (the finding already proposed exactly this fix).
Verdict
REPRODUCED. The bug is real and confirmed live. mpls_forward() at sys/netproto/mpls/mpls_input.c:199-203 does rtalloc(cache_rt); if (cache_rt->ro_rt==NULL){ /* route not found */ return; } with NO m_freem(m) before the return, orphaning the input mbuf. This is the ONLY drop path in the file that fails to free m -- every sibling (lines 156, 163-165, 169-170, 217-218) calls m_freem(m); the mplss_cantforward stat is also skipped. Confirmed by a live flood on a kernel built with options MPLS (the documented precondition; default X86_64_GENERIC does NOT compile MPLS -- sys/conf/files:1866 marks the files optional mpls, and ether_demux MPLS routing is #ifdef MPLS at if_ethersubr.c:1618). With the MPLS route table empty, flooding 20000 unroutable-label MPLS frames (ethertype 0x8847) into /dev/tap0 grew 'mbufs in use' by exactly +20000 (1 mbuf/packet); a 1000-frame flood grew it by exactly +1000. The mbuf pool caps at 146632, so ~7 such floods exhaust it -> remote unauthenticated memory-exhaustion DoS on any MPLS-enabled box. No memory corruption (pure resource leak), so no escalation chain.
No comments yet.