BPF List
 help / color / mirror / Atom feed
* [PATCH bpf-next v2 0/2] Fix acyclic ownership checks
@ 2026-09-14 13:24 Kumar Kartikeya Dwivedi
  2026-09-14 13:24 ` [PATCH bpf-next v2 1/2] bpf: Bound ownership depth through local kptrs and graph roots Kumar Kartikeya Dwivedi
                   ` (2 more replies)
  0 siblings, 3 replies; 5+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-09-14 13:24 UTC (permalink / raw)
  To: bpf
  Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Eduard Zingerman, Emil Tsalapatis, Nicholas Carlini, kkd,
	kernel-team

Bound and ensure acyclic ownership graphs for native data structures to
fix a bug reported by Nicholas. See commit logs and tests for details.

The existing list/rbtree rule already rejects graph-only cycles and bounds
those chains conservatively. It misses ownership through local referenced
kptrs, which can produce unbounded synchronous field destruction. Validate
all local ownership edges together, with an explicit depth bound, and allow
longer acyclic graph-only layouts within that bound.

Changelog:
----------
v1 -> v2
v1: https://lore.kernel.org/bpf/20260905090750.4064411-1-memxor@gmail.com/

 * Fold the graph-walk and local-kptr changes into one complete fix. (Alexei)
 * Explain why the original rule catches graph-only cycles, its three-type
   chain bound, and the missing local-kptr ownership edges. (Alexei)
 * Distinguish synchronous recursive field destruction from the deferred
   RCU freeing of object storage.
 * Add depth-boundary tests with child-first BTF ordering and a shared
   suffix reached with different remaining budgets.
 * Cover list and rbtree chains at the original three-type bound and at the
   new eight-type bound, including rejected over-limit cases.

Kumar Kartikeya Dwivedi (2):
  bpf: Bound ownership depth through local kptrs and graph roots
  selftests/bpf: Check local object ownership depth

 kernel/bpf/btf.c                              | 134 +++++---
 .../selftests/bpf/prog_tests/linked_list.c    |   4 +-
 .../bpf/prog_tests/local_kptr_ownership.c     | 296 ++++++++++++++++++
 3 files changed, 384 insertions(+), 50 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c


base-commit: a41c69c6ea14596cfd95978483166d4eff52435e
-- 
2.53.0


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

* [PATCH bpf-next v2 1/2] bpf: Bound ownership depth through local kptrs and graph roots
  2026-09-14 13:24 [PATCH bpf-next v2 0/2] Fix acyclic ownership checks Kumar Kartikeya Dwivedi
@ 2026-09-14 13:24 ` Kumar Kartikeya Dwivedi
  2026-09-14 13:24 ` [PATCH bpf-next v2 2/2] selftests/bpf: Check local object ownership depth Kumar Kartikeya Dwivedi
  2026-09-19  5:30 ` [PATCH bpf-next v2 0/2] Fix acyclic ownership checks patchwork-bot+netdevbpf
  2 siblings, 0 replies; 5+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-09-14 13:24 UTC (permalink / raw)
  To: bpf
  Cc: Nicholas Carlini, Alexei Starovoitov, Andrii Nakryiko,
	Daniel Borkmann, Eduard Zingerman, Emil Tsalapatis, kkd,
	kernel-team

Program-allocated objects can own other local objects through referenced
kptrs. bpf_obj_free_fields() follows those pointers through
__bpf_obj_drop_impl() synchronously, before the object storage is freed
through RCU. A self-referential local kptr type therefore permits arbitrarily
deep object chains, and dropping the head can exhaust the kernel stack.
Long acyclic type chains have the same problem.

btf_check_and_fixup_fields() still assumes referenced kptrs only point to
kernel types and checks ownership through list and rbtree roots only. Its
existing rule is sufficient for graph-only cycles: the target of each graph
edge must contain a node, so every type in a cycle has both a root and a
node. The rule rejects such a type owning another root, breaking every
cycle. It also limits graph-only chains to three types, or two if the first
type contains a node, and conservatively rejects longer acyclic chains.
The missing local-kptr edges, rather than a missed graph-only cycle, are the
bug introduced by support for bpf_kptr_xchg() into local kptrs.

Replace that restriction with one bounded ownership walk covering graph
roots and local referenced kptrs. Run it after all BTF records have been
fixed up, reject cycles and paths deeper than eight record-bearing types,
and cache each type's suffix depth while checking it against the remaining
budget. This also permits the longer acyclic graph-only layouts rejected
by the old rule; update their existing BTF tests accordingly.

Keep the bound independent of MAX_CALL_FRAMES because recursive destruction
can run below a BPF call chain. A plain local pointee without special-field
metadata adds only a final non-recursing drop. Non-owning kptrs and
kernel-BTF kptrs do not recurse through local records and remain outside the
walk. Include local percpu-kptr edges too, although allocation of percpu
objects with special fields is currently forbidden, so that relaxing that
restriction cannot bypass the ownership bound.

btf_check_and_fixup_fields() continues to initialize graph_root.value_rec,
including for separately allocated map records. The ownership relationships
belong to immutable program BTF and only need validation at BTF load time.

Fixes: b0966c724584 ("bpf: Support bpf_kptr_xchg into local kptr")
Reported-by: Nicholas Carlini <npc@anthropic.com>
Suggested-by: Nicholas Carlini <npc@anthropic.com>
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
 kernel/bpf/btf.c                              | 134 +++++++++++-------
 .../selftests/bpf/prog_tests/linked_list.c    |   4 +-
 2 files changed, 88 insertions(+), 50 deletions(-)

diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 7daf4c286c9b..92217a14b9d3 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -4284,13 +4284,10 @@ int btf_check_and_fixup_fields(const struct btf *btf, struct btf_record *rec)
 {
 	int i;
 
-	/* There are three types that signify ownership of some other type:
-	 *  kptr_ref, bpf_list_head, bpf_rb_root.
-	 * kptr_ref only supports storing kernel types, which can't store
-	 * references to program allocated local types.
-	 *
-	 * Hence we only need to ensure that bpf_{list_head,rb_root} ownership
-	 * does not form cycles.
+	/*
+	 * Check fields which require the complete BTF and initialize runtime
+	 * metadata. Ownership relationships are validated after every record has
+	 * been fixed up.
 	 */
 	if (IS_ERR_OR_NULL(rec) || !(rec->field_mask & (BPF_GRAPH_ROOT | BPF_UPTR)))
 		return 0;
@@ -4321,51 +4318,88 @@ int btf_check_and_fixup_fields(const struct btf *btf, struct btf_record *rec)
 		if (!meta)
 			return -EFAULT;
 		rec->fields[i].graph_root.value_rec = meta->record;
+	}
+	return 0;
+}
 
-		/* We need to set value_rec for all root types, but no need
-		 * to check ownership cycle for a type unless it's also a
-		 * node type.
-		 */
-		if (!(rec->field_mask & BPF_GRAPH_NODE))
+static int btf_owned_type_idx(const struct btf *btf, struct btf_struct_metas *tab,
+			      const struct btf_field *field)
+{
+	struct btf_struct_meta *meta;
+	u32 btf_id;
+
+	if (field->type & BPF_GRAPH_ROOT) {
+		btf_id = field->graph_root.value_btf_id;
+	} else if (field->type == BPF_KPTR_REF || field->type == BPF_KPTR_PERCPU) {
+		if (btf_is_kernel(field->kptr.btf))
+			return -ENOENT;
+		btf_id = field->kptr.btf_id;
+	} else {
+		return -ENOENT;
+	}
+
+	meta = btf_find_struct_meta(btf, btf_id);
+	if (!meta)
+		return field->type & BPF_GRAPH_ROOT ? -EFAULT : -ENOENT;
+	return meta - tab->types;
+}
+
+/*
+ * Each ownership edge adds kernel frames through bpf_obj_free_fields() and
+ * __bpf_obj_drop_impl(). Keep the bound deliberately small because object
+ * destruction can itself run below a BPF call chain. A final pointee without
+ * special fields is not present in the struct metadata table and adds only a
+ * non-recursing drop.
+ */
+#define BTF_MAX_OWNERSHIP_DEPTH 8
+
+static int btf_ownership_depth(const struct btf *btf,
+			       struct btf_struct_metas *tab, u8 *depth,
+			       int idx, int depth_left)
+{
+	const struct btf_record *rec = tab->types[idx].record;
+	int i, ret, max_depth = 0;
+
+	if (!depth_left)
+		return -ELOOP;
+	if (depth[idx])
+		goto done;
+
+	for (i = 0; i < rec->cnt; i++) {
+		ret = btf_owned_type_idx(btf, tab, &rec->fields[i]);
+		if (ret == -ENOENT)
 			continue;
+		if (ret < 0)
+			return ret;
+		ret = btf_ownership_depth(btf, tab, depth, ret, depth_left - 1);
+		if (ret < 0)
+			return ret;
+		max_depth = max(max_depth, ret);
+	}
+	depth[idx] = max_depth + 1;
+done:
+	return depth[idx] > depth_left ? -ELOOP : depth[idx];
+}
 
-		/* We need to ensure ownership acyclicity among all types. The
-		 * proper way to do it would be to topologically sort all BTF
-		 * IDs based on the ownership edges, since there can be multiple
-		 * bpf_{list_head,rb_node} in a type. Instead, we use the
-		 * following resaoning:
-		 *
-		 * - A type can only be owned by another type in user BTF if it
-		 *   has a bpf_{list,rb}_node. Let's call these node types.
-		 * - A type can only _own_ another type in user BTF if it has a
-		 *   bpf_{list_head,rb_root}. Let's call these root types.
-		 *
-		 * We ensure that if a type is both a root and node, its
-		 * element types cannot be root types.
-		 *
-		 * To ensure acyclicity:
-		 *
-		 * When A is an root type but not a node, its ownership
-		 * chain can be:
-		 *	A -> B -> C
-		 * Where:
-		 * - A is an root, e.g. has bpf_rb_root.
-		 * - B is both a root and node, e.g. has bpf_rb_node and
-		 *   bpf_list_head.
-		 * - C is only an root, e.g. has bpf_list_node
-		 *
-		 * When A is both a root and node, some other type already
-		 * owns it in the BTF domain, hence it can not own
-		 * another root type through any of the ownership edges.
-		 *	A -> B
-		 * Where:
-		 * - A is both an root and node.
-		 * - B is only an node.
-		 */
-		if (meta->record->field_mask & BPF_GRAPH_ROOT)
-			return -ELOOP;
+static int btf_check_ownership_depth(const struct btf *btf,
+				     struct btf_struct_metas *tab)
+{
+	u8 *depth;
+	int i, ret = 0;
+
+	depth = kvcalloc(tab->cnt, sizeof(*depth), GFP_KERNEL | __GFP_NOWARN);
+	if (!depth)
+		return -ENOMEM;
+
+	for (i = 0; i < tab->cnt; i++) {
+		ret = btf_ownership_depth(btf, tab, depth, i,
+					  BTF_MAX_OWNERSHIP_DEPTH);
+		if (ret < 0)
+			break;
+		ret = 0;
 	}
-	return 0;
+	kvfree(depth);
+	return ret;
 }
 
 static void __btf_struct_show(const struct btf *btf, const struct btf_type *t,
@@ -6060,6 +6094,10 @@ static struct btf *btf_parse(const union bpf_attr *attr, bpfptr_t uattr,
 			if (err < 0)
 				goto errout_meta;
 		}
+
+		err = btf_check_ownership_depth(btf, struct_meta_tab);
+		if (err < 0)
+			goto errout_meta;
 	}
 
 	err = bpf_log_attr_finalize(attr_log, &env->log);
diff --git a/tools/testing/selftests/bpf/prog_tests/linked_list.c b/tools/testing/selftests/bpf/prog_tests/linked_list.c
index c3d133c6a00d..52fabbee3dd5 100644
--- a/tools/testing/selftests/bpf/prog_tests/linked_list.c
+++ b/tools/testing/selftests/bpf/prog_tests/linked_list.c
@@ -714,7 +714,7 @@ static void test_btf(void)
 			break;
 
 		err = btf__load_into_kernel(btf);
-		ASSERT_EQ(err, -ELOOP, "check btf");
+		ASSERT_EQ(err, 0, "check btf");
 		btf__free(btf);
 		break;
 	}
@@ -773,7 +773,7 @@ static void test_btf(void)
 			break;
 
 		err = btf__load_into_kernel(btf);
-		ASSERT_EQ(err, -ELOOP, "check btf");
+		ASSERT_EQ(err, 0, "check btf");
 		btf__free(btf);
 		break;
 	}
-- 
2.53.0


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

* [PATCH bpf-next v2 2/2] selftests/bpf: Check local object ownership depth
  2026-09-14 13:24 [PATCH bpf-next v2 0/2] Fix acyclic ownership checks Kumar Kartikeya Dwivedi
  2026-09-14 13:24 ` [PATCH bpf-next v2 1/2] bpf: Bound ownership depth through local kptrs and graph roots Kumar Kartikeya Dwivedi
@ 2026-09-14 13:24 ` Kumar Kartikeya Dwivedi
  2026-09-14 14:16   ` bot+bpf-ci
  2026-09-19  5:30 ` [PATCH bpf-next v2 0/2] Fix acyclic ownership checks patchwork-bot+netdevbpf
  2 siblings, 1 reply; 5+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-09-14 13:24 UTC (permalink / raw)
  To: bpf
  Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Eduard Zingerman, Emil Tsalapatis, Nicholas Carlini, kkd,
	kernel-team

Build raw program BTF records to exercise local object ownership without
constructing a runtime chain deep enough to threaten the kernel stack.

Cover referenced-kptr and percpu-kptr self-cycles, a two-type cycle, and a
cycle mixing a graph root with a referenced kptr. The existing graph-only
check accepts these local-kptr cycles and over-limit kptr chains; the fix
rejects them with -ELOOP.

Pin the eight-record depth boundary with a terminal plain object. Exercise
both parent-first and child-first BTF orders, and a shared suffix reached
first through a shorter path. These cases require cached suffix depths to
be checked against the remaining depth budget on each path.

Check list and rbtree chains of three, four, eight, and nine types. This
covers the old graph-only depth boundary and the new explicit bound. The
existing linked-list BTF tests still reject pure graph cycles and now accept
the longer acyclic layouts previously rejected by the conservative rule.

Although bpf_percpu_obj_new() currently rejects types with special fields,
require the percpu cycle to fail at BTF load so future support cannot bypass
the ownership bound. Keep a positive control for a non-owning kptr, which
remains outside the ownership graph.

Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
 .../bpf/prog_tests/local_kptr_ownership.c     | 296 ++++++++++++++++++
 1 file changed, 296 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c

diff --git a/tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c b/tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c
new file mode 100644
index 000000000000..a487aa68f2ee
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c
@@ -0,0 +1,296 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <bpf/btf.h>
+#include <linux/btf.h>
+#include <test_progs.h>
+
+#define SPIN_LOCK 2
+#define LIST_HEAD 3
+#define LIST_NODE 4
+/* Keep in sync with BTF_MAX_OWNERSHIP_DEPTH. */
+#define MAX_OWNERSHIP_DEPTH 8
+
+static struct btf *init_btf(void)
+{
+	struct btf *btf;
+	int id;
+
+	btf = btf__new_empty();
+	if (!ASSERT_OK_PTR(btf, "btf__new_empty"))
+		return NULL;
+	id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED);
+	if (!ASSERT_EQ(id, 1, "btf__add_int"))
+		goto err_out;
+	id = btf__add_struct(btf, "bpf_spin_lock", 4);
+	if (!ASSERT_EQ(id, SPIN_LOCK, "btf__add_struct bpf_spin_lock"))
+		goto err_out;
+	id = btf__add_struct(btf, "bpf_list_head", 16);
+	if (!ASSERT_EQ(id, LIST_HEAD, "btf__add_struct bpf_list_head"))
+		goto err_out;
+	id = btf__add_struct(btf, "bpf_list_node", 24);
+	if (!ASSERT_EQ(id, LIST_NODE, "btf__add_struct bpf_list_node"))
+		goto err_out;
+	return btf;
+
+err_out:
+	btf__free(btf);
+	return NULL;
+}
+
+static int add_local_kptr(struct btf *btf, int pointee_id, const char *tag)
+{
+	int id;
+
+	id = btf__add_type_tag(btf, tag, pointee_id);
+	if (!ASSERT_GT(id, 0, "btf__add_type_tag"))
+		return id;
+	id = btf__add_ptr(btf, id);
+	ASSERT_GT(id, 0, "btf__add_ptr");
+	return id;
+}
+
+static void test_self_cycle(const char *tag, int expected_err)
+{
+	struct btf *btf;
+	int id, err;
+
+	btf = init_btf();
+	if (!ASSERT_OK_PTR(btf, "init_btf"))
+		return;
+	id = add_local_kptr(btf, 7, tag);
+	if (id <= 0)
+		goto out;
+	id = btf__add_struct(btf, "self_cycle", 8);
+	if (!ASSERT_EQ(id, 7, "btf__add_struct self_cycle"))
+		goto out;
+	err = btf__add_field(btf, "next", 6, 0, 0);
+	if (!ASSERT_OK(err, "btf__add_field self_cycle::next"))
+		goto out;
+
+	err = btf__load_into_kernel(btf);
+	ASSERT_EQ(err, expected_err, "check btf");
+out:
+	btf__free(btf);
+}
+
+static void test_aba_cycle(void)
+{
+	struct btf *btf;
+	int id, err;
+
+	btf = init_btf();
+	if (!ASSERT_OK_PTR(btf, "init_btf"))
+		return;
+	id = add_local_kptr(btf, 10, "kptr");
+	if (id <= 0)
+		goto out;
+	id = add_local_kptr(btf, 9, "kptr");
+	if (id <= 0)
+		goto out;
+	id = btf__add_struct(btf, "cycle_a", 8);
+	if (!ASSERT_EQ(id, 9, "btf__add_struct cycle_a"))
+		goto out;
+	err = btf__add_field(btf, "b", 6, 0, 0);
+	if (!ASSERT_OK(err, "btf__add_field cycle_a::b"))
+		goto out;
+	id = btf__add_struct(btf, "cycle_b", 8);
+	if (!ASSERT_EQ(id, 10, "btf__add_struct cycle_b"))
+		goto out;
+	err = btf__add_field(btf, "a", 8, 0, 0);
+	if (!ASSERT_OK(err, "btf__add_field cycle_b::a"))
+		goto out;
+
+	err = btf__load_into_kernel(btf);
+	ASSERT_EQ(err, -ELOOP, "check btf");
+out:
+	btf__free(btf);
+}
+
+static void test_mixed_cycle(void)
+{
+	struct btf *btf;
+	int id, err;
+
+	btf = init_btf();
+	if (!ASSERT_OK_PTR(btf, "init_btf"))
+		return;
+	id = add_local_kptr(btf, 7, "kptr");
+	if (id <= 0)
+		goto out;
+	id = btf__add_struct(btf, "mixed_owner", 20);
+	if (!ASSERT_EQ(id, 7, "btf__add_struct mixed_owner"))
+		goto out;
+	err = btf__add_field(btf, "root", LIST_HEAD, 0, 0);
+	if (!ASSERT_OK(err, "btf__add_field mixed_owner::root"))
+		goto out;
+	err = btf__add_field(btf, "lock", SPIN_LOCK, 128, 0);
+	if (!ASSERT_OK(err, "btf__add_field mixed_owner::lock"))
+		goto out;
+	id = btf__add_decl_tag(btf, "contains:mixed_node:node", 7, 0);
+	if (!ASSERT_EQ(id, 8, "btf__add_decl_tag mixed_owner"))
+		goto out;
+	id = btf__add_struct(btf, "mixed_node", 32);
+	if (!ASSERT_EQ(id, 9, "btf__add_struct mixed_node"))
+		goto out;
+	err = btf__add_field(btf, "node", LIST_NODE, 0, 0);
+	if (!ASSERT_OK(err, "btf__add_field mixed_node::node"))
+		goto out;
+	err = btf__add_field(btf, "owner", 6, 192, 0);
+	if (!ASSERT_OK(err, "btf__add_field mixed_node::owner"))
+		goto out;
+
+	err = btf__load_into_kernel(btf);
+	ASSERT_EQ(err, -ELOOP, "check btf");
+out:
+	btf__free(btf);
+}
+
+static void test_acyclic_depth(int depth, bool child_first, bool shared_suffix, int expected_err)
+{
+	int ptr_id[MAX_OWNERSHIP_DEPTH + 1];
+	int first_struct_id;
+	struct btf *btf;
+	int id, err, i, n, pointee_id;
+
+	btf = init_btf();
+	if (!ASSERT_OK_PTR(btf, "init_btf"))
+		return;
+	first_struct_id = 5 + 2 * depth;
+	for (i = 0; i < depth; i++) {
+		if (i == depth - 1)
+			pointee_id = first_struct_id + depth;
+		else
+			pointee_id = first_struct_id + (child_first ? depth - 2 - i : i + 1);
+		ptr_id[i] = add_local_kptr(btf, pointee_id, "kptr");
+		if (ptr_id[i] <= 0)
+			goto out;
+	}
+	for (n = 0; n < depth; n++) {
+		char name[32];
+		int offset = 0;
+
+		i = child_first ? depth - 1 - n : n;
+		snprintf(name, sizeof(name), "owner_%d", i);
+		id = btf__add_struct(btf, name, shared_suffix && !i ? 16 : 8);
+		if (!ASSERT_EQ(id, first_struct_id + n, "btf__add_struct owner"))
+			goto out;
+		if (shared_suffix && !i) {
+			/*
+			 * Visit the shared suffix through the shorter path before
+			 * reaching it again with less remaining depth.
+			 */
+			err = btf__add_field(btf, "suffix", ptr_id[1], 0, 0);
+			if (!ASSERT_OK(err, "btf__add_field owner::suffix"))
+				goto out;
+			offset = 64;
+		}
+		err = btf__add_field(btf, "next", ptr_id[i], offset, 0);
+		if (!ASSERT_OK(err, "btf__add_field owner::next"))
+			goto out;
+	}
+	id = btf__add_struct(btf, "plain_leaf", 4);
+	if (!ASSERT_EQ(id, first_struct_id + depth, "btf__add_struct plain_leaf"))
+		goto out;
+
+	err = btf__load_into_kernel(btf);
+	ASSERT_EQ(err, expected_err, "check btf");
+out:
+	btf__free(btf);
+}
+
+static void test_graph_depth(bool rbtree, int depth, int expected_err)
+{
+	int root_type = LIST_HEAD, node_type = LIST_NODE, node_size = 24;
+	int id, err, i, lock_off, root_off, size;
+	struct btf *btf;
+
+	btf = init_btf();
+	if (!ASSERT_OK_PTR(btf, "init_btf"))
+		return;
+	if (rbtree) {
+		root_type = btf__add_struct(btf, "bpf_rb_root", 16);
+		if (!ASSERT_GT(root_type, 0, "btf__add_struct bpf_rb_root"))
+			goto out;
+		node_type = btf__add_struct(btf, "bpf_rb_node", 32);
+		if (!ASSERT_GT(node_type, 0, "btf__add_struct bpf_rb_node"))
+			goto out;
+		node_size = 32;
+	}
+
+	for (i = 0; i < depth; i++) {
+		char name[32], tag[64];
+
+		lock_off = i ? node_size : 0;
+		root_off = lock_off + 8;
+		size = i == depth - 1 ? node_size : root_off + 16;
+		snprintf(name, sizeof(name), "graph_owner_%d", i);
+		id = btf__add_struct(btf, name, size);
+		if (!ASSERT_GT(id, 0, "btf__add_struct graph_owner"))
+			goto out;
+		if (i) {
+			err = btf__add_field(btf, "node", node_type, 0, 0);
+			if (!ASSERT_OK(err, "btf__add_field graph_owner::node"))
+				goto out;
+		}
+		if (i == depth - 1)
+			continue;
+		err = btf__add_field(btf, "lock", SPIN_LOCK, lock_off * 8, 0);
+		if (!ASSERT_OK(err, "btf__add_field graph_owner::lock"))
+			goto out;
+		err = btf__add_field(btf, "root", root_type, root_off * 8, 0);
+		if (!ASSERT_OK(err, "btf__add_field graph_owner::root"))
+			goto out;
+		snprintf(tag, sizeof(tag), "contains:graph_owner_%d:node", i + 1);
+		err = btf__add_decl_tag(btf, tag, id, i ? 2 : 1);
+		if (!ASSERT_GT(err, 0, "btf__add_decl_tag graph_owner"))
+			goto out;
+	}
+
+	err = btf__load_into_kernel(btf);
+	ASSERT_EQ(err, expected_err, "check btf");
+out:
+	btf__free(btf);
+}
+
+void test_local_kptr_ownership(void)
+{
+	if (test__start_subtest("self_cycle"))
+		test_self_cycle("kptr", -ELOOP);
+	if (test__start_subtest("untrusted_self_cycle"))
+		test_self_cycle("kptr_untrusted", 0);
+	if (test__start_subtest("percpu_self_cycle"))
+		test_self_cycle("percpu_kptr", -ELOOP);
+	if (test__start_subtest("ABA_cycle"))
+		test_aba_cycle();
+	if (test__start_subtest("mixed_graph_root_cycle"))
+		test_mixed_cycle();
+	if (test__start_subtest("max_acyclic"))
+		test_acyclic_depth(MAX_OWNERSHIP_DEPTH, false, false, 0);
+	if (test__start_subtest("too_deep_acyclic"))
+		test_acyclic_depth(MAX_OWNERSHIP_DEPTH + 1, false, false, -ELOOP);
+	if (test__start_subtest("max_acyclic_child_first"))
+		test_acyclic_depth(MAX_OWNERSHIP_DEPTH, true, false, 0);
+	if (test__start_subtest("too_deep_acyclic_child_first"))
+		test_acyclic_depth(MAX_OWNERSHIP_DEPTH + 1, true, false, -ELOOP);
+	if (test__start_subtest("max_acyclic_shared_suffix"))
+		test_acyclic_depth(MAX_OWNERSHIP_DEPTH, false, true, 0);
+	if (test__start_subtest("too_deep_acyclic_shared_suffix"))
+		test_acyclic_depth(MAX_OWNERSHIP_DEPTH + 1, false, true, -ELOOP);
+	if (test__start_subtest("list_three_types"))
+		test_graph_depth(false, 3, 0);
+	if (test__start_subtest("list_four_types"))
+		test_graph_depth(false, 4, 0);
+	if (test__start_subtest("list_max_depth"))
+		test_graph_depth(false, MAX_OWNERSHIP_DEPTH, 0);
+	if (test__start_subtest("list_too_deep"))
+		test_graph_depth(false, MAX_OWNERSHIP_DEPTH + 1, -ELOOP);
+	if (test__start_subtest("rbtree_three_types"))
+		test_graph_depth(true, 3, 0);
+	if (test__start_subtest("rbtree_four_types"))
+		test_graph_depth(true, 4, 0);
+	if (test__start_subtest("rbtree_max_depth"))
+		test_graph_depth(true, MAX_OWNERSHIP_DEPTH, 0);
+	if (test__start_subtest("rbtree_too_deep"))
+		test_graph_depth(true, MAX_OWNERSHIP_DEPTH + 1, -ELOOP);
+}
-- 
2.53.0


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

* Re: [PATCH bpf-next v2 2/2] selftests/bpf: Check local object ownership depth
  2026-09-14 13:24 ` [PATCH bpf-next v2 2/2] selftests/bpf: Check local object ownership depth Kumar Kartikeya Dwivedi
@ 2026-09-14 14:16   ` bot+bpf-ci
  0 siblings, 0 replies; 5+ messages in thread
From: bot+bpf-ci @ 2026-09-14 14:16 UTC (permalink / raw)
  To: memxor, bpf
  Cc: ast, andrii, daniel, eddyz87, emil, npc, kkd, kernel-team, ast,
	andrii, daniel, martin.lau, eddyz87, yonghong.song, mason,
	ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 6314 bytes --]

> diff --git a/tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c b/tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c
> new file mode 100644
> index 000000000000..a487aa68f2ee
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/local_kptr_ownership.c
> @@ -0,0 +1,296 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
> +
> +#include <bpf/btf.h>
> +#include <linux/btf.h>
> +#include <test_progs.h>
> +
> +#define SPIN_LOCK 2
> +#define LIST_HEAD 3
> +#define LIST_NODE 4
> +/* Keep in sync with BTF_MAX_OWNERSHIP_DEPTH. */
> +#define MAX_OWNERSHIP_DEPTH 8
> +
> +static struct btf *init_btf(void)
> +{
> +	struct btf *btf;
> +	int id;
> +
> +	btf = btf__new_empty();
> +	if (!ASSERT_OK_PTR(btf, "btf__new_empty"))
> +		return NULL;
> +	id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED);
> +	if (!ASSERT_EQ(id, 1, "btf__add_int"))
> +		goto err_out;
> +	id = btf__add_struct(btf, "bpf_spin_lock", 4);
> +	if (!ASSERT_EQ(id, SPIN_LOCK, "btf__add_struct bpf_spin_lock"))
> +		goto err_out;
> +	id = btf__add_struct(btf, "bpf_list_head", 16);
> +	if (!ASSERT_EQ(id, LIST_HEAD, "btf__add_struct bpf_list_head"))
> +		goto err_out;
> +	id = btf__add_struct(btf, "bpf_list_node", 24);
> +	if (!ASSERT_EQ(id, LIST_NODE, "btf__add_struct bpf_list_node"))
> +		goto err_out;
> +	return btf;
> +
> +err_out:
> +	btf__free(btf);
> +	return NULL;
> +}

This isn't a bug, but init_btf() and the SPIN_LOCK/LIST_HEAD/LIST_NODE id
macros are a line-for-line copy of prog_tests/linked_list.c. Both copies
encode the same fixed id-to-type mapping, so any future change to that
prologue has to be made in two places or the two files silently disagree
about which id means what.

Would it be worth moving init_btf() and the SPIN_LOCK/LIST_HEAD/LIST_NODE
ids into test_btf.h (which linked_list.c already includes) so the two
BTF-construction tests share one prologue?

> +
> +static int add_local_kptr(struct btf *btf, int pointee_id, const char *tag)
> +{
> +	int id;
> +
> +	id = btf__add_type_tag(btf, tag, pointee_id);
> +	if (!ASSERT_GT(id, 0, "btf__add_type_tag"))
> +		return id;
> +	id = btf__add_ptr(btf, id);
> +	ASSERT_GT(id, 0, "btf__add_ptr");
> +	return id;
> +}
> +
> +static void test_self_cycle(const char *tag, int expected_err)
> +{
> +	struct btf *btf;
> +	int id, err;
> +
> +	btf = init_btf();
> +	if (!ASSERT_OK_PTR(btf, "init_btf"))
> +		return;
> +	id = add_local_kptr(btf, 7, tag);
> +	if (id <= 0)
> +		goto out;
> +	id = btf__add_struct(btf, "self_cycle", 8);
> +	if (!ASSERT_EQ(id, 7, "btf__add_struct self_cycle"))
> +		goto out;
> +	err = btf__add_field(btf, "next", 6, 0, 0);
                                          ^
This isn't a bug, but add_local_kptr() returns the id of the pointer type
it just created. test_self_cycle(), test_aba_cycle() and test_mixed_cycle()
use the return value only for the id <= 0 error check and then hardcode
that same id as a literal in btf__add_field() (6 in test_self_cycle and
test_mixed_cycle; 6 and 8 in test_aba_cycle). The literals are correct
today only because init_btf() ends at id 4 and add_local_kptr() adds
exactly two types. test_acyclic_depth() in this same file keeps the
returned ids in ptr_id[], so the two styles sit side by side.

Could the cycle tests keep the value add_local_kptr() returns (as
test_acyclic_depth already does) instead of re-spelling it as 6/8, so the
field type cannot drift from the pointer that was actually added?

> +	if (!ASSERT_OK(err, "btf__add_field self_cycle::next"))
> +		goto out;
> +
> +	err = btf__load_into_kernel(btf);
> +	ASSERT_EQ(err, expected_err, "check btf");
> +out:
> +	btf__free(btf);
> +}

[ ... ]

> +static void test_graph_depth(bool rbtree, int depth, int expected_err)
> +{
> +	int root_type = LIST_HEAD, node_type = LIST_NODE, node_size = 24;
> +	int id, err, i, lock_off, root_off, size;
> +	struct btf *btf;
> +
> +	btf = init_btf();
> +	if (!ASSERT_OK_PTR(btf, "init_btf"))
> +		return;
> +	if (rbtree) {
> +		root_type = btf__add_struct(btf, "bpf_rb_root", 16);
> +		if (!ASSERT_GT(root_type, 0, "btf__add_struct bpf_rb_root"))
> +			goto out;
> +		node_type = btf__add_struct(btf, "bpf_rb_node", 32);
> +		if (!ASSERT_GT(node_type, 0, "btf__add_struct bpf_rb_node"))
> +			goto out;
> +		node_size = 32;
> +	}
> +
> +	for (i = 0; i < depth; i++) {
> +		char name[32], tag[64];
> +
> +		lock_off = i ? node_size : 0;
> +		root_off = lock_off + 8;
> +		size = i == depth - 1 ? node_size : root_off + 16;
> +		snprintf(name, sizeof(name), "graph_owner_%d", i);
> +		id = btf__add_struct(btf, name, size);
> +		if (!ASSERT_GT(id, 0, "btf__add_struct graph_owner"))
> +			goto out;
> +		if (i) {
> +			err = btf__add_field(btf, "node", node_type, 0, 0);
> +			if (!ASSERT_OK(err, "btf__add_field graph_owner::node"))
> +				goto out;
> +		}
> +		if (i == depth - 1)
> +			continue;
> +		err = btf__add_field(btf, "lock", SPIN_LOCK, lock_off * 8, 0);
> +		if (!ASSERT_OK(err, "btf__add_field graph_owner::lock"))
> +			goto out;
> +		err = btf__add_field(btf, "root", root_type, root_off * 8, 0);
> +		if (!ASSERT_OK(err, "btf__add_field graph_owner::root"))
> +			goto out;
> +		snprintf(tag, sizeof(tag), "contains:graph_owner_%d:node", i + 1);
> +		err = btf__add_decl_tag(btf, tag, id, i ? 2 : 1);
                ^^^

This isn't a bug, but btf__add_decl_tag() returns a BTF type id, yet here
it is stored in err and checked with ASSERT_GT(err, 0). The same call in
test_mixed_cycle() uses id (id = btf__add_decl_tag(...); ASSERT_EQ(id, 8,
...)). id is already declared in test_graph_depth and every other
btf__add_* id in the function goes through it, so this one assignment
reads like an error code when it is not.

Since id is already in scope here, would using it for the decl-tag id keep
the id/err split consistent with the rest of the file?

> +		if (!ASSERT_GT(err, 0, "btf__add_decl_tag graph_owner"))
> +			goto out;
> +	}
> +
> +	err = btf__load_into_kernel(btf);
> +	ASSERT_EQ(err, expected_err, "check btf");
> +out:
> +	btf__free(btf);
> +}

[ ... ]


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34850327177

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

* Re: [PATCH bpf-next v2 0/2] Fix acyclic ownership checks
  2026-09-14 13:24 [PATCH bpf-next v2 0/2] Fix acyclic ownership checks Kumar Kartikeya Dwivedi
  2026-09-14 13:24 ` [PATCH bpf-next v2 1/2] bpf: Bound ownership depth through local kptrs and graph roots Kumar Kartikeya Dwivedi
  2026-09-14 13:24 ` [PATCH bpf-next v2 2/2] selftests/bpf: Check local object ownership depth Kumar Kartikeya Dwivedi
@ 2026-09-19  5:30 ` patchwork-bot+netdevbpf
  2 siblings, 0 replies; 5+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-09-19  5:30 UTC (permalink / raw)
  To: Kumar Kartikeya Dwivedi
  Cc: bpf, ast, andrii, daniel, eddyz87, emil, npc, kkd, kernel-team

Hello:

This series was applied to bpf/bpf.git (master)
by Alexei Starovoitov <ast@kernel.org>:

On Mon, 14 Sep 2026 15:24:41 +0200 you wrote:
> Bound and ensure acyclic ownership graphs for native data structures to
> fix a bug reported by Nicholas. See commit logs and tests for details.
> 
> The existing list/rbtree rule already rejects graph-only cycles and bounds
> those chains conservatively. It misses ownership through local referenced
> kptrs, which can produce unbounded synchronous field destruction. Validate
> all local ownership edges together, with an explicit depth bound, and allow
> longer acyclic graph-only layouts within that bound.
> 
> [...]

Here is the summary with links:
  - [bpf-next,v2,1/2] bpf: Bound ownership depth through local kptrs and graph roots
    https://git.kernel.org/bpf/bpf/c/bfc888f04588
  - [bpf-next,v2,2/2] selftests/bpf: Check local object ownership depth
    https://git.kernel.org/bpf/bpf/c/0288ed67482b

You are awesome, thank you!
-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html



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

end of thread, other threads:[~2026-09-19  5:31 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-14 13:24 [PATCH bpf-next v2 0/2] Fix acyclic ownership checks Kumar Kartikeya Dwivedi
2026-09-14 13:24 ` [PATCH bpf-next v2 1/2] bpf: Bound ownership depth through local kptrs and graph roots Kumar Kartikeya Dwivedi
2026-09-14 13:24 ` [PATCH bpf-next v2 2/2] selftests/bpf: Check local object ownership depth Kumar Kartikeya Dwivedi
2026-09-14 14:16   ` bot+bpf-ci
2026-09-19  5:30 ` [PATCH bpf-next v2 0/2] Fix acyclic ownership checks patchwork-bot+netdevbpf

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox