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)
PoC verification
Evidence pack
findings/poc/DF-2576 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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(-Werrorclean; seefix_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; theip_fw_x_headerpattern mirrors DF-2580.run.shβ setsnet.filters_default_to_accept=1beforekldload(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
fixedVALIDATED: 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).
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.
No comments yet.