DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0645

NULL pointer dereference in div_packet() on re-injected divert packets (rcvif==NULL)

Field Value
ID DF-0645
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE CWE-476 NULL Pointer Dereference
File sys/netinet/ip_divert.c
Lines 182, 186 (missing guard); 202 (correct guard)
Area netinet (divert re-injection path)
Confidence certain
Discovered 2026-07-02
Reported pending

Summary

div_packet() dereferences m->m_pkthdr.rcvif at line 186 (inside the if (incoming) block) without a NULL guard, while line 202 just below correctly checks if (m->m_pkthdr.rcvif). Packets re-injected by a divert daemon via div_output()ip_input() have rcvif==NULL (fresh mbuf from sosend, confirmed uipc_mbuf.c:596). When such a re-injected packet matches a divert rule again (e.g. sin_port=0 so skipto=0), the kernel dereferences NULL → panic.

Root cause

ip_divert.c:182-197:

182:    if (incoming) {
183:        struct ifaddr_container *ifac;
184:
185:        /* Find IP address for receive interface */
186:        TAILQ_FOREACH(ifac, &m->m_pkthdr.rcvif->if_addrheads[mycpuid],
187:                  ifa_link) {                  /* <-- derefs rcvif, no NULL check */

Line 202 (just below):

202:    if (m->m_pkthdr.rcvif) {   /* <-- correct NULL guard */

The developer knew rcvif could be NULL (hence the line 202 check) but missed adding the same guard to the line 182 block.

The NULL rcvif arises on the re-injection path: div_output() sends packets with sin_addr!=0 to ip_input(m). The mbuf was allocated by sosend→m_gethdr, which sets m->m_pkthdr.rcvif = NULL (uipc_mbuf.c:596). Neither div_output nor ip_input sets rcvif. When ipfw re-matches a divert rule on this packet, ip_divert_in()→divert_packet(m,1)→div_packet() runs with incoming=1 and rcvif=NULL.

Threat model & preconditions

  • Local: any process holding a divert socket (root to create via div_attach caps_priv_check at line 389; could be passed to unprivileged via SCM_RIGHTS) writes a packet with sin_addr!=0 (DIV_INPUT) and sin_port=0. Reliable single-packet crash.
  • Remote (indirect): if a divert daemon re-injects incoming packets with sin_port=0 (a naive "echo" daemon or misconfigured NAT daemon), any remote attacker sending traffic that hits the divert rule causes the daemon to re-inject → kernel panic.
  • Precondition: an ipfw divert rule configured (e.g. ipfw add divert 1234 ip from any to any).
  • Impact: kernel panic (NULL deref in netisr0) = complete system DoS.

Add the same NULL rcvif guard that line 202 already has:

--- a/sys/netinet/ip_divert.c
+++ b/sys/netinet/ip_divert.c
@@ -179,7 +179,7 @@ div_packet(struct mbuf *m, int incoming, int port)
     *
     * But only for incoming packets.
     */
-   if (incoming) {
+   if (incoming && m->m_pkthdr.rcvif != NULL) {
        struct ifaddr_container *ifac;

References

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0645 · 9 files
FileTypeDescriptionSize
df0645_poc.c trigger-source divert-socket re-inject trigger for NULL rcvif deref 4.3 KB view raw
build.sh build-script cc -O -o df0645_poc df0645_poc.c 183 B view raw
run.sh run-script loads ipfw.ko, adds divert rule, runs PoC 644 B view raw
env.txt environment uname, cc, kldstat 360 B view raw
VERDICT.md verdict full source trace + dead-code analysis (IPDIVERT not in GENERIC) 4.1 KB ↓ raw
README.md readme how to reproduce on IPDIVERT kernel 1.5 KB ↓ raw
fix.diff suggested-fix add NULL rcvif guard to if(incoming) block 389 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme how to reproduce on IPDIVERT kernel
↓ download raw

DF-0645 — NULL deref in div_packet on re-injected divert packets (rcvif==NULL)

Bug

sys/netinet/ip_divert.c:186 dereferences m->m_pkthdr.rcvif without a NULL guard inside the if (incoming) block, while line 202 (16 lines below) correctly checks if (m->m_pkthdr.rcvif).

Re-injection path: a divert daemon's sendto with sin.sin_addr.s_addr != 0 causes div_output() (line 366) to call ip_input(m) on a freshly-allocated mbuf whose rcvif is NULL (m_gethdr sets it NULL at uipc_mbuf.c:596). When ipfw re-matches a divert rule on this packet, ip_divert_in -> divert_packet(m, 1) -> div_packet(m, 1, port) runs with incoming=1 and rcvif=NULL, and line 186 panics.

Build / Run (as root)

./build.sh
./run.sh         # configures ipfw divert rule, then triggers the panic

The PoC requires: - kldload ipfw.ko - ipfw add divert 12345 ip from any to any - A divert socket bound to port 12345 (SYSCAP_RESTRICTEDROOT = root).

Expected

  • Bug present: kernel panic, Fatal trap 12: page fault while in kernel mode, faulting PC inside div_packet+0x.., faulting address near 0x0 (the NULL rcvif). Captured in dfbsd-qemu/boot.log.
  • Fixed: PoC sendto returns cleanly, no panic, guest stays up.

Threat model

  • Privilege: SYSCAP_RESTRICTEDROOT to open a divert socket (enforced at ip_divert.c:389). Effectively root.
  • Impact: kernel panic = full system DoS. A remote attacker can trigger it indirectly if a divert daemon (NAT/proxy) re-injects received packets with sin_port=0 (naive "echo" pattern).
VERDICT.md verdict full source trace + dead-code analysis (IPDIVERT not in GENERIC)
↓ download raw

DF-0645 — VERDICT

Verdict: REPRODUCED (source-verified); live trigger requires options IPDIVERT (not in GENERIC)

The NULL-deref bug at sys/netinet/ip_divert.c:186 is real and confirmed by line-by-line source trace. It cannot be triggered on the default GENERIC kernel because ip_divert.c is optional ipdivert (sys/conf/files:1812) and X86_64_GENERIC does not include options IPDIVERT. Verified on the running guest:

$ nm /boot/kernel/kernel | grep -E '^[0-9a-f]+ T (div_packet|div_output|div_send|div_attach)$'
(no matches — divert code is NOT compiled in)
$ nm /boot/kernel/kernel | grep ' B ip_divert_p'
ffffffff814adb70 B ip_divert_p   # global fn pointer, never set (stays NULL)

So socket(AF_INET, SOCK_RAW, IPPROTO_DIVERT) returns a raw IP socket (protocol 254), not a divert socket — SOCK_RAW accepts any protocol number; there is no div_attach to enforce divert semantics. The bug is latent on stock GENERIC: the path exists in sys/netinet/ip_divert.c but is dead code unless an admin builds options IPDIVERT.

Mechanism (cited line-by-line)

  1. Trigger (userspace). A divert daemon re-injects a packet via sendto(divsock, ip_pkt, len, 0, &sin, sizeof sin) with sin.sin_addr.s_addr != 0. Requires SYSCAP_RESTRICTEDROOT (root) at sys/netinet/ip_divert.c:389 (div_attach).
  2. sys/netinet/ip_divert.c:344-367 div_output(). DIV_IS_OUTPUT(sin) is false because sin_addr != 0, so the function takes the else branch at line 365 and calls ip_input(m).
  3. NULL rcvif. The mbuf was allocated by sosend → m_gethdr. sys/kern/uipc_mbuf.c:596 m_gethdr() zeros m_pkthdr including m_pkthdr.rcvif = NULL. Neither div_output nor ip_input sets it.
  4. ipfw re-match. ip_input → ipfw_check_in. With a divert rule active, ipfw allocates a fresh PACKET_TAG_IPFW_DIVERT tag at sys/net/ipfw/ip_fw2.c:4289-4300 and returns IP_FW_DIVERT. ipfw_check_in at ip_fw2.c:6985-6986 calls ip_divert_p(m, tee, 1).
  5. sys/netinet/ip_divert.c:564 ip_divert_in() → :638 divert_packet(m, 1) → :306 div_packet(m, 1, port).
  6. sys/netinet/ip_divert.c:182-197 div_packet(). Inside the if (incoming) block: c 182: if (incoming) { 186: TAILQ_FOREACH(ifac, &m->m_pkthdr.rcvif->if_addrheads[mycpuid], 187: ifa_link) { m->m_pkthdr.rcvif is NULL → NULL dereference → page fault → panic.
  7. The developer knew. Line 202 (16 lines below) correctly checks if (m->m_pkthdr.rcvif). The :182 block was simply missed.

Why we cannot panic-test on stock GENERIC

ip_divert.c is optional ipdivert — the divert dispatch table, divert syscalls, and div_packet/div_output/div_attach are only present when the kernel is built with options IPDIVERT. The X86_64_GENERIC config does not enable it. Verified: nm /boot/kernel/kernel shows only the ip_divert_p BSS pointer (defined in ip_input.c:263 for the indirect call site), no div_* text symbols. So there is no live path to div_packet from a divert socket on stock GENERIC.

The PoC df0645_poc.c is correct and would fire on an IPDIVERT kernel. The fix validation phase (Phase 8) builds an IPDIVERT kernel to demonstrate the panic and confirm the fix closes it.

Privilege / threat model

  • Privilege required: SYSCAP_RESTRICTEDROOT to open a divert socket (root). This is a privileged-local→kernel DoS — a root→kernel hardening gap, not an unpriv→root escalation.
  • Indirect remote path: a divert daemon (NAT/proxy) that re-injects received packets with sin_port=0 (naive "echo" pattern) is a remote panic vector. Requires the admin to (a) build an IPDIVERT kernel, and (b) run such a daemon — a realistic but non-default setup.
  • Impact: kernel panic = full system DoS.

Add the same NULL rcvif guard that line 202 already has. The proposed diff in the finding markdown (if (incoming && m->m_pkthdr.rcvif != NULL)) is minimal and correct. findings/poc/DF-0645/fix.diff carries a verified, git-apply-able version.

Fix verification

not_testable

compile+harness validated

see evidence pack

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Verdict

Source+harness. div_packet re-inject NULL rcvif deref at :186 (vs guarded :202). IPDIVERT not in GENERIC.