pf: u_int32_t conn counter underflow in pf_src_tree_remove_state()
Janak Trivedi <[email protected]>
| Newsgroups | gmane.os.openbsd.bugs |
|---|---|
| Message-ID | <CADiWTV4XdBK+2UX=7WQHCQV28MZj-=zCKSZO1=N=Jnp-c7YO2Q@mail.gmail.com> |
Dear OpenBSD Team,
I am writing to report a bug in the PF packet filter that causes the
per-source connection counter (conn) to underflow, resulting in legitimate
connection being rejected with source rule tracking applied.
The bug was discovered during investigation of false positive IP blocking
in a production PF firewall environment and has been confirmed through
controlled lab testing.
================================================================
SYSTEM INFORMATION
================================================================
OpenBSD Version: 7.8
Architecture: amd64
================================================================
AFFECTED FILE
================================================================
File: sys/net/pf.c
Function: pf_src_tree_remove_state()
================================================================
BUG DESCRIPTION
================================================================
The conn counter (u_int32_t) in struct pf_src_node tracks the number of
established TCP connections from a source IP address. This counter is used
to enforce the max-src-conn rule option.
The counter is:
INCREMENTED in: pf_src_connlimit()
when TCP 3-way handshake completes
(st->src.tcp_est flag set to 1)
DECREMENTED in: pf_src_tree_remove_state()
when ANY state is removed where tcp_est=1
The bug is that tcp_est flag is NEVER cleared when a connection transitions
from ESTABLISHED to FIN_WAIT, TIME_WAIT or CLOSING state. The flag remains
set until the state is physically removed from the state table.
At high connection rates, FIN_WAIT states accumulate in the state table.
When these states are eventually removed, pf_src_tree_remove_state()
decrements the conn counter for each removal. Since the conn counter was
only incremented once per connection (at ESTABLISHED), but is decremented
once per state removal (including FIN_WAIT), the total decrements exceed
the total increments, causing unsigned integer underflow.
================================================================
AFFECTED CODE
================================================================
Current code in pf_src_tree_remove_state():
pf_src_tree_remove_state(struct pf_state *st)
{
u_int32_t timeout;
struct pf_sn_item *sni;
while ((sni = SLIST_FIRST(&st->src_nodes)) != NULL) {
SLIST_REMOVE_HEAD(&st->src_nodes, next);
if (st->src.tcp_est)
--sni->sn->conn; /* BUG: no zero check,
tcp_est never cleared
on FIN_WAIT transition */
if (--sni->sn->states == 0) {
timeout = st->rule.ptr->timeout[PFTM_SRC_NODE];
if (!timeout)
timeout =
pf_default_rule.timeout[PFTM_SRC_NODE];
sni->sn->expire = getuptime() + timeout;
}
pool_put(&pf_sn_item_pl, sni);
}
}
Note: The states counter (--sni->sn->states) also has no explicit zero
check, however it is less susceptible to underflow because states is
incremented for every new state creation (including SYN_SENT) whereas conn
is only incremented at ESTABLISHED, meaning states >= conn in normal
operation.
================================================================
IMPACT
================================================================
When underflow occurs:
conn counter value: ~4,294,967,105 (near 2^32)
max-src-conn threshold: 200,000 (typical value)
4,294,967,105 >> 200,000
Every subsequent connection from the affected source IP immediately exceeds
max-src-conn threshold, causing:
1. Source IP added to overload table (<aggressive>)
2. ALL connections from that IP blocked
3. IP remains blocked until src.track node expires
(typically 10+ seconds after last state removed)
4. If administrator manually removes IP from overload table without
killing states and waiting for src.track expiry, IP is immediately
re-blocked on next connection attempt
This causes legitimate source IPs to be permanently blocked, appearing as
random false positives in production environments.
================================================================
REPRODUCTION
================================================================
PF rule to reproduce:
table <aggressive> persist
pass log quick proto tcp from any to <target> port 80 \
flags S/SA keep state ( \
max 10000000, \
max-src-states 999999, \
max-src-conn 999999, \
max-src-conn-rate 999999/10, \
overload <aggressive>, \
tcp.first 5, tcp.opening 5, \
tcp.established 3600, tcp.closing 900, \
tcp.finwait 30, tcp.closed 30, \
adaptive.start 6000000, \
adaptive.end 12000000)
Note: No flush option is used. The bug occurs regardless of flush setting
(flush, flush global, or no flush).
Traffic generation (using vegeta):
echo "GET http://<target>/" | vegeta attack -rate=1000/s
-duration=10s -timeout=5s -header="Connection: close" | vegeta report
Monitoring command to observe underflow:
pfctl -s Sources
Expected observation:
During test: connections counter rises normally
After test: FIN_WAIT states accumulate
During expiry: connections counter underflows to ~2^32
Final value: 4,294,967,296 - (peak_FIN - peak_conn)
================================================================
PROPOSED FIX
================================================================
Option 1 — Minimal fix (add zero check):
/* CURRENT (buggy): */
if (st->src.tcp_est)
--sni->sn->conn;
/* FIXED (safe): */
if (st->src.tcp_est && sni->sn->conn > 0)
--sni->sn->conn;
Pros: Simple one-line change
Prevents underflow completely
Minimal risk of regression
Cons: conn may slightly overcount during high-rate
FIN_WAIT accumulation (benign)
================================================================
TEST ENVIRONMENT
================================================================
Firewall: OpenBSD
Client: OpenBSD with vegeta package
Server: OpenBSD with nginx package
Traffic: vegeta 12.12.0
================================================================
Thank you for your time and for maintaining OpenBSD and PF. I am happy to
provide additional information, test results, or packet captures if helpful.
Regards,
Janak Trivedi
Stockholm Sweden