IP_FW_SET_TOGGLE/IP_FW_SET_GET operate only on CPU0's per-CPU sets bitmap β ipfw3 set enable/disable silently ineffective for all flows hashed to CPUs 1..N-1 (deterministic per-flow firewall policy divergence on SMP)
Summary
ip_fw3_ctl_set_toggle XORs fw3_ctx[mycpuid]->sets and set_get reads it, but every ipfw3 sockopt runs under ASSERT_NETISR0 so mycpuid==0 always: only CPU0's bitmap ever changes. Rule matching runs on the CPU the flow hashes to and consults that CPU's private context (skip if the set bit is set). After any 'ipfw3 set toggle N', rules in set N are skipped for CPU0-hashed flows and remain enforced for all others, while 'ipfw3 set show'/'ipfw3 -S list' (both read CPU0) confirm 'disabled'. The four sibling handlers in the same file broadcast via netisr_domsg, and ipfw2's toggle broadcasts - only this one was missed. VERIFIED on the 6-CPU guest: count rule in set 3 with 'disable: 3' - source aliases {4,8,13,19,20,24} (toeplitz CPU0-class) skipped while 17 others still hit the rule, reversible across 4 phases. The same skip branch governs deny/allow/skipto: an operator who disables a set (emergency lifting of deny rules, incident response) leaves those rules enforced for ~5/6 of flows - deterministic, silent, invisible to set show. Root-only trigger; victim is firewall policy. Fix validated by module rebuild + differential re-run (broadcast the toggle like the siblings): uniform enforcement restored.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3462 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| udp1shot.c | β | 1.6 KB | view raw | |
| udprx.c | β | 1.6 KB | view raw | |
| srcscan.sh | β | 696 B | view raw | |
| addrscan.sh | β | 477 B | view raw | |
| run.sh | β | 144 B | view raw | |
| build.sh | β | 150 B | view raw | |
| build.log | β | 7.4 KB | view raw | |
| run.log | β | 2.8 KB | view raw | |
| run.patched.log | β | 2.8 KB | view raw | |
| env.txt | β | 252 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| panic.teardown.txt | β | 1.7 KB | view raw | |
| verdict.json | β | 6.1 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw |
DF-3462 β ipfw3 set toggle only flips CPU0's per-CPU sets bitmap
File: sys/net/ipfw3/ip_fw3_set.c
Verdict: reproduced (policy-divergence logic bug, fix validated)
Impact: per-CPU firewall policy divergence β ipfw3 set toggle N
(= IP_FW_SET_TOGGLE, used by ipfw3 set disable/enable semantics)
takes effect only for flows hashed to CPU0; every other CPU keeps the
old enable/disable state forever.
What to look at
run.logβ baseline (stockipfw3.ko), the decisive four-phase run:- ENABLED-1: all 23 source aliases counted (delta 2 = OUT+IN hooks).
- DISABLED-1 (
ipfw3 set showβdisable: 3): sources 127.0.0.{4,8,13,19,20,24} β delta 0 (rule skipped, CPU0-hashed), the other 17 sources β delta 2 (rule STILL enforced β bug). - ENABLED-2 / DISABLED-2: the differential is reversible and deterministic per flow.
run.patched.logβ same script after installing thefix.diffmodule: DISABLED phases now yield delta 0 for every source (uniform skip across all 6 CPUs); ENABLED phases unchanged. The divergence is gone.srcscan.sh+udp1shot.cβ the measurement instrument: one bound-source UDP datagram per 127.0.0.X alias to local 127.0.0.1; the flow CPU isnetisr_hashcpu(toeplitz(dst,src))(sys/netinet/ip_demux.c:71-80, addresses only β ports ignored).build.logβ module-only rebuild ofipfw3.kowithfix.diff(cc 8.3 [DragonFly]).panic.teardown.txtβ an unrelated stockkldunload ipfw3crash inrn_flushobserved during an earlier teardown (independent lead, not on the DF-3462 path).
Reproduce (root on the guest)
sysctl net.filters_default_to_accept=1 # avoid deny-all on module load
kldload ipfw3 && kldload ipfw3_basic && kldload ipfw3_layer4
for x in 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24; do
ifconfig lo0 inet 127.0.0.$x netmask 255.255.255.255 alias
done
cc -O -o /root/udp1shot udp1shot.c
ipfw3 add 104 set 3 count udp dst-port 44990
./run.sh # baseline: DISABLED phases show delta 0 only for
# {4,8,13,19,20,24}; delta 2 for all others <-- BUG
Fixed module:
cd /usr/src && patch -p1 < fix.diff
cd sys/net/ipfw3 && make && make install
<reboot or load the patched module from a clean boot>
./run.sh # DISABLED phases now show delta 0 for ALL sources
Success criterion (baseline): with disable: 3 reported by
ipfw3 set show, a strict non-empty subset of flows (CPU0-hashed) skips
set-3 rules while the majority still enforces them. After fix.diff:
uniform behavior. See VERDICT.md for the full mechanism and source
citations.
DF-3462 VERDICT β reproduced; fix validated (module rebuild + rerun)
Finding
ip_fw3_ctl_set_toggle() (sys/net/ipfw3/ip_fw3_set.c:191-204) mutates
only fw3_ctx[mycpuid]->sets. Every ipfw3 sockopt handler runs under
ASSERT_NETISR0 (sys/netinet/raw_ip.c:308), so mycpuid == 0 always:
the toggle flips CPU0's bitmap only. Per-packet rule matching,
however, runs on whichever CPU the flow hashes to and consults that
CPU's private context:
ip_fw3_chk():ctx = fw3_ctx[mycpuid](sys/net/ipfw3/ip_fw3.c:350)- rule skip:
if (ctx->sets & (1 << f->set)) continue;(sys/net/ipfw3/ip_fw3.c:487-489) β applies to any action (deny / allow / count / skipto / fwd β¦).
The four sibling handlers in the same file broadcast their state change
to every CPU via netisr_domsg/netisr_forwardmsg_all
(ip_fw3_set.c:114 move_set, :148 move_rule, :186 swap, :250
flush), and ipfw2's equivalent (ipfw_set_disable_dispatch,
sys/net/ipfw/ip_fw2.c:5849-5875) broadcasts as well β only the
toggle was missed. The consequence: after any odd number of
ipfw3 set toggle N, rules in set N are skipped for CPU0-hashed flows
and remain enforced for all others (deterministic per address pair β
INP_MPORT_HASH_UDP hashes addresses only, sys/netinet/ip_demux.c:71-80).
An operator who disables a set (emergency removal of bad deny rules,
incident response) sees ipfw3 set show / ipfw3 -S list confirm
"disabled" (both read CPU0's bitmap, ip_fw3_set.c:211,
ip_fw3.c:1016) while the set's rules keep firing on ~5/6 of the
traffic. There is no CPU where the reverse holds, because CPUs 1..N-1
never receive the bit at all.
How it was reproduced (guest, 6 CPUs, DragonFly 6.5-DEVELOPMENT #0)
Observable: rule 104 set 3 count udp dst-port 44990. IP_FW_GET
aggregates the per-CPU sibling counters (ip_fw3.c:1020-1025), so a
datagram is counted iff its checking CPU has set 3 enabled. Flow CPU
control: datagrams from bound source aliases 127.0.0.X to local
127.0.0.1; CPU = netisr_hashcpu(toeplitz(127.0.0.1, 127.0.0.X)).
Baseline (run.log, stock module):
state (ipfw3 set show) |
127.0.0.{4,8,13,19,20,24} | other 17 sources |
|---|---|---|
enabled (disable: empty) |
delta 2 (counted) | delta 2 (counted) |
disable: 3 |
delta 0 (skipped) | delta 2 (STILL enforced) |
| toggle back | delta 2 | delta 2 |
Deterministic, reversible, reproduced across four phases and three
independent experiment rounds (destination-side scan, source-side
scan, repeated runs). The skip branch used by count is the same
branch that skips deny/allow β the enforcement divergence applies
to every action in the set.
Dead ends honestly recorded: (1) an attempt to demonstrate the deny
direction through packet delivery was confounded by DFly's per-CPU
UDP wildcard-socket placement ("dropped due to no socket",
netstat -s -p udp) β unrelated to ipfw3, excluded from the claim;
(2) nc -u -l exits immediately when stdin closes β replaced by
udprx.c; (3) pinning the sending process does not steer the flow
CPU because udp_send redispatches by 4-tuple-address hash
(sys/netinet/udp_usrreq.c:1137-1147).
Fix validation
fix.diff (authored after the baseline reproduction) broadcasts the
toggle exactly like the file's own set_swap/flush handlers:
void
set_toggle_dispatch(netmsg_t smsg)
{
struct netmsg_sets *msg = (struct netmsg_sets *)smsg;
struct ipfw3_context *ctx = fw3_ctx[mycpuid];
ctx->sets = ctx->sets ^ (1 << msg->set_to);
netisr_forwardmsg_all(&smsg->base, mycpuid + 1);
}
Module-only rebuild + install in the guest (build.log), loaded from
a clean vm.sh reset with-src boot, identical test (run.patched.log):
| state | 127.0.0.{4,8,13,19,20,24} | other 17 sources |
|---|---|---|
| enabled | delta 2 | delta 2 |
disable: 3 |
delta 0 | delta 0 |
Uniform behavior β divergence eliminated; enable direction unchanged.
ip_fw3_ctl_set_get needs no change once all bitmaps agree (it reads
CPU0's, which equals every CPU's after the broadcast).
Unrelated incident
kldunload ipfw3 (stock fini path) panicked once in rn_flush during
an early teardown β see panic.teardown.txt. Not on the DF-3462 code
path (the toggle handler is never invoked during module ops); kept as
an independent lead for the maintainers.
Bottom line
Certain. Root-only configuration primitive, but a silent, deterministic per-CPU firewall-policy divergence on a multi-CPU system: set enable/disable (toggle) affects only CPU0-hashed flows. Medium severity (firewall policy bypass/incorrect enforcement; requires the firewall operator to use set toggles). Fix is three lines of broadcast + one prototype, validated by module rebuild and differential re-run.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff applied to /usr/src in-guest, ipfw3.ko module rebuilt+installed (build.log), loaded fresh after vm.sh reset with-src. The exact decisive script re-run: DISABLED phases now yield delta 0 for ALL 23 source aliases (uniform skip across the 6 per-cpu contexts) and ENABLED phases are identical to baseline (all delta 2) - the per-flow divergence signature ({4,8,13,19,20,24}->0 vs others->2 in DISABLED state) is gone. Baseline signature preserved in run.log.
run.patched.log (patched: DISABLED => all-zero deltas, both disabled phases); run.log (baseline divergence); build.log (ipfw3.ko rebuild); fix.diff
Confirmed kernel references
- sys/net/ipfw3/ip_fw3_set.c:191-204
- sys/net/ipfw3/ip_fw3_set.c:206-215
- sys/net/inet/raw_ip.c:308
- sys/net/ipfw3/ip_fw3.c:350
- sys/net/ipfw3/ip_fw3.c:485-489
- sys/net/ipfw3/ip_fw3.c:1020-1027
- sys/netinet/ip_demux.c:71-80
- sys/netinet/udp_usrreq.c:1137-1147
- sys/net/ipfw/ip_fw2.c:5849-5875
Detail
Exploit chain
Operator scenario: incident responder runs ipfw3 set disable N (toggle) to stop rules in set N (e.g. a misfiring deny set, or to lift a quarantine set) - ipfw3 set show confirms disabled, but the set's deny/allow rules remain enforced for all flows whose toeplitz address-hash lands on CPUs 1..N-1 (deterministic per address pair; ~5/6 of flows here). Conversely there is no way to disable a set for those flows at all. Not an unpriv->root chain: the trigger is the root-only IP_FW_SET_TOGGLE sockopt; the victim is the firewall policy itself.
Evidence (decisive lines)
run.log (baseline, decisive 4-phase run: DISABLED phases show delta 0 for {4,8,13,19,20,24} and delta 2 for 17 others); run.patched.log (same script, fix.diff module: DISABLED => all delta 0); VERDICT.md (mechanism + source cites); srcscan.sh/udp1shot.c (instrument); build.log (module rebuild); panic.teardown.txt (unrelated stock kldunload ipfw3 rn_flush crash, side lead)
PoC changes
no seed: finding + instrument authored in this run (pass 2). Key instrument learnings: the flow cpu is chosen by udp_send's 4-tuple-address redispatch (udp_usrreq.c:1137-1147), not by the sending process's cpu, so flows are steered via bound 127/8 source aliases against a fixed local dst; IP_FW_GET aggregates per-cpu sibling counters so one counter delta per flow is the cleanest per-cpu enforcement observable; ipfw3 -S show is required to read rules inside a disabled set (CLI hides them otherwise, sbin/ipfw3/ipfw3.c:751-754).
Verified recommended fix
Broadcast the toggle to all CPUs like the sibling handlers: add set_toggle_dispatch() that XORs fw3_ctx[mycpuid]->sets on each cpu via netisr_forwardmsg_all and call it from ip_fw3_ctl_set_toggle through netisr_domsg (fix.diff, validated).
Verdict
Reproduced deterministically on the 6-CPU INVARIANTS guest: with rule 104 (count udp dst-port 44990) in set 3 and ipfw3 set toggle 3 applied (ipfw3 set show => disable: 3), datagrams from bound source aliases 127.0.0.{4,8,13,19,20,24} to local 127.0.0.1 (flows hashing to CPU0 via toeplitz over the address pair, INP_MPORT_HASH_UDP) are skipped by the rule, while the other 17 source aliases still hit it (aggregated per-CPU sibling counters via IP_FW_GET). Toggling back restores uniformity, and the differential is reversible/repeatable across four phases (run.log). Root cause is certain from source: ip_fw3_ctl_set_toggle (ip_fw3_set.c:191-204) mutates only fw3_ctx[mycpuid] with mycpuid==0 forced by ASSERT_NETISR0 (raw_ip.c:308), while per-packet matching uses fw3_ctx[mycpuid] of the flow's cpu (ip_fw3.c:350,487); every sibling set handler (move_set/move_rule/swap/flush) and ipfw2's ipfw_set_disable_dispatch (ip_fw2.c:5849-5875) broadcast via netisr_domsg - only toggle was missed. fix.diff adds the same broadcast (set_toggle_dispatch + netisr_domsg); with the rebuilt ipfw3.ko the DISABLED phases show delta 0 for ALL sources (uniform skip) and ENABLED phases are unchanged (run.patched.log) - divergence eliminated.
No comments yet.