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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3370 Β· 11 files
FileTypeDescriptionSize
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:

  1. Multicast + IP_MULTICAST_IF (ip_output.c:309-317): ifp comes from imo->imo_multicast_ifp and the route lookup is skipped entirely.
  2. 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) (see panic.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 (see run.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.

VERDICT.md
↓ download raw

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)

  1. 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).
  2. IP_MULTICAST_IF (index form) β†’ imo->imo_multicast_ifp = vtnet0 (ip_output.c:1494-1558 via rip_ctloutput β†’ ip_ctloutput β†’ ip_setmoptions).
  3. 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_ifp branch 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) β†’ :712 ro->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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Fri Sep 11 14:47:59 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.