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)
PoC verification
Evidence pack
findings/poc/DF-3377 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 probesbuild.sh/run.shrun.logβ decisive run (stops mid-stream at the panic)panic.txtβ serial console capturefix.diffβ two-layer fix (per-CPU stats alloc + input-side scope guard)VERDICT.md,manifest.json,verdict.json
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)
- 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 setsmif6table[0].m6_ifp = &multicast_register_if) βMRT6_ADD_MFC(... parent=0). - 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).
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.- 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_pcpuis allocated only byif_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). - layer 2 (latent behind 1) β
ip6_input()runsin6_ifawithifp()(sys/netinet6/ip6_input.c:531-534,deliverifp= fake ifnet from :412) iteratingifp->if_addrheads[mycpuid](sys/netinet6/in6.c:2318) β also allocated only byif_attach()(sys/net/if.c:550-553) β NULL β panicin6_ifawithifp+0x35: movq (%rax),%rbx(observed on the first fix iteration, panic.2.txt). - layer 3 (latent behind 1+2) β
ip6_mdq()callsin6_addr2zoneid(ifp, &ip6->ip6_dst/src, ...)(ip6_mroute.c:1420-1421);SID(ifp)=if_afdata[AF_INET6]->scope6_idis NULL for the fake ifnet and scope6.c:278-288 derefssid->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
fixedfix.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)
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.
No comments yet.