git: 25f04bf42786 - main - pf: do not free the caller's ruleset when attaching a table fails

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Mon, 12 Oct 2026 16:05:58 UTC
The branch main has been updated by kp:

URL: https://cgit.FreeBSD.org/src/commit/?id=25f04bf42786806e2661b7a09c283b83f568380a

commit 25f04bf42786806e2661b7a09c283b83f568380a
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2026-10-12 08:00:11 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2026-10-12 13:08:59 +0000

    pf: do not free the caller's ruleset when attaching a table fails
    
    pfr_attach_table() takes a struct pf_ruleset * from its caller and, on its
    own error paths, calls pfr_destroy_ktable() on the table it just made.  That
    drops the table's reference on the ruleset and calls
    pf_remove_if_empty_ruleset(), which may unlink and free the anchor the
    ruleset is embedded in -- and then pfr_attach_table() returns NULL to a
    caller still holding the pointer.
    
    Reached from userland with DIOCADDRULE and a PF_ADDR_DYNIFTL source, after
    DIOCSETLIMIT has lowered PF_LIMIT_TABLES to one item of headroom: the first
    pfr_create_ktable() takes it and the root table's fails.
    
    Hold a table reference on the ruleset for the duration instead, so the
    ruleset outlives every failure path inside pfr_attach_table() and stays the
    caller's to remove.
    
    OK sashan@
    
    Obtained from:  OpenBSD, gnezdo <gnezdo@openbsd.org>, e0a25791f4
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
---
 sys/netpfil/pf/pf_table.c | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/sys/netpfil/pf/pf_table.c b/sys/netpfil/pf/pf_table.c
index 7fc3b0a6380b..94ea5181ee74 100644
--- a/sys/netpfil/pf/pf_table.c
+++ b/sys/netpfil/pf/pf_table.c
@@ -2285,15 +2285,26 @@ pfr_attach_table(struct pf_kruleset *rs, char *name)
 		strlcpy(tbl.pfrt_anchor, ac->path, sizeof(tbl.pfrt_anchor));
 	kt = pfr_lookup_table(&tbl);
 	if (kt == NULL) {
+		/*
+		 * Hold rs across the table creation below.  A new table takes
+		 * a reference on the ruleset, and pfr_destroy_ktable() drops
+		 * it again on the failure paths, which lets
+		 * pf_remove_if_empty_ruleset() free the anchor rs points into
+		 * while our caller still holds rs.
+		 */
+		rs->tables++;
 		kt = pfr_create_ktable(&tbl, time_second, 1);
-		if (kt == NULL)
+		if (kt == NULL) {
+			rs->tables--;
 			return (NULL);
+		}
 		if (ac != NULL) {
 			bzero(tbl.pfrt_anchor, sizeof(tbl.pfrt_anchor));
 			rt = pfr_lookup_table(&tbl);
 			if (rt == NULL) {
 				rt = pfr_create_ktable(&tbl, 0, 1);
 				if (rt == NULL) {
+					rs->tables--;
 					pfr_destroy_ktable(kt, 0);
 					return (NULL);
 				}
@@ -2302,6 +2313,7 @@ pfr_attach_table(struct pf_kruleset *rs, char *name)
 			kt->pfrkt_root = rt;
 		}
 		pfr_insert_ktable(kt);
+		rs->tables--;
 	}
 	if (!kt->pfrkt_refcnt[PFR_REFCNT_RULE]++)
 		pfr_setflags_ktable(kt, kt->pfrkt_flags|PFR_TFLAG_REFERENCED);