β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0752

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0752 Β· 17 files
FileTypeDescriptionSize
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
README.md readme human reproduce instructions
↓ download 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).

  1. ./build.sh β€” build the injector (mpls_flood)
  2. On an options MPLS kernel: sudo ifconfig tap0 create; sudo ifconfig tap0 up
  3. sudo ./run.sh β€” flood 20000 unroutable-label MPLS frames into tap0, printing netstat -m mbuf 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/tap0
  • build.sh / run.sh β€” exact build/run
  • fix.diff β€” adds m_freem(m); mplsstat.mplss_cantforward++; before return;
  • VERDICT.md β€” full narrative
  • manifest.json β€” artifact catalog
VERDICT.md verdict full narrative: mechanism, reachability, fix, validation
↓ download raw

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:

  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:

--- 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

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 9 09:31:12 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC (sha256 8d5bb36d23252e1b71b9ca2b46c8f971561dbdb4e83a1e1e54aef6fdc4909d09)

Confirmed kernel references

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.