All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH nft] src: release scope symbols by reference count
@ 2026-09-11 19:20 Avinash Duduskar
  2026-09-11 20:38 ` Florian Westphal
  0 siblings, 1 reply; 2+ messages in thread
From: Avinash Duduskar @ 2026-09-11 19:20 UTC (permalink / raw)
  To: netfilter-devel; +Cc: Pablo Neira Ayuso, Florian Westphal, Kamil Kwiek

nft -f aborts when a define in table or chain scope takes its value from
another variable:

  define outer = 111

  table inet t {
          define inner = $outer
  }

  nft: src/rule.c:579: scope_release: Assertion `sym->refcnt == 1' failed.

$outer becomes a variable expression that takes a reference on the
symbol, and it is still holding that reference when the top level scope
is released: the table has gone into the cache, so cmd_free() drops one
of its references rather than freeing it.  Using $inner in a rule does
not release it either, expr_evaluate_variable() shares or clones
sym->expr rather than destroying it.

It happens without the cache too.  When the table already exists,
cmd_free() frees it together with its scope, while the rule commands
that nft_cmd_expand() split out of the same block are still holding
their variable expressions.

scope_release() asserted the count was 1 and then freed the symbol
whatever it was, so on a build with NDEBUG this is a use-after-free
rather than an abort.  The same gap leaks a symbol that is undefined
while a define still refers to it.

Ordering fixes, as in b73298405cda ("rule: fix sym refcount assertion"),
settle one instance at a time.  Make the count own the symbol instead:
scope_release() and variable_expr_destroy() both release through
symbol_put(), so the symbol goes away with its last reference and the
order these run in stops mattering.

Fixes: 12a223ced7f6 ("libnftables: release top level scope")
Reported-by: Kamil Kwiek <kamil.kwiek@gmail.com>
Closes: https://lore.kernel.org/netfilter/20260910190351.38f2aafd@devtravel.lan/
Assisted-by: LLM
Signed-off-by: Avinash Duduskar <avinash.duduskar@gmail.com>
---
Both new tests fail without the patch, nft aborts rather than returning, and
pass with it.  The rest of tests/shell is unchanged here: 498 ok, 10 skipped,
and 6 failures that are there with or without the patch.

Built with -DNDEBUG and ASan, both shapes above report a heap-use-after-free
before the patch and are clean after it, as is the whole of tests/shell (no
reports with ASAN_OPTIONS=log_path set).  With LSan, the undefine case leaks
the symbol before and nothing after.

The patch drops the assert from scope_release().  The same check still holds
in scope_free(), which runs once the cache has been released, so I can add it
there in a second patch if you would rather keep it somewhere.

 include/rule.h                                |  1 +
 src/expression.c                              |  3 +-
 src/rule.c                                    |  7 +--
 .../shell/testcases/nft-f/0033define_scope_0  | 22 ++++++++
 .../nft-f/0034define_scope_existing_1         | 25 +++++++++
 .../nft-f/dumps/0033define_scope_0.json-nft   | 53 +++++++++++++++++++
 .../nft-f/dumps/0033define_scope_0.nft        |  5 ++
 .../0034define_scope_existing_1.json-nft      | 18 +++++++
 .../dumps/0034define_scope_existing_1.nft     |  2 +
 9 files changed, 129 insertions(+), 7 deletions(-)
 create mode 100755 tests/shell/testcases/nft-f/0033define_scope_0
 create mode 100755 tests/shell/testcases/nft-f/0034define_scope_existing_1
 create mode 100644 tests/shell/testcases/nft-f/dumps/0033define_scope_0.json-nft
 create mode 100644 tests/shell/testcases/nft-f/dumps/0033define_scope_0.nft
 create mode 100644 tests/shell/testcases/nft-f/dumps/0034define_scope_existing_1.json-nft
 create mode 100644 tests/shell/testcases/nft-f/dumps/0034define_scope_existing_1.nft

diff --git a/include/rule.h b/include/rule.h
index 77023da2..7b7b7eb1 100644
--- a/include/rule.h
+++ b/include/rule.h
@@ -126,6 +126,7 @@ extern struct symbol *symbol_lookup(const struct scope *scope,
 struct symbol *symbol_lookup_fuzzy(const struct scope *scope,
 				   const char *identifier);
 struct symbol *symbol_get(const struct scope *scope, const char *identifier);
+void symbol_put(struct symbol *sym);
 
 enum table_flags {
 	TABLE_F_DORMANT		= (1 << 0),
diff --git a/src/expression.c b/src/expression.c
index 39221ba8..c450746a 100644
--- a/src/expression.c
+++ b/src/expression.c
@@ -352,8 +352,7 @@ static void variable_expr_clone(struct expr *new, const struct expr *expr)
 
 static void variable_expr_destroy(struct expr *expr)
 {
-	assert_refcount_safe(expr->sym->refcnt);
-	expr->sym->refcnt--;
+	symbol_put(expr->sym);
 }
 
 static const struct expr_ops variable_expr_ops = {
diff --git a/src/rule.c b/src/rule.c
index 85376e63..0ffe2ca1 100644
--- a/src/rule.c
+++ b/src/rule.c
@@ -576,11 +576,8 @@ void scope_release(const struct scope *scope)
 	struct symbol *sym, *next;
 
 	list_for_each_entry_safe(sym, next, &scope->symbols, list) {
-		assert(sym->refcnt == 1);
 		list_del(&sym->list);
-		free_const(sym->identifier);
-		expr_free(sym->expr);
-		free(sym);
+		symbol_put(sym);
 	}
 }
 
@@ -616,7 +613,7 @@ struct symbol *symbol_get(const struct scope *scope, const char *identifier)
 	return sym;
 }
 
-static void symbol_put(struct symbol *sym)
+void symbol_put(struct symbol *sym)
 {
 	assert_refcount_safe(sym->refcnt);
 	if (--sym->refcnt == 0) {
diff --git a/tests/shell/testcases/nft-f/0033define_scope_0 b/tests/shell/testcases/nft-f/0033define_scope_0
new file mode 100755
index 00000000..dcb258a7
--- /dev/null
+++ b/tests/shell/testcases/nft-f/0033define_scope_0
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# a define in table or chain scope holds a reference to the outer variable it
+# expands to.  The table is kept in the cache once the ruleset is committed, so
+# that reference outlives the release of the top-level scope.
+
+set -e
+
+RULESET="define outer = 111
+
+table inet t {
+	define t_unused = \$outer
+	define t_used = \$outer
+
+	chain c {
+		define c_unused = \$outer
+
+		meta iifgroup { \$t_used } counter
+	}
+}"
+
+$NFT -f - <<< "$RULESET"
diff --git a/tests/shell/testcases/nft-f/0034define_scope_existing_1 b/tests/shell/testcases/nft-f/0034define_scope_existing_1
new file mode 100755
index 00000000..a14a58dd
--- /dev/null
+++ b/tests/shell/testcases/nft-f/0034define_scope_existing_1
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Same defines, but the table already exists in the kernel, so table_evaluate()
+# takes no reference on the parsed one.  nft_cmd_expand() has moved the rules
+# into commands of their own, and cmd_free() frees the table and its scope while
+# a rule command still holds the variable expression.  The rule is rejected on
+# purpose, which is what leaves that expression unevaluated.
+#
+# The ruleset must be rejected with 1, not aborted.
+
+set -e
+
+$NFT add table inet t
+
+RULESET="table inet t {
+	define B = 111
+
+	chain c {
+		tcp dport 70000 meta iifgroup \$B counter
+	}
+}"
+
+rc=0
+$NFT -f - <<< "$RULESET" || rc=$?
+[ "$rc" -eq 1 ]
diff --git a/tests/shell/testcases/nft-f/dumps/0033define_scope_0.json-nft b/tests/shell/testcases/nft-f/dumps/0033define_scope_0.json-nft
new file mode 100644
index 00000000..0aa83c42
--- /dev/null
+++ b/tests/shell/testcases/nft-f/dumps/0033define_scope_0.json-nft
@@ -0,0 +1,53 @@
+{
+  "nftables": [
+    {
+      "metainfo": {
+        "version": "VERSION",
+        "release_name": "RELEASE_NAME",
+        "json_schema_version": 1
+      }
+    },
+    {
+      "table": {
+        "family": "inet",
+        "name": "t",
+        "handle": 0
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "t",
+        "name": "c",
+        "handle": 0
+      }
+    },
+    {
+      "rule": {
+        "family": "inet",
+        "table": "t",
+        "chain": "c",
+        "handle": 0,
+        "expr": [
+          {
+            "match": {
+              "op": "==",
+              "left": {
+                "meta": {
+                  "key": "iifgroup"
+                }
+              },
+              "right": 111
+            }
+          },
+          {
+            "counter": {
+              "packets": 0,
+              "bytes": 0
+            }
+          }
+        ]
+      }
+    }
+  ]
+}
diff --git a/tests/shell/testcases/nft-f/dumps/0033define_scope_0.nft b/tests/shell/testcases/nft-f/dumps/0033define_scope_0.nft
new file mode 100644
index 00000000..d206007a
--- /dev/null
+++ b/tests/shell/testcases/nft-f/dumps/0033define_scope_0.nft
@@ -0,0 +1,5 @@
+table inet t {
+	chain c {
+		iifgroup 111 counter packets 0 bytes 0
+	}
+}
diff --git a/tests/shell/testcases/nft-f/dumps/0034define_scope_existing_1.json-nft b/tests/shell/testcases/nft-f/dumps/0034define_scope_existing_1.json-nft
new file mode 100644
index 00000000..10372b0e
--- /dev/null
+++ b/tests/shell/testcases/nft-f/dumps/0034define_scope_existing_1.json-nft
@@ -0,0 +1,18 @@
+{
+  "nftables": [
+    {
+      "metainfo": {
+        "version": "VERSION",
+        "release_name": "RELEASE_NAME",
+        "json_schema_version": 1
+      }
+    },
+    {
+      "table": {
+        "family": "inet",
+        "name": "t",
+        "handle": 0
+      }
+    }
+  ]
+}
diff --git a/tests/shell/testcases/nft-f/dumps/0034define_scope_existing_1.nft b/tests/shell/testcases/nft-f/dumps/0034define_scope_existing_1.nft
new file mode 100644
index 00000000..17838bdf
--- /dev/null
+++ b/tests/shell/testcases/nft-f/dumps/0034define_scope_existing_1.nft
@@ -0,0 +1,2 @@
+table inet t {
+}

base-commit: fcef13a358a0755a0bd980910d47989b064a756d
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [PATCH nft] src: release scope symbols by reference count
  2026-09-11 19:20 [PATCH nft] src: release scope symbols by reference count Avinash Duduskar
@ 2026-09-11 20:38 ` Florian Westphal
  0 siblings, 0 replies; 2+ messages in thread
From: Florian Westphal @ 2026-09-11 20:38 UTC (permalink / raw)
  To: Avinash Duduskar; +Cc: netfilter-devel, Pablo Neira Ayuso, Kamil Kwiek

Avinash Duduskar <avinash.duduskar@gmail.com> wrote:
> nft -f aborts when a define in table or chain scope takes its value from
> another variable:
> 
>   define outer = 111

Applied, thanks.

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-09-11 20:38 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-11 19:20 [PATCH nft] src: release scope symbols by reference count Avinash Duduskar
2026-09-11 20:38 ` Florian Westphal

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.