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

Network input paths X_ipip_input and pim_input dereference viftable/last_encap_vif without mroute_token racing vif teardown to NULL deref or 5.5MB OOB read

Summary

X_ipip_input and pim_input PIM-REGISTER branch both walk viftable/numvifs/last_encap_vif/reg_vif_num WITHOUT acquiring mroute_token while del_vif and X_ip_mrouter_done mutate exactly those fields under token. Concurrent teardown (mrouted restart config reload MRT_DONE) on netisr0 can bzero cached vif or rewrite reg_vif_num=VIFI_INVALID(65535) between validation and use producing NULL deref on v_ifp panic OR viftable[65535] read ~5.5MB past 32-entry static array used as struct ifnet pointer garbage deref. X_ipip_input last_encap_vif deref after vif zeroed v_ifp NULL ip_input.c:544 deref panic. pim_input reg_vif_num VIFI_INVALID viftable[65535] garbage ifp if_simloop deref. tbf_reprocess_q saved vifp also tokenless.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2541 Β· 11 files
FileTypeDescriptionSize
poc.c trigger-source root-configured MRT_INIT + tunnel vif + ipip flood + concurrent del_vif race -> panic 6.9 KB view raw
build.sh build-script cc -O0 -o poc poc.c 151 B view raw
run.sh run-script kldload ip_mroute; ./poc (root) 485 B view raw
build.log build-log successful build output 92 B view raw
run.log run-log unpatched run 1 -> panic 217 B view raw
run.2.log run-log unpatched run 2 (reproducibility) -> panic 380 B view raw
panic.txt panic-signature Fatal trap 12 IP=0x0 Idle 432 B view raw
env.txt environment uname + cc version 192 B view raw
fix.diff suggested-fix add mroute_token to X_ipip_input (entry+4 returns) β€” partial; insufficient alone 1.3 KB view raw
fix_results.txt fix-validation patched module objdump-verified + re-run still panics; root cause of insufficiency 1.2 KB view raw
VERDICT.md verdict full narrative 5.4 KB ↓ raw
VERDICT.md verdict full narrative
↓ download raw

DF-2541 β€” ip_mroute X_ipip_input / pim_input race (NULL-deref / panic)

Verdict: REPRODUCED (panic); fix_status = fix_failed (minimal token fix insufficient)

What the finding claimed

X_ipip_input (sys/net/ip_mroute/ip_mroute.c:1710) and the PIM-REGISTER branch of pim_input (:3020) read viftable/numvifs/last_encap_vif/reg_vif_num without holding mroute_token, while del_vif (:873) and X_ip_mrouter_done (:605) mutate exactly those fields under mroute_token (including bzero() of viftable slots at :909 and :634). A concurrent teardown can therefore zero a vif between validation and use.

Reachability established on this guest (6.5-DEVELOPMENT #0, X86_64_GENERIC)

  1. pim_input is DEAD CODE here. It is entirely inside #ifdef PIM (sys/net/ip_mroute/ip_mroute.c:3250 #endif /* PIM */). The GENERIC kernel has no options PIM (sys/config/X86_64_GENERIC), and the ip_mroute.ko module Makefile defines only -DMROUTING, not -DPIM (sys/net/ip_mroute/Makefile). Confirmed at runtime: sysctl net.inet.pim β‡’ unknown oid; nm /boot/kernel/kernel | grep pim_input β‡’ empty. The PIM-REGISTER NULL-deref at :3230 is unreachable on this guest.
  2. X_ipip_input is reachable via the encap4_input() backward-compat hook (sys/netinet/ip_encap.c:231 if (proto == IPPROTO_IPV4 && ipip_input)) once the module is kldload'd (MOD_LOAD sets ipip_input = X_ipip_input, :3266). But its first guard (:1716 if (!have_encap_tunnel)) returns early unless root has added a VIFF_TUNNEL vif (set in add_vif :785-786). MRT_INIT requires SOCK_RAW/IPPROTO_IGMP and root (ip_mrouter_init :571-572). So this is a root-configured race β€” the realistic threat is an admin running mrouted while an attacker floods during MRT_DONE/restart.

Reproduction (root-configured race β†’ panic)

poc.c (run as root): opens the IGMP mrouter socket, MRT_INIT, adds a VIFF_TUNNEL vif (rmt=10.0.2.99) → have_encap_tunnel=1, primes the last_encap_vif cache by flooding IP-in-IP packets (outer proto=4, inner dst=multicast), then races MRT_DEL_VIF(0)+re-add against the packet flood. del_vif bzero()s viftable[0] (v_ifp→0) while a packet reuses the stale cached last_encap_vif; the decapsulated multicast packet is then forwarded into torn-down state.

Observed (unpatched #0, twice): Fatal trap 12: page fault while in kernel mode, instruction pointer = 0x8:0x0 (NULL function-pointer call), current process = Idle (netisr context). Reproducible in <2 s of the race loop. See panic.txt, run.log, run.2.log.

Mechanism (cited hops)

  • X_ipip_input :1734-1747 β€” walks viftable/numvifs and caches last_encap_src/last_encap_vif` with no token.
  • del_vif :883 acquires mroute_token; :888-891 resets the cache (only if vifp == last_encap_vif); :909 bzero((caddr_t)vifp, sizeof(*vifp)) β†’ v_ifp = 0.
  • X_ipip_input :1763 m->m_pkthdr.rcvif = last_encap_vif->v_ifp; (stale cache β‡’ NULL), :1765 netisr_queue(NETISR_IP, m) β‡’ ip_input runs with a NULL/torn-down rcvif β‡’ NULL function-pointer call β‡’ panic.
  • Downstream: the decapsulated multicast packet is forwarded by X_ip_mforward (:1154) / ip_mdq, which access viftable/numvifs without the token at entry, so a concurrent teardown also corrupts the forwarding path.

Fix attempt + validation

fix.diff adds lwkt_gettoken(&mroute_token) to X_ipip_input (entry + all 4 return paths). lwkt_gettoken does serialize cross-CPU on contention (sys/kern/lwkt_token.c:686-716: _lwkt_trytokref_spin then lwkt_switch()).

Built the patched ip_mroute.ko, objdump-verified lwkt_gettoken at offset 0x14e + 4 lwkt_reltoken in X_ipip_input, installed (sha256 de5fc1… vs original 7a4a68…), reloaded, re-ran the SAME PoC. Result: still panics, identical signature.

Why the minimal fix is insufficient (fix_failed): protecting only X_ipip_input's v_ifp read does not close the race, because the decapsulated multicast packet is subsequently forwarded by X_ip_mforward/ip_mdq (:1154+), which read viftable/ numvifs without the token at entry; a concurrent del_vif/X_ip_mrouter_done bzero()s viftable under the token, so the forwarding path still derefs zeroed vif state β‡’ same NULL-pointer panic.

Concrete next iteration for a complete fix: the teardown paths (del_vif and X_ip_mrouter_done) must synchronize against in-flight netisr packets before bzero() β€” e.g. a netisr drain/barrier (analogous to netisr_wait) on teardown β€” and/or the entry of X_ip_mforward/ip_mdq must acquire mroute_token for the full viftable walk. This is a broader locking change beyond a single-function patch.

PoC changes

  • Wrote poc.c from scratch (no prior scaffold existed). Fixed vifi_t (was u_int8_t; the kernel's is u_short, sys/net/ip_mroute/ip_mroute.h:82 β€” first build returned EINVAL), and used the correct SOCK_RAW/IPPROTO_IGMP mrouter socket required by ip_mrouter_init (:571).

Impact

Kernel panic / DoS. Reachable only with root-configured multicast routing (MRT_INIT is privileged) plus concurrent reconfiguration; an unprivileged user cannot trigger it. Not a privilege escalation β€” no memory-corruption primitive is attacker-shaped here, it is a torn-down-state deref. Medium severity is appropriate (admin-config DoS; pim_input half is dead code on GENERIC).

Fix verification

fix_failed
baseline reproduced→ patch + rebuild →patched reproduced

fix_failed. Built patched ip_mroute.ko (objdump-confirmed lwkt_gettoken at X_ipip_input+0x14e plus 4 lwkt_reltoken for 4 return paths), installed (sha de5fc1... vs original 7a4a68...), reloaded, re-ran SAME PoC. STILL PANICS with identical signature (Fatal trap 12, IP=0x0, Idle). Minimal token fix in X_ipip_input insufficient because decapsulated multicast packet is subsequently forwarded downstream by X_ip_mforward/ip_mdq (ip_mroute.c:1154+), which read viftable/numvifs WITHOUT token at entry; concurrent del_vif/X_ip_mrouter_done bzeros viftable under token, so forwarding path still derefs zeroed vif state => same NULL-pointer panic.

baseline (unpatched #0): PoC -> panic 'Fatal trap 12 IP=0x0 Idle'. patched ip_mroute.ko (objdump-verified token added): SAME PoC -> STILL panic 'Fatal trap 12 IP=0x0 Idle' (identical signature). Conclusion: token in X_ipip_input alone does not close race; complete fix needs to protect full X_ip_mforward/ip_mdq entry paths with mroute_token OR drain netisr in del_vif/X_ip_mrouter_done before bzero.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (patched ip_mroute.ko: de5fc1309842e2ca71708262210d00e1d19b2b6d97d8c40d2e3ff9deb63b6735, objdump-verified lwkt_gettoken@0x14e + 4 lwkt_reltoken in X_ipip_input)

Confirmed kernel references

Detail

Exploit chain

none β€” not memory corruption; race-induced torn-down-state NULL/func-ptr deref (DoS panic). No attacker-shaped write primitive, no escalation chain.

Evidence (decisive lines)

Fatal trap 12: page fault while in kernel mode / fault virtual address=0x0 / instruction pointer=0x8:0x0 / current process=Idle / kernel: type 12 trap, code=10 / Stopped at 0: (run 1 and run 2 identical). Mechanism: X_ipip_input :1734-1763 (no token) vs del_vif :883,:909 (bzero under token).

PoC changes

Wrote poc.c from scratch. Fixed vifi_t (was u_int8_t; kernel's is u_short, ip_mroute.h:82 β€” first build returned EINVAL) and used SOCK_RAW/IPPROTO_IGMP mrouter socket required by ip_mrouter_init (:571). fix.diff adds lwkt_gettoken(&mroute_token) to X_ipip_input (entry+4 returns).

Verified recommended fix

Partial: add lwkt_gettoken(&mroute_token) to X_ipip_input so viftable walk + last_encap_vif->v_ifp read is atomic w.r.t. del_vif (fix.diff). Supersedes finding's 'NULL check before deref' framing by targeting cited root cause (missing token). NOTE: this minimal fix alone does NOT close the race β€” see fix_status. Complete fix needs netisr drain/barrier in del_vif & X_ip_mrouter_done before bzero, OR acquire mroute_token across X_ip_mforward/ip_mdq entry paths.

Verdict

REPRODUCED (root-configured race panic). pim_input is DEAD CODE on this guest: entirely inside #ifdef PIM (ip_mroute.c:3250), and neither X86_64_GENERIC (no 'options PIM') nor ip_mroute.ko Makefile (defines only -DMROUTING, not -DPIM) compile it β€” sysctl net.inet.pim 'unknown oid', nm shows no pim_input symbol. So PIM-REGISTER NULL-deref half of claim unreachable. X_ipip_input IS reachable via encap4_input (ip_encap.c:231) after kldload, but only after root adds a VIFF_TUNNEL vif (have_encap_tunnel guard, :1716). A root PoC (MRT_INIT + tunnel vif + IP-in-IP flood racing MRT_DEL_VIF) panics deterministically in <2s: 'Fatal trap 12 page fault, IP=0x0 (NULL func-ptr call), current process=Idle' (netisr), reproduced twice. Root cause: X_ipip_input reads viftable/last_encap_vif WITHOUT mroute_token while del_vif bzeros the cached vif under the token; the stale cached last_encap_vif->v_ifp becomes 0 -> rcvif=NULL -> ip_input NULL func-ptr call. Requires root (MRT_INIT privileged) so admin-config DoS, not unpriv->root.