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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0751 Β· 18 files
FileTypeDescriptionSize
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
README.md readme human-facing reproduce guide
↓ download 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
VERDICT.md verdict full narrative: bug path:line, mechanism, harness + live proof, fix validation
↓ download raw

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)

  1. Built mpls.ko from 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].
  2. kldload mpls.ko β€” registers NETISR_MPLS handler.
  3. Injected one MPLS frame via bpf write with BIOCSFEEDBACK on vtnet0: - Ethernet header: dst ff:ff:ff:ff:ff:ff, src 52:54:00:12:34:56, EtherType 0x8847 - MPLS shim (network byte order): 00 00 00 40 β†’ label=0, exp=0, S=0, TTL=64
  4. Result: guest hard-wedged. vm.sh status β†’ down. ssh β†’ RC=124 (timeout). Zero panics in boot.log (silent busy-loop, NOT a crash). boot.log frozen 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:

  1. m_adj(m, sizeof(struct mpls)) before each goto again (:134 and :155 post-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.

  2. 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 without m_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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (kernel unchanged - MPLS is a KLD module, not compiled into GENERIC; validated by rebuilding+loading the patched mpls.ko, sha256 21e72a39987d61b2778eec3236158caa39ea0567c3c9a57d3c50d0156833f37c)

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.