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

PIM6 REGISTER decap panics: fake multicast_register_if is never if_attach()ed β€” if_simloop() writes through NULL if_data_pcpu (3 stacked NULL derefs: if_simloop stats, in6_ifawithifp if_addrheads, in6_addr2zoneid scope6)

Summary

pim6_input() re-injects every decapsulated PIM REGISTER via if_simloop(mif6table[reg_mif_num].m6_ifp,...). For a register mif that ifnet is the static multicast_register_if which add_m6if() 'initializes' with only if_xname/IFF_LOOPBACK/if_index and never if_attach()es - so if_data_pcpu, if_addrheads[] and if_afdata[AF_INET6] (scope6) are all NULL. if_simloop()'s IFNET_STAT_INC does *(NULL+percpu_offset)+=1 -> supervisor WRITE at VA 0x0 -> Fatal trap 12. Two more NULL derefs stack behind it: in6_ifawithifp over NULL if_addrheads and in6_addr2zoneid over NULL scope6_id (the output side already skips register mifs; the input side forgot). VERIFIED on stock guest: one unauthenticated IPv6 proto-103 PIM REGISTER to the RP (root ran MRT6_INIT + ADD_MIF(MIFF_REGISTER)) -> 'Fatal trap 12 ... fault VA = 0x0 ... Stopped at if_simloop+0x112: addq $0x1,(%rax)'. Distinct from DF-0423 (needs a DELETED register mif; this fires during normal RP operation with a valid mif, no MFC match needed). Write primitive is +=1 at VA 0x0 - unmapped, not attacker-controlled: pure remote DoS. Fix validated (allocate the if_attach()-provided per-CPU structures in add_m6if + register-parent scope guard): full decap->mforward->mdq traversal, no panic.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3377 Β· 15 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 6.2 KB ↓ raw
trigger_pim6_reg.c β€” 7.3 KB view raw
build.sh β€” 110 B view raw
run.sh β€” 104 B view raw
build.log β€” 173 B view raw
run.log β€” 512 B view raw
fix_run.log β€” 873 B view raw
fix_build.log β€” 1.2 KB view raw
panic.txt β€” 654 B view raw
panic.2.txt β€” 1.1 KB view raw
env.txt β€” 359 B view raw
fix.diff β€” 2.2 KB view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 4.7 KB view raw

DF-3377 β€” PIM6 REGISTER decap panics: fake multicast_register_if is never attached; if_simloop() writes through NULL if_data_pcpu

What

pim6_input() re-injects every decapsulated PIM REGISTER packet through if_simloop(mif6table[reg_mif_num].m6_ifp, ...) (sys/netinet6/ip6_mroute.c:1842). For a register mif that ifnet is the static struct ifnet multicast_register_if (ip6_mroute.c:175), which add_m6if() "initializes" with only three fields β€” if_xname, if_flags |= IFF_LOOPBACK, if_index (ip6_mroute.c:585-596) β€” it is never if_attach()ed. if_simloop() bumps per-CPU ifnet statistics via IFNET_STAT_INC(ifp, ipackets, 1) = (ifp)->if_data_pcpu[mycpuid].ifd_ipackets += 1 (sys/net/if_loop.c:284, sys/net/if_var.h:685-688). if_data_pcpu is allocated by if_attach() (sys/net/if.c:591-593) β€” for the fake ifnet it is NULL β†’ supervisor write to VA 0x0 β†’ kernel panic.

A second latent layer sits behind it: even with stats fixed, ip6_mdq() calls in6_addr2zoneid(ifp, ...) on the register ifnet (ip6_mroute.c:1420-1421); SID(ifp) is NULL (no if_afdata[AF_INET6]) and scope6.c:278-288 derefs sid->s6id_list[scope] for any non-global-scope src/dst β†’ second NULL panic. The output side of the same function already skips the scope check for register interfaces (ip6_mroute.c:1433-1435) β€” the input side forgot.

Who can trigger it

  • Setup (root, normal PIM-SM RP operation): MRT6_INIT, MRT6_ADD_MIF with MIFF_REGISTER, MRT6_ADD_MFC with the register mif as parent.
  • Trigger: one unauthenticated PIM REGISTER packet (IPv6 proto 103) from any host that can reach the RP. No PIM protocol state is needed β€” the inner packet need only be a valid IPv6 header with multicast dst; with the layer-1 crash the inner (S,G) does not even need to match an MFC entry (the panic is in the re-injection, before any forwarding lookup).

Reproduce

# on the guest (root):
cc -O -Wall -o /tmp/df3377 /root/poc/trigger_pim6_reg.c
/tmp/df3377
# expected (stock kernel #0):
#   [setup] ... rc=0 ...
#   [trigger] variant A ... sendto: rc=52
#   -> Fatal trap 12: page fault while in kernel mode
#      fault virtual address = 0x0
#      Stopped at if_simloop+0x112: addq $0x1,(%rax)
# expected (patched kernel): completes, "no panic observed: guest still alive"

The PoC injects the REGISTER locally (raw socket to ::1) standing in for a remote sender; pim6_input processes it identically.

Files

  • trigger_pim6_reg.c β€” instrumented trigger + bisection probes
  • build.sh / run.sh
  • run.log β€” decisive run (stops mid-stream at the panic)
  • panic.txt β€” serial console capture
  • fix.diff β€” two-layer fix (per-CPU stats alloc + input-side scope guard)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-3377 VERDICT β€” REPRODUCED (remote-class kernel panic via PIM6 REGISTER decap; 3 stacked fake-ifnet NULL derefs; fix validated)

Bottom line

A single IPv6 protocol-103 PIM REGISTER packet, sent to a host whose routing daemon has configured a register mif (normal PIM-SM RP operation), panics the kernel deterministically: pim6_input() re-injects the decapsulated inner packet through if_simloop(multicast_register_if, ...) and the fake register "interface" β€” a static struct ifnet that is never if_attach()ed (add_m6if() only sets if_xname / IFF_LOOPBACK / if_index, sys/netinet6/ip6_mroute.c:585-596) β€” lacks every per-CPU structure if_attach() would have allocated. Three separate NULL derefs stack on this path; the first one hit on the stock kernel is the ifnet statistics write. Impact is a reliable unauthenticated remote panic (write of +1 to VA 0x0 / reads at VA 0x0); page 0 is unmapped, the written value/address are not attacker-controlled, so there is no escalation path β€” pure DoS. Reproduced on guest stock kernel #0 (X86_64_GENERIC), fixed and re-verified on rebuilt kernel #1.

Exploit chain / crash chain (all path:line into this tree)

  1. root daemon setup (standard pim6sd RP mode): MRT6_INIT (needs the 8-byte variant because of DF-3378!) β†’ MRT6_ADD_MIF(mifi=0, MIFF_REGISTER) (ip6_mroute.c:585-596 sets mif6table[0].m6_ifp = &multicast_register_if) β†’ MRT6_ADD_MFC(... parent=0).
  2. attacker: one IPv6 proto-103 packet, PIM ver2 type REGISTER, non-null, correct checksum, inner = valid IPv6 header with multicast dst (no PIM adjacency, no MFC match needed for the layer-1 crash).
  3. pim6_input() (ip6_mroute.c:1730-1847) validates, m_adj() strips outer+PIM headers, if_simloop(mif6table[reg_mif_num].m6_ifp, m, ...) at ip6_mroute.c:1842.
  4. layer 1 β€” if_simloop() IFNET_STAT_INC(ifp, ipackets, 1) (sys/net/if_loop.c:284) = (ifp)->if_data_pcpu[mycpuid].ifd_ipackets += 1 (sys/net/if_var.h:685-688); if_data_pcpu is allocated only by if_attach() (sys/net/if.c:591-593) β†’ NULL β†’ supervisor WRITE at VA 0x0 β†’ panic. Observed on stock kernel #0: Stopped at if_simloop+0x112: addq $0x1,(%rax) (panic.txt).
  5. layer 2 (latent behind 1) β€” ip6_input() runs in6_ifawithifp() (sys/netinet6/ip6_input.c:531-534, deliverifp = fake ifnet from :412) iterating ifp->if_addrheads[mycpuid] (sys/netinet6/in6.c:2318) β€” also allocated only by if_attach() (sys/net/if.c:550-553) β†’ NULL β†’ panic in6_ifawithifp+0x35: movq (%rax),%rbx (observed on the first fix iteration, panic.2.txt).
  6. layer 3 (latent behind 1+2) β€” ip6_mdq() calls in6_addr2zoneid(ifp, &ip6->ip6_dst/src, ...) (ip6_mroute.c:1420-1421); SID(ifp) = if_afdata[AF_INET6]->scope6_id is NULL for the fake ifnet and scope6.c:278-288 derefs sid->s6id_list[scope] for any non-global scope src/dst (e.g. inner src fe80::/10 or dst ff05::/16) β†’ panic. The output side of the same function already skips the scope check for register mifs (ip6_mroute.c:1433-1435) β€” the input side forgot.

Not the same bug as DF-0423: DF-0423 requires a deleted register mif (stale reg_mif_num β†’ m6_ifp NULL); DF-3377 fires with a valid, in-use register mif during normal RP forwarding, at a different crash site, and even without any MFC entry matching (crash is in the re-injection itself).

Reproduction (guest, stock kernel #0)

[setup] MRT6_INIT: rc=0                       (8-byte arg)
[setup] MRT6_ADD_MIF(0, REGISTER): rc=0
[setup] MRT6_ADD_MFC(fe80::1, ff0e::42, parent=0): rc=0
[setup] MRT6_ADD_MFC(2001:db8::1, ff05::42, parent=0): rc=0
[trigger] variant A: inner fe80::1 -> ff0e::42
[trigger] sendto: rc=52
<connection dies β€” serial console:>
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x0
fault code      = supervisor write data, page not present
Stopped at      if_simloop+0x112:       addq    $0x1,(%rax)

(run.log, panic.txt). The local raw-socket injection stands in for a remote sender; pim6_input processes it identically (no source restrictions).

First run did not panic β€” the v1 PoC sent the REGISTER before verifying the cache-hit path with an uninstrumented single-variant binary; the instrumented v2 (counters probes + upcall drain + two scoped variants) panicked on the very first decap, proving the crash does not even need the MFC match (crash precedes the forwarding lookup).

Fix validation (kernel #1, DF-3377+DF-3378 diffs applied)

Rebuilt X86_64_GENERIC with fix.diff (allocate if_data_pcpu AND if_addrheads[] for the register ifnet in add_m6if, mirroring if.c:550-553/591-593, plus the input-side register scope guard in ip6_mdq). Same PoC, same setup:

[trigger] variant A ... sendto: rc=52
[probe] mif 0: in=1 out=0 ibytes=40 ...
[probe] sg(fe80::1,ff0e::42): pkt=1 bytes=40 ...
[trigger] variant B ... 
[probe] mif 0: in=2 ... sg(2001:db8::1,ff05::42): pkt=1 ...
[trigger] no panic observed: guest still alive

The counters prove the decapsulated packets traversed the entire register-decap β†’ ip6_input β†’ ip6_mforward β†’ ip6_mdq path (the counter bumps are in ip6_mdq itself). Baseline panic gone; fix_status=fixed.

Why not uid0

The write primitive is *(u64*)(0 + percpu_index*sizeof)+= 1 with a NULL base β€” page 0 is unmapped on amd64 and cannot be mapped by an unprivileged process; fault value/address are not attacker-controlled. Pure DoS.

poc_changes vs the seed sketch

Seed assumed the crash would be the scope6.c NULL deref (layer 3); the stock kernel actually dies two layers earlier (if_simloop stats write). PoC needed: sys/param.h include (NBBY), local SIOCGET*MIFCNT defines, hand-computed PIM REGISTER checksum (in6_cksum semantics: pseudo-header len = PIM_MINLEN, nxt = 103, no link-local scope-skip needed for ::1), 8-byte MRT6_INIT arg (DF-3378), and 2-byte-if_index-free inner src (fe80::1 stays unmodified because multicast_register_if.if_index = mifi = 0).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src, X86_64_GENERIC rebuilt+installed+rebooted; identical PoC run completes both variants through the full register-decap forwarding path with counters incremented and no panic; baseline panic signature gone. First fix iteration (if_data_pcpu only) still crashed at in6_ifawithifp, proving layer 2 - final diff allocates both arrays.

fix_run.log (kernel #1 clean run), fix_build.log (build summary), panic.2.txt (intermediate-layer crash proving the depth of the missing-initialization problem)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Fri Sep 11 20:41:14 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

Confirmed kernel references

Detail

Exploit chain

root daemon: MRT6_INIT(8-byte) -> MRT6_ADD_MIF(0,MIFF_REGISTER) -> MRT6_ADD_MFC(parent=0); attacker: 1x IPv6/PIM REGISTER (ver2 type1, non-null, valid cksum, inner=valid ip6 hdr w/ multicast dst) -> pim6_input -> if_simloop(multicast_register_if) -> IFNET_STAT_INC writes NULL+percpu -> supervisor write VA 0x0 -> panic. No uid0 route: page 0 unmapped, write value fixed +1.

Evidence (decisive lines)

['run.log (baseline run cut at sendto by the panic)', 'panic.txt (Fatal trap 12, Stopped at if_simloop+0x112, fault VA 0x0)', 'panic.2.txt (second-layer crash in6_ifawithifp+0x35 on partial fix)', "fix_run.log (kernel #1: both variants forwarded, counters mif0 in=2, sg pkt=1+1, 'no panic observed')", 'VERDICT.md (full 3-layer chain with path:line)']

PoC changes

v1 did not panic (single uninstrumented variant); v2 added sys/param.h (NBBY), local SIOCGETSGCNT/SIOCGETMIFCNT defines, counter/upcall probes, second scoped variant, hand-computed PIM REGISTER checksum (pseudo-header len=PIM_MINLEN nxt=103), 8-byte MRT6_INIT arg (DF-3378). Crash site moved two layers earlier than the seed's scope6 prediction - documented.

Verified recommended fix

Allocate if_data_pcpu and if_addrheads[] for multicast_register_if in add_m6if (mirror if_attach) and skip the input-side in6_addr2zoneid for register-parent routes in ip6_mdq (mirror the existing output-side skip).

Verdict

One unauthenticated IPv6 proto-103 PIM REGISTER packet panics any kernel whose routing daemon has configured a register mif: pim6_input() re-injects the decapsulated packet through if_simloop(multicast_register_if) (ip6_mroute.c:1842) and the fake register ifnet is never if_attach()ed (add_m6if:585-596 sets only if_xname/IFF_LOOPBACK/if_index), so if_simloop's IFNET_STAT_INC writes through NULL if_data_pcpu (if_loop.c:284, if_var.h:685-688) -> 'Fatal trap 12, fault VA 0x0, Stopped at if_simloop+0x112: addq $0x1,(%rax)' on stock kernel #0. Two further NULL derefs stack behind it (in6_ifawithifp via ip6_input.c:531-534 over NULL if_addrheads[]; in6_addr2zoneid via ip6_mdq:1420-1421 over NULL scope6_id) - the second was hit on the first fix iteration, the third is guarded the same way the output side already is (ip6_mroute.c:1433-1435). Distinct from DF-0423 (which needs a deleted register mif): this fires during normal RP operation with a valid mif and needs no MFC match. Write primitive is +=1 at VA 0x0 (unmapped, value/address not attacker-controlled) - DoS only, no escalation. Fix (allocate if_data_pcpu + if_addrheads[] in add_m6if, input-side register scope guard in ip6_mdq) validated on rebuilt kernel #1: both scoped variants traverse the full decap->mforward->mdq path (counters incremented), no panic.