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

ip_fw3_ctl_zero_entry KKASSERT(zmsg.start_rule==NULL) fires deterministically on every zero/resetlog of a specific rule (root DoS panic on default INVARIANTS kernel)

Summary

ip_fw3_ctl_zero_entry sets zmsg.start_rule=rule at :932 when caller passes non-zero rulenum. ip_fw3_zero_entry_dispatch never clears zmsg.start_rule. After netisr_domsg returns unconditional KKASSERT(zmsg.start_rule==NULL) at :939 fires panicking any INVARIANTS-enabled kernel (default X86_64_GENERIC). stock ipfw3 zero N and ipfw3 resetlog N CLI commands invoke exactly this path. KKASSERT gated by INVARIANTS default kernel config enables it.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2576 Β· 11 files
FileTypeDescriptionSize
poc.c trigger-source setsockopt(IPPROTO_IP, IP_FW_X=49, {opcode=53 IP_FW_ZERO, rulenum}) -> ip_fw3_ctl_zero_entry 3.5 KB view raw
build.sh build-script cc -o poc poc.c 42 B view raw
run.sh run-script sets filters_default_to_accept=1 before kldload ipfw3(+basic); runs ./poc 846 B view raw
build.log build-log poc build rc=0 68 B view raw
run.log run-log unpatched: panic at ip_fw3.c:939 857 B view raw
fix_build.log build-log ipfw3.ko rebuild with fix, rc=0 (-Werror clean) 7.5 KB view raw
fix_run.log run-log patched ipfw3.ko: 4x setsockopt rc=0, no panic, guest up 365 B view raw
panic.txt panic-signature panic: assertion zmsg.start_rule==NULL failed ... ip_fw3.c:939 400 B view raw
env.txt environment uname, cc, loaded ipfw3 modules, KKASSERT absent in patched source 295 B view raw
fix.diff suggested-fix remove the incorrect KKASSERT(zmsg.start_rule==NULL) at ip_fw3.c:939 325 B view raw
VERDICT.md verdict full analysis 5.5 KB ↓ raw
VERDICT.md verdict full analysis
↓ download raw

DF-2576 β€” ip_fw3_ctl_zero_entry KKASSERT(start_rule==NULL) panic β€” VERDICT

Verdict: REPRODUCED (deterministic KKASSERT panic on default INVARIANTS kernel); fix VALIDATED

  • status: reproduced
  • reproduced: 1
  • impact: panic (root-reachable deterministic DoS; fires on default X86_64_GENERIC which ships options INVARIANTS)
  • confidence: certain
  • fix_status: fixed

The bug (confirmed in source, line-by-line)

sys/net/ipfw3/ip_fw3.c ip_fw3_ctl_zero_entry() (:904–944):

909:    struct ipfw3_context *ctx = fw3_ctx[mycpuid];
...
917:    if (rulenum == 0) {
...
920:    } else {
921:        struct ip_fw *rule;
926:        for (rule = ctx->rules; rule; rule = rule->next) {
927:            if (rule->rulenum == rulenum)
928:                break;
929:        }
930:        if (rule == NULL)
931:            return (EINVAL);
932:        zmsg.start_rule = rule;          /* <-- set, never used by dispatch */
...
938:    netisr_domsg(nmsg, 0);              /* runs ip_fw3_zero_entry_dispatch on all CPUs */
939:    KKASSERT(zmsg.start_rule == NULL);  /* <-- PANIC: dispatch never clears it */

ip_fw3_zero_entry_dispatch() (:876–894) only touches zmsg->rulenum and zmsg->log_only; it never writes zmsg->start_rule. So after netisr_domsg() returns, zmsg.start_rule is still the non-NULL rule pointer set at :932, and the unconditional KKASSERT(zmsg.start_rule == NULL) at :939 fires on every INVARIANTS-enabled kernel. netmsg_zent.start_rule (:125) is a dead field β€” set at :932, never read anywhere β€” paired with an incorrect assertion.

KKASSERT expands to an inline panic() (it is INVARIANTS-gated and compiled into the default X86_64_GENERIC kernel, so it fires on the stock kernel β€” verified by the panic observed).

Reachability

setsockopt(SOCK_RAW, IPPROTO_IP, IP_FW_X=49, {opcode=IP_FW_ZERO=53, rulenum}) β†’ rip_ctloutput β†’ ip_fw3_sockopt β†’ ip_fw3_ctl (case IP_FW_X :1058) β†’ ip_fw3_ctl_x strips the 4-byte ip_fw_x_header, sets sopt_name=53 (:1043-1044) β†’ ip_fw3_ctl (case IP_FW_ZERO :1066) β†’ ip_fw3_ctl_sockopt (case IP_FW_ZERO :1158) β†’ reads the 4-byte rulenum (:1162) β†’ ip_fw3_ctl_zero_entry(rulenum, log_only) (:1168).

The default rule (rulenum IPFW_DEFAULT_RULE = 65535, ip_fw3.h:87) is always present in ctx->rules (ctx_init_dispatch, :1432-1433, runs on every CPU at kldload), so zeroing rule 65535 needs no prior rule to be added β€” the bug is triggerable immediately after kldload ipfw3 ipfw3_basic.

Privilege: a raw IP socket requires caps_priv_check(SYSCAP_NONET_RAW) (root), and kldload requires root. This is therefore a root-reachable deterministic DoS β€” but on a system where ipfw3 is the active firewall (a normal production config), any root-driven ipfw3 zero N / ipfw3 resetlog N (N != 0, matching an existing rule) panics the kernel.

Reproduction (unpatched #0 baseline)

run.sh sets net.filters_default_to_accept=1 before kldload (the sysctl is a base-kernel tunable in sys/net/pfil.c:83-87, read at MOD_LOAD in ctx_init_dispatch :1425, so setting it first keeps ssh alive), loads ipfw3 + ipfw3_basic, then issues the zero opcode for rule 65535.

Result on the unpatched 6.5-DEVELOPMENT #0 kernel (boot.log):

panic: assertion "zmsg.start_rule == NULL" failed in ip_fw3_ctl_zero_entry at /usr/src/sys/net/ipfw3/ip_fw3.c:939
ip_fw3_ctl_zero_entry() at ip_fw3_ctl_zero_entry+0x1c9 0xffffffff82601d39
ip_fw3_ctl_sockopt() at ip_fw3_ctl_sockopt+0xe2 0xffffffff826020c2
Stopped at      Debugger+0x7c:  movb    $0,0xbdaf09(%rip)

Exact match to the cited line 939, through the cited call chain. Guest dead.

Fix

fix.diff removes the incorrect KKASSERT (the field is dead and the assertion can never hold):

    netisr_domsg(nmsg, 0);
-   KKASSERT(zmsg.start_rule == NULL);

    if (sysctl_var_fw3_verbose)

Fix validation

ipfw3 is a loadable module (/boot/kernel/ipfw3.ko). The fix was validated by rebuilding just ipfw3.ko, reinstalling it, reloading, and re-running the same PoC:

  • Apply: patch -p1 < fix.diff β†’ Hunk #1 succeeded at 936.
  • Build: cd sys/net/ipfw3 && make β†’ rc=0 (-Werror clean; see fix_build.log).
  • Install: cp ipfw3.ko /boot/kernel/ipfw3.ko.
  • Re-run (rulenum 65535 Γ—3, plus rulenum 0 Γ—1): every call returns setsockopt rc=0, no panic, guest stays up.

Before/after contrast: | kernel | same PoC (zero rule 65535) | |----------------------|------------------------------------------------------------------------------| | unpatched #0 | panic: assertion "zmsg.start_rule == NULL" failed ... ip_fw3.c:939, guest dead | | patched ipfw3.ko | setsockopt rc=0, no panic, guest up (deterministic over 4 runs) |

The fix closes the bug.

Threat model

DoS only (a kernel assertion panic, no attacker-controlled write). Root-only reachability via raw socket + kldload. There is no memory-corruption primitive, hence no escalation chain.

PoC changes / artifacts

  • poc.c β€” setsockopt(IPPROTO_IP, IP_FW_X=49, {opcode=53, rulenum}) PoC; the ip_fw_x_header pattern mirrors DF-2580.
  • run.sh β€” sets net.filters_default_to_accept=1 before kldload (base-kernel tunable; read at MOD_LOAD) to avoid the default-deny rule locking out ssh, then loads ipfw3(+basic) and runs the PoC.
  • build.sh, build.log, run.log, fix_build.log, fix_run.log, panic.txt, env.txt, fix.diff, manifest.json.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: same PoC (setsockopt IP_FW_X opcode=ZERO rulenum=65535) PANICS on unpatched #0 baseline ('panic: assertion "zmsg.start_rule == NULL" failed ... ip_fw3.c:939', guest dead) and does NOT panic on single-fix ipfw3.ko (setsockopt rc=0, no panic, guest up β€” deterministic over 4 runs: rulenum 65535 x3 plus rulenum 0 x1). Removing incorrect KKASSERT closes the bug.

baseline: 'panic: assertion "zmsg.start_rule == NULL" failed in ip_fw3_ctl_zero_entry at ...ip_fw3.c:939' / 'Stopped at Debugger+0x7c' (guest DOWN). patched ipfw3.ko run1/2/3 (rulenum=65535): setsockopt rc=0 (returned cleanly -> KKASSERT did NOT fire / FIXED kernel) (guest UP). run4 (rulenum=0, all-rules path): setsockopt rc=0 (guest UP). fix_build: 'Hunk #1 succeeded at 936' / '=== IPFW3_BUILD_DONE rc=0 ===' (-Werror clean).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (base kernel unchanged; ipfw3 is module-only, so only /boot/kernel/ipfw3.ko rebuilt+reinstalled with fix)

Confirmed kernel references

Detail

Exploit chain

none β€” deterministic INVARIANTS assertion panic (KKASSERT -> inline panic()), not memory-corruption primitive. No attacker-controlled write, no escalation chain. Impact is root-reachable DoS (raw socket + kldload both require root); on host running ipfw3 as firewall, any root-driven 'ipfw3 zero N' / 'ipfw3 resetlog N' with matching existing rule panics kernel.

Evidence (decisive lines)

=== BASELINE (unpatched #0) run === [*] ipfw3 loaded; default policy: 1 / [*] setsockopt(IPPROTO_IP, IP_FW_X=49, opcode=ZERO, rulenum=65535) ... (ssh died -- kernel panicked) === panic signature (boot.log) === panic: assertion "zmsg.start_rule == NULL" failed in ip_fw3_ctl_zero_entry at /usr/src/sys/net/ipfw3/ip_fw3.c:939 / ip_fw3_ctl_zero_entry() at ip_fw3_ctl_zero_entry+0x1c9 / Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)

PoC changes

Wrote poc.c (setsockopt(IPPROTO_IP, IP_FW_X=49, {opcode=53 IP_FW_ZERO, rulenum}) following DF-2580 ip_fw_x_header pattern), build.sh, run.sh, VERDICT.md, fix.diff (removes incorrect KKASSERT(zmsg.start_rule==NULL) at :939), manifest.json. KEY run.sh detail: net.filters_default_to_accept is base-kernel tunable (sys/net/pfil.c:83-87) read at MOD_LOAD (ctx_init_dispatch :1425); MUST be set to 1 BEFORE kldload ipfw3, otherwise default-deny rule kills ssh before PoC output observed.

Verified recommended fix

In sys/net/ipfw3/ip_fw3.c ip_fw3_ctl_zero_entry, remove line 'KKASSERT(zmsg.start_rule == NULL);' at :939. netmsg_zent.start_rule is a dead field (set at :932, never read by ip_fw3_zero_entry_dispatch nor anywhere else), so assertion incorrect and can never hold after netisr_domsg returns. Removing is minimal correct fix. Matches finding proposal. Full git-apply-able diff in findings/poc/DF-2576/fix.diff.

Verdict

REPRODUCED. ip_fw3_ctl_zero_entry (sys/net/ipfw3/ip_fw3.c:904-944) sets zmsg.start_rule = rule at :932 when a non-zero rulenum matches an existing rule, then netisr_domsg at :938 runs ip_fw3_zero_entry_dispatch (:876) on all CPUs β€” but dispatch ONLY touches zmsg->rulenum and zmsg->log_only and NEVER clears zmsg->start_rule. The unconditional KKASSERT(zmsg.start_rule == NULL) at :939 therefore fires on every INVARIANTS-enabled kernel (default X86_64_GENERIC ships options INVARIANTS). netmsg_zent.start_rule (:125) is a dead field. Confirmed on unpatched #0: 'panic: assertion "zmsg.start_rule == NULL" failed in ip_fw3_ctl_zero_entry at /usr/src/sys/net/ipfw3/ip_fw3.c:939'. Triggered via setsockopt(SOCK_RAW, IPPROTO_IP, IP_FW_X=49, {opcode=IP_FW_ZERO=53, rulenum=65535}); rulenum 65535 = IPFW_DEFAULT_RULE always present in ctx->rules.