From nobody Tue Oct 20 12:52:41 2026 X-Original-To: dev-commits-src-main@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4j8C4T2QV8z6wyR2 for ; Tue, 20 Oct 2026 12:52:41 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by mx1.freebsd.org (Postfix) with ESMTPS id 4j8C4T1fQtz4tkM for ; Tue, 20 Oct 2026 12:52:41 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1792500761; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=ThtsOvrbfM/LA/IlIYB97AoV043PFQ8yr9kGqconbIA=; b=DyrUkDw9Yp5zvPuLdD0LbXt/yAHH1Ccnc/R6a7DjOjGMBgauqOzZLpY1hzD2zhB96JXXMG sqV5ckGXKaAxl/s03AMppx8rK3wLXv2DR2Ax2xJlBNOAfytyh0jUYFZHz8IuCPemYheyXr ujmA0E84T4Zaw/nzXmD/fy2SJq1/VQmZE++AXRrCF5V/CQZJ560skKCWBmDj32hCYcKzf+ CK8hFVr5CSfraODTI8GDzSbOxVB+ubRXe16z7PP0ddKTkj9ZUgxAtxO4wTRfYV8LOCCE+a khPdzbMkhQWp8V8CnA+/R++PlblvIiyDBOVE/IfrRSXD4dtk6L8OOBHzzqzYrw== ARC-Seal: i=1; a=rsa-sha256; d=freebsd.org; s=dkim; cv=none; t=1792500761; b=tv19xtz3TM/bmm2+GmHcW0CW3mygSst00M7XpQu/BB0oZl5fdl8nPrW4gxRUjLHzIbZOVD flQSd54zUma5j/5ux3salrL5Pmjqar8yvzK0XgfIA/xnPUSkhgKejkYBrPQDPwezIJCbJr /werx7zLIAWOKy9Bm3WAkST3CzApFyzKxfI8UHWbv5Ynm2RCMZy0trP6g5F/TpoHHBOpcG E9SIKqgxywc3HHc0ENNcv1MLjNeFvnPEt3vMSl3Oxr7/6x4PrqvxfAOjbf3fBHqKbIseOl aaRmYYNCYnHHmHftUEGL9x2j36I/sjWMrTtKEIc7yRZpYIihM8p5CcHQh1Fbsw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1792500761; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=ThtsOvrbfM/LA/IlIYB97AoV043PFQ8yr9kGqconbIA=; b=jgEk9xjnPsnaDVjaKdsC5Hw15rJ9pSbzN85OMhOeZCHVhUmjfGEa1XK79eNhJ7+S73OweN j+DJSWIzOEsX1WHsYHtTcYvyphFuQmm5zrA2l3d0Gs81LqTbOrEa8kK+cQ2dVQ9RVnRx0L /UHY/m8p5irhIdnIVBCobBflx99Kd8RQVtTsHU8e4fn2hozPu/K/qTbdt9y5CCZ73maY0I PeEPRV92WZBDC+r4YnzWURyYAeaEPdd+AcFt1cc+cH/KKhJN5VSdc0hqbxLqXXuwjx50sv yOsy/iRVBQeK+moAcmJymZDX6vSk+eeSufFajq5OEgmZ98FlXzfxZ4OKIKH/kA== ARC-Authentication-Results: i=1; mx1.freebsd.org; none Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4j8C4T1HL1z17D3 for ; Tue, 20 Oct 2026 12:52:41 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 3929e by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Tue, 20 Oct 2026 12:52:41 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: R. Christian McDonald Subject: git: a3173175193e - main - pf: Keep every chunk of a table that a ruleset defines List-Archive: https://lists.freebsd.org/archives/dev-commits-src-main X-BeenThere: dev-commits-src-main@freebsd.org Sender: owner-dev-commits-src-main@FreeBSD.org List-Id: Commit messages for the main branch of the src repository List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: rcm X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: a3173175193e111f2a92c999842bc3cfbcc2d945 Auto-Submitted: auto-generated Date: Tue, 20 Oct 2026 12:52:41 +0000 Message-Id: <6ad76419.3929e.12df9d5b@gitrepo.freebsd.org> The branch main has been updated by rcm: URL: https://cgit.FreeBSD.org/src/commit/?id=a3173175193e111f2a92c999842bc3cfbcc2d945 commit a3173175193e111f2a92c999842bc3cfbcc2d945 Author: R. Christian McDonald AuthorDate: 2026-10-20 12:45:35 +0000 Commit: R. Christian McDonald CommitDate: 2026-10-20 12:52:00 +0000 pf: Keep every chunk of a table that a ruleset defines libpfctl sends a table definition in chunks of 256 addresses, each as its own PFNL_CMD_INA_DEFINE. The kernel treated every chunk as a new definition and replaced the shadow table, so only the last chunk survived. Mark the chunks after the first with PF_ID_APPEND and have the kernel add them to the shadow table in progress. A request without the attribute still replaces the shadow; one with it and no definition in progress fails with EINVAL. Old kernels ignore the attribute, so both halves are needed; the ioctl sends all addresses at once and is not affected. Mask the flags userland passes to PFR_FLAG_ALLMASK, as PFR_FLAG_APPEND is kernel-internal. Add a test that defines a table of 1,000 addresses, also in an anchor. Reviewed by: kp Fixes: 219ce3063840 ("pf: convert DIOCRINADEFINE to netlink") Sponsored by: Rubicon Communications, LLC ("Netgate") Differential Revision: https://reviews.freebsd.org/D60876 --- lib/libpfctl/libpfctl.c | 10 +++++-- sys/net/pfvar.h | 1 + sys/netpfil/pf/pf_ioctl.c | 3 +- sys/netpfil/pf/pf_nl.c | 5 ++++ sys/netpfil/pf/pf_nl.h | 1 + sys/netpfil/pf/pf_table.c | 59 ++++++++++++++++++++++++++++++++++++ tests/sys/netpfil/pf/table.sh | 70 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 3 deletions(-) diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c index 6d7c45ff993f..f8ce51e2df18 100644 --- a/lib/libpfctl/libpfctl.c +++ b/lib/libpfctl/libpfctl.c @@ -4044,7 +4044,7 @@ SNL_DECLARE_PARSER(ina_define_parser, struct genlmsghdr, snl_f_p_empty, ap_ina_d static int _pfctl_ina_define(struct pfctl_handle *h, struct pfr_table *tbl, struct pfr_addr *addr, int size, int *nadd, int *naddr, - int ticket, int flags) + int ticket, int flags, bool append) { struct snl_writer nw; struct snl_errmsg_data e = {}; @@ -4061,6 +4061,7 @@ _pfctl_ina_define(struct pfctl_handle *h, struct pfr_table *tbl, snl_add_msg_attr_table(&nw, PF_ID_TABLE, tbl); snl_add_msg_attr_u32(&nw, PF_ID_TICKET, ticket); snl_add_msg_attr_u32(&nw, PF_ID_FLAGS, flags); + snl_add_msg_attr_bool(&nw, PF_ID_APPEND, append); for (int i = 0; i < size; i++) snl_add_msg_attr_pfr_addr(&nw, PF_ID_ADDR, &addr[i]); @@ -4106,10 +4107,15 @@ pfctl_ina_define(struct pfctl_handle *h, struct pfr_table *tbl, if (naddr) *naddr = 0; + /* + * The addresses go in chunks. The first one defines the table, the + * others add to it; without that, each would define it anew and + * only the last would be left. + */ do { chunk_size = MIN(size - off, 256); ret = _pfctl_ina_define(h, tbl, &addr[off], chunk_size, - &partial_add, &partial_addr, ticket, flags); + &partial_add, &partial_addr, ticket, flags, off > 0); if (ret != 0) break; if (nadd) diff --git a/sys/net/pfvar.h b/sys/net/pfvar.h index 533cb4f08283..6c7c7d364ba2 100644 --- a/sys/net/pfvar.h +++ b/sys/net/pfvar.h @@ -2442,6 +2442,7 @@ struct pfioc_trans { #define PFR_FLAG_ALLMASK 0x000000FF #ifdef _KERNEL #define PFR_FLAG_USERIOCTL 0x10000000 +#define PFR_FLAG_APPEND 0x20000000 #endif struct pfioc_table { diff --git a/sys/netpfil/pf/pf_ioctl.c b/sys/netpfil/pf/pf_ioctl.c index 8685a877a00f..55a5c38e1301 100644 --- a/sys/netpfil/pf/pf_ioctl.c +++ b/sys/netpfil/pf/pf_ioctl.c @@ -6152,7 +6152,8 @@ DIOCCHANGEADDR_error: PF_RULES_WLOCK(); error = pfr_ina_define(&io->pfrio_table, pfras, io->pfrio_size, &io->pfrio_nadd, &io->pfrio_naddr, - io->pfrio_ticket, io->pfrio_flags | PFR_FLAG_USERIOCTL); + io->pfrio_ticket, + (io->pfrio_flags & PFR_FLAG_ALLMASK) | PFR_FLAG_USERIOCTL); PF_RULES_WUNLOCK(); free(pfras, M_PF); break; diff --git a/sys/netpfil/pf/pf_nl.c b/sys/netpfil/pf/pf_nl.c index d35545bbfe32..de7994b314c4 100644 --- a/sys/netpfil/pf/pf_nl.c +++ b/sys/netpfil/pf/pf_nl.c @@ -2911,6 +2911,7 @@ struct nl_parsed_ina_define { size_t addr_count; uint32_t ticket; uint32_t flags; + bool append; }; #define _OUT(_field) offsetof(struct nl_parsed_ina_define, _field) @@ -2919,6 +2920,7 @@ static const struct nlattr_parser nla_ina_define_parser[] = { { .type = PF_ID_TICKET, .off = _OUT(ticket), .cb = nlattr_get_uint32 }, { .type = PF_ID_FLAGS, .off = _OUT(flags), .cb = nlattr_get_uint32 }, { .type = PF_ID_ADDR, .off = _OUT(addrs), .cb = nlattr_get_pfr_addr }, + { .type = PF_ID_APPEND, .off = _OUT(append), .cb = nlattr_get_bool }, }; #undef _OUT NL_DECLARE_PARSER(ina_define_parser, struct genlmsghdr, nlf_p_empty, nla_ina_define_parser); @@ -2935,6 +2937,9 @@ pf_handle_ina_define(struct nlmsghdr *hdr, struct nl_pstate *npt) error = nl_parse_nlmsg(hdr, &ina_define_parser, npt, &attrs); if (error != 0) return (error); + attrs.flags &= PFR_FLAG_ALLMASK; + if (attrs.append) + attrs.flags |= PFR_FLAG_APPEND; PF_RULES_WLOCK(); error = pfr_ina_define(&attrs.table, attrs.addrs.addrs, diff --git a/sys/netpfil/pf/pf_nl.h b/sys/netpfil/pf/pf_nl.h index d7adad2e8bd6..347265dcdefa 100644 --- a/sys/netpfil/pf/pf_nl.h +++ b/sys/netpfil/pf/pf_nl.h @@ -607,6 +607,7 @@ enum pf_ina_define_t { PF_ID_ADDR = 4, /* pfr_addr */ PF_ID_NADD = 5, /* u32 */ PF_ID_NADDR = 6, /* u32 */ + PF_ID_APPEND = 7, /* bool */ }; enum pf_osfp_t { diff --git a/sys/netpfil/pf/pf_table.c b/sys/netpfil/pf/pf_table.c index b1bb68935ab5..b0bbd3a9f67a 100644 --- a/sys/netpfil/pf/pf_table.c +++ b/sys/netpfil/pf/pf_table.c @@ -1544,6 +1544,53 @@ pfr_ina_begin(struct pfr_table *trs, u_int32_t *ticket, int *ndel, int flags) return (0); } +/* + * Add addresses to the shadow table of a definition that is in progress. + * The shadow is left as it was unless all of them are valid and there is + * memory for them. + */ +static int +pfr_ina_append(struct pfr_ktable *shadow, struct pfr_addr *addr, int size, + int *nadd, int *naddr) +{ + struct pfr_kentryworkq addrq; + struct pfr_kentry *p; + struct pfr_addr *ad; + int i, xaddr = 0; + + PF_RULES_WASSERT(); + + if (shadow->pfrkt_cnt == NO_ADDRESSES) + return (EINVAL); + for (i = 0, ad = addr; i < size; i++, ad++) + if (pfr_validate_addr(ad)) + return (EINVAL); + SLIST_INIT(&addrq); + for (i = 0, ad = addr; i < size; i++, ad++) { + if (pfr_lookup_addr(shadow, ad, 1) != NULL) + continue; + p = pfr_create_kentry(ad, + (shadow->pfrkt_flags & PFR_TFLAG_COUNTERS) != 0); + if (p == NULL) { + pfr_clean_node_mask(shadow, &addrq); + pfr_destroy_kentries(&addrq); + return (ENOMEM); + } + if (pfr_route_kentry(shadow, p)) { + pfr_destroy_kentry(p); + continue; + } + SLIST_INSERT_HEAD(&addrq, p, pfrke_workq); + xaddr++; + } + shadow->pfrkt_cnt += xaddr; + if (nadd != NULL) + *nadd = 0; + if (naddr != NULL) + *naddr = xaddr; + return (0); +} + int pfr_ina_define(struct pfr_table *tbl, struct pfr_addr *addr, int size, int *nadd, int *naddr, u_int32_t ticket, int flags) @@ -1570,6 +1617,18 @@ pfr_ina_define(struct pfr_table *tbl, struct pfr_addr *addr, int size, tbl->pfrt_flags |= PFR_TFLAG_INACTIVE; SLIST_INIT(&tableq); kt = RB_FIND(pfr_ktablehead, &V_pfr_ktables, (struct pfr_ktable *)tbl); + /* + * The addresses of one definition may arrive in several requests. + * All but the first ask for theirs to be added to the shadow table + * that the first one made, instead of replacing it. A dry run makes + * no shadow table; it only checks the addresses, below. + */ + if ((flags & (PFR_FLAG_APPEND | PFR_FLAG_DUMMY)) == PFR_FLAG_APPEND) { + if (kt == NULL || kt->pfrkt_shadow == NULL) + return (EINVAL); + return (pfr_ina_append(kt->pfrkt_shadow, addr, size, nadd, + naddr)); + } if (kt == NULL) { kt = pfr_create_ktable(tbl, 0, 1); if (kt == NULL) diff --git a/tests/sys/netpfil/pf/table.sh b/tests/sys/netpfil/pf/table.sh index e21d998b4ece..2b7c84032d41 100644 --- a/tests/sys/netpfil/pf/table.sh +++ b/tests/sys/netpfil/pf/table.sh @@ -949,6 +949,75 @@ load_cleanup() pft_cleanup } +atf_test_case "define_large" "cleanup" +define_large_head() +{ + atf_set descr 'Define a table of more addresses than fit in one request' + atf_set require.user root +} + +define_large_body() +{ + pft_init + pwd=$(pwd) + + vnet_mkjail alcatraz + jexec alcatraz pfctl -e + + for i in `seq 1 4`; do + for j in `seq 1 250`; do + echo "192.0.${i}.${j}" >> ${pwd}/foo.lst + done + done + expected=$(wc -l < ${pwd}/foo.lst | awk '{ print $1; }') + + # libpfctl passes the addresses of a definition to the kernel in + # chunks of 256. The kernel used to start the table over with each + # of them, so that only the last chunk was left. The second pass + # redefines a table that is in use, which the kernel commits by + # merging instead of swapping. + for pass in 1 2; do + pft_set_rules noflush alcatraz \ + "table persist file \"${pwd}/foo.lst\"" \ + "pass in from " + actual=$(jexec alcatraz pfctl -t foo -T show | wc -l | \ + awk '{ print $1; }') + if [ ${actual} -ne ${expected} ]; then + atf_fail "pass ${pass}: ${actual} of ${expected} addresses" + fi + done + atf_check -s exit:0 -o match:"192.0.1.1" -o match:"192.0.4.250" \ + jexec alcatraz pfctl -t foo -T show + + # The tables of an anchor are defined after its rules were loaded, + # by another path through pfctl. + pft_set_rules alcatraz \ + "anchor \"bar\" {" \ + " table persist file \"${pwd}/foo.lst\"" \ + " pass in from " \ + "}" + actual=$(jexec alcatraz pfctl -a bar -t baz -T show | wc -l | \ + awk '{ print $1; }') + if [ ${actual} -ne ${expected} ]; then + atf_fail "anchor: ${actual} of ${expected} addresses" + fi + + # A table defined twice in one ruleset is its last definition: the + # first request of a definition replaces what is there, only the + # later chunks of the same definition add to it. + pft_set_rules alcatraz \ + "table { 192.0.2.1 }" \ + "table { 192.0.2.2 }" \ + "pass in from " + atf_check -s exit:0 -o match:"192.0.2.2" -o not-match:"192.0.2.1" \ + jexec alcatraz pfctl -t foo -T show +} + +define_large_cleanup() +{ + pft_cleanup +} + atf_test_case "test" "cleanup" test_head() { @@ -1095,6 +1164,7 @@ atf_init_test_cases() atf_add_test_case "replace_verbose" atf_add_test_case "replace_create" atf_add_test_case "load" + atf_add_test_case "define_large" atf_add_test_case "test" atf_add_test_case "test_verbose" atf_add_test_case "show_no_counters"