ip_output: NULL rtentry dereference in the DF/EMSGSIZE path when the packet was routed without a route entry (multicast via IP_MULTICAST_IF) β deterministic kernel panic
Summary
ip_output.c:712 dereferences ro->ro_rt->rt_flags with no NULL check inside the IP_DF EMSGSIZE branch. Two paths reach the MTU-exceeded+DF state with ro->ro_rt == NULL: the multicast IP_MULTICAST_IF branch which takes ifp from imo->imo_multicast_ifp and never performs a route lookup, and IP_ROUTETOIF (kernel-internal). The ip6 twin guards the analogous path. VERIFIED deterministically on stock INVARIANTS guest: raw socket (IP_HDRINCL) + IP_MULTICAST_IF(vtnet0) + 2000-byte IP_DF packet to 224.0.0.5 -> Fatal trap 12, fault VA 0x70 (offsetof(rtentry, rt_flags)), 'Stopped at ip_output+0x5ee'. Root-triggerable today (raw gated by SYSCAP_NONET_RAW); becomes an unpriv panic for any future unprivileged DF-setter. Fix validated by full in-guest kernel rebuild (one-line NULL guard): sendto returns EMSGSIZE, no panic.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3370 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| rawdf.c | β | 3.4 KB | view raw | |
| run.sh | β | 98 B | view raw | |
| build.log | β | 500 B | view raw | |
| run.log | β | 373 B | view raw | |
| run.fixed.log | β | 416 B | view raw | |
| panic.txt | β | 1.0 KB | view raw | |
| env.txt | β | 380 B | view raw | |
| fix.diff | β | 535 B | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-3370 β ip_output(): NULL rtentry dereference when DF-set packet exceeds MTU on a route-less path (kernel panic)
Target: sys/netinet/ip_output.c:712-715 β the DF/EMSGSIZE path dereferences
ro->ro_rt without a NULL check:
if (ip->ip_off & htons(IP_DF)) {
error = EMSGSIZE;
...
if ((ro->ro_rt->rt_flags & (RTF_UP | RTF_HOST)) && /* :712 β NULL! */
Reachable without a route
Two paths reach senditβDF branch with ro->ro_rt == NULL:
- Multicast +
IP_MULTICAST_IF(ip_output.c:309-317):ifpcomes fromimo->imo_multicast_ifpand the route lookup is skipped entirely. IP_ROUTETOIF(ip_output.c:299-308), kernel-internal.
DragonFly has no IP_DONTFRAG (sys/netinet/in.h:405 β commented out) and raw
sockets are privileged (SYSCAP_NONET_RAW, raw_ip.c:473), so the DF bit comes
from a raw IP_HDRINCL socket (root) or TCP (which always has a route).
Trigger = root: raw socket + IP_HDRINCL + IP_MULTICAST_IF(vtnet0) +
2000-byte packet with IP_DF to 224.0.0.5 β multicast branch (ro_rt NULL,
kernel #0) β ip_len > if_mtu + DF β page fault at NULL+0x70.
Note the ip6 twin guards this (ip6_output.c:1059-1064 checks
ro_pmtu->ro_rt == NULL); the v4 code never got the check.
Build / run (guest, root)
cd /root/rawdf && cc -O -Wall -o rawdf rawdf.c && ./rawdf
- stock kernel #0:
Fatal trap 12 ... fault virtual address = 0x70 ... Stopped at ip_output+0x5ee: testb $0x5,0x70(%rax)(seepanic.txt) β 0x70 = offsetof(struct rtentry, rt_flags). - kernel with
fix.diff(NULL guard, rebuilt in-guest, kernel #2):sendto returned -1 errno=40 (Message too long)β EMSGSIZE, no panic (seerun.fixed.log).
Severity is Low only because the trigger is privileged; the missing-NULL-check itself is deterministic and would also fire for any future unprivileged DF-setter.
DF-3370 β VERDICT
Status: reproduced (deterministic kernel NULL-deref panic on the stock INVARIANTS kernel; one-line fix validated on a rebuilt kernel).
What was proven, on the guest
Baseline (stock kernel #0, Thu Jul 2 06:02:54 UTC 2026), as root:
/root/rawdf/rawdf # raw socket + IP_HDRINCL + IP_MULTICAST_IF(vtnet0)
# 2000-byte IP_DF packet to 224.0.0.5
β ssh session never returns; serial console:
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x70
Stopped at ip_output+0x5ee: testb $0x5,0x70(%rax)
0x70 is offsetof(struct rtentry, rt_flags); testb $0x5, 0x70(%rax) is
the compile of ro->ro_rt->rt_flags & (RTF_UP | RTF_HOST) at
sys/netinet/ip_output.c:712 with ro->ro_rt == NULL. Deterministic,
reproduced on first send, no race.
Reachability trace (why ro_rt is NULL)
- raw socket (IPPROTO_RAW, IP_HDRINCL) β privileged (raw_ip.c:473
SYSCAP_NONET_RAW), DFly has no unprivileged IP_DONTFRAG (netinet/in.h:405 commented out; udp never sets IP_DF β verified by grep: only tcp_output.c:1245, tcp_syncache.c:1236, ip_carp.c:1380 set IP_DF, all unicast-with-route or kernel-internal). IP_MULTICAST_IF(index form) βimo->imo_multicast_ifp = vtnet0(ip_output.c:1494-1558 via rip_ctloutput β ip_ctloutput β ip_setmoptions).- sendto 2000 B to 224.0.0.5 β
ip_output(rip_output passes&inp->inp_route, ro_rt NULL on first send): - ip_output.c:309-317:IN_MULTICAST && imo && imo->imo_multicast_ifpbranch selects ifp without any route lookup β ro_rt stays NULL; - :345-455: multicast glue (TTL, src) βgoto sendit; - :655:ip_len(2000) > if_mtu(1500); not TSO/CSUM_FRAGMENT; - :703:ip->ip_off & htons(IP_DF)β :712ro->ro_rt->rt_flagsβ panic.
Severity stays Low because the trigger needs privilege on this system; the missing NULL check is nonetheless a real defect (the v6 twin guards the equivalent path: ip6_output.c:1059-1064), and any future DF-setter reachable unprivileged would promote this to an unprivileged panic.
Fix validation (full kernel rebuild)
fix.diff (one hunk: ro->ro_rt != NULL && guard at ip_output.c:712):
applied to the guest's /usr/src, make -j6 nativekernel && make nativekernel
installkernel, rebooted into kernel #2 (Fri Sep 11 14:47:59 UTC 2026).
Re-running the exact PoC:
DF-3370: sendto returned -1 errno=40 (Message too long) DF-3370: EMSGSIZE β patched behaviour, no panic
Guest alive, correct EMSGSIZE error returned to the socket. Baseline panic vs patched clean-return recorded in panic.txt / run.fixed.log.
Kernel refs: sys/netinet/ip_output.c:712-715 (missing NULL check), :309-317 (route-less multicast branch), :345-455, :655-658, :703-719; sys/netinet6/ip6_output.c:1059-1064 (guarded twin); sys/netinet/raw_ip.c:473 (SYSCAP_NONET_RAW), :220-290 (rip_output), :606-633 (rip_send); sys/netinet/in.h:405 (no IP_DONTFRAG).
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 (one-line NULL guard) applied to guest /usr/src, kernel rebuilt with 'make -j6 nativekernel' and installed (kernel #2, 2026-09-11 14:47:59). Exact baseline PoC re-run: sendto() returns -1 EMSGSIZE (errno 40), no panic, guest remains up - the panic is gone and the correct error is delivered.
run.fixed.log; build.log (kernel build + install completion lines); env.txt (patched uname #2).
Confirmed kernel references
Detail
Exploit chain
root: socket(SOCK_RAW, IPPROTO_RAW) -> IP_HDRINCL + IP_MULTICAST_IF(vtnet0 idx) -> sendto(2000B, dst 224.0.0.5, ip_off=IP_DF) -> ip_output multicast branch leaves ro_rt NULL -> ip_len>mtu with DF -> NULL+0x70 read -> kernel panic. Unprivileged trigger not available on this system (no IP_DONTFRAG; raw gated by SYSCAP_NONET_RAW); TCP DF always has a route.
Evidence (decisive lines)
panic.txt: 'Fatal trap 12 ... fault virtual address = 0x70' 'Stopped at ip_output+0x5ee: testb $0x5,0x70(%rax)' on stock kernel #0; run.fixed.log: 'sendto returned -1 errno=40 (Message too long)' on patched kernel #2 (fix.diff, make nativekernel), guest alive.
PoC changes
authored this pass; userland ip_mreqn comes from guest netinet/in.h (removed local def), IP header version/IHL set via raw byte 0x45 (userland struct ip has no ip_vhl).
Verified recommended fix
Add 'ro->ro_rt != NULL &&' guard before the rt_flags/rt_rmx dereference at ip_output.c:712 (see fix.diff).
Verdict
ip_output's DF/EMSGSIZE path dereferences ro->ro_rt at sys/netinet/ip_output.c:712 with no NULL check. Reachable with ro_rt==NULL via the multicast IP_MULTICAST_IF branch (:309-317) which selects ifp from imo without any route lookup: raw socket (privileged; DFly has no unprivileged IP_DONTFRAG, in.h:405) + IP_HDRINCL + IP_MULTICAST_IF(vtnet0) + 2000-byte IP_DF packet to 224.0.0.5. Reproduced deterministically on the stock INVARIANTS kernel: Fatal trap 12, fault VA 0x70 (= offsetof(rtentry, rt_flags)), 'Stopped at ip_output+0x5ee: testb $0x5,0x70(%rax)'. Severity Low only because the trigger requires privilege on this system; the v6 twin guards the analogous path (ip6_output.c:1059-1064). Fix validated by full kernel rebuild: patched kernel #2 returns EMSGSIZE, no panic.
No comments yet.