Infinite loop for MPLS explicit-NULL label 0/2 with clear bottom-of-stack bit β single-packet remote hard hang
Summary
mpls_input.c:101 again: label. :110 mpls=mtod(m) reads first label. :113 case 0 (IPv4 explicit NULL): :117 if(MPLS_STACK) S-bit-set path correctly m_adj+netisr_queue+return. BUT S-bit-CLEAR path :123 goto again WITHOUT m_adj β mbuf never advanced mtod returns same pointer same label re-read infinite loop. :132 case 2 (IPv6 explicit NULL) identical :142 goto again without m_adj. No label-stack depth bound anywhere. Trigger: single MPLS Ethernet frame EtherType 0x8847 label=0 S=0 TTL=64. mpls_hashfn only checks >=4 bytes reads first label. mpls_input_handler holds mplock :81-83. Hard hang one CPU at 100% no panic no dmesg power-cycle required. Remote unauth on-link attacker. Fix: m_adj(m,sizeof(struct mpls)) before goto again + depth cap.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0751 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| mpls_loop_harness.c | trigger-source | deterministic harness embedding mpls_input loop verbatim with depth-counter escape hatch; proves infinite loop (1e6 iters) for label=0/S=0 and termination under fix | 8.3 KB | view raw |
| mpls_trigger.c | trigger-source | live trigger: injects one MPLS frame via bpf BIOCSFEEDBACK on vtnet0 (label=0, S=0, TTL=64) | 4.0 KB | view raw |
| Makefile | build-file | KLD Makefile for mpls.ko (MPLS is optional, not in GENERIC) | 112 B | β download |
| build.sh | build-script | build harness/module/trigger | 1.1 KB | view raw |
| run.sh | run-script | run harness (deterministic) or module (live wedge) | 930 B | view raw |
| VERDICT.md | verdict | full narrative: bug path:line, mechanism, harness + live proof, fix validation | 6.5 KB | β raw |
| README.md | readme | human-facing reproduce guide | 2.9 KB | β raw |
| fix.diff | suggested-fix | git-apply-able: m_adj before each goto again + depth cap (MPLS_LABEL_TTL_MAX=32) | 1.4 KB | view raw |
| build.log | build-log | harness build (cc -O2), exit 0 | 13 B | view raw |
| run.log | run-log | harness decisive run: 1000001 iters (unpatched), 10 iters (patched), control terminates | 1.2 KB | view raw |
| module_build.log | build-log | mpls.ko KLD build from /usr/src, exit 0 | 3.3 KB | view raw |
| live_wedge.txt | run-log | live kernel wedge evidence: vm.sh status=down, ssh RC=124, 0 panics | 927 B | view raw |
| boot_wedge_live.log | panic-signature | boot.log at wedge time (frozen, no panic = silent busy-loop) | 12.9 KB | view raw |
| fix_build.log | build-log | patched mpls.ko build (sha256 21e72a39...), exit 0 | 9.0 KB | view raw |
| fix_run.log | run-log | patched-module trigger results: 3x frame, guest stays alive | 1.3 KB | view raw |
| env.txt | environment | uname, kern.version, cc 8.3, kldstat | 475 B | 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-0751 β MPLS explicit-NULL infinite loop β PoC & evidence pack
TL;DR
A single MPLS Ethernet frame (EtherType 0x8847, label=0 or 2, S-bit CLEAR)
causes mpls_input() (sys/netproto/mpls/mpls_input.c) to enter an infinite
goto again loop because the mbuf cursor is never advanced (m_adj is missing).
One CPU pegs at 100% holding the mplock; the kernel freezes silently (no panic,
no dmesg). Power-cycle required. Remote unauthenticated on-link DoS.
Verdict: REPRODUCED (live + harness). Fix VALIDATED.
Files
| file | purpose |
|---|---|
mpls_loop_harness.c |
Primary deterministic proof. Embeds mpls_input's loop verbatim with a depth-counter escape hatch. Run as unprivileged user. Prints iteration count: 1,000,001 (capped = infinite) for label=0/S=0; 10 (terminates) when the fix is applied. |
mpls_trigger.c |
Live trigger. Injects one MPLS frame via bpf write with BIOCSFEEDBACK on vtnet0. Requires root (to open /dev/bpf); simulates an on-link attacker. |
Makefile |
KLD module Makefile for mpls.ko (MPLS is optional mpls, not in GENERIC). |
fix.diff |
Standalone git apply-able fix: m_adj before each goto again + depth cap (32). |
VERDICT.md |
Full narrative with path:line cites, mechanism, before/after. |
build.log / run.log |
Harness build + decisive run (full untrimmed). |
module_build.log |
mpls.ko build output. |
live_wedge.txt |
Live-kernel wedge evidence (vm.sh status, ssh RC, panic count). |
boot_wedge_live.log |
boot.log snapshot at wedge time (frozen, no panic). |
fix_build.log |
Patched mpls.ko build output. |
fix_run.log |
Patched-module trigger results (3Γ frame, guest stays alive). |
env.txt |
Guest uname, cc version, kldstat. |
manifest.json |
Machine-readable artifact catalog. |
Reproduce
Path A β harness (deterministic, no module load, unprivileged)
./build.sh harness # cc -O2 -o mpls_loop_harness mpls_loop_harness.c
./run.sh harness # ./mpls_loop_harness
Expected: *** INFINITE LOOP CONFIRMED *** (1,000,001 iterations, cursor
never advances) for the unpatched loop; loop TERMINATES cleanly for the
patched loop.
Path B β live kernel wedge (requires root on guest, needs mpls.ko)
./build.sh module # builds mpls.ko from /usr/src (Makefile included)
./run.sh module # kldload + fire trigger; guest wedges β vm.sh reset to recover
WARNING: Path B hard-wedges the guest. Only run if you can power-cycle. The harness (Path A) is the recommended deterministic proof.
Fix validation
cd /usr/src && patch -p1 < fix.diff # apply fix
cd sys/netproto/mpls && make && cp mpls.ko /boot/kernel/ # rebuild
kldload mpls.ko # load patched
/root/mpls_trigger vtnet0 # re-run trigger
# guest stays alive β fix is effective
DF-0751 β MPLS explicit-NULL label 0/2 infinite loop (single-packet remote DoS)
Verdict: REPRODUCED (live kernel + harness) β FIX VALIDATED
Impact: dos / hard-hang. A single MPLS Ethernet frame (EtherType 0x8847,
label=0 or label=2 with the bottom-of-stack bit CLEAR) drives mpls_input()
into an infinite goto again loop that never advances the mbuf cursor,
pinning one CPU's netisr thread at 100% while holding the mplock. No panic,
no dmesg, no recovery short of power-cycling the guest. Remote unauthenticated
on-link attacker. exploit_chain: none (pure busy-loop DoS β no memory
corruption, no escalation primitive).
The bug (confirmed line-by-line in sys/netproto/mpls/mpls_input.c)
The mpls_input() label-switch loop at mpls_input.c:88-171:
again: // :101
mpls = mtod(m, struct mpls*); // :110 β reads CURRENT label
label = MPLS_LABEL(ntohl(mpls->mpls_shim)); // :111
switch (label) {
case 0: // :113 β IPv4 explicit NULL
if (MPLS_STACK(ntohl(mpls->mpls_shim))) { // :117 β S-bit set?
m_adj(m, sizeof(struct mpls)); // :119 β CORRECT: advance
netisr_queue(NETISR_IP, m);
return;
}
goto again; // :123 *** BUG: NO m_adj before goto ***
case 2: // :132 β IPv6 explicit NULL
if (MPLS_STACK(ntohl(mpls->mpls_shim))) { // :136
m_adj(m, sizeof(struct mpls)); // :138 β CORRECT
netisr_queue(NETISR_IPV6, m);
return;
}
goto again; // :142 *** BUG: NO m_adj before goto ***
On the S-bit-CLEAR path, goto again jumps back to :101 without calling
m_adj(m, sizeof(struct mpls)). mtod(m) therefore returns the same
pointer every iteration, the same mpls_shim is re-read, the same case 0
branch is taken, and the loop never terminates. There is no depth counter,
no iteration cap, no TTL decrement anywhere in mpls_input() or its caller.
The caller mpls_input_handler() (:77-85) only does get_mplock() /
mpls_input() / rel_mplock(), so the spinning thread holds the mplock
forever, wedging the whole kernel.
Reachability
MPLS is optional mpls (sys/conf/files:1865-1868) and is not compiled
into X86_64_GENERIC (no mpls symbols in /boot/kernel/kernel). However it
builds cleanly as a KLD module (mpls.ko) β the audit guest has full /usr/src
and gcc 8.3. An administrator who needs MPLS (kldload mpls) gets the
vulnerable code path.
The dispatch chain (all verified in source):
- sys/net/if_ethersubr.c:1146-1150 β ETHERTYPE_MPLS (0x8847) β NETISR_MPLS
- sys/netproto/mpls/mpls_proto.c β DOMAIN_SET(mpls) β mpls_init() via pr_init
- mpls_input.c:73 β netisr_register(NETISR_MPLS, mpls_input_handler, mpls_hashfn)
- mpls_demux.c:mpls_hashfn β only checks >= 4 bytes (mpls_lengthcheck) and
reads the first label for hashing. No depth validation.
Reproduction β TWO independent proofs
Proof 1: Live kernel (guest hard-wedge)
- Built
mpls.kofrom clean/usr/src/sys/netproto/mpls(Makefile:KMOD=mpls; SRCS=mpls_demux.c mpls_input.c mpls_output.c mpls_proto.c; .include <bsd.kmod.mk>).cc 8.3 [DragonFly]. kldload mpls.koβ registersNETISR_MPLShandler.- Injected one MPLS frame via
bpfwrite withBIOCSFEEDBACKonvtnet0: - Ethernet header: dstff:ff:ff:ff:ff:ff, src52:54:00:12:34:56, EtherType0x8847- MPLS shim (network byte order):00 00 00 40β label=0, exp=0, S=0, TTL=64 - Result: guest hard-wedged.
vm.sh statusβdown. ssh β RC=124 (timeout). Zero panics inboot.log(silent busy-loop, NOT a crash).boot.logfrozen at boot timestamp β the kernel could not write to the serial console. Power-cycle (vm.sh reset) required to recover.
Proof 2: Deterministic harness (faithful loop transcription)
mpls_loop_harness.c embeds the mpls_input() label-switch loop verbatim
with userspace stand-ins for mbuf/mtod/m_adj/netisr_queue. The only
addition is a depth-counter escape hatch (cap = 1,000,000) the production code
lacks. Run as unprivileged user maxx:
Frame A: label=0 S=0 TTL=64
UNPATCHED: iterations=1000001 exit=DEPTH CAP HIT (would loop forever)
cursor adv: off=0 (NEVER ADVANCED β same label re-read every iter)
VERDICT: *** INFINITE LOOP CONFIRMED ***
PATCHED: iterations=10 exit=m_pullup too small -> drop cursor adv=off=36
VERDICT: loop TERMINATES cleanly. Fix is effective.
Frame B: label=2 S=0 (IPv6 explicit NULL, mpls_input.c:142)
UNPATCHED: iterations=1000001 VERDICT: *** INFINITE LOOP CONFIRMED ***
Control: label=0 S=1 (bottom-of-stack)
UNPATCHED: iterations=1 exit=netisr_queue(NETISR_IP) -> return
VERDICT: terminates (S-bit path is correct)
Impact ceiling
Remote unauthenticated on-link single-frame hard hang. One CPU pinned at
100% holding mplock β entire kernel unresponsive. No panic, no log, no
auto-recovery. This is a pure availability denial β no memory corruption, no
read/write primitive, no privilege escalation. exploit_chain: none.
CVSS AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (7.5 High) is appropriate.
The fix (validated β fix.diff)
Two-part defence-in-depth, both targeting the root cause:
-
m_adj(m, sizeof(struct mpls))before eachgoto again(:134and:155post-patch) β advances the mbuf cursor so the next iteration reads the next label on the stack, matching the S-bit-set path at:119/::138. -
Depth cap (
MPLS_LABEL_TTL_MAX = 32) β a counter incremented each iteration; if exceeded,mplss_invalid++/m_freem/return. Prevents unbounded looping even if another label-switch path is later added withoutm_adj. RFC 3032 doesn't bound the stack, but legitimate stacks are a handful deep; 32 is generous (matches other BSDs).
Fix validation (Phase 8 β single-fix module built + booted)
unpatched mpls.ko |
patched mpls.ko (sha256 21e72a39β¦) |
|
|---|---|---|
| trigger | 1 frame (label=0, S=0) via bpf feedback | same trigger, 3Γ |
| result | guest hard-wedged; status: down; ssh RC=124; 0 panics |
all 3 triggers return exit 0; guest STILL_ALIVE; load 0.13; CPU idle |
The fix closes the bug completely. The harness corroborates: the patched loop
terminates in 10 iterations (cursor advances 0β36, then m_pullup fails on the
exhausted buffer β clean drop).
Fix verification
fixedVALIDATED. Validated via single-fix KLD module rebuild (not full kernel rebuild - MPLS is optional mpls, loadable as mpls.ko). Applied fix.diff to /usr/src/sys/netproto/mpls/mpls_input.c (5 hunks, all succeeded), rebuilt mpls.ko with cc 8.3, kldload'd it. BEFORE (unpatched mpls.ko): the SAME trigger frame (label=0, S=0, TTL=64 via bpf feedback) caused an immediate hard-wedge - vm.sh status=down, ssh RC=124, 0 panics (silent busy-loop). AFTER (patched mpls.ko sha256 21e72a39...): fired the SAME trigger 3 times consecutively - all return exit 0, guest STILL_ALIVE, load average 0.13, CPU idle. The fix closes the bug completely. The harness corroborates: patched loop terminates in 10 iterations (cursor advances 0->36, then m_pullup fails on exhausted buffer -> clean drop) vs 1000001 (capped=infinite) unpatched.
findings/poc/DF-0751/fix.diff (git apply --check passes, RC=0) | fix_build.log (patched mpls.ko build, BUILD_EXIT=0, sha256 21e72a39...) | fix_run.log (3x trigger on patched module, guest STILL_ALIVE, load 0.13) | contrast with live_wedge.txt (unpatched: status=down, ssh RC=124, 0 panics). BEFORE: single frame -> hard wedge. AFTER: 3x identical frames -> guest responsive.
Confirmed kernel references
Detail
Exploit chain
none - pure DoS busy-loop. No memory corruption, no read/write primitive, no privilege escalation. The bug is a missing m_adj(m, sizeof(struct mpls)) before two goto again statements that re-read the same mbuf label forever, pinning the netisr thread + mplock. Impact ceiling: remote unauthenticated on-link single-frame hard hang (one CPU 100%, whole kernel unresponsive via mplock, no panic, no auto-recovery, power-cycle required). CVSS AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 7.5 High.
Evidence (decisive lines)
findings/poc/DF-0751/ contains: mpls_loop_harness.c (deterministic proof - 1e6 iters unpatched, 10 iters patched), mpls_trigger.c (live bpf injector), Makefile (KLD), fix.diff, build.sh/run.sh, VERDICT.md, run.log (harness output: 'INFINITE LOOP CONFIRMED ... cursor adv: off=0 (NEVER ADVANCED)'), live_wedge.txt (vm.sh status=down, ssh RC=124, panic count=0), boot_wedge_live.log (frozen boot.log, no panic), fix_build.log + fix_run.log (patched module: 3x trigger, guest STILL_ALIVE), env.txt, manifest.json.
PoC changes
Created findings/poc/DF-0751/ from scratch. (1) mpls_loop_harness.c - faithful transcription of mpls_input()'s label-switch loop with userspace mbuf/mtod/m_adj stand-ins and a depth-counter escape hatch (the ONLY change from production code); proves the loop is infinite for label=0/S=0 and label=2/S=0, and terminates under the fix. (2) mpls_trigger.c - bpf frame injector with BIOCSFEEDBACK + BIOCSHDRCMPLT to loop a crafted MPLS frame back into ether_input as RX (standard bpf injection technique; needed because bpfwrite defaults to if_output only). (3) Makefile - KLD Makefile for mpls.ko (no Makefile existed in sys/netproto/mpls/; modeled on sys/netproto/802_11/wlan/Makefile). (4) fix.diff - m_adj before each goto again + MPLS_LABEL_TTL_MAX depth cap.
Verified recommended fix
In sys/netproto/mpls/mpls_input.c: add m_adj(m, sizeof(struct mpls)) before BOTH goto again statements (currently at :123 case-0 and :142 case-2), so the mbuf cursor advances to the next label on each iteration - matching the correct S-bit-set path at :119/:138. Additionally add a depth cap (MPLS_LABEL_TTL_MAX=32) as defence-in-depth so a future label-switch path added without m_adj cannot spin forever. Full git-apply-able diff in findings/poc/DF-0751/fix.diff. This supersedes the finding proposal (finding suggested m_adj before each goto again 'plus a depth cap' - the implemented fix does exactly that, with the cap set to 32 matching other BSDs).
Verdict
REPRODUCED via two independent proofs. (1) LIVE KERNEL: built mpls.ko from /usr/src/sys/netproto/mpls (MPLS is optional mpls, not in X86_64_GENERIC - builds cleanly as KLD), kldload'd it, injected ONE MPLS frame (EtherType 0x8847, label=0, S-bit CLEAR, TTL=64) via bpf write with BIOCSFEEDBACK on vtnet0. Result: guest HARD-WEDGED - vm.sh status=down, ssh RC=124, ZERO panics in boot.log (silent busy-loop, NOT a crash), boot.log frozen at boot timestamp. Power-cycle required. This is exactly the claimed symptom. Confirmed the bug path line-by-line: mpls_input.c:123 goto again (case 0, IPv4 explicit NULL, S-clear path) and :142 goto again (case 2, IPv6 explicit NULL) BOTH jump back to the :101 again: label WITHOUT calling m_adj(m, sizeof(struct mpls)), so mtod(m) at :110 returns the same pointer every iteration, re-reads the same label, and loops forever. No depth counter or iteration cap exists anywhere in mpls_input() or its caller mpls_input_handler() (:77-85, which only does get_mplock/mpls_input/rel_mplock - so the spinning thread holds mplock forever, wedging the kernel). (2) DETERMINISTIC HARNESS (mpls_loop_harness.c): embeds the mpls_input label-switch loop verbatim with a depth-counter escape hatch (cap=1e6) the production code lacks. Unpatched: 1000001 iterations, cursor never advanced (off=0). Patched: 10 iterations, cursor advanced 0->36, terminates cleanly. Control (label=0/S=1): 1 iteration, terminates correctly. The bug is real, reachable via a single on-link MPLS frame, and produces a silent hard hang.
No comments yet.