From: sashiko-bot@kernel.org
To: "Jay Wang" <wanjay@amazon.com>,
bpf@vger.kernel.org, "Alexei Starovoitov" <ast@kernel.org>,
"Daniel Borkmann" <daniel@iogearbox.net>,
"Andrii Nakryiko" <andrii@kernel.org>,
"Eduard Zingerman" <eddyz87@gmail.com>,
"Kumar Kartikeya Dwivedi" <memxor@gmail.com>
Cc: "Ihor Solodrai" <ihor.solodrai@linux.dev>,
"Alan Maguire" <almagui@kernel.org>,
"Martin KaFai Lau" <martin.lau@linux.dev>,
"Yonghong Song" <yonghong.song@linux.dev>,
"Jiri Olsa" <jolsa@kernel.org>,
"Steven Rostedt" <rostedt@goodmis.org>,
"Masami Hiramatsu" <mhiramat@kernel.org>,
"Mathieu Desnoyers" <mathieu.desnoyers@efficios.com>,
linux-trace-kernel@vger.kernel.org,
"Nathan Chancellor" <nathan@kernel.org>,
"Nicolas Schier" <nsc@kernel.org>,
linux-kbuild@vger.kernel.org, "Arnd Bergmann" <arnd@arndb.de>,
x86@kernel.org, "Hazem Mohamed Abuelfotoh" <abuehaze@amazon.com>,
"Bjoern Doebel" <doebel@amazon.de>,
"Martin Pohlack" <mpohlack@amazon.de>,
jay.wang.upstream@gmail.com
Subject: Re: [PATCH bpf-next 10/13] bpf: defer kfunc and struct_ops registrations until first BTF use
Date: Sat, 31 Oct 2026 13:38:17 +0000 [thread overview]
Message-ID: <sashiko-outbox-191982@kernel.org> (raw)
In-Reply-To: <20261031130959.23472-11-wanjay@amazon.com>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] bpf: propagate st_ops->init() error in bpf_struct_ops_desc_init()
- [Medium] bpf: premature btf_idr publication of vmlinux BTF before btf_apply_deferred()
- [Medium] bpf: missing bpf_get_btf_vmlinux() in cgroup_bpf_prog_query()
--
commit ee51b32030c948007ccf4ba129182f790083514d
Author: Jay Wang <wanjay@amazon.com>
bpf: defer kfunc and struct_ops registrations until first BTF use
When CONFIG_DEBUG_INFO_BTF_LAZY is enabled, this commit queues kfunc set,
dtor kfunc, and struct_ops registrations made before vmlinux BTF is parsed
and replays them synchronously inside bpf_get_btf_vmlinux() before publishing
btf_vmlinux. It also triggers vmlinux BTF parsing from btf_get_fd_by_id() and
BPF_BTF_GET_NEXT_ID so kernel BTF IDs are allocated before IDR lookups.
> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> index a7bb780743fb2..a5b1cd63a1e43 100644
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c
> @@ -6930,17 +6930,54 @@ struct btf *btf_parse_vmlinux(void)
> }
>
> static DEFINE_MUTEX(btf_vmlinux_lock);
> +/* CONFIG_DEBUG_INFO_BTF_LAZY: the task applying deferred work, and its BTF */
> +static struct task_struct *btf_vmlinux_builder;
> +static struct btf *btf_vmlinux_new;
> +static int btf_apply_deferred(void);
> +static void btf_drop_all_deferred(int err);
> +
> +static bool btf_vmlinux_building(void)
> +{
> + return in_task() && READ_ONCE(btf_vmlinux_builder) == current;
> +}
>
> struct btf *bpf_get_btf_vmlinux(void)
> {
> /* Pairs with the smp_store_release() on the parse path below. */
> struct btf *btf = smp_load_acquire(&btf_vmlinux);
>
> - if (!btf && IS_ENABLED(CONFIG_DEBUG_INFO_BTF)) {
> - mutex_lock(&btf_vmlinux_lock);
> - btf = btf_vmlinux;
> - if (!btf) {
> - btf = btf_parse_vmlinux();
> + /* with CONFIG_DEBUG_INFO_BTF_LAZY any call may be the one that parses */
> + if (IS_ENABLED(CONFIG_DEBUG_INFO_BTF_LAZY))
> + might_sleep();
> + if (btf || !IS_ENABLED(CONFIG_DEBUG_INFO_BTF))
> + return btf;
> + /* the registrations btf_apply_deferred() applies see the new BTF */
> + if (btf_vmlinux_building())
> + return btf_vmlinux_new;
> +
> + mutex_lock(&btf_vmlinux_lock);
> + btf = btf_vmlinux;
> + if (!btf) {
> + btf = btf_parse_vmlinux();
> + if (IS_ENABLED(CONFIG_DEBUG_INFO_BTF_LAZY) && !IS_ERR(btf)) {
> + int err;
> +
> + btf_vmlinux_new = btf;
> + WRITE_ONCE(btf_vmlinux_builder, current);
> + err = btf_apply_deferred();
> + WRITE_ONCE(btf_vmlinux_builder, NULL);
> + btf_vmlinux_new = NULL;
> + if (err) {
> + /* nothing saw it: drop it with what was applied to it */
> + bpf_ctx_convert.t = NULL;
> + btf_put(btf);
> + btf = ERR_PTR(err);
> + }
> + }
> + /* a lazy parse that ran out of memory is retried by the next caller */
> + if (!IS_ENABLED(CONFIG_DEBUG_INFO_BTF_LAZY) || btf != ERR_PTR(-ENOMEM)) {
> + if (IS_ERR(btf))
> + btf_drop_all_deferred(PTR_ERR(btf));
> /*
> * Order the parsed BTF contents and the globals the
> * parse populated (e.g. bpf_ctx_convert.t) before
> @@ -6949,8 +6986,8 @@ struct btf *bpf_get_btf_vmlinux(void)
> */
> smp_store_release(&btf_vmlinux, btf);
> }
> - mutex_unlock(&btf_vmlinux_lock);
> }
> + mutex_unlock(&btf_vmlinux_lock);
> return btf;
> }
>
> @@ -9302,6 +9339,10 @@ int btf_get_fd_by_id(u32 id)
> struct btf *btf;
> int fd;
>
> + /* kernel BTFs get their ids while parsed, before they are published */
> + if (IS_ENABLED(CONFIG_DEBUG_INFO_BTF_LAZY))
> + bpf_get_btf_vmlinux();
> +
> rcu_read_lock();
> btf = idr_find(&btf_idr, id);
> if (!btf || !refcount_inc_not_zero(&btf->refcnt))
[Severity: Medium]
Can a concurrent caller of btf_get_fd_by_id() or __sys_bpf() (for
BPF_BTF_GET_NEXT_ID) observe and pin btf_vmlinux_new via btf_idr while
btf_apply_deferred() is still mutating it or rolling it back on -ENOMEM?
In kernel/bpf/btf.c, btf_parse_vmlinux() allocates the IDR entry before
btf_apply_deferred() runs:
btf_parse_vmlinux()
err = btf_alloc_id(btf);
Both btf_get_fd_by_id() and the BPF_BTF_GET_NEXT_ID case in __sys_bpf()
call bpf_get_btf_vmlinux() and ignore its return value before querying
btf_idr without holding btf_vmlinux_lock.
If task A calls btf_get_fd_by_id() and its bpf_get_btf_vmlinux() attempt
fails with ERR_PTR(-ENOMEM) (for example, if task A is killed or hits a
memory limit during vmalloc()), bpf_get_btf_vmlinux() leaves btf_vmlinux
as NULL and releases btf_vmlinux_lock. Because btf_get_fd_by_id() ignores
the error, task A proceeds to idr_find(&btf_idr, id).
If task B concurrently acquires btf_vmlinux_lock, runs btf_parse_vmlinux()
(publishing btf_vmlinux_new into btf_idr via btf_alloc_id()), and enters
btf_apply_deferred(), task A can find task B's unpublished btf_vmlinux_new
in btf_idr and increment btf->refcnt from 1 to 2 via
refcount_inc_not_zero().
This exposes btf_vmlinux_new to userspace while btf_apply_deferred() is
still mutating kfunc_set_tab, dtor_kfunc_tab, and struct_ops_tab via
krealloc() and sort(). Furthermore, if task B's btf_apply_deferred() then
fails with -ENOMEM, the rollback path in bpf_get_btf_vmlinux() assumes
nothing else saw btf:
if (err) {
/* nothing saw it: drop it with what was applied to it */
bpf_ctx_convert.t = NULL;
btf_put(btf);
btf = ERR_PTR(err);
}
Because task A holds an extra reference, btf_put(btf) will not drop
btf->refcnt to zero or call btf_free_id(btf), leaving the aborted vmlinux
BTF in btf_idr while a subsequent caller parses and allocates a second
vmlinux BTF.
[ ... ]
> @@ -11009,6 +11131,9 @@ int __register_bpf_struct_ops(struct bpf_struct_ops *st_ops)
> struct btf *btf;
> int err = 0;
>
> + if (btf_defer_reg(st_ops->owner, BTF_DEFER_ST_OPS, 0, st_ops, 0, &err))
> + return err;
> +
> btf = btf_get_module_btf(st_ops->owner);
> if (!btf)
> return check_btf_kconfigs(st_ops->owner, "struct_ops");
[Severity: Medium]
When __register_bpf_struct_ops() is deferred until the first
bpf_get_btf_vmlinux() call, could BPF_PROG_QUERY with attach_type
BPF_STRUCT_OPS fail with -ENOENT if invoked before vmlinux BTF is parsed?
Normally, built-in cgroup struct_ops (such as bpf_tcp_ops registered in
__bpf_tcp_ops_init()) are registered during initcalls via:
__register_bpf_struct_ops()
btf_add_struct_ops()
cgroup_bpf_struct_ops_register()
which populates cgroup_struct_ops[cgroup_atype].type_id.
With CONFIG_DEBUG_INFO_BTF_LAZY enabled, btf_defer_reg() defers
__register_bpf_struct_ops(), leaving cgroup_struct_ops[].type_id as 0 until
bpf_get_btf_vmlinux() runs btf_apply_deferred().
If a user reads the struct_ops type_id from /sys/kernel/btf/vmlinux (which
decompresses raw BTF via btf_vmlinux_data() without calling
bpf_get_btf_vmlinux()) and invokes BPF_PROG_QUERY with attach_type
BPF_STRUCT_OPS, execution reaches kernel/bpf/cgroup.c:
bpf_prog_query()
cgroup_bpf_prog_query()
cgroup_bpf_query()
__cgroup_bpf_query()
In __cgroup_bpf_query():
if (type == BPF_STRUCT_OPS) {
u32 type_id = attr->query.type_id;
atype = find_atype_by_struct_ops_id(type_id);
if (atype == CGROUP_BPF_ATTACH_TYPE_INVALID)
return -ENOENT;
Because neither cgroup_bpf_prog_query() nor __cgroup_bpf_query() calls
bpf_get_btf_vmlinux(), the deferred struct_ops registrations have not been
applied yet, so find_atype_by_struct_ops_id() returns
CGROUP_BPF_ATTACH_TYPE_INVALID and the query fails with -ENOENT instead of
succeeding with prog_cnt = 0.
> @@ -11034,6 +11159,93 @@ int __register_bpf_struct_ops(struct bpf_struct_ops *st_ops)
> EXPORT_SYMBOL_GPL(__register_bpf_struct_ops);
> #endif
>
> +static void btf_free_deferred_reg(struct btf_deferred_reg *reg)
> +{
> + list_del(®->list);
> + if (reg->kind != BTF_DEFER_ST_OPS)
> + kfree(reg->data);
> + kfree(reg);
> +}
> +
> +/*
> + * Called under btf_vmlinux_lock, by the task that parsed the vmlinux BTF and
> + * before it publishes it, so that nothing sees the BTF without its kfuncs
> + * and struct_ops. bpf_get_btf_vmlinux() returns the new BTF to
> + * this task, so the registrations are applied as if they were made now, and
> + * any they make in turn (a struct_ops ->init() may register kfuncs) too.
> + * They run in whichever task needs the BTF first, with the locks it holds:
> + * they must not load modules or take locks that users of the BTF hold.
> + *
> + * Returns -ENOMEM if memory ran out, which may be only because that task is
> + * being killed: the caller then drops the new BTF with whatever was applied
> + * to it, and the next caller starts over with the queue intact, as for the
> + * vmlinux BTF itself; a struct_ops ->init() then runs again with the BTF of
> + * that round. Any other error of a registration is only logged.
> + */
> +static int btf_apply_deferred(void)
> +{
> + struct btf_deferred_reg *reg, *tmp;
> + int err;
> +
> + list_for_each_entry(reg, &btf_deferred_regs, list) {
> + /* a going module's exit() runs before GOING drops its entries */
> + if (!try_module_get(reg->owner))
> + continue;
> + err = 0;
> + switch (reg->kind) {
> + case BTF_DEFER_KFUNCS:
> + err = __register_btf_kfunc_id_set(reg->hook, reg->data);
> + break;
> + case BTF_DEFER_DTORS:
> + err = register_btf_id_dtor_kfuncs(reg->data, reg->cnt, reg->owner);
> + break;
> + case BTF_DEFER_ST_OPS:
> +#if defined(CONFIG_BPF_JIT)
> + err = __register_bpf_struct_ops(reg->data);
> +#endif
> + break;
> + }
> + module_put(reg->owner);
> + if (err == -ENOMEM)
> + return err;
[Severity: Medium]
What happens if a struct_ops ->init() callback fails with -ENOMEM while
registering kfuncs during btf_apply_deferred()?
In-tree struct_ops ->init() callbacks such as bm_bpf_init() in
fs/binfmt_misc_bpf.c and bpf_io_init() in io_uring/bpf-ops.c call
register_btf_kfunc_id_set() and return its error code.
When btf_apply_deferred() replays BTF_DEFER_ST_OPS while
btf_vmlinux_building() is true, register_btf_kfunc_id_set() runs directly
against btf_vmlinux_new via btf_populate_kfunc_set(). If memory allocation
fails with -ENOMEM in btf_populate_kfunc_set(), it jumps to label end and
frees the entire kfunc_set_tab on btf_vmlinux_new:
btf_populate_kfunc_set()
...
end:
btf_free_kfunc_set_tab(btf);
return ret;
However, in kernel/bpf/bpf_struct_ops.c, bpf_struct_ops_desc_init()
discards the -ENOMEM return value from st_ops->init(btf) and replaces it
with -EINVAL:
if (st_ops->init(btf)) {
pr_warn("Error in init bpf_struct_ops %s\n",
st_ops->name);
err = -EINVAL;
goto errout;
}
As a result, __register_bpf_struct_ops() returns -EINVAL instead of
-ENOMEM to btf_apply_deferred(). Because btf_apply_deferred() only aborts
the round when err == -ENOMEM, wouldn't it log a warning, free all queued
entries in btf_deferred_regs, and return 0, causing bpf_get_btf_vmlinux()
to permanently publish a btf_vmlinux whose kfunc_set_tab was wiped out by
btf_free_kfunc_set_tab()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261031130959.23472-1-wanjay@amazon.com?part=10
next prev parent reply other threads:[~2026-10-31 13:38 UTC|newest]
Thread overview: 31+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-31 13:09 [PATCH bpf-next 00/13] bpf: keep the vmlinux BTF compressed until first use (CONFIG_DEBUG_INFO_BTF_LAZY) to save ~6 MB memory Jay Wang
2026-10-31 13:09 ` [PATCH bpf-next 01/13] bpf: match program context types by name without vmlinux BTF Jay Wang
2026-10-31 13:43 ` bot+bpf-ci
2026-10-31 13:09 ` [PATCH bpf-next 02/13] bpf: fetch the vmlinux BTF where kernel types enter a program Jay Wang
2026-10-31 13:42 ` sashiko-bot
2026-10-31 13:55 ` bot+bpf-ci
2026-10-31 13:09 ` [PATCH bpf-next 03/13] bpf: check CAP_SYS_ADMIN before looking up bpffs delegation names Jay Wang
2026-10-31 13:43 ` bot+bpf-ci
2026-10-31 17:03 ` Alexei Starovoitov
2026-10-31 13:09 ` [PATCH bpf-next 04/13] bpf: move bpf_get_btf_vmlinux() to btf.c Jay Wang
2026-10-31 13:43 ` bot+bpf-ci
2026-10-31 13:09 ` [PATCH bpf-next 05/13] bpf: let btf_parse_module() adopt the caller's copy of the module BTF Jay Wang
2026-10-31 13:09 ` [PATCH bpf-next 06/13] bpf: look up the struct_ops common value type in the given BTF Jay Wang
2026-10-31 13:09 ` [PATCH bpf-next 07/13] bpf: parse the vmlinux BTF before taking cand_cache_mutex Jay Wang
2026-10-31 13:43 ` bot+bpf-ci
2026-10-31 13:09 ` [PATCH bpf-next 08/13] tracing: parse the vmlinux BTF when BTF-typed output is enabled Jay Wang
2026-10-31 13:37 ` sashiko-bot
2026-10-31 13:43 ` bot+bpf-ci
2026-10-31 13:09 ` [PATCH bpf-next 09/13] bpf: keep the vmlinux BTF zstd-compressed in the kernel image Jay Wang
2026-10-31 13:33 ` sashiko-bot
2026-10-31 13:55 ` bot+bpf-ci
2026-10-31 13:09 ` [PATCH bpf-next 10/13] bpf: defer kfunc and struct_ops registrations until first BTF use Jay Wang
2026-10-31 13:38 ` sashiko-bot [this message]
2026-10-31 13:55 ` bot+bpf-ci
2026-10-31 17:07 ` Alexei Starovoitov
2026-10-31 13:09 ` [PATCH bpf-next 11/13] bpf: keep module BTF until the vmlinux BTF is parsed Jay Wang
2026-10-31 13:37 ` sashiko-bot
2026-10-31 13:55 ` bot+bpf-ci
2026-10-31 17:10 ` Alexei Starovoitov
2026-10-31 13:09 ` [PATCH bpf-next 12/13] selftests/bpf: keep ksyms and linked_vars working without __start_BTF Jay Wang
2026-10-31 13:09 ` [PATCH bpf-next 13/13] Documentation/bpf: describe CONFIG_DEBUG_INFO_BTF_LAZY Jay Wang
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=sashiko-outbox-191982@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=abuehaze@amazon.com \
--cc=almagui@kernel.org \
--cc=andrii@kernel.org \
--cc=arnd@arndb.de \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=doebel@amazon.de \
--cc=eddyz87@gmail.com \
--cc=ihor.solodrai@linux.dev \
--cc=jay.wang.upstream@gmail.com \
--cc=jolsa@kernel.org \
--cc=linux-kbuild@vger.kernel.org \
--cc=linux-trace-kernel@vger.kernel.org \
--cc=martin.lau@linux.dev \
--cc=mathieu.desnoyers@efficios.com \
--cc=memxor@gmail.com \
--cc=mhiramat@kernel.org \
--cc=mpohlack@amazon.de \
--cc=nathan@kernel.org \
--cc=nsc@kernel.org \
--cc=rostedt@goodmis.org \
--cc=sashiko-reviews@lists.linux.dev \
--cc=wanjay@amazon.com \
--cc=x86@kernel.org \
--cc=yonghong.song@linux.dev \
/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