git: 1ecbabf77217 - stable/14 - pf: do not loop on an address that is cleared twice in pfr_clr_astats()

From: R. Christian McDonald <rcm_at_FreeBSD.org>
Date: Wed, 28 Oct 2026 20:41:25 UTC
The branch stable/14 has been updated by rcm:

URL: https://cgit.FreeBSD.org/src/commit/?id=1ecbabf7721788f680ba3d3f2a5fc9e1b8ecb96e

commit 1ecbabf7721788f680ba3d3f2a5fc9e1b8ecb96e
Author:     R. Christian McDonald <rcm@FreeBSD.org>
AuthorDate: 2026-09-29 00:07:22 +0000
Commit:     R. Christian McDonald <rcm@FreeBSD.org>
CommitDate: 2026-10-28 20:38:02 +0000

    pf: do not loop on an address that is cleared twice in pfr_clr_astats()
    
    pfr_clr_astats() looks up each address it is given and inserts the entry
    it finds at the head of a work queue. If the same address is given more
    than once, the entry is inserted twice and the second insertion makes it
    its own successor. pfr_clstats_kentries() then walks the queue forever,
    with the rules lock held for writing, so packet processing and every
    other pf operation in that vnet stop as well. To reproduce:
    
    pfctl -e
    pfctl -t foo -T add 192.0.2.1
    pfctl -t foo -T zero 192.0.2.1 192.0.2.1
    
    Do as pfr_del_addrs() does: clear pfrke_mark on the entries named, then
    queue an entry only the first time it is seen. An address given more
    than once is cleared, and counted, once. Validate all addresses before
    any entry is touched.
    
    Add a regression test.
    
    Reviewed by:            kp
    Approved by:            kp (mentor)
    MFC after:              1 week
    Sponsored by:           Rubicon Communications, LLC ("Netgate")
    Differential Revision:  https://reviews.freebsd.org/D60103
    
    (cherry picked from commit ee05a360b02e4615d50eae7cababd2ab2d05d488)
---
 sys/netpfil/pf/pf_table.c     | 11 +++++++++--
 tests/sys/netpfil/pf/table.sh | 34 ++++++++++++++++++++++++++++++++++
 2 files changed, 43 insertions(+), 2 deletions(-)

diff --git a/sys/netpfil/pf/pf_table.c b/sys/netpfil/pf/pf_table.c
index 3004609e1702..a064aca5bebe 100644
--- a/sys/netpfil/pf/pf_table.c
+++ b/sys/netpfil/pf/pf_table.c
@@ -653,16 +653,23 @@ pfr_clr_astats(struct pfr_table *tbl, struct pfr_addr *addr, int size,
 	kt = pfr_lookup_table(tbl);
 	if (kt == NULL || !(kt->pfrkt_flags & PFR_TFLAG_ACTIVE))
 		return (ESRCH);
-	SLIST_INIT(&workq);
 	for (i = 0, ad = addr; i < size; i++, ad++) {
 		if (pfr_validate_addr(ad))
 			senderr(EINVAL);
 		p = pfr_lookup_addr(kt, ad, 1);
+		if (p != NULL)
+			p->pfrke_mark = 0;
+	}
+	SLIST_INIT(&workq);
+	for (i = 0, ad = addr; i < size; i++, ad++) {
+		p = pfr_lookup_addr(kt, ad, 1);
 		if (flags & PFR_FLAG_FEEDBACK) {
 			ad->pfra_fback = (p != NULL) ?
 			    PFR_FB_CLEARED : PFR_FB_NONE;
 		}
-		if (p != NULL) {
+		/* An address given more than once is cleared once. */
+		if (p != NULL && !p->pfrke_mark) {
+			p->pfrke_mark = 1;
 			SLIST_INSERT_HEAD(&workq, p, pfrke_workq);
 			xzero++;
 		}
diff --git a/tests/sys/netpfil/pf/table.sh b/tests/sys/netpfil/pf/table.sh
index f655463f3c03..728b82e7cd29 100644
--- a/tests/sys/netpfil/pf/table.sh
+++ b/tests/sys/netpfil/pf/table.sh
@@ -194,6 +194,39 @@ zero_one_cleanup()
 	pft_cleanup
 }
 
+atf_test_case "zero_twice" "cleanup"
+zero_twice_head()
+{
+	atf_set descr 'Test zeroing an address that is given twice'
+	atf_set require.user root
+	atf_set timeout 30
+}
+
+zero_twice_body()
+{
+	pft_init
+
+	vnet_mkjail alcatraz
+	jexec alcatraz pfctl -e
+
+	pft_set_rules alcatraz \
+	    "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \
+	    "pass in from <foo> to any"
+
+	# This used to hang the kernel with the rules lock held:
+	# pfr_clr_astats() put the entry on its work queue twice.
+	atf_check -s exit:0 -e "match:1/2 addresses cleared." \
+	    jexec alcatraz pfctl -t foo -T zero 192.0.2.1 192.0.2.1
+	atf_check -s exit:0 -e "match:2/4 addresses cleared." \
+	    jexec alcatraz pfctl -t foo -T zero 192.0.2.3 192.0.2.1 \
+	    192.0.2.3 192.0.2.5
+}
+
+zero_twice_cleanup()
+{
+	pft_cleanup
+}
+
 atf_test_case "reset_nonzero" "cleanup"
 reset_nonzero_head()
 {
@@ -489,6 +522,7 @@ atf_init_test_cases()
 	atf_add_test_case "v4_counters"
 	atf_add_test_case "v6_counters"
 	atf_add_test_case "zero_one"
+	atf_add_test_case "zero_twice"
 	atf_add_test_case "reset_nonzero"
 	atf_add_test_case "pr251414"
 	atf_add_test_case "automatic"