BPF List
 help / color / mirror / Atom feed
From: Kumar Kartikeya Dwivedi <memxor@gmail.com>
To: bpf@vger.kernel.org
Cc: Alexei Starovoitov <ast@kernel.org>,
	Andrii Nakryiko <andrii@kernel.org>,
	Daniel Borkmann <daniel@iogearbox.net>,
	Eduard Zingerman <eddyz87@gmail.com>,
	Emil Tsalapatis <emil@etsalapatis.com>,
	Nicholas Carlini <npc@anthropic.com>,
	kkd@meta.com, kernel-team@meta.com
Subject: [PATCH bpf-next v2 2/2] selftests/bpf: Check local object ownership depth
Date: Mon, 14 Sep 2026 15:24:43 +0200	[thread overview]
Message-ID: <20260914132444.2564218-3-memxor@gmail.com> (raw)
In-Reply-To: <20260914132444.2564218-1-memxor@gmail.com>

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


  parent reply	other threads:[~2026-09-14 13:24 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
2026-09-14 14:16   ` [PATCH bpf-next v2 2/2] selftests/bpf: Check local object ownership depth bot+bpf-ci
2026-09-19  5:30 ` [PATCH bpf-next v2 0/2] Fix acyclic ownership checks patchwork-bot+netdevbpf

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260914132444.2564218-3-memxor@gmail.com \
    --to=memxor@gmail.com \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=eddyz87@gmail.com \
    --cc=emil@etsalapatis.com \
    --cc=kernel-team@meta.com \
    --cc=kkd@meta.com \
    --cc=npc@anthropic.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox