On 2026/08/06 11:31, Janak Trivedi wrote:
> 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 */

Surely the bug is that tcp_est isn't cleared, wouldn't this zero
check just be hiding the problem?


> 
>           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

Reply via email to