DF-3468 / verdict.json
{ "finding_id": "DF-3468", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "mpls_forward() leaks the mbuf on every AF_MPLS route miss (mpls_input.c:200-203 returns without m_freem(m) after mpls_input() transferred ownership at :159-161). Reproduced on an options-MPLS kernel: netstat -m cluster count climbs exactly +1 per injected MPLS frame (256->356 on 100 frames, 356->856 on 500) and never recovers (+28s recheck); unknown-ethertype control frames leak nothing. ~33k frames exhaust nmbclusters=33296 -> permanent network-wide ENOBUFS DoS until reboot. Empirically pinned reachability: kldloadd mpls.ko on the stock kernel does NOT receive ETHERTYPE_MPLS frames (if_ethersubr.c:1146/1619 case labels are #ifdef MPLS; 200-frame control on stock kernel: flat), so the exposure is options-MPLS kernels. Unauthenticated on-link attacker, one-way traffic, label>15 (mplsforwarding is hardwired 1). Fix validated: m_freem(m) before the return; patched kernel shows zero growth over 1000 frames.", "exploit_chain": "none \u2014 resource-exhaustion primitive only (permanent mbuf+cluster leak per packet); no memory corruption, no disclosure, no escalation", "evidence": "run.log (baseline: clusters 256->856 after 500 frames, still 856 at +28s, control 0x88B7 frames +0); run.patched.log (patched kernel: 256->256 after 1000 frames); env.txt (MPLSAUDIT kernel uname, nmbclusters=33296); kernel_build.log (full nativekernel build)", "kernel_refs": [ "sys/netproto/mpls/mpls_input.c:200-203", "sys/netproto/mpls/mpls_input.c:159-166", "sys/netproto/mpls/mpls_input.c:51-55", "sys/net/if_ethersubr.c:1146-1150", "sys/net/if_ethersubr.c:1619-1622", "sys/conf/files:1865-1868" ], "poc_changes": "authored from scratch (no seed): (1) trigger injects via bpf+BIOCSFEEDBACK but must write the ether header TWICE \u2014 DF bpf_movein(DLT_EN10MB) strips the leading 14 bytes into the output sockaddr and the feedback m_dup starts at the stripped mbuf, so a single header never reaches ether_input intact; (2) label=1000 S=1 to avoid wedging on the known DF-0751 label-0/2 loop; (3) stock kernel + kldload mpls.ko was tested first and ruled out (compile-gated), requiring an options-MPLS nativekernel build", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Sun Sep 13 10:07:16 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/MPLSAUDIT x86_64", "runtime_sec": 3100, "guest_dirty": 0, "build_cmd": "in-guest: cc -O2 -o /root/mpls_leak /root/mpls_leak.c [env: cp /usr/src/sys/config/X86_64_GENERIC /usr/src/sys/config/MPLSAUDIT && echo options MPLS >> MPLSAUDIT && make -j6 nativekernel KERNCONF=MPLSAUDIT && make installkernel KERNCONF=MPLSAUDIT && reboot]", "run_cmd": "/root/mpls_leak vtnet0 500; sleep 3; netstat -m | head -2; sleep 25; netstat -m | head -2; /root/mpls_leak vtnet0 500 88b7", "code_hash": "ed080e24f9d969fb55ba4543543158f0851a493b83f94a77202e1dd9321fa71a", "notes": "reachability fact for the finding record: DF-0751s pack implied a kldload-only live path; measured here, kld mpls.ko on stock kernel does NOT receive MPLS frames (frames die in ether_demux default case) \u2014 all MPLS-input findings need options-MPLS kernels, which this pack proves both by source (#ifdef gates) and experiment. Also answers the orchestrators DF-3321 question: DF-3321s fix.diff contains ONLY the M_HASH|M_LENCHECKED flag-clearing hunks for mpls_input.c label-0/2 decap paths; the DF-0751 goto-again loop fix is NOT in it and the tree remains unfixed. Guest reset to clean snapshot after validation.", "recommended_fix": "add m_freem(m) before the route-not-found return in mpls_forward() (sys/netproto/mpls/mpls_input.c:202); validated in fix.diff", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Sep 13 10:23:43 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/MPLSAUDIT x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Applied fix.diff (m_freem(m) on route miss) to guest /usr/src, incremental nativekernel rebuild, reboot, re-ran the exact PoC: 500+500 frames, cluster count 256->256->256 (zero leak), guest healthy. Baseline on the same kernel before patch: 256->856 permanent. Bad behavior gone.", "fix_evidence": "run.patched.log (full patched-kernel run); fix.diff (git-apply-able); second build completion stamp in kernel_build.log tail (10:27:31 UTC)" } |