* [PATCH bpf-next 01/13] bpf: match program context types by name without vmlinux BTF
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 ` 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
` (11 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
From: Ihor Solodrai <ihor.solodrai@linux.dev>
btf_is_prog_ctx_type() and btf_validate_prog_ctx_type() decide whether
a BTF function argument is the program's context. They look up the
canonical context type of the program type in bpf_ctx_convert, a type
that only exists in the vmlinux BTF, but then use nothing but its name:
the argument's type name is compared with it, with the __sk_buff ->
sk_buff and xdp_md -> xdp_buff projections, and with one typedef
resolution step for bpf_user_pt_regs_t.
btf_prepare_func_args() runs this check for the main program of every
load that carries func_info (e.g. the socket filter and cgroup programs
systemd loads at boot) and for subprograms. Once the vmlinux BTF is
parsed on first use (CONFIG_DEBUG_INFO_BTF_LAZY, later in this series),
fetching the BTF here would make all of them pay for parsing it, and
skipping the check when the BTF is not there yet would make the
verification result (e.g. func_info "unreliable") depend on whether
something else parsed the BTF first.
Take the names from include/linux/bpf_types.h instead: a table indexed
by program type is built from the prog_ctx_type argument of
BPF_PROG_TYPE(), and prog_ctx_tname() returns the struct tag, or the
typedef name. For bpf_user_pt_regs_t it also returns the name of the
underlying struct, found with __builtin_types_compatible_p() the same
way btf_validate_prog_ctx_type() already does for perf_event programs.
The other forms are equivalent to what the BTF lookup produced:
- typedefs of integers (__u64, __u32): the old code compared the
typedef name and then the integer type name, which never equals a
struct tag; only the typedef name is compared now;
- "void *" (tracing, struct_ops, ext, lsm, syscall): no named context
type, as before;
- s390 typedefs bpf_user_pt_regs_t to an anonymous struct: nothing
matches, as before; only the __arg_ctx error message changes from
"should be `struct *`" to "should be void pointer";
- program types that are not configured in used to index
bpf_ctx_convert_map[] with 0 (socket filter); they now get no name.
Such programs cannot be loaded anyway.
find_kern_ctx_type_id() and get_kern_ctx_btf_id() still need the kernel
context type (tracing/EXT access to a BPF program's context argument,
bpf_cast_to_kern_ctx()). Kernel types enter the program there, so they
now fetch the vmlinux BTF with bpf_get_btf_vmlinux() instead of relying
on bpf_check() having done it.
No change with CONFIG_DEBUG_INFO_BTF=y. With CONFIG_DEBUG_INFO_BTF=n,
global subprograms taking e.g. a struct __sk_buff *ctx now verify
instead of failing with "btf_vmlinux is malformed", as they do with =y.
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
[split the BPF_PROG_TYPE() definition over two lines]
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
kernel/bpf/btf.c | 116 ++++++++++++++++++++++-------------------------
1 file changed, 53 insertions(+), 63 deletions(-)
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 94d43612d211..1c7536a22a63 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -6557,20 +6557,46 @@ static u8 bpf_ctx_convert_map[] = {
#undef BPF_MAP_TYPE
#undef BPF_LINK_TYPE
-static const struct btf_type *find_canonical_prog_ctx_type(enum bpf_prog_type prog_type)
+#define BPF_MAP_TYPE(_id, _ops)
+#define BPF_LINK_TYPE(_id, _name)
+#define BPF_PROG_TYPE(_id, _name, prog_ctx_type, kern_ctx_type) \
+ [_id] = #prog_ctx_type,
+static const char * const bpf_prog_ctx_tname[] = {
+#include <linux/bpf_types.h>
+};
+#undef BPF_PROG_TYPE
+#undef BPF_MAP_TYPE
+#undef BPF_LINK_TYPE
+
+struct user_regs_struct;
+struct user_pt_regs;
+
+/*
+ * Name of the context struct of @prog_type, as written in bpf_types.h, or NULL
+ * if it has none ("void *"). Only names are compared against the program's
+ * BTF, so the vmlinux BTF is not needed. *@alt is set to the struct behind
+ * the bpf_user_pt_regs_t typedef. Typedefs of integers (__u64, __u32) never
+ * match a struct name.
+ */
+static const char *prog_ctx_tname(enum bpf_prog_type prog_type, const char **alt)
{
- const struct btf_type *conv_struct;
- const struct btf_member *ctx_type;
+ const char *name = prog_type < ARRAY_SIZE(bpf_prog_ctx_tname) ?
+ bpf_prog_ctx_tname[prog_type] : NULL;
- conv_struct = bpf_ctx_convert.t;
- if (!conv_struct)
+ *alt = NULL;
+ if (!name || strchr(name, '*'))
return NULL;
- /* prog_type is valid bpf program type. No need for bounds check. */
- ctx_type = btf_type_member(conv_struct) + bpf_ctx_convert_map[prog_type] * 2;
- /* ctx_type is a pointer to prog_ctx_type in vmlinux.
- * Like 'struct __sk_buff'
- */
- return btf_type_by_id(btf_vmlinux, ctx_type->type);
+ if (str_has_prefix(name, "struct "))
+ return name + strlen("struct ");
+ if (!strcmp(name, "bpf_user_pt_regs_t")) {
+ if (__builtin_types_compatible_p(bpf_user_pt_regs_t, struct pt_regs))
+ *alt = "pt_regs";
+ else if (__builtin_types_compatible_p(bpf_user_pt_regs_t, struct user_pt_regs))
+ *alt = "user_pt_regs";
+ else if (__builtin_types_compatible_p(bpf_user_pt_regs_t, struct user_regs_struct))
+ *alt = "user_regs_struct";
+ }
+ return name;
}
static int find_kern_ctx_type_id(enum bpf_prog_type prog_type)
@@ -6578,9 +6604,10 @@ static int find_kern_ctx_type_id(enum bpf_prog_type prog_type)
const struct btf_type *conv_struct;
const struct btf_member *ctx_type;
- conv_struct = bpf_ctx_convert.t;
- if (!conv_struct)
+ /* bpf_ctx_convert.t is set when the vmlinux BTF is parsed */
+ if (IS_ERR_OR_NULL(bpf_get_btf_vmlinux()))
return -EFAULT;
+ conv_struct = bpf_ctx_convert.t;
/* prog_type is valid bpf program type. No need for bounds check. */
ctx_type = btf_type_member(conv_struct) + bpf_ctx_convert_map[prog_type] * 2 + 1;
/* ctx_type is a pointer to prog_ctx_type in vmlinux.
@@ -6602,8 +6629,7 @@ bool btf_is_prog_ctx_type(struct bpf_verifier_log *log, const struct btf *btf,
const struct btf_type *t, enum bpf_prog_type prog_type,
int arg)
{
- const struct btf_type *ctx_type;
- const char *tname, *ctx_tname;
+ const char *tname, *ctx_tname, *alt;
t = btf_type_by_id(btf, t->type);
@@ -6637,21 +6663,9 @@ bool btf_is_prog_ctx_type(struct bpf_verifier_log *log, const struct btf *btf,
return false;
}
- ctx_type = find_canonical_prog_ctx_type(prog_type);
- if (!ctx_type) {
- bpf_log(log, "btf_vmlinux is malformed\n");
- /* should not happen */
- return false;
- }
-again:
- ctx_tname = btf_name_by_offset(btf_vmlinux, ctx_type->name_off);
- if (!ctx_tname) {
- /* should not happen */
- bpf_log(log, "Please fix kernel include/linux/bpf_types.h\n");
- return false;
- }
/* program types without named context types work only with arg:ctx tag */
- if (ctx_tname[0] == '\0')
+ ctx_tname = prog_ctx_tname(prog_type, &alt);
+ if (!ctx_tname)
return false;
/* only compare that prog's ctx type name is the same as
* kernel expects. No need to compare field by field.
@@ -6660,37 +6674,18 @@ bool btf_is_prog_ctx_type(struct bpf_verifier_log *log, const struct btf *btf,
* int socket_filter_bpf_prog(struct __sk_buff *skb)
* { // no fields of skb are ever used }
*/
- if (btf_is_projection_of(ctx_tname, tname))
+ if (btf_is_projection_of(ctx_tname, tname) || !strcmp(ctx_tname, tname))
return true;
- if (strcmp(ctx_tname, tname)) {
- /* bpf_user_pt_regs_t is a typedef, so resolve it to
- * underlying struct and check name again
- */
- if (!btf_type_is_modifier(ctx_type))
- return false;
- while (btf_type_is_modifier(ctx_type))
- ctx_type = btf_type_by_id(btf_vmlinux, ctx_type->type);
- goto again;
- }
- return true;
+ /* bpf_user_pt_regs_t is a typedef, check the underlying struct too */
+ return alt && !strcmp(alt, tname);
}
-/* forward declarations for arch-specific underlying types of
- * bpf_user_pt_regs_t; this avoids the need for arch-specific #ifdef
- * compilation guards below for BPF_PROG_TYPE_PERF_EVENT checks, but still
- * works correctly with __builtin_types_compatible_p() on respective
- * architectures
- */
-struct user_regs_struct;
-struct user_pt_regs;
-
static int btf_validate_prog_ctx_type(struct bpf_verifier_log *log, const struct btf *btf,
const struct btf_type *t, int arg,
enum bpf_prog_type prog_type,
enum bpf_attach_type attach_type)
{
- const struct btf_type *ctx_type;
- const char *tname, *ctx_tname;
+ const char *tname, *ctx_tname, *alt;
if (!btf_is_ptr(t)) {
bpf_log(log, "arg#%d type isn't a pointer\n", arg);
@@ -6790,27 +6785,20 @@ static int btf_validate_prog_ctx_type(struct bpf_verifier_log *log, const struct
break;
}
- ctx_type = find_canonical_prog_ctx_type(prog_type);
- if (!ctx_type) {
- /* should not happen */
- bpf_log(log, "btf_vmlinux is malformed\n");
- return -EINVAL;
- }
-
/* resolve typedefs and check that underlying structs are matching as well */
- while (btf_type_is_modifier(ctx_type))
- ctx_type = btf_type_by_id(btf_vmlinux, ctx_type->type);
+ ctx_tname = prog_ctx_tname(prog_type, &alt);
+ if (ctx_tname && !str_has_prefix(bpf_prog_ctx_tname[prog_type], "struct "))
+ ctx_tname = alt;
/* if program type doesn't have distinctly named struct type for
* context, then __arg_ctx argument can only be `void *`, which we
* already checked above
*/
- if (!__btf_type_is_struct(ctx_type)) {
+ if (!ctx_tname) {
bpf_log(log, "arg#%d should be void pointer\n", arg);
return -EINVAL;
}
- ctx_tname = btf_name_by_offset(btf_vmlinux, ctx_type->name_off);
if (!__btf_type_is_struct(t) || strcmp(ctx_tname, tname) != 0) {
bpf_log(log, "arg#%d should be `struct %s *`\n", arg, ctx_tname);
return -EINVAL;
@@ -6837,6 +6825,8 @@ int get_kern_ctx_btf_id(struct bpf_verifier_log *log, enum bpf_prog_type prog_ty
const struct btf_type *kctx_type;
u32 kctx_type_id;
+ if (IS_ERR_OR_NULL(bpf_get_btf_vmlinux()))
+ return -EINVAL;
conv_struct = bpf_ctx_convert.t;
/* get member for kernel ctx type */
kctx_member = btf_type_member(conv_struct) + bpf_ctx_convert_map[prog_type] * 2 + 1;
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 01/13] bpf: match program context types by name without vmlinux BTF
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
0 siblings, 0 replies; 26+ messages in thread
From: bot+bpf-ci @ 2026-10-31 13:43 UTC (permalink / raw)
To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
Cc: ihor.solodrai, almagui, martin.lau, yonghong.song, jolsa, rostedt,
mhiramat, mathieu.desnoyers, linux-trace-kernel, nathan, nsc,
linux-kbuild, arnd, linux-arch, x86, linux-doc, linux-kernel,
abuehaze, doebel, mpohlack, jay.wang.upstream, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4138 bytes --]
> bpf: match program context types by name without vmlinux BTF
TL;DR: Commit message only: a "TODO-NEEDS-IHOR: Signed-off-by:" placeholder
isn't a real trailer, so the author's Signed-off-by is missing (checkpatch
would flag it). It should be fixed or removed; other series commits may have
it too.
> bpf: match program context types by name without vmlinux BTF
>
> btf_is_prog_ctx_type() and btf_validate_prog_ctx_type() decide whether
> a BTF function argument is the program's context. They look up the
> canonical context type of the program type in bpf_ctx_convert, a type
> that only exists in the vmlinux BTF, but then use nothing but its name:
> the argument's type name is compared with it, with the __sk_buff ->
> sk_buff and xdp_md -> xdp_buff projections, and with one typedef
> resolution step for bpf_user_pt_regs_t.
>
> btf_prepare_func_args() runs this check for the main program of every
> load that carries func_info (e.g. the socket filter and cgroup programs
> systemd loads at boot) and for subprograms. Once the vmlinux BTF is
> parsed on first use (CONFIG_DEBUG_INFO_BTF_LAZY, later in this series),
> fetching the BTF here would make all of them pay for parsing it, and
> skipping the check when the BTF is not there yet would make the
> verification result (e.g. func_info "unreliable") depend on whether
> something else parsed the BTF first.
>
> Take the names from include/linux/bpf_types.h instead: a table indexed
> by program type is built from the prog_ctx_type argument of
> BPF_PROG_TYPE(), and prog_ctx_tname() returns the struct tag, or the
> typedef name. For bpf_user_pt_regs_t it also returns the name of the
> underlying struct, found with __builtin_types_compatible_p() the same
> way btf_validate_prog_ctx_type() already does for perf_event programs.
> The other forms are equivalent to what the BTF lookup produced:
>
> - typedefs of integers (__u64, __u32): the old code compared the
> typedef name and then the integer type name, which never equals a
> struct tag; only the typedef name is compared now;
> - "void *" (tracing, struct_ops, ext, lsm, syscall): no named context
> type, as before;
> - s390 typedefs bpf_user_pt_regs_t to an anonymous struct: nothing
> matches, as before; only the __arg_ctx error message changes from
> "should be `struct *`" to "should be void pointer";
> - program types that are not configured in used to index
> bpf_ctx_convert_map[] with 0 (socket filter); they now get no name.
> Such programs cannot be loaded anyway.
>
> find_kern_ctx_type_id() and get_kern_ctx_btf_id() still need the kernel
> context type (tracing/EXT access to a BPF program's context argument,
> bpf_cast_to_kern_ctx()). Kernel types enter the program there, so they
> now fetch the vmlinux BTF with bpf_get_btf_vmlinux() instead of relying
> on bpf_check() having done it.
>
> No change with CONFIG_DEBUG_INFO_BTF=y. With CONFIG_DEBUG_INFO_BTF=n,
> global subprograms taking e.g. a struct __sk_buff *ctx now verify
> instead of failing with "btf_vmlinux is malformed", as they do with =y.
>
> TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> [split the BPF_PROG_TYPE() definition over two lines]
> Signed-off-by: Jay Wang <wanjay@amazon.com>
Is the "TODO-NEEDS-IHOR: Signed-off-by:" line at the end of the commit
message meant to stay in the submitted patch?
Because of the prefix, git and checkpatch do not parse that line as a
trailer, so the only real Signed-off-by is Jay Wang's, while the patch is
authored by Ihor Solodrai. checkpatch would report "Missing Signed-off-by:
line by nominal patch author".
Should this become a real Signed-off-by line from the author (or be removed),
as required by the Developer's Certificate of Origin in
Documentation/process/submitting-patches.rst? The same placeholder appears in
other commits of this series (for example 32ad8adbb1a8).
---
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/41205212136
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH bpf-next 02/13] bpf: fetch the vmlinux BTF where kernel types enter a program
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:09 ` Jay Wang
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
` (10 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
bpf_check() fetches the vmlinux BTF up front for every program, whether
the program uses kernel types or not. That is free today because the
BTF is parsed at boot anyway. Once it can be parsed on first use
(CONFIG_DEBUG_INFO_BTF_LAZY, later in this series), it would make every
program load pay for parsing 5-7 MB of BTF, including the socket filters
systemd loads at boot.
Drop the up-front fetch and fetch where kernel types enter the verifier
state instead:
- bpf_add_kfunc_call(), for the first kfunc call of a program;
- check_ptr_to_map_access(), for map_ptr field access;
- check_pseudo_btf_id(), for ldimm64 of a kernel variable (ksym);
- check_helper_call(), for helpers whose prototype takes
ARG_PTR_TO_BTF_ID or returns RET_PTR_TO_BTF_ID, and for
bpf_snprintf_btf() and bpf_seq_printf_btf(), which take a kernel
type id inside struct btf_ptr (helper_uses_vmlinux_btf()).
The other ways in already fetch the BTF themselves: attach_btf in
bpf_prog_load(), struct_ops map creation, CO-RE candidate lookup,
bpf_find_btf_id(), btf_prepare_func_args() for kernel-typed arguments
and the kernel context type lookups. The remaining reads of the
btf_vmlinux global in the verifier are on paths dominated by one of
these fetches. A program that uses none of them, such as a socket
filter, does not need the vmlinux BTF any more, and verifies the same
whether or not it has been parsed.
Four related changes keep it that way:
- The "in-kernel BTF is malformed" check in bpf_check() read the
global right after the up-front fetch. Without the fetch it would
pass or fail depending on whether something else had parsed the BTF
before, so drop it; every fetch point reports a missing BTF itself
and passes on an error of the parse, as that check did.
- check_ptr_to_map_access() checks allow_ptr_leaks before it fetches
the BTF, and bpf_prog_load() falls back to the vmlinux BTF for an
attach_btf_id only with CAP_BPF, which all program types that take
one need, so that an unprivileged load cannot make the kernel parse
it only to be refused. The map_ptr permission error can then not
name the map's struct any more and says "map_ptr access is allowed
only to CAP_PERFMON and CAP_SYS_ADMIN". A ksym, which unprivileged
programs may use, still parses it.
- Add bpf_peek_btf_vmlinux(), which returns the vmlinux BTF only if it
is parsed already, and use it in bpf_btf_printf_prepare(). The BTF
printf helpers run in program context, which may be atomic, and the
verifier has fetched the BTF for every program that calls them.
- is_kfunc_dynptr_write(), reached from the argument checks that
helpers and kfuncs share, compared meta->btf with the global, which
a helper's NULL matches while the global is NULL. Check that the
call is a kfunc first, as the other kfunc predicates there do.
Helpers taking ARG_PTR_TO_BTF_ID_SOCK_COMMON are left out on purpose:
they take sockets without BTF types too, the shared argument check reads
the global only for a PTR_TO_BTF_ID register, and every source of such
a register fetched the BTF.
No functional change with CONFIG_DEBUG_INFO_BTF=y, where kfunc
registrations at boot parse the BTF, other than that error message and
that, if the vmlinux BTF fails to parse (a broken toolchain), programs
that use no kernel types now load instead of failing. Without BTF the
new helper check is compiled out and the existing error paths are
unchanged.
Co-developed-by: Ihor Solodrai <ihor.solodrai@linux.dev>
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
include/linux/bpf.h | 1 +
kernel/bpf/syscall.c | 8 +++-
kernel/bpf/verifier.c | 98 ++++++++++++++++++++++++++++++----------
kernel/trace/bpf_trace.c | 3 +-
4 files changed, 83 insertions(+), 27 deletions(-)
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 580f89c450b5..1fd6ef2ef5c6 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -3257,6 +3257,7 @@ static inline s32 bpf_call_args_imm(s16 idx)
#endif
struct btf *bpf_get_btf_vmlinux(void);
+struct btf *bpf_peek_btf_vmlinux(void);
/* Map specifics */
struct xdp_frame;
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index ca72b35cdcc9..9273b7dee0aa 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -3052,8 +3052,12 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
goto put_token;
}
}
- } else if (attr->attach_btf_id || multi_func) {
- /* fall back to vmlinux BTF, if BTF type ID is specified */
+ } else if ((attr->attach_btf_id || multi_func) && bpf_cap) {
+ /*
+ * fall back to vmlinux BTF, if BTF type ID is specified; only
+ * program types that need CAP_BPF take one, and
+ * bpf_prog_load_check_attach() refuses it for the others
+ */
attach_btf = bpf_get_btf_vmlinux();
if (IS_ERR(attach_btf)) {
err = PTR_ERR(attach_btf);
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 5601d2056218..1b901e3cb3c2 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -3060,6 +3060,18 @@ static int fetch_kfunc_meta(struct bpf_verifier_env *env,
static int gen_kfunc_arg_proto(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,
const struct btf_func_model *fm, struct bpf_func_proto *proto);
+/*
+ * The vmlinux BTF failed to parse where a program first needs it: pass the
+ * error on, as bpf_check() did when it fetched the BTF up front. It need
+ * not be for good: a parse on first use that runs out of memory is retried
+ * by the next caller.
+ */
+static int vmlinux_btf_err(struct bpf_verifier_env *env, const struct btf *btf)
+{
+ verbose(env, "in-kernel BTF could not be parsed: %ld\n", PTR_ERR(btf));
+ return PTR_ERR(btf);
+}
+
int bpf_add_kfunc_call(struct bpf_verifier_env *env, u32 func_id, u16 offset)
{
struct bpf_call_arg_meta meta;
@@ -3076,7 +3088,11 @@ int bpf_add_kfunc_call(struct bpf_verifier_env *env, u32 func_id, u16 offset)
tab = prog_aux->kfunc_tab;
btf_tab = prog_aux->kfunc_btf_tab;
if (!tab) {
- if (!btf_vmlinux) {
+ struct btf *btf = bpf_get_btf_vmlinux();
+
+ if (IS_ERR(btf))
+ return vmlinux_btf_err(env, btf);
+ if (!btf) {
verbose(env, "calling kernel function is not supported without CONFIG_DEBUG_INFO_BTF\n");
return -ENOTSUPP;
}
@@ -6867,30 +6883,33 @@ static int check_ptr_to_map_access(struct bpf_verifier_env *env,
enum bpf_type_flag flag = 0;
const struct btf_type *t;
const char *tname;
+ struct btf *btf;
u32 btf_id;
int ret;
- if (!btf_vmlinux) {
- verbose(env, "map_ptr access not supported without CONFIG_DEBUG_INFO_BTF\n");
- return -ENOTSUPP;
- }
-
if (!map->ops->map_btf_id || !*map->ops->map_btf_id) {
verbose(env, "map_ptr access not supported for map type %d\n",
map->map_type);
return -ENOTSUPP;
}
- t = btf_type_by_id(btf_vmlinux, *map->ops->map_btf_id);
- tname = btf_name_by_offset(btf_vmlinux, t->name_off);
-
+ /* before the BTF fetch: unprivileged loads must not parse it */
if (!env->allow_ptr_leaks) {
- verbose(env,
- "'struct %s' access is allowed only to CAP_PERFMON and CAP_SYS_ADMIN\n",
- tname);
+ verbose(env, "map_ptr access is allowed only to CAP_PERFMON and CAP_SYS_ADMIN\n");
return -EPERM;
}
+ btf = bpf_get_btf_vmlinux();
+ if (IS_ERR(btf))
+ return vmlinux_btf_err(env, btf);
+ if (!btf) {
+ verbose(env, "map_ptr access not supported without CONFIG_DEBUG_INFO_BTF\n");
+ return -ENOTSUPP;
+ }
+
+ t = btf_type_by_id(btf, *map->ops->map_btf_id);
+ tname = btf_name_by_offset(btf, t->name_off);
+
if (off < 0) {
verbose(env, "%s is %s invalid negative access: off=%d\n",
reg_arg_name(env, argno), tname, off);
@@ -12606,6 +12625,22 @@ static int release_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
return err;
}
+/* Does helper @fn bring kernel BTF types into the program? */
+static bool helper_uses_vmlinux_btf(enum bpf_func_id func_id, const struct bpf_func_proto *fn)
+{
+ int i;
+
+ /* these take a kernel type id in struct btf_ptr */
+ if (func_id == BPF_FUNC_snprintf_btf || func_id == BPF_FUNC_seq_printf_btf)
+ return true;
+ if (base_type(fn->ret_type) == RET_PTR_TO_BTF_ID)
+ return true;
+ for (i = 0; i < ARRAY_SIZE(fn->arg_type); i++)
+ if (base_type(fn->arg_type[i]) == ARG_PTR_TO_BTF_ID)
+ return true;
+ return false;
+}
+
static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
int *insn_idx_p)
{
@@ -12675,6 +12710,14 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
return err;
}
+ if (IS_ENABLED(CONFIG_DEBUG_INFO_BTF) && helper_uses_vmlinux_btf(func_id, fn)) {
+ /* never NULL with CONFIG_DEBUG_INFO_BTF */
+ struct btf *btf = bpf_get_btf_vmlinux();
+
+ if (IS_ERR(btf))
+ return vmlinux_btf_err(env, btf);
+ }
+
if (fn->might_sleep && !in_sleepable_context(env)) {
const char *suggestion;
@@ -13900,7 +13943,7 @@ BTF_SET_END(dynptr_write_kfuncs)
static bool is_kfunc_dynptr_write(struct bpf_call_arg_meta *meta)
{
- return meta->btf == btf_vmlinux &&
+ return is_kfunc(meta) && meta->btf == btf_vmlinux &&
btf_id_set_contains(&dynptr_write_kfuncs, meta->func_id);
}
@@ -20898,12 +20941,14 @@ static int check_pseudo_btf_id(struct bpf_verifier_env *env,
return -EINVAL;
}
} else {
- if (!btf_vmlinux) {
+ btf = bpf_get_btf_vmlinux();
+ if (IS_ERR(btf))
+ return vmlinux_btf_err(env, btf);
+ if (!btf) {
verbose(env, "kernel is missing BTF, make sure CONFIG_DEBUG_INFO_BTF=y is specified in Kconfig.\n");
return -EINVAL;
}
- btf_get(btf_vmlinux);
- btf = btf_vmlinux;
+ btf_get(btf);
}
err = __check_pseudo_btf_id(env, insn, aux, btf);
@@ -23122,6 +23167,13 @@ struct btf *bpf_get_btf_vmlinux(void)
return btf;
}
+/* The vmlinux BTF if it is parsed already: for callers that cannot sleep */
+struct btf *bpf_peek_btf_vmlinux(void)
+{
+ /* Pairs with the smp_store_release() in bpf_get_btf_vmlinux() */
+ return smp_load_acquire(&btf_vmlinux);
+}
+
/*
* The add_fd_from_fd_array() is executed only if fd_array_cnt is non-zero. In
* this case expect that every file descriptor in the array is either a map or
@@ -23845,7 +23897,12 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
if (ret)
goto err_prep;
- bpf_get_btf_vmlinux();
+ /*
+ * The vmlinux BTF is not fetched here but where kernel types enter
+ * the program (attach_btf, kfuncs, ksyms, map_ptr, BTF-typed helpers,
+ * CO-RE, kernel ctx types), so that programs which use none of them
+ * do not need it (CONFIG_DEBUG_INFO_BTF_LAZY).
+ */
/* Serialize verification of unprivileged programs. */
if (!is_priv)
@@ -23866,13 +23923,6 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
mark_verifier_state_clean(env);
- if (IS_ERR(btf_vmlinux)) {
- /* Either gcc or pahole or kernel are broken. */
- verbose(env, "in-kernel BTF is malformed\n");
- ret = PTR_ERR(btf_vmlinux);
- goto skip_full_check;
- }
-
env->strict_alignment = !!(attr->prog_flags & BPF_F_STRICT_ALIGNMENT);
if (!IS_ENABLED(CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS))
env->strict_alignment = true;
diff --git a/kernel/trace/bpf_trace.c b/kernel/trace/bpf_trace.c
index dfac5e1b8f40..84335e594388 100644
--- a/kernel/trace/bpf_trace.c
+++ b/kernel/trace/bpf_trace.c
@@ -1018,7 +1018,8 @@ static int bpf_btf_printf_prepare(struct btf_ptr *ptr, u32 btf_ptr_size,
if (btf_ptr_size != sizeof(struct btf_ptr))
return -EINVAL;
- *btf = bpf_get_btf_vmlinux();
+ /* fetched by the verifier, see helper_uses_vmlinux_btf() */
+ *btf = bpf_peek_btf_vmlinux();
if (IS_ERR_OR_NULL(*btf))
return IS_ERR(*btf) ? PTR_ERR(*btf) : -EINVAL;
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 02/13] bpf: fetch the vmlinux BTF where kernel types enter a program
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:55 ` bot+bpf-ci
0 siblings, 0 replies; 26+ messages in thread
From: bot+bpf-ci @ 2026-10-31 13:55 UTC (permalink / raw)
To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
Cc: ihor.solodrai, almagui, martin.lau, yonghong.song, jolsa, rostedt,
mhiramat, mathieu.desnoyers, linux-trace-kernel, nathan, nsc,
linux-kbuild, arnd, linux-arch, x86, linux-doc, linux-kernel,
abuehaze, doebel, mpohlack, jay.wang.upstream, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 7735 bytes --]
> bpf: fetch the vmlinux BTF where kernel types enter a program
TL;DR:
- check_ptr_to_map_access(): without BTF, the map_ptr error now names the map
type, not the missing BTF, and the new !btf branch looks unreachable
- Co-developed-by is followed by a placeholder, not a Signed-off-by (commit
message)
> bpf: fetch the vmlinux BTF where kernel types enter a program
>
> bpf_check() fetches the vmlinux BTF up front for every program, whether
> the program uses kernel types or not. That is free today because the
> BTF is parsed at boot anyway. Once it can be parsed on first use
> (CONFIG_DEBUG_INFO_BTF_LAZY, later in this series), it would make every
> program load pay for parsing 5-7 MB of BTF, including the socket filters
> systemd loads at boot.
> Drop the up-front fetch and fetch where kernel types enter the verifier
> state instead:
> - bpf_add_kfunc_call(), for the first kfunc call of a program;
> - check_ptr_to_map_access(), for map_ptr field access;
> - check_pseudo_btf_id(), for ldimm64 of a kernel variable (ksym);
> - check_helper_call(), for helpers whose prototype takes
> ARG_PTR_TO_BTF_ID or returns RET_PTR_TO_BTF_ID, and for
> bpf_snprintf_btf() and bpf_seq_printf_btf(), which take a kernel
> type id inside struct btf_ptr (helper_uses_vmlinux_btf()).
> The other ways in already fetch the BTF themselves: attach_btf in
> bpf_prog_load(), struct_ops map creation, CO-RE candidate lookup,
> bpf_find_btf_id(), btf_prepare_func_args() for kernel-typed arguments
> and the kernel context type lookups. The remaining reads of the
> btf_vmlinux global in the verifier are on paths dominated by one of
> these fetches. A program that uses none of them, such as a socket
> filter, does not need the vmlinux BTF any more, and verifies the same
> whether or not it has been parsed.
> Four related changes keep it that way:
> - The "in-kernel BTF is malformed" check in bpf_check() read the
> global right after the up-front fetch. Without the fetch it would
> pass or fail depending on whether something else had parsed the BTF
> before, so drop it; every fetch point reports a missing BTF itself
> and passes on an error of the parse, as that check did.
> - check_ptr_to_map_access() checks allow_ptr_leaks before it fetches
> the BTF, and bpf_prog_load() falls back to the vmlinux BTF for an
> attach_btf_id only with CAP_BPF, which all program types that take
> one need, so that an unprivileged load cannot make the kernel parse
> it only to be refused. The map_ptr permission error can then not
> name the map's struct any more and says "map_ptr access is allowed
> only to CAP_PERFMON and CAP_SYS_ADMIN". A ksym, which unprivileged
> programs may use, still parses it.
> - Add bpf_peek_btf_vmlinux(), which returns the vmlinux BTF only if it
> is parsed already, and use it in bpf_btf_printf_prepare(). The BTF
> printf helpers run in program context, which may be atomic, and the
> verifier has fetched the BTF for every program that calls them.
> - is_kfunc_dynptr_write(), reached from the argument checks that
> helpers and kfuncs share, compared meta->btf with the global, which
> a helper's NULL matches while the global is NULL. Check that the
> call is a kfunc first, as the other kfunc predicates there do.
> Helpers taking ARG_PTR_TO_BTF_ID_SOCK_COMMON are left out on purpose:
> they take sockets without BTF types too, the shared argument check reads
> the global only for a PTR_TO_BTF_ID register, and every source of such
> a register fetched the BTF.
> No functional change with CONFIG_DEBUG_INFO_BTF=y, where kfunc
> registrations at boot parse the BTF, other than that error message and
> that, if the vmlinux BTF fails to parse (a broken toolchain), programs
> that use no kernel types now load instead of failing. Without BTF the
> new helper check is compiled out and the existing error paths are
> unchanged.
> Co-developed-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> Signed-off-by: Jay Wang <wanjay@amazon.com>
This isn't a bug, but the Co-developed-by: tag is followed by a placeholder
line, "TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai
<ihor.solodrai@linux.dev>", rather than a real Signed-off-by: line.
Documentation/process/submitting-patches.rst asks for a Co-developed-by: to
be immediately followed by that co-author's Signed-off-by:. Should the
placeholder be replaced with the real tag before this is applied?
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 1b6c0674ff6a..18b2bd009d58 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -6867,30 +6883,33 @@ static int check_ptr_to_map_access(struct bpf_verifier_env *env,
> enum bpf_type_flag flag = 0;
> const struct btf_type *t;
> const char *tname;
> + struct btf *btf;
> u32 btf_id;
> int ret;
>
> - if (!btf_vmlinux) {
> - verbose(env, "map_ptr access not supported without CONFIG_DEBUG_INFO_BTF\n");
> - return -ENOTSUPP;
> - }
> -
> if (!map->ops->map_btf_id || !*map->ops->map_btf_id) {
> verbose(env, "map_ptr access not supported for map type %d\n",
> map->map_type);
> return -ENOTSUPP;
> }
>
> - t = btf_type_by_id(btf_vmlinux, *map->ops->map_btf_id);
> - tname = btf_name_by_offset(btf_vmlinux, t->name_off);
> -
> + /* before the BTF fetch: unprivileged loads must not parse it */
> if (!env->allow_ptr_leaks) {
> - verbose(env,
> - "'struct %s' access is allowed only to CAP_PERFMON and CAP_SYS_ADMIN\n",
> - tname);
> + verbose(env, "map_ptr access is allowed only to CAP_PERFMON and CAP_SYS_ADMIN\n");
> return -EPERM;
> }
>
> + btf = bpf_get_btf_vmlinux();
> + if (IS_ERR(btf))
> + return vmlinux_btf_err(env, btf);
> + if (!btf) {
> + verbose(env, "map_ptr access not supported without CONFIG_DEBUG_INFO_BTF\n");
> + return -ENOTSUPP;
> + }
> +
> + t = btf_type_by_id(btf, *map->ops->map_btf_id);
> + tname = btf_name_by_offset(btf, t->name_off);
> +
> if (off < 0) {
Can the new "if (!btf)" branch ever be reached?
Without CONFIG_DEBUG_INFO_BTF, every map type's .map_btf_id points at a
zero-filled array:
include/linux/btf_ids.h:
#define BTF_ID_LIST_SINGLE(name, prefix, typename) static u32 __maybe_unused name[1];
so "!*map->ops->map_btf_id" is true for every map, and the function returns
at the first check with "map_ptr access not supported for map type %d".
With CONFIG_DEBUG_INFO_BTF=y, bpf_get_btf_vmlinux() returns either a BTF or
an ERR_PTR, never NULL, so the branch is unreachable in that configuration
too.
Before this patch, "if (!btf_vmlinux)" was the first test. On a kernel
without BTF, a privileged program doing "r0 = *(u32 *)(map_ptr + off)" was
told:
map_ptr access not supported without CONFIG_DEBUG_INFO_BTF
It is now told, for example:
map_ptr access not supported for map type 1
That message points at the map type rather than at the missing BTF. The
errno (-ENOTSUPP) is unchanged.
This also looks at odds with the commit message statement "Without BTF the
new helper check is compiled out and the existing error paths are
unchanged."
Would an early IS_ENABLED(CONFIG_DEBUG_INFO_BTF) test with the old message,
placed before the map_btf_id check, be a way to keep the old diagnostic? It
would not fetch or parse anything, so the allow_ptr_leaks-before-fetch
ordering would stay as is.
No later commit in the series changes check_ptr_to_map_access().
---
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/41205212136
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH bpf-next 03/13] bpf: check CAP_SYS_ADMIN before looking up bpffs delegation names
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:09 ` [PATCH bpf-next 02/13] bpf: fetch the vmlinux BTF where kernel types enter a program Jay Wang
@ 2026-10-31 13:09 ` 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
` (9 subsequent siblings)
12 siblings, 2 replies; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
From: Ihor Solodrai <ihor.solodrai@linux.dev>
bpf_parse_param() resolves the symbolic values of the delegate_cmds,
delegate_maps, delegate_progs and delegate_attachs mount options with
find_bpffs_btf_enums(), which fetches the vmlinux BTF, and only then
refuses a non-zero mask from a caller without CAP_SYS_ADMIN. bpffs can
be mounted in a user namespace, so any local user can run
"unshare -Urm mount -t bpf -o delegate_cmds=any bpf /mnt" and get
EPERM, after the lookup.
That is free today because the vmlinux BTF is parsed at boot. Once it
is parsed on first use (CONFIG_DEBUG_INFO_BTF_LAZY, later in this
series), it would let any user make the kernel parse it and keep it
parsed until reboot, which is what the option is meant to avoid on
systems that do not use BTF.
Check the capability first. An unprivileged caller could only ever set
an empty mask; such a no-op delegate option, and a malformed value,
now fail with EPERM as well instead of being accepted or failing with
EINVAL. Privileged callers are not affected.
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
kernel/bpf/inode.c | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/kernel/bpf/inode.c b/kernel/bpf/inode.c
index c6f328e4752e..39750d4ad401 100644
--- a/kernel/bpf/inode.c
+++ b/kernel/bpf/inode.c
@@ -1059,6 +1059,13 @@ static int bpf_parse_param(struct fs_context *fc, struct fs_parameter *param)
char *p, *str;
int val;
+ /*
+ * Setting delegation mount options requires privileges. Check
+ * before the lookup, which may have to parse the vmlinux BTF.
+ */
+ if (!capable(CAP_SYS_ADMIN))
+ return -EPERM;
+
/* ignore errors, fallback to hex */
(void)find_bpffs_btf_enums(&info);
@@ -1100,10 +1107,6 @@ static int bpf_parse_param(struct fs_context *fc, struct fs_parameter *param)
}
}
- /* Setting delegation mount options requires privileges */
- if (msk && !capable(CAP_SYS_ADMIN))
- return -EPERM;
-
*delegate_msk |= msk;
break;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 03/13] bpf: check CAP_SYS_ADMIN before looking up bpffs delegation names
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
1 sibling, 0 replies; 26+ messages in thread
From: bot+bpf-ci @ 2026-10-31 13:43 UTC (permalink / raw)
To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
Cc: ihor.solodrai, almagui, martin.lau, yonghong.song, jolsa, rostedt,
mhiramat, mathieu.desnoyers, linux-trace-kernel, nathan, nsc,
linux-kbuild, arnd, linux-arch, x86, linux-doc, linux-kernel,
abuehaze, doebel, mpohlack, jay.wang.upstream, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2506 bytes --]
> bpf: check CAP_SYS_ADMIN before looking up bpffs delegation names
TL;DR: Commit message only: the "TODO-NEEDS-IHOR:" prefix on Ihor Solodrai's
Signed-off-by line looks like a leftover placeholder, so the tag isn't parsed.
The code change itself looks fine.
> bpf: check CAP_SYS_ADMIN before looking up bpffs delegation names
>
> bpf_parse_param() resolves the symbolic values of the delegate_cmds,
> delegate_maps, delegate_progs and delegate_attachs mount options with
> find_bpffs_btf_enums(), which fetches the vmlinux BTF, and only then
> refuses a non-zero mask from a caller without CAP_SYS_ADMIN. bpffs can
> be mounted in a user namespace, so any local user can run
> "unshare -Urm mount -t bpf -o delegate_cmds=any bpf /mnt" and get
> EPERM, after the lookup.
>
> That is free today because the vmlinux BTF is parsed at boot. Once it
> is parsed on first use (CONFIG_DEBUG_INFO_BTF_LAZY, later in this
> series), it would let any user make the kernel parse it and keep it
> parsed until reboot, which is what the option is meant to avoid on
> systems that do not use BTF.
>
> Check the capability first. An unprivileged caller could only ever set
> an empty mask; such a no-op delegate option, and a malformed value,
> now fail with EPERM as well instead of being accepted or failing with
> EINVAL. Privileged callers are not affected.
>
> TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> Signed-off-by: Jay Wang <wanjay@amazon.com>
Is the "TODO-NEEDS-IHOR:" prefix on the first sign-off line a leftover
placeholder?
With that prefix the line is not parsed as a Signed-off-by tag by git, b4 or
checkpatch, so the only valid sign-off on the commit is the one from Jay Wang,
and the commit has no Signed-off-by matching its From: identity.
The same prefix appears to be present on the other commits in this series.
Should the line be resolved before this is applied, either to a plain:
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
or dropped?
The code change itself looks fine. The unconditional capable(CAP_SYS_ADMIN)
runs strictly before the old "msk && !capable(CAP_SYS_ADMIN)" test, and
privileged callers see no difference.
---
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/41205212136
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH bpf-next 03/13] bpf: check CAP_SYS_ADMIN before looking up bpffs delegation names
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
1 sibling, 0 replies; 26+ messages in thread
From: Alexei Starovoitov @ 2026-10-31 17:03 UTC (permalink / raw)
To: Jay Wang, bpf, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman,
Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
On Sat, Oct 31, 2026 at 06:09 AM Jay Wang <wanjay@amazon.com> wrote:
> That is free today because the vmlinux BTF is parsed at boot. Once it
> is parsed on first use (CONFIG_DEBUG_INFO_BTF_LAZY, later in this
> series), it would let any user make the kernel parse it and keep it
> parsed until reboot, which is what the option is meant to avoid on
> systems that do not use BTF.
Any user can do that without this mount.
/sys/kernel/btf/vmlinux is 0444. With patch 9 one read() decompresses
5.5 MB into vmalloc_user() memory that is never freed.
The parse is the other 2 MB.
After the first use the kernel has what every BTF=y kernel has today.
There is nothing to guard.
> Check the capability first. An unprivileged caller could only ever set
> an empty mask; such a no-op delegate option, and a malformed value,
> now fail with EPERM as well instead of being accepted or failing with
> EINVAL. Privileged callers are not affected.
That changes what user space sees, with or without the option.
Drop this patch.
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH bpf-next 04/13] bpf: move bpf_get_btf_vmlinux() to btf.c
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
` (2 preceding siblings ...)
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:09 ` 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
` (8 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
From: Ihor Solodrai <ihor.solodrai@linux.dev>
Move bpf_get_btf_vmlinux(), bpf_peek_btf_vmlinux() and btf_vmlinux_lock
from verifier.c to btf.c, next to btf_parse_vmlinux().
Parsing the vmlinux BTF on first use (later in this series) makes the
accessor apply deferred kfunc, dtor kfunc and struct_ops registrations
and kept module BTF under btf_vmlinux_lock. That state lives in btf.c;
with the accessor and its lock there, it can stay static.
No functional change.
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
kernel/bpf/btf.c | 32 ++++++++++++++++++++++++++++++++
kernel/bpf/verifier.c | 31 -------------------------------
2 files changed, 32 insertions(+), 31 deletions(-)
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 1c7536a22a63..58d8b21d9895 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -6924,6 +6924,38 @@ struct btf *btf_parse_vmlinux(void)
return btf;
}
+static DEFINE_MUTEX(btf_vmlinux_lock);
+
+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();
+ /*
+ * Order the parsed BTF contents and the globals the
+ * parse populated (e.g. bpf_ctx_convert.t) before
+ * the pointer publication. Pairs with the acquire
+ * on the lockless fast path above.
+ */
+ smp_store_release(&btf_vmlinux, btf);
+ }
+ mutex_unlock(&btf_vmlinux_lock);
+ }
+ return btf;
+}
+
+/* The vmlinux BTF if it is parsed already: for callers that cannot sleep */
+struct btf *bpf_peek_btf_vmlinux(void)
+{
+ /* Pairs with the smp_store_release() in bpf_get_btf_vmlinux() */
+ return smp_load_acquire(&btf_vmlinux);
+}
+
/* If .BTF_ids section was created with distilled base BTF, both base and
* split BTF ids will need to be mapped to actual base/split ids for
* BTF now that it has been relocated.
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 1b901e3cb3c2..e1514a0c2289 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -317,7 +317,6 @@ static const char *btf_type_name(const struct btf *btf, u32 id)
}
static DEFINE_MUTEX(bpf_verifier_lock);
-static DEFINE_MUTEX(btf_vmlinux_lock);
static DEFINE_MUTEX(bpf_percpu_ma_lock);
__printf(2, 3) static void verbose(void *private_data, const char *fmt, ...)
@@ -23144,36 +23143,6 @@ int bpf_check_attach_btf_id_multi(struct btf *btf, struct bpf_prog *prog, u32 bt
return 0;
}
-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();
- /*
- * Order the parsed BTF contents and the globals the
- * parse populated (e.g. bpf_ctx_convert.t) before
- * the pointer publication. Pairs with the acquire
- * on the lockless fast path above.
- */
- smp_store_release(&btf_vmlinux, btf);
- }
- mutex_unlock(&btf_vmlinux_lock);
- }
- return btf;
-}
-
-/* The vmlinux BTF if it is parsed already: for callers that cannot sleep */
-struct btf *bpf_peek_btf_vmlinux(void)
-{
- /* Pairs with the smp_store_release() in bpf_get_btf_vmlinux() */
- return smp_load_acquire(&btf_vmlinux);
-}
-
/*
* The add_fd_from_fd_array() is executed only if fd_array_cnt is non-zero. In
* this case expect that every file descriptor in the array is either a map or
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 04/13] bpf: move bpf_get_btf_vmlinux() to btf.c
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
0 siblings, 0 replies; 26+ messages in thread
From: bot+bpf-ci @ 2026-10-31 13:43 UTC (permalink / raw)
To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
Cc: ihor.solodrai, almagui, martin.lau, yonghong.song, jolsa, rostedt,
mhiramat, mathieu.desnoyers, linux-trace-kernel, nathan, nsc,
linux-kbuild, arnd, linux-arch, x86, linux-doc, linux-kernel,
abuehaze, doebel, mpohlack, jay.wang.upstream, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1790 bytes --]
> bpf: move bpf_get_btf_vmlinux() to btf.c
TL;DR: Commit message only: the author's Signed-off-by is behind a
TODO-NEEDS-IHOR placeholder, so it won't parse as a trailer; most other
patches in the series have the same placeholder.
> bpf: move bpf_get_btf_vmlinux() to btf.c
>
> Move bpf_get_btf_vmlinux(), bpf_peek_btf_vmlinux() and btf_vmlinux_lock
> from verifier.c to btf.c, next to btf_parse_vmlinux().
>
> Parsing the vmlinux BTF on first use (later in this series) makes the
> accessor apply deferred kfunc, dtor kfunc and struct_ops registrations
> and kept module BTF under btf_vmlinux_lock. That state lives in btf.c;
> with the accessor and its lock there, it can stay static.
>
> No functional change.
>
> TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> Signed-off-by: Jay Wang <wanjay@amazon.com>
The author's sign-off is behind a placeholder prefix, so git
interpret-trailers will read that line as a TODO-NEEDS-IHOR trailer and not
as a Signed-off-by. That leaves only the Signed-off-by from Jay Wang in the
trailer block.
Documentation/process/submitting-patches.rst asks for the author's
Signed-off-by ahead of the sender's. Should the placeholder line be
replaced with a real "Signed-off-by: Ihor Solodrai
<ihor.solodrai@linux.dev>" once the author provides it?
The same placeholder shows up in most of the other commits in this series
(2a58754b9ead, 32ad8adbb1a8, cc9b43c0e23c, 699a8e9536e8, c97dea7ddbb0,
38e2751cdb40, 66c4a6a9293d, e46b5971bc91, cae96a9f7395), so those would need
the same fix.
---
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/41205212136
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH bpf-next 05/13] bpf: let btf_parse_module() adopt the caller's copy of the module BTF
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
` (3 preceding siblings ...)
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:09 ` 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
` (7 subsequent siblings)
12 siblings, 0 replies; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
btf_parse_module() makes its own kvmemdup() of the module's .BTF
section, and of its .BTF.inline section through btf_parse_split().
Move both copies to the caller, btf_module_notify(), and let the result
take ownership of them: the new struct btf the module BTF copy, and the
returned inline BTF the inline copy, relocated in place. On failure,
and for an inline BTF that fails validation, the caller frees what was
not taken.
No functional change: /sys/kernel/btf/<module> and <module>.inline keep
serving btf->data and the relocated inline BTF, which are the same
buffers as before.
This prepares for parsing the vmlinux BTF on first use (later in this
series): a module that loads before that keeps its copies unparsed and
exposes them in sysfs right away. Parsing them later must adopt those
buffers rather than make second copies, so that the sysfs files stay
valid and the data exists only once.
Co-developed-by: Ihor Solodrai <ihor.solodrai@linux.dev>
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
kernel/bpf/btf.c | 47 ++++++++++++++++++++++++++++++++++-------------
1 file changed, 34 insertions(+), 13 deletions(-)
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 58d8b21d9895..59d6f44cbe17 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -6969,8 +6969,9 @@ __u32 btf_relocate_id(const struct btf *btf, __u32 id)
#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+/* @data is a copy owned by the returned BTF; the caller frees it on error */
static struct btf *btf_parse_split(struct btf_verifier_env *env,
- const char *name, const void *data,
+ const char *name, void *data,
unsigned int data_size, struct btf *base_btf)
{
struct btf *btf;
@@ -6986,11 +6987,7 @@ static struct btf *btf_parse_split(struct btf_verifier_env *env,
btf->named_start_id = 0;
strscpy(btf->name, name);
- btf->data = kvmemdup(data, data_size, GFP_KERNEL | __GFP_NOWARN);
- if (!btf->data) {
- err = -ENOMEM;
- goto errout;
- }
+ btf->data = data;
btf->data_size = data_size;
err = btf_parse_hdr(env);
@@ -7014,6 +7011,7 @@ static struct btf *btf_parse_split(struct btf_verifier_env *env,
return btf;
errout:
+ btf->data = NULL;
btf_free(btf);
return ERR_PTR(err);
}
@@ -7085,10 +7083,16 @@ static int btf_rebase_inline(struct btf *inline_btf,
return 0;
}
-static struct btf *btf_parse_module(const char *module_name, const void *data,
+/*
+ * @data and @inline_data are copies made by the caller. On success the
+ * returned BTF owns @data, and *@relocated_inline_data is @inline_data,
+ * relocated in place, unless the inline BTF is invalid. The caller frees
+ * the copies this does not take.
+ */
+static struct btf *btf_parse_module(const char *module_name, void *data,
unsigned int data_size, void *base_data,
unsigned int base_data_size,
- const void *inline_data,
+ void *inline_data,
unsigned int inline_data_size,
void **relocated_inline_data)
{
@@ -7156,6 +7160,7 @@ static struct btf *btf_parse_module(const char *module_name, const void *data,
if (err) {
pr_warn("failed to relocate module [%s] inline BTF: %d\n",
module_name, err);
+ inline_btf->data = NULL;
btf_free(inline_btf);
} else {
*relocated_inline_data = inline_btf->data;
@@ -7179,12 +7184,16 @@ static struct btf *btf_parse_module(const char *module_name, const void *data,
errout:
kvfree(module_str_map);
btf_verifier_env_free(env);
- if (inline_btf)
+ if (inline_btf) {
+ inline_btf->data = NULL;
btf_free(inline_btf);
+ }
if (!IS_ERR(base_btf) && base_btf != vmlinux_btf)
btf_free(base_btf);
- if (btf)
+ if (btf) {
+ btf->data = NULL;
btf_free(btf);
+ }
return ERR_PTR(err);
}
@@ -9445,7 +9454,7 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
struct module *mod = module;
struct bin_attribute *attr;
struct btf *btf;
- void *inline_data = NULL, *relocated_inline_data = NULL;
+ void *data, *inline_data = NULL, *relocated_inline_data = NULL;
unsigned int inline_data_size = 0;
int err = 0;
@@ -9473,11 +9482,23 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
err = -ENOMEM;
goto out;
}
- btf = btf_parse_module(mod->name, mod->btf_data, mod->btf_data_size,
+ data = kvmemdup(mod->btf_data, mod->btf_data_size, GFP_KERNEL | __GFP_NOWARN);
+ if (inline_data) {
+ inline_data = kvmemdup(inline_data, inline_data_size,
+ GFP_KERNEL | __GFP_NOWARN);
+ if (!inline_data)
+ pr_warn("failed to validate module [%s] inline BTF: %d\n",
+ mod->name, -ENOMEM);
+ }
+ btf = !data ? ERR_PTR(-ENOMEM) :
+ btf_parse_module(mod->name, data, mod->btf_data_size,
mod->btf_base_data, mod->btf_base_data_size,
- inline_data, inline_data_size,
+ inline_data, inline_data ? inline_data_size : 0,
&relocated_inline_data);
+ if (inline_data != relocated_inline_data)
+ kvfree(inline_data);
if (IS_ERR(btf)) {
+ kvfree(data);
kfree(btf_mod);
if (!IS_ENABLED(CONFIG_MODULE_ALLOW_BTF_MISMATCH)) {
pr_warn("failed to validate module [%s] BTF: %ld\n",
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* [PATCH bpf-next 06/13] bpf: look up the struct_ops common value type in the given BTF
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
` (4 preceding siblings ...)
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 ` Jay Wang
2026-10-31 13:09 ` [PATCH bpf-next 07/13] bpf: parse the vmlinux BTF before taking cand_cache_mutex Jay Wang
` (6 subsequent siblings)
12 siblings, 0 replies; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
is_valid_value_type() looks up bpf_struct_ops_common_value in the
btf_vmlinux global, while every other lookup in it uses the btf it is
passed. The id is a vmlinux id, and a module BTF resolves vmlinux ids
through its base BTF, so using the passed btf gives the same result.
Once the vmlinux BTF can be parsed on first use (later in this series),
struct_ops registrations made before that are applied by the parsing
task before it publishes the BTF, while btf_vmlinux is still NULL; the
registration then has to use the BTF it is given.
No functional change.
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
kernel/bpf/bpf_struct_ops.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/kernel/bpf/bpf_struct_ops.c b/kernel/bpf/bpf_struct_ops.c
index 1178acd72296..a3d801a7e99f 100644
--- a/kernel/bpf/bpf_struct_ops.c
+++ b/kernel/bpf/bpf_struct_ops.c
@@ -103,8 +103,8 @@ static bool is_valid_value_type(struct btf *btf, s32 value_id,
}
member = btf_type_member(vt);
mt = btf_type_by_id(btf, member->type);
- common_value_type = btf_type_by_id(btf_vmlinux,
- st_ops_ids[IDX_ST_OPS_COMMON_VALUE_ID]);
+ /* a vmlinux id: btf may be the vmlinux BTF being published, or a module's */
+ common_value_type = btf_type_by_id(btf, st_ops_ids[IDX_ST_OPS_COMMON_VALUE_ID]);
if (mt != common_value_type) {
pr_warn("The first member of %s should be bpf_struct_ops_common_value\n",
value_name);
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* [PATCH bpf-next 07/13] bpf: parse the vmlinux BTF before taking cand_cache_mutex
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
` (5 preceding siblings ...)
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 ` 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
` (5 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
From: Ihor Solodrai <ihor.solodrai@linux.dev>
bpf_core_find_cands() calls bpf_get_btf_vmlinux() with cand_cache_mutex
held, from btf_get_ptr_to_btf_id() and bpf_core_apply(). Once the
vmlinux BTF is parsed on first use (later in this series), parsing also
parses the module BTFs kept until then, under btf_module_mutex, while a
module going away takes btf_module_mutex and then cand_cache_mutex
(purge_cand_cache()). That would be an ABBA deadlock:
cand_cache_mutex -> btf_vmlinux_lock -> btf_module_mutex
btf_module_mutex -> cand_cache_mutex
Have both callers fetch the BTF before they take cand_cache_mutex, and
make bpf_core_find_cands() only peek with bpf_peek_btf_vmlinux(). The
ordering was found by design review. With the whole series applied
and the fetch moved back under the mutex, PROVE_LOCKING reports this
cycle at the first rmmod of a BTF module after a kernel CO-RE load
parsed the BTF; with this patch the same sequence is clean.
No functional change: the BTF is parsed at the same points, just
before the mutex is taken; a broken BTF fails the load with the error
of its parse, and bpf_core_find_cands() handles a missing BTF as before.
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
[pass the error of the fetch on instead of -EINVAL from the peek]
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
kernel/bpf/btf.c | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 59d6f44cbe17..70adc52f5d7c 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -8596,6 +8596,7 @@ static int btf_get_ptr_to_btf_id(struct bpf_verifier_log *log, int arg_idx,
.btf = btf,
.log = log,
};
+ struct btf *vmlinux_btf;
u32 kern_type_id, type_id;
int err = 0;
@@ -8607,6 +8608,10 @@ static int btf_get_ptr_to_btf_id(struct bpf_verifier_log *log, int arg_idx,
t = btf_type_by_id(btf, t->type);
}
+ /* parse it, if this is its first use, before taking cand_cache_mutex */
+ vmlinux_btf = bpf_get_btf_vmlinux();
+ if (IS_ERR(vmlinux_btf))
+ return PTR_ERR(vmlinux_btf);
mutex_lock(&cand_cache_mutex);
cc = bpf_core_find_cands(&ctx, type_id);
if (IS_ERR(cc)) {
@@ -10610,7 +10615,8 @@ bpf_core_find_cands(struct bpf_core_ctx *ctx, u32 local_type_id)
const char *name;
int id;
- main_btf = bpf_get_btf_vmlinux();
+ /* callers parse it before taking cand_cache_mutex, see bpf_core_apply() */
+ main_btf = bpf_peek_btf_vmlinux();
if (IS_ERR(main_btf))
return ERR_CAST(main_btf);
if (!main_btf)
@@ -10713,8 +10719,18 @@ int bpf_core_apply(struct bpf_core_ctx *ctx, const struct bpf_core_relo *relo,
if (need_cands) {
struct bpf_cand_cache *cc;
+ struct btf *main_btf;
int i;
+ /*
+ * Parsing the vmlinux BTF takes btf_module_mutex, which is
+ * taken before cand_cache_mutex when a module goes away.
+ */
+ main_btf = bpf_get_btf_vmlinux();
+ if (IS_ERR(main_btf)) {
+ kfree(specs);
+ return PTR_ERR(main_btf);
+ }
mutex_lock(&cand_cache_mutex);
cc = bpf_core_find_cands(ctx, relo->type_id);
if (IS_ERR(cc)) {
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 07/13] bpf: parse the vmlinux BTF before taking cand_cache_mutex
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
0 siblings, 0 replies; 26+ messages in thread
From: bot+bpf-ci @ 2026-10-31 13:43 UTC (permalink / raw)
To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
Cc: ihor.solodrai, almagui, martin.lau, yonghong.song, jolsa, rostedt,
mhiramat, mathieu.desnoyers, linux-trace-kernel, nathan, nsc,
linux-kbuild, arnd, linux-arch, x86, linux-doc, linux-kernel,
abuehaze, doebel, mpohlack, jay.wang.upstream, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 5325 bytes --]
> bpf: parse the vmlinux BTF before taking cand_cache_mutex
TL;DR:
- New early returns on vmlinux BTF fetch failure may skip verifier log
messages in btf.c, contradicting "No functional change"
- Author's Signed-off-by is a TODO placeholder
> bpf: parse the vmlinux BTF before taking cand_cache_mutex
>
> bpf_core_find_cands() calls bpf_get_btf_vmlinux() with cand_cache_mutex
> held, from btf_get_ptr_to_btf_id() and bpf_core_apply(). Once the
> vmlinux BTF is parsed on first use (later in this series), parsing also
> parses the module BTFs kept until then, under btf_module_mutex, while a
> module going away takes btf_module_mutex and then cand_cache_mutex
> (purge_cand_cache()). That would be an ABBA deadlock:
>
> cand_cache_mutex -> btf_vmlinux_lock -> btf_module_mutex
> btf_module_mutex -> cand_cache_mutex
>
> Have both callers fetch the BTF before they take cand_cache_mutex, and
> make bpf_core_find_cands() only peek with bpf_peek_btf_vmlinux(). The
> ordering was found by design review. With the whole series applied
> and the fetch moved back under the mutex, PROVE_LOCKING reports this
> cycle at the first rmmod of a BTF module after a kernel CO-RE load
> parsed the BTF; with this patch the same sequence is clean.
>
> No functional change: the BTF is parsed at the same points, just
> before the mutex is taken; a broken BTF fails the load with the error
> of its parse, and bpf_core_find_cands() handles a missing BTF as before.
>
> TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> [pass the error of the fetch on instead of -EINVAL from the peek]
> Signed-off-by: Jay Wang <wanjay@amazon.com>
The author's sign-off is the placeholder line "TODO-NEEDS-IHOR:
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>", and the only real
Signed-off-by tag is the one from Jay Wang.
Can this be applied with that placeholder in place? The submitting-patches
document (Documentation/process/submitting-patches.rst) expects a
Signed-off-by from the author, and checkpatch will flag both the unknown tag
and the missing sign-off.
Separately, the commit message says "No functional change", but the verifier
log differs when the vmlinux BTF fetch fails. Before this patch the fetch
ran inside bpf_core_find_cands() and both callers logged the failure. After
it, the new early returns skip that logging (see the comments on the diff
below). Could that sentence be reworded to match?
> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> index 3e947bdb224c..095362c0a1cd 100644
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c
[ ... ]
> @@ -8608,6 +8609,10 @@ static int btf_get_ptr_to_btf_id(struct bpf_verifier_log *log, int arg_idx,
> t = btf_type_by_id(btf, t->type);
> }
>
> + /* parse it, if this is its first use, before taking cand_cache_mutex */
> + vmlinux_btf = bpf_get_btf_vmlinux();
> + if (IS_ERR(vmlinux_btf))
> + return PTR_ERR(vmlinux_btf);
> mutex_lock(&cand_cache_mutex);
> cc = bpf_core_find_cands(&ctx, type_id);
> if (IS_ERR(cc)) {
Previously a failed fetch came back through bpf_core_find_cands() and ended
up in the bpf_log() just below:
bpf_log(log, "arg#%d reference type('%s %s') candidate matching error: %d\n",
arg_idx, btf_type_str(t), __btf_name_by_offset(btf, t->name_off),
err);
Doesn't this early return skip that message, so a load that fails because
btf_vmlinux is an ERR_PTR (btf_parse_vmlinux() failing with -ENOMEM or on
malformed BTF) now returns a bare errno with nothing in the verifier log?
Earlier in this series, commit 32ad8adbb1a8 ("bpf: fetch the vmlinux BTF
where kernel types enter a program") added vmlinux_btf_err() in verifier.c so
that every fetch point reports a failed parse:
verbose(env, "in-kernel BTF could not be parsed: %ld\n", PTR_ERR(btf));
Would a bpf_log() before this return, along the lines of that message, keep
the diagnostic? None of the later patches in this series appears to add one
at this site.
[ ... ]
> @@ -10725,8 +10731,18 @@ int bpf_core_apply(struct bpf_core_ctx *ctx, const struct bpf_core_relo *relo,
>
> if (need_cands) {
> struct bpf_cand_cache *cc;
> + struct btf *main_btf;
> int i;
>
> + /*
> + * Parsing the vmlinux BTF takes btf_module_mutex, which is
> + * taken before cand_cache_mutex when a module goes away.
> + */
> + main_btf = bpf_get_btf_vmlinux();
> + if (IS_ERR(main_btf)) {
> + kfree(specs);
> + return PTR_ERR(main_btf);
> + }
> mutex_lock(&cand_cache_mutex);
> cc = bpf_core_find_cands(ctx, relo->type_id);
> if (IS_ERR(cc)) {
The same applies here. A failed fetch used to reach the bpf_log() in this
branch:
bpf_log(ctx->log, "target candidate search failed for %d\n",
relo->type_id);
and, at BPF_LOG_LEVEL2, the candidate cache dump that follows it.
Can this return PTR_ERR(main_btf) leave the CO-RE relocation failing with no
log output at all? Would a bpf_log() here, matching the other fetch points
in the series, make sense?
---
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/41205212136
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH bpf-next 08/13] tracing: parse the vmlinux BTF when BTF-typed output is enabled
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
` (6 preceding siblings ...)
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:09 ` Jay Wang
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
` (4 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
From: Ihor Solodrai <ihor.solodrai@linux.dev>
func-args of the function tracer, and funcgraph-args and
funcgraph-retval of the function_graph tracer, print arguments and
return values by their BTF types: print_function_args() and
btf_trim_retval() look the function up with btf_find_func_proto(),
which parses the vmlinux BTF if nobody has yet. Since commit
42643ba35deb ("tracing: Avoid BTF lookup in atomic context") they do not
do that in atomic context, but they still can in the readers of the
trace: s_show() and tracing_read_pipe() print under trace_event_sem and
the trace access lock, while the tracer keeps recording, by default
into the buffer being read.
Today the BTF is always parsed at boot, so this cannot happen. To keep
it that way once the vmlinux BTF can be parsed on first use
(CONFIG_DEBUG_INFO_BTF_LAZY, later in this series), parse it when such
output is enabled instead: trace_btf_prepare() is called from
select_trace_function() when func-args is set, from graph_trace_init()
when funcgraph-args or funcgraph-retval is set, and from
func_graph_set_flag() when either is turned on, all with
trace_types_lock held. The two printers only peek at the BTF and fall
back to the output without it if it is not parsed, which then cannot
happen while the option is in effect.
"trace_options=" on the command line sets the options from
early_trace_init() in start_kernel(), with interrupts disabled and
before btf_alloc_id() can allocate; while the system is still booting
(system_state < SYSTEM_SCHEDULING) trace_btf_prepare() only records the
request, and a core_initcall parses the BTF.
Reading an event's btf_ids file looks the BTF up under event_mutex;
parse it before taking the mutex, as the CO-RE candidate lookup does
before cand_cache_mutex.
No functional change with today's eager parse, except that with such
an option on the command line the BTF is parsed at core_initcall time,
before the initcalls that would otherwise parse it.
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
[moved trace_func_args_prepare() to trace_btf.c as trace_btf_prepare();
extended it to funcgraph-retval and the btf_ids file; the printers check
the BTF with bpf_peek_btf_vmlinux() in front of btf_find_func_proto()]
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
kernel/trace/trace_btf.c | 32 ++++++++++++++++++++++++++++
kernel/trace/trace_btf.h | 3 +++
kernel/trace/trace_events.c | 4 ++++
kernel/trace/trace_functions.c | 4 ++++
kernel/trace/trace_functions_graph.c | 15 +++++++++++--
kernel/trace/trace_output.c | 3 +++
6 files changed, 59 insertions(+), 2 deletions(-)
diff --git a/kernel/trace/trace_btf.c b/kernel/trace/trace_btf.c
index f0e0c04d58d9..693c1a1083c5 100644
--- a/kernel/trace/trace_btf.c
+++ b/kernel/trace/trace_btf.c
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: GPL-2.0
+#include <linux/bpf.h>
#include <linux/btf.h>
#include <linux/kernel.h>
#include <linux/slab.h>
@@ -141,6 +142,34 @@ const struct btf_member *btf_find_struct_member(struct btf *btf,
return member;
}
+static bool trace_btf_deferred;
+
+/*
+ * func-args, funcgraph-args and funcgraph-retval print arguments and return
+ * values by their BTF types, and the readers of the trace use the vmlinux
+ * BTF only if it is parsed already: parse it when such output is enabled.
+ * With CONFIG_DEBUG_INFO_BTF_LAZY that may be its first use, which should
+ * not happen in a reader that holds the trace read locks while the tracer
+ * records its work.
+ */
+void trace_btf_prepare(void)
+{
+ /* trace_options= on the command line is applied by early_trace_init() */
+ if (system_state < SYSTEM_SCHEDULING) {
+ trace_btf_deferred = true;
+ return;
+ }
+ bpf_get_btf_vmlinux();
+}
+
+static int __init trace_btf_init(void)
+{
+ if (trace_btf_deferred)
+ bpf_get_btf_vmlinux();
+ return 0;
+}
+core_initcall(trace_btf_init);
+
void btf_trim_retval(unsigned long func, unsigned long *retval, bool *print_retval,
int *fmt, bool hex)
{
@@ -151,6 +180,9 @@ void btf_trim_retval(unsigned long func, unsigned long *retval, bool *print_retv
bool signed_type;
int kind;
+ /* see trace_btf_prepare() */
+ if (IS_ERR_OR_NULL(bpf_peek_btf_vmlinux()))
+ return;
if (lookup_symbol_name(func, name))
return;
diff --git a/kernel/trace/trace_btf.h b/kernel/trace/trace_btf.h
index b3d408e6a7f6..47407eff1339 100644
--- a/kernel/trace/trace_btf.h
+++ b/kernel/trace/trace_btf.h
@@ -23,9 +23,12 @@ const struct btf_member *btf_find_struct_member(struct btf *btf,
#ifdef CONFIG_DEBUG_INFO_BTF
void btf_trim_retval(unsigned long func, unsigned long *retval, bool *print_retval,
int *fmt, bool hex);
+void trace_btf_prepare(void);
#else
static inline void btf_trim_retval(unsigned long func, unsigned long *retval,
bool *print_retval, int *fmt, bool hex)
{
}
+
+static inline void trace_btf_prepare(void) { }
#endif
diff --git a/kernel/trace/trace_events.c b/kernel/trace/trace_events.c
index 49d135bc2fbf..adc422fbe16e 100644
--- a/kernel/trace/trace_events.c
+++ b/kernel/trace/trace_events.c
@@ -23,6 +23,7 @@
#include <linux/sort.h>
#include <linux/slab.h>
#include <linux/delay.h>
+#include <linux/bpf.h>
#include <linux/btf.h>
#include <kunit/visibility.h>
@@ -2282,6 +2283,9 @@ event_btf_ids_read(struct file *filp, char __user *ubuf, size_t cnt, loff_t *ppo
char buf[128];
int len;
+ /* parse the vmlinux BTF, if this is its first use, without event_mutex */
+ bpf_get_btf_vmlinux();
+
/* Module unload could free call->class and ids[] mid-read. */
scoped_guard(mutex, &event_mutex) {
file = event_file_file(filp);
diff --git a/kernel/trace/trace_functions.c b/kernel/trace/trace_functions.c
index acaba990aaaa..2cebaacda526 100644
--- a/kernel/trace/trace_functions.c
+++ b/kernel/trace/trace_functions.c
@@ -18,6 +18,7 @@
#include <linux/fs.h>
#include "trace.h"
+#include "trace_btf.h"
static void tracing_start_function_trace(struct trace_array *tr);
static void tracing_stop_function_trace(struct trace_array *tr);
@@ -115,6 +116,9 @@ void ftrace_destroy_function_files(struct trace_array *tr)
static ftrace_func_t select_trace_function(u32 flags_val)
{
+ if (flags_val & TRACE_FUNC_OPT_ARGS)
+ trace_btf_prepare();
+
switch (flags_val & TRACE_FUNC_OPT_MASK) {
case TRACE_FUNC_NO_OPTS:
return function_trace_call;
diff --git a/kernel/trace/trace_functions_graph.c b/kernel/trace/trace_functions_graph.c
index 78a608315dd3..cf5f433dd26e 100644
--- a/kernel/trace/trace_functions_graph.c
+++ b/kernel/trace/trace_functions_graph.c
@@ -474,6 +474,10 @@ static int graph_trace_init(struct trace_array *tr)
{
int ret;
+ if (tracer_flags_is_set(tr, TRACE_GRAPH_ARGS) ||
+ tracer_flags_is_set(tr, TRACE_GRAPH_PRINT_RETVAL))
+ trace_btf_prepare();
+
if (tracer_flags_is_set(tr, TRACE_GRAPH_ARGS))
tr->gops->entryfunc = trace_graph_entry_args;
else
@@ -507,10 +511,12 @@ static int ftrace_graph_trace_args(struct trace_array *tr, int set)
{
trace_func_graph_ent_t entry;
- if (set)
+ if (set) {
+ trace_btf_prepare();
entry = trace_graph_entry_args;
- else
+ } else {
entry = trace_graph_entry;
+ }
/* See if there's any changes */
if (tr->gops->entryfunc == entry)
@@ -1746,6 +1752,11 @@ func_graph_set_flag(struct trace_array *tr, u32 old_flags, u32 bit, int set)
case TRACE_GRAPH_ARGS:
return ftrace_graph_trace_args(tr, set);
+
+ case TRACE_GRAPH_PRINT_RETVAL:
+ if (set)
+ trace_btf_prepare();
+ break;
}
return 0;
diff --git a/kernel/trace/trace_output.c b/kernel/trace/trace_output.c
index a5ad76175d10..cdb926c9d4e2 100644
--- a/kernel/trace/trace_output.c
+++ b/kernel/trace/trace_output.c
@@ -739,6 +739,9 @@ void print_function_args(struct trace_seq *s, unsigned long *args,
if (lookup_symbol_name(func, name))
goto out;
+ /* see trace_btf_prepare() */
+ if (IS_ERR_OR_NULL(bpf_peek_btf_vmlinux()))
+ goto out;
/* TODO: Pass module name here too */
t = btf_find_func_proto(name, &btf);
if (IS_ERR_OR_NULL(t))
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 08/13] tracing: parse the vmlinux BTF when BTF-typed output is enabled
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:43 ` bot+bpf-ci
0 siblings, 0 replies; 26+ messages in thread
From: bot+bpf-ci @ 2026-10-31 13:43 UTC (permalink / raw)
To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
Cc: ihor.solodrai, almagui, martin.lau, yonghong.song, jolsa, rostedt,
mhiramat, mathieu.desnoyers, linux-trace-kernel, nathan, nsc,
linux-kbuild, arnd, linux-arch, x86, linux-doc, linux-kernel,
abuehaze, doebel, mpohlack, jay.wang.upstream, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 6408 bytes --]
> tracing: parse the vmlinux BTF when BTF-typed output is enabled
TL;DR:
- irqsoff/wakeup tracers may print function lines without arguments, since
they never call trace_btf_prepare() and the lazy BTF may be unparsed
- Author's Signed-off-by is a TODO placeholder
> tracing: parse the vmlinux BTF when BTF-typed output is enabled
>
> func-args of the function tracer, and funcgraph-args and
> funcgraph-retval of the function_graph tracer, print arguments and
> return values by their BTF types: print_function_args() and
> btf_trim_retval() look the function up with btf_find_func_proto(),
> which parses the vmlinux BTF if nobody has yet. Since commit
> 42643ba35deb ("tracing: Avoid BTF lookup in atomic context") they do not
> do that in atomic context, but they still can in the readers of the
> trace: s_show() and tracing_read_pipe() print under trace_event_sem and
> the trace access lock, while the tracer keeps recording, by default
> into the buffer being read.
>
> Today the BTF is always parsed at boot, so this cannot happen. To keep
> it that way once the vmlinux BTF can be parsed on first use
> (CONFIG_DEBUG_INFO_BTF_LAZY, later in this series), parse it when such
> output is enabled instead: trace_btf_prepare() is called from
> select_trace_function() when func-args is set, from graph_trace_init()
> when funcgraph-args or funcgraph-retval is set, and from
> func_graph_set_flag() when either is turned on, all with
> trace_types_lock held. The two printers only peek at the BTF and fall
> back to the output without it if it is not parsed, which then cannot
> happen while the option is in effect.
>
> "trace_options=" on the command line sets the options from
> early_trace_init() in start_kernel(), with interrupts disabled and
> before btf_alloc_id() can allocate; while the system is still booting
> (system_state < SYSTEM_SCHEDULING) trace_btf_prepare() only records the
> request, and a core_initcall parses the BTF.
>
> Reading an event's btf_ids file looks the BTF up under event_mutex;
> parse it before taking the mutex, as the CO-RE candidate lookup does
> before cand_cache_mutex.
>
> No functional change with today's eager parse, except that with such
> an option on the command line the BTF is parsed at core_initcall time,
> before the initcalls that would otherwise parse it.
>
> TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> [moved trace_func_args_prepare() to trace_btf.c as trace_btf_prepare();
> extended it to funcgraph-retval and the btf_ids file; the printers check
> the BTF with bpf_peek_btf_vmlinux() in front of btf_find_func_proto()]
> Signed-off-by: Jay Wang <wanjay@amazon.com>
The patch is authored by Ihor Solodrai, but the only Signed-off-by in the
trailer block is Jay Wang's. Is this line meant to be the author's
sign-off?
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
With the "TODO-NEEDS-IHOR:" prefix it is not a Signed-off-by tag, so the
patch carries no sign-off from its author, which
Documentation/process/submitting-patches.rst requires. The placeholder
text would also end up in the changelog as is.
Should the prefix be dropped before this is applied?
> diff --git a/kernel/trace/trace_output.c b/kernel/trace/trace_output.c
> index a5ad76175d10..cdb926c9d4e2 100644
> --- a/kernel/trace/trace_output.c
> +++ b/kernel/trace/trace_output.c
[ ... ]
> @@ -739,6 +739,9 @@ void print_function_args(struct trace_seq *s, unsigned long *args,
> if (lookup_symbol_name(func, name))
> goto out;
>
> + /* see trace_btf_prepare() */
> + if (IS_ERR_OR_NULL(bpf_peek_btf_vmlinux()))
> + goto out;
> /* TODO: Pass module name here too */
> t = btf_find_func_proto(name, &btf);
> if (IS_ERR_OR_NULL(t))
Are func-args, funcgraph-args and funcgraph-retval the only producers of
entries that carry arguments?
Since commit 76fe0337c219 ("ftrace: Add arguments to function tracer"), the
latency tracers pass fregs straight to trace_function(), whatever the
func-args option says:
irqsoff_tracer_call() (kernel/trace/trace_irqsoff.c):
trace_function(tr, ip, parent_ip, trace_ctx, fregs);
wakeup_tracer_call() (kernel/trace/trace_sched_wakeup.c):
trace_function(tr, ip, parent_ip, trace_ctx, fregs);
trace_function() reserves room for FTRACE_REGS_MAX_ARGS arguments and
stores them whenever fregs is non-NULL. On x86_64 and arm64, which are the
only architectures CONFIG_DEBUG_INFO_BTF_LAZY allows, ftrace_caller always
passes a non-NULL fregs.
When the irqsoff, preemptoff, preemptirqsoff, wakeup, wakeup_rt or
wakeup_dl tracer runs with function tracing on (the default, not in
display-graph mode), irqsoff_print_line()/wakeup_print_line() return
TRACE_TYPE_UNHANDLED. The entry then goes through:
trace_fn_trace()
print_fn_trace()
print_function_args()
with args != NULL.
None of these tracers calls trace_btf_prepare(), and they have no func-args
option that would. With CONFIG_DEBUG_INFO_BTF_LAZY (added later in this
series by 66c4a6a9293d, e46b5971bc91 and cae96a9f7395), if nothing else has
parsed the vmlinux BTF yet, the peek returns NULL and every function line
in these tracers prints as "func()" where it used to print
"func(a=..., b=0x...)". Before this patch the reader would have parsed the
BTF on demand.
The same applies to a boot-mapped persistent instance that holds the
previous boot's func-args entries. It is read-only and no tracer is
started on it, so nothing prepares the BTF.
The commit message says the fallback "then cannot happen while the option
is in effect", and the comment above trace_btf_prepare() lists only
func-args, funcgraph-args and funcgraph-retval. Neither covers these
producers, and no later commit in 38e2751cdb40..4d32ca0c95ea touches
trace_irqsoff.c or trace_sched_wakeup.c or adds another
trace_btf_prepare() call.
Should the irqsoff/wakeup tracers call trace_btf_prepare() from their init
when function tracing records args, or should their trace_function() calls
pass NULL fregs unless args are wanted?
---
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/41205212136
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH bpf-next 09/13] bpf: keep the vmlinux BTF zstd-compressed in the kernel image
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
` (7 preceding siblings ...)
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:09 ` Jay Wang
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
` (3 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
The vmlinux BTF is 5-7 MB of read-only data in every kernel built with
CONFIG_DEBUG_INFO_BTF=y, resident whether or not anything on the system
ever uses BTF. Add CONFIG_DEBUG_INFO_BTF_LAZY to keep it compressed
with zstd in the image instead, at about a quarter of the size, and
decompress it in the kernel when it is first needed. This is the
transport half of the option; the next two patches defer the
parsing.
kbuild: gen_btf_o() in scripts/gen-btf.sh compresses the final
${ELF}.BTF with zstd -19 and adds it as a second section, .init.BTF.zst,
of the same .btf.o, so neither an extra link pass nor a size
reservation is needed. .BTF stays in the vmlinux ELF file as a
non-alloc section: pahole --btf_base, resolve_btfids for modules,
bpftool and libbpf's on-disk fallback find it by name and keep working
unchanged, and so do out-of-tree module builds. The linker script
puts .init.BTF.zst in INIT_DATA, bounded by __start_BTF_zst and
__stop_BTF_zst, and .BTF in ELF_DETAILS at address 0. The BTF macro
is renamed RO_BTF, because the token BTF in ".BTF 0 : { ... }" would
otherwise be macro-expanded. x86 strips .BTF from the ELF it puts in
the compressed boot image, and arm64 makes its Image with objcopy
-O binary, which drops non-alloc sections. The option is limited to
these two architectures for now: several others build their boot
image from a stripped ELF copy of vmlinux, which keeps non-alloc
sections and would carry the uncompressed BTF. The inline BTF
(CONFIG_DEBUG_INFO_BTF_INLINE) is left as it is: built in, .BTF.inline
stays in the image; as a module it is not in memory anyway.
kernel: btf_vmlinux_data() returns the raw BTF. With the option it
decompresses the blob once, under a leaf mutex, into vmalloc_user()
memory that is never freed; the size comes from the zstd frame header,
so /sys/kernel/btf/vmlinux gets its size without decompressing. A
late_initcall_sync copies the blob out of init memory before that is
freed, unless it is decompressed by then, and the copy is freed after
decompression, so a system that uses BTF ends up with the same
footprint as today. The copy uses vmalloc(): kvmemdup() of the
~1.4 MB blob would get a 2 MB kmalloc_large() allocation. If the copy
fails, the BTF is unavailable for the rest of the boot: that is logged
once, and later fetches fail with -ENOMEM right away instead of
allocating the raw buffer for nothing each time. A blob that does not
decompress is logged once and dropped, and fetches fail with -EINVAL
from then on. btf_parse_vmlinux() and the read and mmap handlers of
/sys/kernel/btf/vmlinux use btf_vmlinux_data(); mmap of the
decompressed copy uses remap_vmalloc_range(). Without the option
btf_vmlinux_data() returns __start_BTF, and the sysfs file keeps its
current handlers.
btf_vmlinux_data() lives in sysfs_btf.c, which is now built whenever
CONFIG_DEBUG_INFO_BTF is enabled, with the sysfs file itself still
under CONFIG_SYSFS: btf_parse_vmlinux() needs the data without sysfs
too, and with the option the blob has to be copied out of init memory
either way.
Reading /sys/kernel/btf/vmlinux decompresses but never parses, and
only vmalloc and zstd run under btf_vmlinux_data_lock, so the sysfs
mmap path (mmap_lock -> kernfs -> btf_vmlinux_data_lock) cannot form
a cycle with module or cand_cache locks.
On its own this patch only changes where the bytes come from: initcall
kfunc registrations still parse the vmlinux BTF at boot.
Measured with a distribution config: the raw .BTF is 5.47 MB on x86_64
and 5.57 MB on arm64, and 1.44 and 1.46 MB with zstd -19. xz -9e would
save another 0.17 MB but decodes about 6 times slower (72 vs 12 ms in
user space). Decompression in the kernel takes ~7 ms on x86_64.
Trade-offs: the compressed blob stays resident on systems that never
use BTF (~1.4 MB), so with the next two patches they save about 80% of
what the BTF costs rather than all of it (~6 of ~7.5 MB, the raw BTF
plus what parsing it allocates); the x86 bzImage on disk is ~0.3 MB
smaller; building needs the zstd tool; __start_BTF/__stop_BTF do not
exist with the option (the ksyms and linked_vars selftests use them; a
later patch adapts them).
Co-developed-by: Ihor Solodrai <ihor.solodrai@linux.dev>
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
arch/x86/boot/compressed/Makefile | 2 +-
include/asm-generic/vmlinux.lds.h | 24 ++++-
include/linux/btf.h | 5 +
kernel/bpf/Makefile | 2 +-
kernel/bpf/btf.c | 13 ++-
kernel/bpf/sysfs_btf.c | 167 +++++++++++++++++++++++++++++-
lib/Kconfig.debug | 16 +++
scripts/gen-btf.sh | 25 +++--
8 files changed, 232 insertions(+), 22 deletions(-)
diff --git a/arch/x86/boot/compressed/Makefile b/arch/x86/boot/compressed/Makefile
index 680ad803cba7..58d05262dec8 100644
--- a/arch/x86/boot/compressed/Makefile
+++ b/arch/x86/boot/compressed/Makefile
@@ -116,7 +116,7 @@ endif
$(obj)/vmlinux: $(vmlinux-objs-y) $(vmlinux-libs-y) FORCE
$(call if_changed,ld)
-OBJCOPYFLAGS_vmlinux.bin := -R .comment -S
+OBJCOPYFLAGS_vmlinux.bin := -R .comment $(if $(CONFIG_DEBUG_INFO_BTF_LAZY),-R .BTF) -S
$(obj)/vmlinux.bin: vmlinux FORCE
$(call if_changed,objcopy)
diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
index 3eb732942d69..7358aedab95c 100644
--- a/include/asm-generic/vmlinux.lds.h
+++ b/include/asm-generic/vmlinux.lds.h
@@ -553,7 +553,7 @@
\
RO_EXCEPTION_TABLE \
NOTES \
- BTF \
+ RO_BTF \
\
. = ALIGN((align)); \
__end_rodata = .;
@@ -704,12 +704,24 @@
#define BTF_INLINE_LINK
#endif
-#ifdef CONFIG_DEBUG_INFO_BTF
-#define BTF \
+#ifdef CONFIG_DEBUG_INFO_BTF_LAZY
+/* compressed BTF in init data (see sysfs_btf.c), raw BTF not loaded */
+#define BTF_LOADED
+#define BTF_ZST . = ALIGN(8); BOUNDED_SECTION_BY(.init.BTF.zst, _BTF_zst)
+#define BTF_RAW .BTF 0 : { *(.BTF) }
+#else
+#define BTF_LOADED \
. = ALIGN(PAGE_SIZE); \
.BTF : AT(ADDR(.BTF) - LOAD_OFFSET) { \
BOUNDED_SECTION_BY(.BTF, _BTF) \
- } \
+ }
+#define BTF_ZST
+#define BTF_RAW
+#endif
+
+#ifdef CONFIG_DEBUG_INFO_BTF
+#define RO_BTF \
+ BTF_LOADED \
BTF_INLINE \
BTF_INLINE_LINK \
. = ALIGN(PAGE_SIZE); \
@@ -717,7 +729,7 @@
*(.BTF_ids) \
}
#else
-#define BTF
+#define RO_BTF
#endif
/*
@@ -748,6 +760,7 @@
KERNEL_CTORS() \
MCOUNT_REC() \
*(.init.rodata .init.rodata.*) \
+ BTF_ZST \
FTRACE_EVENTS() \
TRACE_SYSCALLS() \
KPROBE_BLACKLIST() \
@@ -880,6 +893,7 @@
/* Required sections not related to debugging. */
#define ELF_DETAILS \
.comment 0 : { *(.comment) } \
+ BTF_RAW \
.symtab 0 : { *(.symtab) } \
.strtab 0 : { *(.strtab) } \
.shstrtab 0 : { *(.shstrtab) } \
diff --git a/include/linux/btf.h b/include/linux/btf.h
index b66e20a18c54..b6d5a7f59852 100644
--- a/include/linux/btf.h
+++ b/include/linux/btf.h
@@ -617,6 +617,11 @@ __u32 *btf_field_iter_next(struct btf_field_iter *it);
const char *btf_name_by_offset(const struct btf *btf, u32 offset);
const char *btf_str_by_offset(const struct btf *btf, u32 offset);
struct btf *btf_parse_vmlinux(void);
+#ifdef CONFIG_DEBUG_INFO_BTF
+void *btf_vmlinux_data(u32 *size);
+#else
+static inline void *btf_vmlinux_data(u32 *size) { return ERR_PTR(-ENOENT); }
+#endif
struct btf *bpf_prog_get_target_btf(const struct bpf_prog *prog);
u32 *btf_kfunc_flags(const struct btf *btf, u32 kfunc_btf_id, const struct bpf_prog *prog);
int btf_kfunc_check_flag(const struct btf *btf, u32 kfunc_btf_id, u32 flag);
diff --git a/kernel/bpf/Makefile b/kernel/bpf/Makefile
index 269810b128c4..4f500927cec8 100644
--- a/kernel/bpf/Makefile
+++ b/kernel/bpf/Makefile
@@ -40,8 +40,8 @@ obj-$(CONFIG_CGROUP_BPF) += cgroup.o
ifeq ($(CONFIG_INET),y)
obj-$(CONFIG_BPF_SYSCALL) += reuseport_array.o
endif
-ifeq ($(CONFIG_SYSFS),y)
obj-$(CONFIG_DEBUG_INFO_BTF) += sysfs_btf.o
+ifeq ($(CONFIG_SYSFS),y)
obj-$(CONFIG_DEBUG_INFO_BTF_INLINE) += btf_vmlinux_inline.o
endif
ifeq ($(CONFIG_BPF_JIT),y)
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 70adc52f5d7c..a7bb780743fb 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -1901,7 +1901,8 @@ static void btf_free(struct btf *btf)
kvfree(btf->resolved_sizes);
kvfree(btf->resolved_ids);
/* vmlinux does not allocate btf->data, it simply points it at
- * __start_BTF.
+ * __start_BTF, or at the decompressed copy with
+ * CONFIG_DEBUG_INFO_BTF_LAZY, which is never freed either.
*/
if (!btf_is_vmlinux(btf))
kvfree(btf->data);
@@ -6523,8 +6524,6 @@ static struct btf *btf_parse(const union bpf_attr *attr, bpfptr_t uattr,
return ERR_PTR(err);
}
-extern char __start_BTF[];
-extern char __stop_BTF[];
extern struct btf *btf_vmlinux;
#define BPF_MAP_TYPE(_id, _ops)
@@ -6900,15 +6899,21 @@ struct btf *btf_parse_vmlinux(void)
struct btf_verifier_env *env = NULL;
struct bpf_verifier_log *log;
struct btf *btf;
+ void *data;
+ u32 size;
int err;
+ data = btf_vmlinux_data(&size);
+ if (IS_ERR(data))
+ return ERR_CAST(data);
+
env = kzalloc_obj(*env, GFP_KERNEL | __GFP_NOWARN);
if (!env)
return ERR_PTR(-ENOMEM);
log = &env->log;
log->level = BPF_LOG_KERNEL;
- btf = btf_parse_base(env, "vmlinux", __start_BTF, __stop_BTF - __start_BTF);
+ btf = btf_parse_base(env, "vmlinux", data, size);
if (IS_ERR(btf))
goto err_out;
diff --git a/kernel/bpf/sysfs_btf.c b/kernel/bpf/sysfs_btf.c
index 556fe92c3649..015cd8221154 100644
--- a/kernel/bpf/sysfs_btf.c
+++ b/kernel/bpf/sysfs_btf.c
@@ -12,11 +12,126 @@
#include <linux/vmalloc.h>
#include <linux/io.h>
#include <linux/btf.h>
+#include <linux/zstd.h>
+#ifdef CONFIG_DEBUG_INFO_BTF_LAZY
+/* See scripts/gen-btf.sh, gen_btf_o() for details */
+extern char __start_BTF_zst[];
+extern char __stop_BTF_zst[];
+
+static DEFINE_MUTEX(btf_vmlinux_data_lock);
+static void *btf_vmlinux_raw;
+static u32 btf_vmlinux_raw_size;
+static bool btf_vmlinux_broken;
+/* Compressed BTF: init data until btf_vmlinux_keep_zst(), freed once decompressed */
+static void *btf_zst = __start_BTF_zst;
+
+static u32 btf_vmlinux_size(void)
+{
+ zstd_frame_header hdr;
+
+ guard(mutex)(&btf_vmlinux_data_lock);
+ if (!btf_vmlinux_raw_size && btf_zst &&
+ !zstd_get_frame_header(&hdr, btf_zst, __stop_BTF_zst - __start_BTF_zst) &&
+ hdr.frameContentSize < U32_MAX)
+ btf_vmlinux_raw_size = hdr.frameContentSize;
+ return btf_vmlinux_raw_size;
+}
+
+static void btf_vmlinux_drop_zst(void)
+{
+ if (btf_zst != __start_BTF_zst)
+ vfree(btf_zst);
+ btf_zst = NULL;
+}
+
+/*
+ * The raw vmlinux BTF, decompressed on first use and never freed, or
+ * ERR_PTR(-ENOMEM) if that cannot be done (now: memory; for good: no copy of
+ * the compressed BTF), or ERR_PTR(-EINVAL) if it is broken.
+ */
+void *btf_vmlinux_data(u32 *size)
+{
+ size_t ret, ws_size = zstd_dctx_workspace_bound();
+ void *raw, *ws;
+
+ /* Pairs with smp_store_release() below */
+ raw = smp_load_acquire(&btf_vmlinux_raw);
+ if (raw) {
+ *size = btf_vmlinux_raw_size;
+ return raw;
+ }
+
+ *size = btf_vmlinux_size();
+ guard(mutex)(&btf_vmlinux_data_lock);
+ if (btf_vmlinux_raw)
+ return btf_vmlinux_raw;
+ if (!btf_zst)
+ return ERR_PTR(btf_vmlinux_broken ? -EINVAL : -ENOMEM);
+ if (!*size) {
+ pr_err("BTF: no size in the compressed vmlinux BTF\n");
+ btf_vmlinux_broken = true;
+ btf_vmlinux_drop_zst();
+ return ERR_PTR(-EINVAL);
+ }
+ /* vmalloc_user(): btf_sysfs_vmlinux_mmap() maps it to user space */
+ raw = vmalloc_user(*size);
+ ws = kvmalloc(ws_size, GFP_KERNEL);
+ if (raw && ws) {
+ ret = zstd_decompress_dctx(zstd_init_dctx(ws, ws_size), raw, *size,
+ btf_zst, __stop_BTF_zst - __start_BTF_zst);
+ if (!zstd_is_error(ret) && ret == *size) {
+ /* Pairs with smp_load_acquire() above */
+ smp_store_release(&btf_vmlinux_raw, raw);
+ } else {
+ pr_err("BTF: cannot decompress the vmlinux BTF: %s\n",
+ zstd_is_error(ret) ? zstd_get_error_name(ret) : "size mismatch");
+ btf_vmlinux_broken = true;
+ }
+ btf_vmlinux_drop_zst();
+ }
+ kvfree(ws);
+ if (btf_vmlinux_raw)
+ return btf_vmlinux_raw;
+ vfree(raw);
+ return ERR_PTR(btf_vmlinux_broken ? -EINVAL : -ENOMEM);
+}
+
+/* Keep a copy of the compressed BTF before init memory is freed */
+static int __init btf_vmlinux_keep_zst(void)
+{
+ size_t size = __stop_BTF_zst - __start_BTF_zst;
+
+ guard(mutex)(&btf_vmlinux_data_lock);
+ if (btf_zst) {
+ /* vmalloc(): kmalloc() would round up to a power of 2 */
+ btf_zst = vmalloc(size);
+ if (btf_zst)
+ memcpy(btf_zst, __start_BTF_zst, size);
+ else
+ pr_err("BTF: out of memory, vmlinux BTF unavailable\n");
+ }
+ return 0;
+}
+late_initcall_sync(btf_vmlinux_keep_zst);
+#else
/* See scripts/link-vmlinux.sh, gen_btf() func for details */
extern char __start_BTF[];
extern char __stop_BTF[];
+static u32 btf_vmlinux_size(void)
+{
+ return __stop_BTF - __start_BTF;
+}
+
+void *btf_vmlinux_data(u32 *size)
+{
+ *size = btf_vmlinux_size();
+ return __start_BTF;
+}
+#endif
+
+#ifdef CONFIG_SYSFS
static int btf_sysfs_mmap_check(void *data, size_t size, struct vm_area_struct *vma)
{
unsigned long pages = PAGE_ALIGN(size) >> PAGE_SHIFT;
@@ -61,10 +176,49 @@ static int btf_sysfs_mmap_image(struct file *filp, struct kobject *kobj,
return remap_pfn_range(vma, vma->vm_start, pfn, vm_size, vma->vm_page_prot);
}
+#ifdef CONFIG_DEBUG_INFO_BTF_LAZY
+/* Reading or mapping the file decompresses the BTF but does not parse it */
+static int btf_sysfs_vmlinux_mmap(struct file *filp, struct kobject *kobj,
+ const struct bin_attribute *attr,
+ struct vm_area_struct *vma)
+{
+ void *data;
+ u32 size;
+ int err;
+
+ data = btf_vmlinux_data(&size);
+ if (IS_ERR(data))
+ return PTR_ERR(data);
+
+ err = btf_sysfs_mmap_check(data, attr->size, vma);
+ if (err)
+ return err;
+
+ return remap_vmalloc_range(vma, data, 0);
+}
+
+static ssize_t btf_sysfs_vmlinux_read(struct file *filp, struct kobject *kobj,
+ const struct bin_attribute *attr, char *buf,
+ loff_t off, size_t count)
+{
+ void *data;
+ u32 size;
+
+ data = btf_vmlinux_data(&size);
+ if (IS_ERR(data))
+ return PTR_ERR(data);
+
+ return memory_read_from_buffer(buf, count, &off, data, size);
+}
+#else
+#define btf_sysfs_vmlinux_read sysfs_bin_attr_simple_read
+#define btf_sysfs_vmlinux_mmap btf_sysfs_mmap_image
+#endif
+
static struct bin_attribute bin_attr_btf_vmlinux __ro_after_init = {
.attr = { .name = "vmlinux", .mode = 0444, },
- .read = sysfs_bin_attr_simple_read,
- .mmap = btf_sysfs_mmap_image,
+ .read = btf_sysfs_vmlinux_read,
+ .mmap = btf_sysfs_vmlinux_mmap,
};
struct kobject *btf_kobj;
@@ -180,8 +334,12 @@ void sysfs_btf_remove(struct bin_attribute *attr)
static int __init btf_vmlinux_init(void)
{
- bin_attr_btf_vmlinux.private = __start_BTF;
- bin_attr_btf_vmlinux.size = __stop_BTF - __start_BTF;
+ u32 size;
+
+ /* with CONFIG_DEBUG_INFO_BTF_LAZY the read and mmap handlers get it */
+ if (!IS_ENABLED(CONFIG_DEBUG_INFO_BTF_LAZY))
+ bin_attr_btf_vmlinux.private = btf_vmlinux_data(&size);
+ bin_attr_btf_vmlinux.size = btf_vmlinux_size();
if (bin_attr_btf_vmlinux.size == 0)
return 0;
@@ -194,3 +352,4 @@ static int __init btf_vmlinux_init(void)
}
subsys_initcall(btf_vmlinux_init);
+#endif /* CONFIG_SYSFS */
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 2d22638ec13a..700c4f67bdd8 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -408,6 +408,22 @@ config DEBUG_INFO_BTF
Turning this on requires pahole v1.22 or later, which will convert
DWARF type info into equivalent deduplicated BTF type info.
+config DEBUG_INFO_BTF_LAZY
+ bool "Compress vmlinux BTF and parse it on first use"
+ depends on DEBUG_INFO_BTF
+ depends on X86 || ARM64
+ select ZSTD_DECOMPRESS
+ help
+ Keep the vmlinux BTF zstd-compressed in the kernel image, at about
+ a quarter of its size, and decompress and parse it the first time
+ something needs it instead of at boot. Systems that never use BTF
+ then keep only the compressed copy in memory, which saves about 80%
+ of what the BTF costs; the first user pays a few tens of
+ milliseconds once. The vmlinux file keeps the uncompressed BTF as
+ a non-loaded section for tools. Building needs the zstd tool.
+
+ If unsure, say N.
+
config PAHOLE_HAS_BTF_TAG
def_bool PAHOLE_VERSION >= 123
depends on CC_IS_CLANG
diff --git a/scripts/gen-btf.sh b/scripts/gen-btf.sh
index b17f1189298a..c490b0630612 100755
--- a/scripts/gen-btf.sh
+++ b/scripts/gen-btf.sh
@@ -86,13 +86,24 @@ gen_btf_o()
btf_data=${ELF_FILE}.btf.o
# Create ${btf_data} which contains just BTF sections but no symbols. Add
- # SHF_ALLOC because .BTF will be part of the vmlinux image. --strip-all
- # deletes all symbols including __start_BTF and __stop_BTF, which will
- # be redefined in the linker script.
+ # SHF_ALLOC to the sections that will be part of the vmlinux image.
+ # --strip-all deletes all symbols including __start_BTF and __stop_BTF,
+ # which will be redefined in the linker script.
echo "" | ${CC} ${CLANG_FLAGS} ${KBUILD_CPPFLAGS} ${KBUILD_CFLAGS} -fno-lto -c -x c -o ${btf_data} -
- ${OBJCOPY} --add-section .BTF=${ELF_FILE}.BTF \
- --set-section-flags .BTF=alloc,readonly ${btf_data}
- ONLY_SEC="--only-section=.BTF"
+ if is_enabled CONFIG_DEBUG_INFO_BTF_LAZY; then
+ # The image gets compressed BTF as init data, .BTF stays in
+ # the ELF file for tools.
+ ${ZSTD} -q -f -19 ${ELF_FILE}.BTF -o ${ELF_FILE}.BTF.zst
+ ${OBJCOPY} --add-section .BTF=${ELF_FILE}.BTF \
+ --set-section-flags .BTF=readonly \
+ --add-section .init.BTF.zst=${ELF_FILE}.BTF.zst \
+ --set-section-flags .init.BTF.zst=alloc,readonly ${btf_data}
+ ONLY_SEC="--only-section=.BTF --only-section=.init.BTF.zst"
+ else
+ ${OBJCOPY} --add-section .BTF=${ELF_FILE}.BTF \
+ --set-section-flags .BTF=alloc,readonly ${btf_data}
+ ONLY_SEC="--only-section=.BTF"
+ fi
btf_inline=${ELF_FILE}.BTF.inline
if [ "${BTF_INLINE}" = "m" ]; then
if [ -f "${btf_inline}" ]; then
@@ -145,7 +156,7 @@ embed_btf_data()
cleanup()
{
rm -f "${ELF_FILE}.BTF.1"
- rm -f "${ELF_FILE}.BTF"
+ rm -f "${ELF_FILE}.BTF" "${ELF_FILE}.BTF.zst"
rm -f "${ELF_FILE}.BTF.inline"
rm -f "${ELF_FILE}.BTF.inline.link"
if [ "${BTFGEN_MODE}" = "module" ]; then
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 09/13] bpf: keep the vmlinux BTF zstd-compressed in the kernel image
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:55 ` bot+bpf-ci
0 siblings, 0 replies; 26+ messages in thread
From: bot+bpf-ci @ 2026-10-31 13:55 UTC (permalink / raw)
To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
Cc: ihor.solodrai, almagui, martin.lau, yonghong.song, jolsa, rostedt,
mhiramat, mathieu.desnoyers, linux-trace-kernel, nathan, nsc,
linux-kbuild, arnd, linux-arch, x86, linux-doc, linux-kernel,
abuehaze, doebel, mpohlack, jay.wang.upstream, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1491 bytes --]
> bpf: keep the vmlinux BTF zstd-compressed in the kernel image
TL;DR: Commit message only: the Co-developed-by tag for Ihor Solodrai is
followed by a "TODO-NEEDS-IHOR" placeholder instead of a real Signed-off-by,
which checkpatch would flag.
> bpf: keep the vmlinux BTF zstd-compressed in the kernel image
>
[ ... ]
>
> Co-developed-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> Signed-off-by: Jay Wang <wanjay@amazon.com>
The Co-developed-by: tag for Ihor Solodrai is followed by a placeholder line,
"TODO-NEEDS-IHOR: Signed-off-by: ...", instead of an actual Signed-off-by:
trailer.
Documentation/process/submitting-patches.rst says that every
Co-developed-by: must be immediately followed by a Signed-off-by: of the
associated co-author. As written, the only parsed Signed-off-by: is Jay
Wang's, and checkpatch.pl would report "Co-developed-by: must be immediately
followed by Signed-off-by:".
The TODO marker would also end up in the git history. None of the later
commits in this series (e46b5971bc91, cae96a9f7395, 4d32ca0c95ea) changes
these trailers.
Should the placeholder be replaced with Ihor's real Signed-off-by:, or the
Co-developed-by: tag be dropped?
---
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/41205212136
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH bpf-next 10/13] bpf: defer kfunc and struct_ops registrations until first BTF use
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
` (8 preceding siblings ...)
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:09 ` Jay Wang
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
` (2 subsequent siblings)
12 siblings, 2 replies; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
With the BTF compressed, the kernel still decompresses and parses it at
boot: initcalls registering kfunc sets, dtor kfuncs and struct_ops look
up their BTF ids. With CONFIG_DEBUG_INFO_BTF_LAZY, queue these
registrations until something actually needs the vmlinux BTF, and then
parse it and apply them synchronously, in the context of whoever needs
it. Nothing waits for user space, so there is no "not available yet"
state and no syscall retry: every caller of bpf_get_btf_vmlinux() gets
the BTF or an error, as today, and verification results do not depend
on whether the vmlinux BTF has been parsed before.
bpf_get_btf_vmlinux() already parses on first use under
btf_vmlinux_lock. With the option, the task that parses it then, still
under the lock and before publishing btf_vmlinux, replays the queued
registrations in order through the unmodified register functions
(btf_apply_deferred()). Meanwhile bpf_get_btf_vmlinux() returns the new
BTF to that task (btf_vmlinux_builder == current, in task context), so
the replayed registrations, and the ones they make in turn (a
struct_ops ->init() may register kfuncs), apply directly. Nobody else
sees btf_vmlinux before its kfuncs and struct_ops are in. The vmlinux
BTF gets its id while it is parsed, before that, so BPF_BTF_GET_FD_BY_ID
parses or waits for the parse first, and so does BPF_BTF_GET_NEXT_ID
for a caller with CAP_SYS_ADMIN (checked without audit, the command
checks it again), since kernel BTFs only have ids once parsed.
The first use can run out of memory, also only because its task is
being killed: vmalloc() gives up then. A round that fails with
-ENOMEM, in the parse or in a replayed registration, is not published:
the new BTF is dropped with whatever was applied to it, the queue stays
as it is, and the next caller starts over (a struct_ops ->init() then
runs again with the new BTF; the in-tree ones only look types up or
store the BTF pointer, which nothing can use before the BTF is
published). Other errors of a replayed
registration are only logged, as they would be at boot. If the vmlinux
BTF fails for good, the queue is dropped with a warning. As any call
may now be the one that parses, bpf_get_btf_vmlinux() calls
might_sleep() with the option.
btf_defer_reg() queues a registration while the vmlinux BTF is not
parsed, with a copy of a kfunc set or dtor array, which callers may
build on the stack. Only registrations that need the vmlinux BTF wait:
built-in ones and those of modules with BTF. A module without BTF
registers nothing but a warning today, and the module notifier that
drops a going module's entries does not see it, so its registrations
are not queued. A queued entry is applied only if a reference on its
owner can be taken: delete_module() runs the module's exit() before the
GOING notifier that drops its entries, and a registration must not run
after, or concurrently with, that exit(). The replayed registrations
run with the locks of whoever needs the BTF first: they must not load
modules or take locks that users of the BTF hold, which the in-tree
ones do not.
The BTF of modules is still parsed when they load, which needs the
vmlinux BTF, so on its own this patch saves the parse at boot only for
kernels whose boot loads no module with BTF; the next patch defers
module BTF too.
Errors of queued registrations are reported with pr_warn() when they
are applied, not to the initcall or module init that made them.
Co-developed-by: Ihor Solodrai <ihor.solodrai@linux.dev>
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
kernel/bpf/btf.c | 224 +++++++++++++++++++++++++++++++++++++++++--
kernel/bpf/syscall.c | 4 +
2 files changed, 222 insertions(+), 6 deletions(-)
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index a7bb780743fb..a5b1cd63a1e4 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))
@@ -9352,6 +9393,7 @@ static LIST_HEAD(btf_modules);
static DEFINE_MUTEX(btf_module_mutex);
static void purge_cand_cache(struct btf *btf);
+static void btf_drop_deferred_regs(struct module *owner);
#if IS_MODULE(CONFIG_DEBUG_INFO_BTF_INLINE)
extern char __start_BTF_inline_link[];
@@ -9573,6 +9615,7 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
case MODULE_STATE_GOING:
if (!mod->btf_data_size)
break;
+ btf_drop_deferred_regs(module);
mutex_lock(&btf_module_mutex);
list_for_each_entry_safe(btf_mod, tmp, &btf_modules, list) {
if (btf_mod->module != module)
@@ -10170,12 +10213,88 @@ u32 *btf_kfunc_is_modify_return(const struct btf *btf, u32 kfunc_btf_id,
return btf_kfunc_id_set_contains(btf, BTF_KFUNC_HOOK_FMODRET, kfunc_btf_id);
}
+enum btf_deferred_kind { BTF_DEFER_KFUNCS, BTF_DEFER_DTORS, BTF_DEFER_ST_OPS };
+
+/* CONFIG_DEBUG_INFO_BTF_LAZY: a registration made before the vmlinux BTF is parsed */
+struct btf_deferred_reg {
+ struct list_head list;
+ struct module *owner;
+ enum btf_deferred_kind kind;
+ enum btf_kfunc_hook hook;
+ void *data;
+ u32 cnt;
+};
+
+/* protected by btf_vmlinux_lock */
+static LIST_HEAD(btf_deferred_regs);
+
+/*
+ * Does a registration by @owner need the vmlinux BTF? Without module BTF a
+ * module's registrations do nothing but warn (check_btf_kconfigs()), and
+ * btf_module_notify(), which drops a going module's queued entries, does not
+ * see such a module.
+ */
+static bool btf_reg_needs_vmlinux(const struct module *owner)
+{
+#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+ return !owner || owner->btf_data_size;
+#else
+ return !owner;
+#endif
+}
+
+/*
+ * Initcalls and modules loaded early register kfuncs, dtor kfuncs and
+ * struct_ops before anything needs the vmlinux BTF. Queue these until it
+ * is parsed (btf_apply_deferred()). Returns true if the registration was
+ * queued, with *err set to 0 or -ENOMEM.
+ */
+static bool btf_defer_reg(struct module *owner, enum btf_deferred_kind kind,
+ enum btf_kfunc_hook hook, const void *data, u32 cnt, int *err)
+{
+ struct btf_deferred_reg *reg;
+
+ if (!IS_ENABLED(CONFIG_DEBUG_INFO_BTF_LAZY) || bpf_peek_btf_vmlinux() ||
+ btf_vmlinux_building() || !btf_reg_needs_vmlinux(owner))
+ return false;
+ guard(mutex)(&btf_vmlinux_lock);
+ if (btf_vmlinux)
+ return false;
+
+ *err = -ENOMEM;
+ reg = kzalloc_obj(*reg);
+ if (!reg)
+ return true;
+ /* callers may build kfunc sets and dtor arrays on the stack */
+ if (kind == BTF_DEFER_KFUNCS)
+ reg->data = kmemdup(data, sizeof(struct btf_kfunc_id_set), GFP_KERNEL);
+ else if (kind == BTF_DEFER_DTORS)
+ reg->data = kmemdup_array(data, cnt, sizeof(struct btf_id_dtor_kfunc),
+ GFP_KERNEL);
+ else
+ reg->data = (void *)data;
+ if (!reg->data) {
+ kfree(reg);
+ return true;
+ }
+ reg->owner = owner;
+ reg->kind = kind;
+ reg->hook = hook;
+ reg->cnt = cnt;
+ list_add_tail(®->list, &btf_deferred_regs);
+ *err = 0;
+ return true;
+}
+
static int __register_btf_kfunc_id_set(enum btf_kfunc_hook hook,
const struct btf_kfunc_id_set *kset)
{
struct btf *btf;
int ret, i;
+ if (btf_defer_reg(kset->owner, BTF_DEFER_KFUNCS, hook, kset, 0, &ret))
+ return ret;
+
btf = btf_get_module_btf(kset->owner);
if (!btf)
return check_btf_kconfigs(kset->owner, "kfunc");
@@ -10292,6 +10411,9 @@ int register_btf_id_dtor_kfuncs(const struct btf_id_dtor_kfunc *dtors, u32 add_c
u32 tab_cnt, i;
int ret;
+ if (btf_defer_reg(owner, BTF_DEFER_DTORS, 0, dtors, add_cnt, &ret))
+ return ret;
+
btf = btf_get_module_btf(owner);
if (!btf)
return check_btf_kconfigs(owner, "dtor kfuncs");
@@ -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");
@@ -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;
+ if (err)
+ pr_warn("failed to apply deferred BTF registration for %s: %d\n",
+ reg->owner ? reg->owner->name : "vmlinux", err);
+ }
+ list_for_each_entry_safe(reg, tmp, &btf_deferred_regs, list)
+ btf_free_deferred_reg(reg);
+ return 0;
+}
+
+/* The vmlinux BTF failed for good: nothing will apply what is queued */
+static void btf_drop_all_deferred(int err)
+{
+ struct btf_deferred_reg *reg, *tmp;
+ int cnt = 0;
+
+ list_for_each_entry_safe(reg, tmp, &btf_deferred_regs, list) {
+ btf_free_deferred_reg(reg);
+ cnt++;
+ }
+ if (cnt)
+ pr_warn("vmlinux BTF unavailable (%d), dropped %d deferred BTF registrations\n",
+ err, cnt);
+}
+
+/* @owner is going away: drop the registrations it queued */
+static void __maybe_unused btf_drop_deferred_regs(struct module *owner)
+{
+ struct btf_deferred_reg *reg, *tmp;
+
+ if (!IS_ENABLED(CONFIG_DEBUG_INFO_BTF_LAZY))
+ return;
+ guard(mutex)(&btf_vmlinux_lock);
+ list_for_each_entry_safe(reg, tmp, &btf_deferred_regs, list) {
+ if (reg->owner == owner)
+ btf_free_deferred_reg(reg);
+ }
+}
+
bool btf_param_match_suffix(const struct btf *btf,
const struct btf_param *arg,
const char *suffix)
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index 9273b7dee0aa..79e65cfc4267 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -6519,6 +6519,10 @@ static int __sys_bpf(enum bpf_cmd cmd, bpfptr_t uattr, unsigned int size,
&map_idr, &map_idr_lock);
break;
case BPF_BTF_GET_NEXT_ID:
+ /* kernel BTFs have ids once parsed (CONFIG_DEBUG_INFO_BTF_LAZY) */
+ if (IS_ENABLED(CONFIG_DEBUG_INFO_BTF_LAZY) &&
+ ns_capable_noaudit(&init_user_ns, CAP_SYS_ADMIN))
+ bpf_get_btf_vmlinux();
err = bpf_obj_get_next_id(&attr, uattr.user,
&btf_idr, &btf_idr_lock);
break;
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 10/13] bpf: defer kfunc and struct_ops registrations until first BTF use
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:55 ` bot+bpf-ci
2026-10-31 17:07 ` Alexei Starovoitov
1 sibling, 0 replies; 26+ messages in thread
From: bot+bpf-ci @ 2026-10-31 13:55 UTC (permalink / raw)
To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
Cc: ihor.solodrai, almagui, martin.lau, yonghong.song, jolsa, rostedt,
mhiramat, mathieu.desnoyers, linux-trace-kernel, nathan, nsc,
linux-kbuild, arnd, linux-arch, x86, linux-doc, linux-kernel,
abuehaze, doebel, mpohlack, jay.wang.upstream, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 5444 bytes --]
> bpf: defer kfunc and struct_ops registrations until first BTF use
TL;DR:
- btf_get_fd_by_id() may return an fd for a half-built vmlinux BTF after a
failed -ENOMEM parse round, possibly racing with struct_ops table updates
- a permanent -ENOMEM may be retried forever, with the queue never dropped
- Co-developed-by lacks a Signed-off-by
> bpf: defer kfunc and struct_ops registrations until first BTF use
>
> [ ... ]
>
> Co-developed-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> Signed-off-by: Jay Wang <wanjay@amazon.com>
The Co-developed-by: tag is followed by a "TODO-NEEDS-IHOR: Signed-off-by:"
placeholder instead of a real Signed-off-by: line from the co-developer.
Should the placeholder be resolved, or the Co-developed-by: tag dropped,
before this is sent? Documentation/process/submitting-patches.rst expects a
Co-developed-by: to be followed immediately by that co-author's
Signed-off-by:.
> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c
[ ... ]
> @@ -6928,33 +6928,70 @@ struct btf *btf_parse_vmlinux(void)
> struct btf *bpf_get_btf_vmlinux(void)
> {
[ ... ]
> + 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)) {
[ ... ]
> + /* 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
> * the pointer publication. Pairs with the acquire
> * on the lockless fast path above.
> */
> smp_store_release(&btf_vmlinux, btf);
> }
Is every -ENOMEM from btf_parse_vmlinux() really transient here?
btf_vmlinux_data() in kernel/bpf/sysfs_btf.c, from the previous patch in the
series, also returns -ENOMEM for a permanent failure. Its comment says
"ERR_PTR(-ENOMEM) if that cannot be done (now: memory; for good: no copy of
the compressed BTF)". If the vmalloc() in btf_vmlinux_keep_zst() fails,
btf_zst becomes NULL and every later call ends up at:
if (!btf_zst)
return ERR_PTR(btf_vmlinux_broken ? -EINVAL : -ENOMEM);
From then on bpf_get_btf_vmlinux() never reaches btf_drop_all_deferred() and
never publishes btf_vmlinux. Every queued built-in registration and its
kmemdup() copy is kept, no "dropped N deferred BTF registrations" warning is
printed, and each later caller (verifier, BPF_BTF_GET_FD_BY_ID, the
CAP_SYS_ADMIN BPF_BTF_GET_NEXT_ID, module BTF parse) takes btf_vmlinux_lock
again to re-run a parse that cannot succeed.
This also looks different from the commit message, which says "If the vmlinux
BTF fails for good, the queue is dropped with a warning."
Should btf_vmlinux_data() return a different errno for the permanent case, or
should this retry test be able to tell the two cases apart?
[ ... ]
> @@ -9300,12 +9337,16 @@ int btf_get_info_by_fd(const struct btf *btf,
> 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))
> btf = ERR_PTR(-ENOENT);
> rcu_read_unlock();
Can this hand out an fd for a vmlinux BTF that is still being built?
The return value of bpf_get_btf_vmlinux() is dropped here. If this task's
own parse round fails with -ENOMEM (the commit message names the case where
the task is being killed and vmalloc() gives up), btf_vmlinux stays NULL and
btf_vmlinux_lock is released. Another task can then take the lock and start
a new round.
btf_parse_vmlinux() gives that new BTF an id in btf_idr via btf_alloc_id()
before btf_apply_deferred() runs, so the idr_find() plus
refcount_inc_not_zero() above can return it while its kfunc and struct_ops
tables are still being filled in under btf_vmlinux_lock.
That seems to conflict with the commit message ("Nobody else sees btf_vmlinux
before its kfuncs and struct_ops are in"), and with the assumption in the
drop path of bpf_get_btf_vmlinux():
/* nothing saw it: drop it with what was applied to it */
bpf_ctx_convert.t = NULL;
btf_put(btf);
If the second round also fails with -ENOMEM, that btf_put() takes the
refcount from 2 to 1. The BTF is not freed and btf_free_id() is not called,
so a second, partially applied "vmlinux" BTF stays in btf_idr and can be
found by BPF_BTF_GET_NEXT_ID and BPF_BTF_GET_FD_BY_ID until that fd is
closed.
The fd could also be used while the other round is still applying
registrations. For example, BPF_PROG_LOAD accepts a kernel BTF fd as
attach_btf_obj_fd, and for struct_ops programs the verifier then walks
btf->struct_ops_tab while btf_add_struct_ops() may krealloc() it.
Would returning the error from btf_get_fd_by_id() when bpf_get_btf_vmlinux()
fails (or at least on -ENOMEM) close this?
---
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/41205212136
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 10/13] bpf: defer kfunc and struct_ops registrations until first BTF use
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:55 ` bot+bpf-ci
@ 2026-10-31 17:07 ` Alexei Starovoitov
1 sibling, 0 replies; 26+ messages in thread
From: Alexei Starovoitov @ 2026-10-31 17:07 UTC (permalink / raw)
To: Jay Wang, bpf, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman,
Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
On Sat, Oct 31, 2026 at 06:09 AM Jay Wang <wanjay@amazon.com> wrote:
> The first use can run out of memory, also only because its task is
> being killed: vmalloc() gives up then. A round that fails with
> -ENOMEM, in the parse or in a replayed registration, is not published:
> the new BTF is dropped with whatever was applied to it, the queue stays
> as it is, and the next caller starts over (a struct_ops ->init() then
> runs again with the new BTF; the in-tree ones only look types up or
> store the BTF pointer, which nothing can use before the BTF is
> published).
can we do the first use from a work item ?
A kworker has no signals and is never an OOM victim.
Then -ENOMEM is as unlikely as in an initcall today, and an error can
be final, like without the option.
workqueue_init() runs before the first initcall, so it works at boot.
No need for the retry, for btf_unparse_kept_modules() in patch 11 and
for ->init() that runs twice.
Something like this (not even compile tested):
static void btf_vmlinux_first_use(struct work_struct *work)
{
struct btf *btf;
guard(mutex)(&btf_vmlinux_lock);
if (btf_vmlinux)
return;
btf = btf_parse_vmlinux();
if (IS_ERR(btf)) {
btf_drop_all_deferred(PTR_ERR(btf));
} else {
btf_vmlinux_new = btf;
WRITE_ONCE(btf_vmlinux_builder, current);
btf_apply_deferred();
WRITE_ONCE(btf_vmlinux_builder, NULL);
btf_vmlinux_new = NULL;
}
smp_store_release(&btf_vmlinux, btf);
}
static DECLARE_WORK(btf_vmlinux_work, btf_vmlinux_first_use);
struct btf *bpf_get_btf_vmlinux(void)
{
struct btf *btf = smp_load_acquire(&btf_vmlinux);
if (btf || !IS_ENABLED(CONFIG_DEBUG_INFO_BTF))
return btf;
if (btf_vmlinux_building())
return btf_vmlinux_new;
queue_work(system_dfl_wq, &btf_vmlinux_work);
flush_work(&btf_vmlinux_work);
return smp_load_acquire(&btf_vmlinux);
}
The undo has a hole already.
bpf_io_init() and bm_bpf_init() don't only look types up. They register
kfuncs. And bpf_struct_ops_desc_init() turns every ->init() error into
-EINVAL:
btf_apply_deferred()
__register_bpf_struct_ops(&bpf_ring_ops)
bpf_struct_ops_desc_init()
bpf_io_init()
register_btf_kfunc_id_set()
btf_populate_kfunc_set() krealloc() fails
btf_free_kfunc_set_tab() frees all vmlinux kfunc sets
returns -ENOMEM
returns -EINVAL
pr_warn(), next entry
The BTF is published. The kfunc sets that were applied before this
entry are gone until reboot.
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH bpf-next 11/13] bpf: keep module BTF until the vmlinux BTF is parsed
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
` (9 preceding siblings ...)
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:09 ` Jay Wang
2026-10-31 13:55 ` bot+bpf-ci
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
12 siblings, 1 reply; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
Module BTF is split BTF on top of the vmlinux BTF, so every module with
BTF that loads parses the vmlinux BTF, and a system that loads any such
module at boot still pays for it there. With
CONFIG_DEBUG_INFO_BTF_LAZY, keep a module's BTF unparsed until the
vmlinux BTF is parsed, and parse it then, together with the vmlinux BTF.
A module that loads before the vmlinux BTF is parsed keeps its copies
of .BTF and .BTF.inline unparsed (btf == NULL) and gets
/sys/kernel/btf/<module> and <module>.inline created from them right
away, before the entry is visible to the parse. In-tree module BTF and
its inline BTF need no relocation, so the files serve the same bytes
before and after. The task that parses the vmlinux BTF parses the kept
module BTFs first, oldest first so that BTF ids follow the module load
order, under btf_vmlinux_lock and before the queued registrations are
applied, so that a module's registrations find its BTF. Modules with
distilled base BTF (.BTF.base) need relocation against the vmlinux BTF
and are parsed at load, which parses the vmlinux BTF. Deferring them
too is left for later, to keep this series small (see the TODO in
btf_module_keep()): their BTF is not usable before the relocation, so
their sysfs files would have to wait for the first use as well.
Walkers of btf_modules skip entries without BTF, and
btf_get_module_btf() parses the vmlinux BTF first, except for a module
without BTF, which has nothing to find. A module that goes away before
the parse just frees its kept copies.
A kept module BTF that fails validation later cannot be refused any
more: the module stays loaded without BTF, logged, and its files go
away, as with CONFIG_MODULE_ALLOW_BTF_MISMATCH, where the module load
fails today without that option. An inline BTF that fails validation
loses its file, as today. A kept module BTF that runs out of memory
fails the whole round, which the previous patch then retries, with the
module BTFs parsed in it freed again.
All kept module BTFs are parsed before any queued registration is
applied, so btf_check_kfunc_name() now also sees the BTF of modules
loaded after the one registering: a kfunc whose name such a module
defines as a function is refused, where today only the later module's
own registration of it would be.
The Kconfig help now describes what still parses the BTF at boot and
what changes with the option.
Co-developed-by: Ihor Solodrai <ihor.solodrai@linux.dev>
TODO-NEEDS-IHOR: Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
kernel/bpf/btf.c | 204 +++++++++++++++++++++++++++++++++++++++-------
lib/Kconfig.debug | 8 ++
2 files changed, 184 insertions(+), 28 deletions(-)
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index a5b1cd63a1e4..631f5a32ec17 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -9382,10 +9382,13 @@ enum {
struct btf_module {
struct list_head list;
struct module *module;
- struct btf *btf;
+ struct btf *btf; /* NULL while kept, see btf_module_keep() */
+ void *data; /* kept copy of .BTF, adopted by btf once parsed */
+ u32 data_size;
struct bin_attribute *sysfs_attr;
struct bin_attribute *sysfs_inline_attr;
void *btf_inline_data;
+ u32 btf_inline_data_size;
int flags;
};
@@ -9395,6 +9398,149 @@ static DEFINE_MUTEX(btf_module_mutex);
static void purge_cand_cache(struct btf *btf);
static void btf_drop_deferred_regs(struct module *owner);
+/* /sys/kernel/btf/<module> and <module>.inline */
+static void btf_module_add_files(struct btf_module *btf_mod, const char *name,
+ void *data, u32 data_size)
+{
+ char inline_name[MODULE_NAME_LEN + sizeof(".inline")];
+ struct bin_attribute *attr;
+
+ attr = sysfs_btf_add(name, data, data_size, false, NULL);
+ if (IS_ERR(attr))
+ return;
+ btf_mod->sysfs_attr = attr;
+
+ if (!btf_mod->btf_inline_data)
+ return;
+ snprintf(inline_name, sizeof(inline_name), "%s.inline", name);
+ attr = sysfs_btf_add(inline_name, btf_mod->btf_inline_data,
+ btf_mod->btf_inline_data_size, false, NULL);
+ if (IS_ERR(attr)) {
+ kvfree(btf_mod->btf_inline_data);
+ btf_mod->btf_inline_data = NULL;
+ return;
+ }
+ btf_mod->sysfs_inline_attr = attr;
+}
+
+/*
+ * CONFIG_DEBUG_INFO_BTF_LAZY: module BTF is split BTF on top of the vmlinux
+ * BTF. Until that is parsed, keep the module's copies of its BTF and inline
+ * BTF as they are, serve them in sysfs, and let btf_parse_kept_modules()
+ * parse them with the vmlinux BTF. Distilled base BTF (.BTF.base) needs
+ * relocation, which needs the vmlinux BTF, so it is parsed right away.
+ *
+ * TODO: defer modules with distilled base BTF as well: keep them here and
+ * relocate them in btf_parse_kept_modules(). Their raw BTF is not usable
+ * before that, so their sysfs files would have to wait for the first use.
+ */
+static bool btf_module_keep(struct btf_module *btf_mod, struct module *mod,
+ void *data, void *inline_data, u32 inline_data_size)
+{
+ if (!IS_ENABLED(CONFIG_DEBUG_INFO_BTF_LAZY) || mod->btf_base_data)
+ return false;
+ guard(mutex)(&btf_vmlinux_lock);
+ if (btf_vmlinux)
+ return false;
+ btf_mod->module = mod;
+ btf_mod->data = data;
+ btf_mod->data_size = mod->btf_data_size;
+ btf_mod->btf_inline_data = inline_data;
+ btf_mod->btf_inline_data_size = inline_data ? inline_data_size : 0;
+ /* before btf_parse_kept_modules() can see the entry */
+ btf_module_add_files(btf_mod, mod->name, data, mod->btf_data_size);
+ mutex_lock(&btf_module_mutex);
+ list_add(&btf_mod->list, &btf_modules);
+ mutex_unlock(&btf_module_mutex);
+ return true;
+}
+
+/* A kept module without BTF: no entry and no files, as if refused at load */
+static void btf_module_drop_kept(struct btf_module *btf_mod)
+{
+ list_del(&btf_mod->list);
+ if (btf_mod->sysfs_attr)
+ sysfs_btf_remove(btf_mod->sysfs_attr);
+ if (btf_mod->sysfs_inline_attr)
+ sysfs_btf_remove(btf_mod->sysfs_inline_attr);
+ kvfree(btf_mod->btf_inline_data);
+ kvfree(btf_mod->data);
+ kfree(btf_mod);
+}
+
+/* Undo btf_parse_kept_modules() of a round that is not published */
+static void btf_unparse_kept_modules(void)
+{
+ struct btf_module *btf_mod;
+
+ lockdep_assert_held(&btf_module_mutex);
+ list_for_each_entry(btf_mod, &btf_modules, list) {
+ if (!btf_mod->data || !btf_mod->btf)
+ continue;
+ btf_mod->btf->data = NULL; /* the kept copy */
+ btf_put(btf_mod->btf);
+ btf_mod->btf = NULL;
+ }
+}
+
+/*
+ * Called under btf_vmlinux_lock by the task that parses the vmlinux BTF,
+ * before that is published. Parses the kept module BTFs in place, oldest
+ * first so that ids follow the load order as without
+ * CONFIG_DEBUG_INFO_BTF_LAZY; in-tree module BTF and its inline BTF need no
+ * relocation, so sysfs serves the same bytes before and after. A module
+ * whose BTF fails validation cannot be refused any more: it stays loaded
+ * without BTF, and its files go away; an invalid inline BTF loses its file.
+ * Returns -ENOMEM, with this round's module BTFs freed again, if memory ran
+ * out.
+ */
+static int btf_parse_kept_modules(void)
+{
+ struct btf_module *btf_mod, *tmp;
+ void *inline_data;
+ struct btf *btf;
+ int err;
+
+ lockdep_assert_held(&btf_vmlinux_lock);
+ guard(mutex)(&btf_module_mutex);
+ list_for_each_entry_safe_reverse(btf_mod, tmp, &btf_modules, list) {
+ if (btf_mod->btf || !btf_mod->data)
+ continue;
+ btf = btf_parse_module(btf_mod->module->name, btf_mod->data,
+ btf_mod->data_size, NULL, 0,
+ btf_mod->btf_inline_data,
+ btf_mod->btf_inline_data_size, &inline_data);
+ if (!IS_ERR(btf)) {
+ err = btf_alloc_id(btf);
+ if (err) {
+ btf->data = NULL; /* the kept copy */
+ btf_free(btf);
+ btf = ERR_PTR(err);
+ }
+ }
+ if (btf == ERR_PTR(-ENOMEM)) {
+ btf_unparse_kept_modules();
+ return -ENOMEM;
+ }
+ if (IS_ERR(btf)) {
+ pr_warn("failed to validate module [%s] BTF: %ld\n",
+ btf_mod->module->name, PTR_ERR(btf));
+ btf_module_drop_kept(btf_mod);
+ continue;
+ }
+ btf_mod->btf = btf;
+ if (btf_mod->btf_inline_data && !inline_data) {
+ if (btf_mod->sysfs_inline_attr)
+ sysfs_btf_remove(btf_mod->sysfs_inline_attr);
+ btf_mod->sysfs_inline_attr = NULL;
+ kvfree(btf_mod->btf_inline_data);
+ btf_mod->btf_inline_data = NULL;
+ btf_mod->btf_inline_data_size = 0;
+ }
+ }
+ return 0;
+}
+
#if IS_MODULE(CONFIG_DEBUG_INFO_BTF_INLINE)
extern char __start_BTF_inline_link[];
extern char __stop_BTF_inline_link[];
@@ -9504,7 +9650,6 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
{
struct btf_module *btf_mod, *tmp;
struct module *mod = module;
- struct bin_attribute *attr;
struct btf *btf;
void *data, *inline_data = NULL, *relocated_inline_data = NULL;
unsigned int inline_data_size = 0;
@@ -9542,6 +9687,9 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
pr_warn("failed to validate module [%s] inline BTF: %d\n",
mod->name, -ENOMEM);
}
+ if (data && btf_module_keep(btf_mod, mod, data, inline_data,
+ inline_data_size))
+ break;
btf = !data ? ERR_PTR(-ENOMEM) :
btf_parse_module(mod->name, data, mod->btf_data_size,
mod->btf_base_data, mod->btf_base_data_size,
@@ -9574,30 +9722,12 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
btf_mod->module = module;
btf_mod->btf = btf;
btf_mod->btf_inline_data = relocated_inline_data;
+ btf_mod->btf_inline_data_size = relocated_inline_data ? inline_data_size : 0;
list_add(&btf_mod->list, &btf_modules);
mutex_unlock(&btf_module_mutex);
- attr = sysfs_btf_add(btf->name, btf->data, btf->data_size, false, NULL);
- if (IS_ERR(attr)) {
- err = 0;
- goto out;
- }
- btf_mod->sysfs_attr = attr;
-
- if (relocated_inline_data) {
- char name[MODULE_NAME_LEN + sizeof(".inline")];
-
- snprintf(name, sizeof(name), "%s.inline", mod->name);
- attr = sysfs_btf_add(name, relocated_inline_data,
- inline_data_size, false, NULL);
- if (IS_ERR(attr)) {
- kvfree(relocated_inline_data);
- btf_mod->btf_inline_data = NULL;
- err = 0;
- break;
- }
- btf_mod->sysfs_inline_attr = attr;
- }
+ /* data: the same as btf->data */
+ btf_module_add_files(btf_mod, mod->name, data, mod->btf_data_size);
break;
case MODULE_STATE_LIVE:
if (!mod->btf_data_size)
@@ -9621,6 +9751,11 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
if (btf_mod->module != module)
continue;
+ if (!btf_mod->btf) { /* kept, see btf_module_keep() */
+ btf_module_drop_kept(btf_mod);
+ break;
+ }
+
/*
* For modules, we do the freeing of BTF IDR as soon as
* module goes away to disable BTF discovery, since the
@@ -9705,9 +9840,12 @@ struct btf *btf_get_module_btf(const struct module *module)
}
#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+ /* kept module BTF is parsed together with the vmlinux BTF */
+ if (!module->btf_data_size || IS_ERR_OR_NULL(bpf_get_btf_vmlinux()))
+ return NULL;
mutex_lock(&btf_module_mutex);
list_for_each_entry_safe(btf_mod, tmp, &btf_modules, list) {
- if (btf_mod->module != module)
+ if (btf_mod->module != module || !btf_mod->btf)
continue;
btf_get(btf_mod->btf);
@@ -9888,7 +10026,7 @@ static int btf_check_kfunc_name(struct btf *btf, const char *func_name, u32 kind
#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
guard(mutex)(&btf_module_mutex);
list_for_each_entry_safe(btf_mod, tmp, &btf_modules, list) {
- if (btf_mod->btf == btf)
+ if (btf_mod->btf == btf || !btf_mod->btf)
continue;
id = btf_find_by_name_kind(btf_mod->btf, func_name, kind);
if (id >= 0) {
@@ -11169,8 +11307,8 @@ static void btf_free_deferred_reg(struct btf_deferred_reg *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
+ * before it publishes it, so that nothing sees the BTF without its module
+ * BTFs, 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:
@@ -11187,6 +11325,11 @@ static int btf_apply_deferred(void)
struct btf_deferred_reg *reg, *tmp;
int err;
+#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+ err = btf_parse_kept_modules();
+ if (err)
+ return err;
+#endif
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))
@@ -11206,8 +11349,13 @@ static int btf_apply_deferred(void)
break;
}
module_put(reg->owner);
- if (err == -ENOMEM)
+ if (err == -ENOMEM) {
+#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+ guard(mutex)(&btf_module_mutex);
+ btf_unparse_kept_modules();
+#endif
return err;
+ }
if (err)
pr_warn("failed to apply deferred BTF registration for %s: %d\n",
reg->owner ? reg->owner->name : "vmlinux", err);
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 700c4f67bdd8..45b2bc4d9a9c 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -422,6 +422,14 @@ config DEBUG_INFO_BTF_LAZY
milliseconds once. The vmlinux file keeps the uncompressed BTF as
a non-loaded section for tools. Building needs the zstd tool.
+ Users of kernel types at boot still parse it at boot: BPF LSM
+ programs, the iterators of BPF_PRELOAD (loaded at boot, or at the
+ first bpffs mount if bpf_preload is a module), and modules with
+ distilled base BTF. Kernel function and struct_ops registrations
+ made before the first use are applied then and report errors only
+ in the kernel log; a module BTF that fails validation then leaves
+ the module loaded without BTF.
+
If unsure, say N.
config PAHOLE_HAS_BTF_TAG
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 11/13] bpf: keep module BTF until the vmlinux BTF is parsed
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:55 ` bot+bpf-ci
2026-10-31 17:10 ` Alexei Starovoitov
0 siblings, 1 reply; 26+ messages in thread
From: bot+bpf-ci @ 2026-10-31 13:55 UTC (permalink / raw)
To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
Cc: ihor.solodrai, almagui, martin.lau, yonghong.song, jolsa, rostedt,
mhiramat, mathieu.desnoyers, linux-trace-kernel, nathan, nsc,
linux-kbuild, arnd, linux-arch, x86, linux-doc, linux-kernel,
abuehaze, doebel, mpohlack, jay.wang.upstream, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 7897 bytes --]
> bpf: keep module BTF until the vmlinux BTF is parsed
TL;DR:
- Possible NULL deref oops in btf_parse_kept_modules() if sysfs_btf_add()
fails for .inline, hanging BTF users
- Possible use-after-free of rolled-back module BTF via btf_idr (low severity)
- Kept module BTFs may not be cleaned up on permanent vmlinux BTF failure
> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> index e31f8b3fecf4..c7234bcba874 100644
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c
[ ... ]
> @@ -9396,6 +9399,149 @@ static DEFINE_MUTEX(btf_module_mutex);
[ ... ]
> +/* /sys/kernel/btf/<module> and <module>.inline */
> +static void btf_module_add_files(struct btf_module *btf_mod, const char *name,
> + void *data, u32 data_size)
> +{
[ ... ]
> + attr = sysfs_btf_add(inline_name, btf_mod->btf_inline_data,
> + btf_mod->btf_inline_data_size, false, NULL);
> + if (IS_ERR(attr)) {
> + kvfree(btf_mod->btf_inline_data);
> + btf_mod->btf_inline_data = NULL;
> + return;
> + }
Can this error path leave btf_inline_data_size non-zero while
btf_inline_data is NULL?
When sysfs_btf_add() fails for the <module>.inline file, btf_inline_data is
freed and set to NULL, but btf_inline_data_size keeps its value. Before
this patch that did no harm, since the field was never read again.
With CONFIG_DEBUG_INFO_BTF_LAZY, btf_module_keep() calls
btf_module_add_files() for a kept module before it is parsed, and
btf_parse_kept_modules() later passes the two fields as a pair:
btf = btf_parse_module(btf_mod->module->name, btf_mod->data,
btf_mod->data_size, NULL, 0,
btf_mod->btf_inline_data,
btf_mod->btf_inline_data_size, &inline_data);
btf_parse_module() checks only the size:
if (inline_data_size) {
inline_btf = btf_parse_split(env, module_name, inline_data,
inline_data_size, btf);
btf_parse_split() then sets btf->data = NULL with btf->data_size = N, and
btf_parse_hdr() does:
hdr = btf->data;
hdr_len = hdr->hdr_len;
Since a real .BTF.inline section is larger than
offsetofend(struct btf_header, hdr_len), the size check before that read
passes and hdr is NULL.
The trigger is CONFIG_DEBUG_INFO_BTF_LAZY=y with
CONFIG_DEBUG_INFO_BTF_INLINE=y or =m, a module with .BTF.inline that loads
before the first BTF use, and a sysfs_btf_add() failure for "<mod>.inline"
(the kzalloc_obj() or kstrdup() in sysfs_btf_add() returning -ENOMEM, or
sysfs_create_bin_file() failing).
The next first user of the vmlinux BTF would then oops inside
btf_parse_kept_modules(), holding btf_vmlinux_lock and btf_module_mutex, so
later BTF users and BTF module load and unload would hang. The later patch
in this series (4d32ca0c95ea) only touches selftests and does not change
this.
The old load-time path guarded this with:
inline_data, inline_data ? inline_data_size : 0
and the new kept path has no such guard. Should btf_inline_data_size also
be cleared here, or should btf_parse_kept_modules() pass
"btf_mod->btf_inline_data ? btf_mod->btf_inline_data_size : 0"?
[ ... ]
> +/* Undo btf_parse_kept_modules() of a round that is not published */
> +static void btf_unparse_kept_modules(void)
> +{
[ ... ]
> + btf_mod->btf->data = NULL; /* the kept copy */
> + btf_put(btf_mod->btf);
> + btf_mod->btf = NULL;
> + }
> +}
[ ... ]
> + if (!IS_ERR(btf)) {
> + err = btf_alloc_id(btf);
Can a module BTF that is rolled back by btf_unparse_kept_modules() still be
reachable through btf_idr?
Kept module BTFs now get btf_idr ids here, inside a lazy parse round, before
the vmlinux BTF is published. If the round fails with -ENOMEM,
btf_unparse_kept_modules() rolls them back with btf->data = NULL followed by
btf_put(), which frees a module BTF only if nobody else holds a reference.
btf_get_fd_by_id() tries to close this window by calling
bpf_get_btf_vmlinux() first, but it ignores the return value:
/* 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))
A possible timeline:
T1: BPF_BTF_GET_FD_BY_ID runs bpf_get_btf_vmlinux(), which runs a round
that fails with -ENOMEM and returns with btf_vmlinux still NULL.
T2: takes btf_vmlinux_lock and starts a new round: btf_parse_vmlinux(), then
btf_parse_kept_modules() gives module M's BTF the id X.
T1: idr_find(X) and refcount_inc_not_zero() succeed, and an fd now pins M's
in-progress BTF.
T2: a registration hits -ENOMEM, so btf_unparse_kept_modules() drops M's BTF
(refcount 2 to 1, so it stays in the idr with data == NULL), and
bpf_get_btf_vmlinux() does btf_put() on the round's vmlinux BTF, which
frees it.
The BTF behind the fd then has base_btf pointing at the freed vmlinux BTF,
since btf_set_base_btf() stores a raw pointer and takes no reference. Its
types[] also point into btf_mod->data, which is later adopted by the next
round's BTF and freed at module unload. Using the fd, for example as
attach_btf_obj_fd, where btf_type_by_id() falls through to base_btf, or with
BPF_OBJ_GET_INFO_BY_FD, which copies btf->data, would read freed memory or
NULL.
Before this patch only the vmlinux BTF was in the idr during a round, and a
stale vmlinux BTF is self-contained (its data is never freed), so this needs
the module BTFs added here. It requires two -ENOMEM rounds plus a
concurrent CAP_SYS_ADMIN lookup, so the severity is low, but nothing
structurally prevents it.
Would returning early in btf_get_fd_by_id() when bpf_get_btf_vmlinux()
fails be enough? That is not complete on its own, since T1 could also race
a later round. Would it be better to allocate the kernel BTF ids only at
publication?
[ ... ]
> @@ -11199,6 +11337,11 @@ static int btf_apply_deferred(void)
[ ... ]
> +#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
> + err = btf_parse_kept_modules();
> + if (err)
> + return err;
> +#endif
Is anything going to clean up the kept module BTFs when the vmlinux BTF
fails permanently?
Kept module BTFs are only parsed or dropped through
btf_parse_kept_modules(), and btf_apply_deferred() is its only caller.
bpf_get_btf_vmlinux() calls btf_apply_deferred() only when
btf_parse_vmlinux() succeeds:
btf = btf_parse_vmlinux();
if (IS_ENABLED(CONFIG_DEBUG_INFO_BTF_LAZY) && !IS_ERR(btf)) {
...
if (IS_ERR(btf))
btf_drop_all_deferred(PTR_ERR(btf));
If the vmlinux BTF fails for any error other than -ENOMEM (for example
-EINVAL from btf_parse_base() or from btf_vmlinux_data()), btf_vmlinux is
published as an ERR_PTR. btf_drop_all_deferred() cleans up the queued
registrations, but nothing does the same for the kept modules.
Every module that loaded before that point then keeps btf_mod->data and
btf_inline_data allocated, and keeps /sys/kernel/btf/<mod> and
<mod>.inline, which serve module BTF that was never validated, until the
module is unloaded. Nothing is logged for these modules.
A module loaded after the failure is not kept, because of this check in
btf_module_keep():
if (btf_vmlinux)
return false;
so it goes to btf_parse_module(), fails with the vmlinux error and is
refused at load (unless CONFIG_MODULE_ALLOW_BTF_MISMATCH). Two modules can
end up in opposite states depending only on whether they loaded before the
first BTF use.
The commit message says a kept module whose BTF cannot be used ends up
"without BTF, logged, and its files go away", and that does not hold on this
path.
Would a counterpart to btf_drop_all_deferred() that walks btf_modules under
btf_module_mutex and calls btf_module_drop_kept() for every entry with
btf == NULL, run on a permanent vmlinux failure, make both cases match?
---
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/41205212136
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH bpf-next 11/13] bpf: keep module BTF until the vmlinux BTF is parsed
2026-10-31 13:55 ` bot+bpf-ci
@ 2026-10-31 17:10 ` Alexei Starovoitov
0 siblings, 0 replies; 26+ messages in thread
From: Alexei Starovoitov @ 2026-10-31 17:10 UTC (permalink / raw)
To: bot+bpf-ci, wanjay, bpf, daniel, andrii, eddyz87, memxor
Cc: ihor.solodrai, almagui, martin.lau, yonghong.song, jolsa, rostedt,
mhiramat, mathieu.desnoyers, linux-trace-kernel, nathan, nsc,
linux-kbuild, arnd, linux-arch, x86, linux-doc, linux-kernel,
abuehaze, doebel, mpohlack, jay.wang.upstream, martin.lau, mason
On Sat, Oct 31, 2026 at 06:55 AM bot+bpf-ci@kernel.org <bot+bpf-ci@kernel.org> wrote:
>> +/* Undo btf_parse_kept_modules() of a round that is not published */
>> +static void btf_unparse_kept_modules(void)
>> +{
> [ ... ]
>> + btf_mod->btf->data = NULL; /* the kept copy */
>> + btf_put(btf_mod->btf);
>> + btf_mod->btf = NULL;
>> + }
>> +}
[...]
> Can a module BTF that is rolled back by btf_unparse_kept_modules() still be
> reachable through btf_idr?
Yes. It's real. The NULL deref in the inline BTF is real too.
This is quite heavy. As you can see both bots poked plenty of holes.
Five of them are in the -ENOMEM rollback of patches 10 and 11:
where what goes wrong
--------------------------- -------------------------------------------
btf_get_fd_by_id() takes a ref on a BTF that is rolled back
btf_free_rcu() reads btf_mod->data that rmmod freed
btf_parse_kept_modules() -ENOMEM in the inline BTF removes its file
bpf_struct_ops_desc_init() -ENOMEM of ->init() becomes -EINVAL
btf_vmlinux_data() -ENOMEM without btf_zst repeats forever
The rollback exists because vmalloc() gives up when the task is killed.
can we run the parse from a work item, schedule_work() + flush_work() ?
A kworker gets no signals, so -ENOMEM can be final, like without the
option. Every parse then ends in smp_store_release(&btf_vmlinux), and
btf_get_fd_by_id() sleeps on btf_vmlinux_lock until that.
Then no need for the retry, btf_unparse_kept_modules(), the 2nd call
of ->init(), etc.
See my other reply.
pw-bot: cr
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH bpf-next 12/13] selftests/bpf: keep ksyms and linked_vars working without __start_BTF
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
` (10 preceding siblings ...)
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:09 ` Jay Wang
2026-10-31 13:09 ` [PATCH bpf-next 13/13] Documentation/bpf: describe CONFIG_DEBUG_INFO_BTF_LAZY Jay Wang
12 siblings, 0 replies; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
test_ksyms.c reads __start_BTF and __stop_BTF as typeless ksyms and
compares their distance with the size of /sys/kernel/btf/vmlinux, and
linked_vars2.c uses __start_BTF as its strong typeless ksym. With
CONFIG_DEBUG_INFO_BTF_LAZY the image carries the vmlinux BTF only
compressed and these symbols do not exist, so libbpf cannot resolve the
externs and both tests fail to load their skeletons.
Make __start_BTF and __stop_BTF weak in test_ksyms.c and expect a size
of 0 when /proc/kallsyms has no __start_BTF, so the rest of the test
still runs, and let linked_vars2.c reference bpf_map_fops instead, which
every kernel that runs BPF has.
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
tools/testing/selftests/bpf/prog_tests/ksyms.c | 8 +++++++-
tools/testing/selftests/bpf/progs/linked_vars2.c | 4 ++--
tools/testing/selftests/bpf/progs/test_ksyms.c | 5 +++--
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/ksyms.c b/tools/testing/selftests/bpf/prog_tests/ksyms.c
index dc7aab532fb1..fa97cc0ee3bf 100644
--- a/tools/testing/selftests/bpf/prog_tests/ksyms.c
+++ b/tools/testing/selftests/bpf/prog_tests/ksyms.c
@@ -10,7 +10,7 @@ void test_ksyms(void)
const char *btf_path = "/sys/kernel/btf/vmlinux";
struct test_ksyms *skel;
struct test_ksyms__data *data;
- __u64 link_fops_addr, per_cpu_start_addr;
+ __u64 link_fops_addr, per_cpu_start_addr, btf_start_addr;
struct stat st;
__u64 btf_size;
int err;
@@ -30,6 +30,12 @@ void test_ksyms(void)
if (!ASSERT_OK(stat(btf_path, &st), "stat_btf"))
return;
btf_size = st.st_size;
+ /*
+ * With CONFIG_DEBUG_INFO_BTF_LAZY the image has the vmlinux BTF only
+ * compressed and no __start_BTF/__stop_BTF: the weak ksyms are 0.
+ */
+ if (kallsyms_find("__start_BTF", &btf_start_addr) == -ENOENT)
+ btf_size = 0;
skel = test_ksyms__open_and_load();
if (!ASSERT_OK_PTR(skel, "test_ksyms__open_and_load"))
diff --git a/tools/testing/selftests/bpf/progs/linked_vars2.c b/tools/testing/selftests/bpf/progs/linked_vars2.c
index e4f5bd388a3c..bbdc3b25b483 100644
--- a/tools/testing/selftests/bpf/progs/linked_vars2.c
+++ b/tools/testing/selftests/bpf/progs/linked_vars2.c
@@ -8,7 +8,7 @@
extern int LINUX_KERNEL_VERSION __kconfig;
/* when an extern is defined as both strong and weak, resulting symbol will be strong */
extern bool CONFIG_BPF_SYSCALL __kconfig;
-extern const void __start_BTF __ksym;
+extern const void bpf_map_fops __ksym;
int input_bss2;
int input_data2 = 2;
@@ -47,7 +47,7 @@ int BPF_PROG(handler2)
*/
output_sink2 = LINUX_KERNEL_VERSION
+ CONFIG_BPF_SYSCALL
- + (long)&__start_BTF;
+ + (long)&bpf_map_fops;
return 0;
}
diff --git a/tools/testing/selftests/bpf/progs/test_ksyms.c b/tools/testing/selftests/bpf/progs/test_ksyms.c
index 6c9cbb5a3bdf..042943088225 100644
--- a/tools/testing/selftests/bpf/progs/test_ksyms.c
+++ b/tools/testing/selftests/bpf/progs/test_ksyms.c
@@ -11,8 +11,9 @@ __u64 out__btf_size = -1;
__u64 out__per_cpu_start = -1;
extern const void bpf_link_fops __ksym;
-extern const void __start_BTF __ksym;
-extern const void __stop_BTF __ksym;
+/* weak: CONFIG_DEBUG_INFO_BTF_LAZY keeps no uncompressed BTF in the image */
+extern const void __start_BTF __ksym __weak;
+extern const void __stop_BTF __ksym __weak;
extern const void __per_cpu_start __ksym;
/* non-existing symbol, weak, default to zero */
extern const void bpf_link_fops1 __ksym __weak;
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread* [PATCH bpf-next 13/13] Documentation/bpf: describe CONFIG_DEBUG_INFO_BTF_LAZY
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
` (11 preceding siblings ...)
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 ` Jay Wang
12 siblings, 0 replies; 26+ messages in thread
From: Jay Wang @ 2026-10-31 13:09 UTC (permalink / raw)
To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Ihor Solodrai, Alan Maguire, Martin KaFai Lau, Yonghong Song,
Jiri Olsa, Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, Nathan Chancellor, Nicolas Schier,
linux-kbuild, Arnd Bergmann, linux-arch, x86, linux-doc,
linux-kernel, Hazem Mohamed Abuelfotoh, Bjoern Doebel,
Martin Pohlack, jay.wang.upstream
Describe what the option does, where it is available, when the
vmlinux BTF gets parsed, and what changes for users: registrations and
module BTF that wait for the first use, the BTF id, the missing
__start_BTF/__stop_BTF symbols, and what still parses it at boot.
Signed-off-by: Jay Wang <wanjay@amazon.com>
---
Documentation/bpf/btf.rst | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/Documentation/bpf/btf.rst b/Documentation/bpf/btf.rst
index 29de1222c3e7..1ebce5cf5783 100644
--- a/Documentation/bpf/btf.rst
+++ b/Documentation/bpf/btf.rst
@@ -1276,6 +1276,36 @@ format.::
.long 58
.long 8206 # Line 8 Col 14
+6.1 Compressed vmlinux BTF
+--------------------------
+
+With ``CONFIG_DEBUG_INFO_BTF_LAZY``, the kernel image carries the vmlinux BTF
+zstd-compressed, at about a quarter of its size, and the kernel decompresses
+and parses it when something first needs it: a BPF program or map that uses
+kernel types, ``BPF_BTF_GET_NEXT_ID``, tracing options that print arguments or
+return values by their BTF types.
+``/sys/kernel/btf/vmlinux`` has its full size from the start; reading or
+mapping it decompresses the BTF but does not parse it. Systems that never use
+BTF keep only the compressed copy in memory. The vmlinux file keeps the
+uncompressed ``.BTF`` section, not loaded, for tools and module builds.
+The option is available on x86 and arm64.
+
+Kernel function, destructor kfunc and struct_ops registrations made before the
+first use, and the BTF of modules loaded before it, are kept and applied by the
+task that parses the vmlinux BTF, before anyone else can see it. Errors of
+those registrations then only show in the kernel log, and a module whose BTF
+fails validation at that point stays loaded without BTF and without its files
+in ``/sys/kernel/btf``. A first use that runs out of memory fails with
+``-ENOMEM`` and leaves all of this as it was, for the next one to start over.
+The vmlinux BTF gets the next free BTF id, not necessarily 1, and the image has
+no ``__start_BTF``/``__stop_BTF`` symbols. The inline BTF
+(``CONFIG_DEBUG_INFO_BTF_INLINE``) is not compressed.
+
+Users of kernel types during boot still parse it during boot, for example BPF
+LSM programs, the iterators of ``CONFIG_BPF_PRELOAD`` (loaded at boot, or at the
+first bpffs mount if ``bpf_preload`` is a module), or a module with a
+``.BTF.base`` section.
+
7. Testing
==========
--
2.47.3
^ permalink raw reply related [flat|nested] 26+ messages in thread