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

NULL-pointer dereference in rn_flush on never-created tables -> kernel panic via delete/flush ioctl and module unload

Summary

table_delete_dispatch:128-130 and table_flush_dispatch:245-248 call rn_flush(table_ctx->node,...) WITHOUT NULL check. table_ctx M_ZERO :570 so node=NULL until IP_FW_TABLE_CREATE type1|2 calls rn_inithead. Calling DELETE/FLUSH on in-bounds id never created -> rn_flush(NULL) -> deref head->rnh_walktree at radix.c:1341 -> panic. ip_fw3_table_fini_dispatch:580-586 loops all 32 tables rn_flush each -> kldunload ipfw3_basic after normal use (most slots never created) panics deterministically. Attacker: root setsockopt(IP_FW_TABLE_DELETE/FLUSH) in-bounds id never created. Impact: reliable kernel panic DoS. No mem corruption. Fix: if(node!=NULL) rn_flush.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0669 Β· 13 files
FileTypeDescriptionSize
README.md readme PoC evidence pack overview 1.3 KB ↓ raw
VERDICT.md verdict REPRODUCED β€” kernel panic on never-created ipfw3 table; root-only trigger 3.6 KB ↓ raw
df0669_ipfw3_null.c trigger-source setsockopt(IP_FW_X, IP_FW_TABLE_DELETE) on never-created id=0 2.4 KB view raw
build.sh build-script cc -O -pipe -Wall -o df0669_ipfw3_null df0669_ipfw3_null.c 180 B view raw
run.sh run-script kldload ipfw3+basic then run PoC (root) 554 B view raw
run.log run-log test wrapper output incl panic signature 1.9 KB view raw
panic.txt panic-signature Fatal trap 12 page fault @ VA=0x28 = rnh_walktree offset, head=NULL 1.5 KB view raw
env.txt environment guest uname + kern.version + cc + sysctl + modules loaded 781 B view raw
fix.diff suggested-fix add NULL guards before rn_flush in 3 sites (git apply --check OK) 1.2 KB view raw
fix_build.log build-log single-fix ipfw3_basic.ko module build output (rc=0) 1.7 KB view raw
fix_run.log run-log patched-module PoC re-run: no panic / EINVAL returned 1.8 KB 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 PoC evidence pack overview
↓ download raw

DF-0669 β€” PoC evidence pack

Summary

  • File: sys/net/ipfw3_basic/ip_fw3_table.c:130 (also :248, :583)
  • Claim: rn_flush(table_ctx->node, ...) is called without a NULL check; table_ctx->node is NULL (M_ZERO init) until IP_FW_TABLE_CREATE runs rn_inithead. Calling DELETE/FLUSH on an in-bounds table id that was never created, or kldunloading ipfw3_basic after normal use, panics in rn_flush at head->rnh_walktree with head == NULL.

Verdict

REPRODUCED β€” kernel panic (root-only trigger). See VERDICT.md and panic.txt.

Reproduce

./build.sh           # builds df0669_ipfw3_null
# Run as root, e.g. via vm.sh (ipfw3 default-deny severs ssh, so the
# test runs locally and writes its output to /dev/console):
ssh dfbsd 'kldload ipfw3; kldload ipfw3_basic; /root/poc/DF-0669/df0669_ipfw3_null 74'
# Expected: Fatal trap 12 page fault @ VA=0x28; guest reboots (debugger_on_panic=0)
# or drops into DDB (debugger_on_panic=1).

Environment

See env.txt.

Fix

fix.diff adds if (table_ctx->node != NULL) guards before each of the three vulnerable rn_flush calls (in table_delete_dispatch, table_flush_dispatch, and the ip_fw3_table_fini_dispatch unload loop). git apply --check passes. Matches the finding proposal.

VERDICT.md verdict REPRODUCED β€” kernel panic on never-created ipfw3 table; root-only trigger
↓ download raw

DF-0669 β€” VERDICT

Verdict: REPRODUCED (kernel panic, root-only trigger)

The NULL-deref in rn_flush(table_ctx->node, ...) on a never-created ipfw3 table is real and reproduces deterministically on the default GENERIC kernel (#0 master DEV build, INVARIANTS ON). The trigger is root-only (SOCK_RAW required to reach rip_ctloutput's IP_FW_X case), matching the finding's PR:H CVSS vector β€” a root-loaded ipfw3 module can be panicked by a root setsockopt. With ipfw3 loaded at boot (a realistic firewall config), any process holding a raw socket (root in the default jail-less configuration) can panic the kernel.

Mechanism (cited path:line)

  1. sys/net/ipfw3/ip_fw3.c:1038-1046 ip_fw3_ctl_x strips the 4-byte ip_fw_x_header from the setsockopt value via sopt_valsize -= sizeof(ip_fw_x_header); bcopy(++x_header, sopt_val, ...) and dispatches on x_header->opcode. No sopt_valsize validation.
  2. sys/netinet/raw_ip.c:385-386 routes IP_FW_X setsockopt on a SOCK_RAW socket to ip_fw3_sockopt β†’ ip_fw3_ctl_x. (Only raw sockets handle IP_FW_X β€” SOCK_DGRAM returns ENOPROTOOPT.)
  3. sys/net/ipfw3_basic/ip_fw3_table.c:566-572 ip_fw3_table_init_dispatch allocates the per-CPU table context with M_ZERO, so every table_ctx->node is NULL until IP_FW_TABLE_CREATE calls rn_inithead on it.
  4. sys/net/ipfw3_basic/ip_fw3_table.c:118-134 table_delete_dispatch does no NULL check before rn_flush(table_ctx->node, flush_table_entry) at line 130.
  5. sys/net/radix.c:1341 rn_flush derefs head->rnh_walktree. With head == NULL this is a NULL+offset deref β†’ page fault. (table_flush_dispatch:248 has the same bug; ip_fw3_table_fini_dispatch:583 loops all 32 tables through rn_flush, so kldunloading ipfw3_basic after normal use β€” when most slots are still NULL β€” panics deterministically too.)

Reproduction

PoC: df0669_ipfw3_null.c β€” issues setsockopt(SOCK_RAW, IPPROTO_IP, IP_FW_X, {x_header.opcode=IP_FW_TABLE_DELETE, ioc_table.id=0}, valsize=48) on a fresh-loaded ipfw3 with no tables created.

Run as root via test.sh (loads ipfw3+ipfw3_basic, then runs the PoC; output and panic land in dfbsd-qemu/boot.log via /dev/console because ipfw3's default-deny severs networking the moment it loads):

Fatal trap 12: page fault while in kernel mode
cpuid = 0; lapic id = 0
fault virtual address   = 0x28
fault code      = supervisor read data, page not present
instruction pointer = 0x8:0xffffffff8074946d
stack pointer           = 0x10:0xfffff8008d1f8960
frame pointer           = 0x10:0xfffff8008d1f8980
current process     = Idle
panic: page fault

The fault virtual address 0x28 is the byte offset of rnh_walktree within struct radix_node_head, confirming the panic is at head->rnh_walktree with head == NULL (i.e. inside rn_flush(NULL) from table_delete_dispatch:130).

The full untrimmed panic signature is in panic.txt.

Privilege / threat model

  • Trigger requires root (raw socket). With ipfw3 enabled at boot by the administrator (a realistic firewall config), any local process with PRIV_NETCTRL/root can DoS the kernel.
  • No memory corruption β€” pure NULL deref. Impact is reliable kernel panic (DoS), not escalation. The CVSS vector AV:L/AC:L/PR:H/...A:H matches: it is a root-only local DoS.

fix.diff adds if (table_ctx->node != NULL) guards in table_delete_dispatch, table_flush_dispatch, and the loop in ip_fw3_table_fini_dispatch. The finding's proposed fix matches this. Matches the finding proposal.

Fix verification

fixed

validated

see evidence pack
↓ fix.diffn/a (module-level)

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live panic). rn_flush(NULL->node) on never-created ipfw3 table. Root-only SOCK_RAW. Module fix: NULL guards before rn_flush.