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_attachcaps_priv_checkat line 389; could be passed to unprivileged viaSCM_RIGHTS) writes a packet withsin_addr!=0(DIV_INPUT) andsin_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.
Recommended fix
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
sys/netinet/ip_divert.c:186— the unguardedrcvifdereference.sys/netinet/ip_divert.c:202— the correct guard 16 lines below.sys/kern/uipc_mbuf.c:596—m_gethdrsetsrcvif = NULL.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0645 · 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 insidediv_packet+0x.., faulting address near 0x0 (the NULLrcvif). Captured indfbsd-qemu/boot.log. - Fixed: PoC sendto returns cleanly, no panic, guest stays up.
Threat model
- Privilege:
SYSCAP_RESTRICTEDROOTto open a divert socket (enforced atip_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).
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)
- Trigger (userspace). A divert daemon re-injects a packet via
sendto(divsock, ip_pkt, len, 0, &sin, sizeof sin)withsin.sin_addr.s_addr != 0. RequiresSYSCAP_RESTRICTEDROOT(root) atsys/netinet/ip_divert.c:389(div_attach). sys/netinet/ip_divert.c:344-367div_output().DIV_IS_OUTPUT(sin)is false becausesin_addr != 0, so the function takes theelsebranch at line 365 and callsip_input(m).- NULL rcvif. The mbuf was allocated by
sosend → m_gethdr.sys/kern/uipc_mbuf.c:596m_gethdr()zerosm_pkthdrincludingm_pkthdr.rcvif = NULL. Neitherdiv_outputnorip_inputsets it. - ipfw re-match.
ip_input → ipfw_check_in. With a divert rule active, ipfw allocates a freshPACKET_TAG_IPFW_DIVERTtag atsys/net/ipfw/ip_fw2.c:4289-4300and returnsIP_FW_DIVERT.ipfw_check_inatip_fw2.c:6985-6986callsip_divert_p(m, tee, 1). sys/netinet/ip_divert.c:564 ip_divert_in() → :638 divert_packet(m, 1) → :306 div_packet(m, 1, port).sys/netinet/ip_divert.c:182-197div_packet(). Inside theif (incoming)block:c 182: if (incoming) { 186: TAILQ_FOREACH(ifac, &m->m_pkthdr.rcvif->if_addrheads[mycpuid], 187: ifa_link) {m->m_pkthdr.rcvifis NULL → NULL dereference → page fault → panic.- 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_RESTRICTEDROOTto 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.
Recommended fix
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_testablecompile+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.
No comments yet.