BPF List
 help / color / mirror / Atom feed
* [PATCH v3 bpf-next 0/7] kbuild, bpf: Support inline info in BTF
@ 2026-10-03 19:43 Alan Maguire
  2026-10-03 19:43 ` [PATCH v3 bpf-next 1/7] resolve_btfids: Build separate main and inline BTF objects Alan Maguire
                   ` (6 more replies)
  0 siblings, 7 replies; 20+ messages in thread
From: Alan Maguire @ 2026-10-03 19:43 UTC (permalink / raw)
  To: ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, Alan Maguire

If CONFIG_DEBUG_INFO_BTF_INLINE=y|m, add the "inline" BTF feature
and encode inline site info in BTF alongside standard BTF data.
resolve_btfids will then partition into non- and inline-related
info and split BTF will be used to store inline info in a
.BTF.inline section in vmlinux and modules.  For
CONFIG_DEBUG_INFO_BTF=m, vmlinux inline info will be stored
in btf_vmlinux_inline.ko and will be loaded on-demand if used
via /sys/kernel/btf/vmlinux.inline.  A .BTF.inline.link section
in vmlinux stores the module name, the SHA of the BTF and the
size so that even without loading the kernel can size the
sysfs representation correctly.  This approach is based on [1]
and the .BTF.inline.link section is modeled on .GNU_debuglink.

Because a module can be built with .BTF.base to allow for
BTF relocation, its inline info needs to be relocated also
so a bit more work needs to be done to handle this.

Patches 1, 2 add resolve_btfids support to partition BTF
into .BTF and .BTF.inline and to support populating
a BTF link section for BTF_INLINE=m.  Patch 3 handles
vmlinux.inline exposure in sysfs.  Patch 4 does prep
work for relocation needed in patch 5 prior to exposing
relocated BTF in sysfs, and patches 6 and 7 test BTF
sysfs representations and inline validity using
bpf_testmod respectively.

To generate inline info the latest pahole is needed [2].

Inline info for vmlinux can be quite large ~10Mb for
over 600,000 inline sites in the kernel, so

CONFIG_DEBUG_INFO_BTF_INLINE=m

is recommended; this brings the on-disk size down below
4Mb.

Changes since v2 [3]

 - Delivered the libbpf, bpftool parts are prerequisites
 - Switched to using Eduard's neater BTF partitioning approach which
   is fully in resolve_btfids instead of libbpf (Eduard, patch 1)
 - Supports mmap()ed vmlinux.inline (Alexei, patch 3, test patch 7)
 - Moved to using .BTF.inline.link to store inline BTF size
   which avoids need to switch to using kernfs; also allows us
   to SHA-verify BTF inline info

[1] https://lore.kernel.org/bpf/20260925224229.1850-1-wanjay@amazon.com/
[2] https://git.kernel.org/pub/scm/devel/pahole/pahole.git/
[3] https://lore.kernel.org/bpf/20260901165757.801449-1-alan.maguire@oracle.com/

Alan Maguire (6):
  resolve_btfids: Populate BTF link section
  kbuild, btf: Expose vmlinux inline BTF through sysfs
  btf: Preserve string relocation maps for split BTF
  btf: Relocate and expose module inline BTF
  selftests/bpf: Test BTF sysfs inline representations
  selftests/bpf: Add a test verifying inline information

Eduard Zingerman (1):
  resolve_btfids: Build separate main and inline BTF objects

 Makefile                                      |   1 +
 include/asm-generic/vmlinux.lds.h             |  27 ++
 include/linux/btf.h                           |  42 +-
 include/linux/module.h                        |   4 +
 kernel/bpf/Makefile                           |   1 +
 kernel/bpf/btf.c                              | 386 ++++++++++++++----
 kernel/bpf/btf_vmlinux_inline.c               |  29 ++
 kernel/bpf/sysfs_btf.c                        | 145 ++++++-
 kernel/module/main.c                          |   7 +
 lib/Kconfig.debug                             |  19 +
 scripts/Makefile.btf                          |   7 +
 scripts/gen-btf.sh                            |  31 +-
 scripts/link-vmlinux.sh                       |   8 +-
 scripts/package/kernel.spec                   |  10 +-
 tools/bpf/resolve_btfids/Build                |   1 +
 tools/bpf/resolve_btfids/btf_colors.c         | 145 +++++++
 tools/bpf/resolve_btfids/btf_colors.h         |  24 ++
 tools/bpf/resolve_btfids/main.c               | 352 +++++++++++++++-
 tools/lib/bpf/btf.c                           |   2 +-
 tools/lib/bpf/btf_relocate.c                  |   7 +-
 tools/lib/bpf/libbpf_internal.h               |   3 +-
 tools/testing/selftests/bpf/Makefile          |   4 +-
 .../selftests/bpf/prog_tests/btf_inline.c     | 110 +++++
 .../selftests/bpf/prog_tests/btf_sysfs.c      |  76 ++++
 .../selftests/bpf/test_kmods/bpf_testmod.c    |   2 +-
 tools/testing/selftests/bpf/trace_helpers.c   |  20 +
 tools/testing/selftests/bpf/trace_helpers.h   |   1 +
 tools/testing/selftests/hid/Makefile          |   2 +
 28 files changed, 1362 insertions(+), 104 deletions(-)
 create mode 100644 kernel/bpf/btf_vmlinux_inline.c
 create mode 100644 tools/bpf/resolve_btfids/btf_colors.c
 create mode 100644 tools/bpf/resolve_btfids/btf_colors.h
 create mode 100644 tools/testing/selftests/bpf/prog_tests/btf_inline.c

-- 
2.43.5


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

* [PATCH v3 bpf-next 1/7] resolve_btfids: Build separate main and inline BTF objects
  2026-10-03 19:43 [PATCH v3 bpf-next 0/7] kbuild, bpf: Support inline info in BTF Alan Maguire
@ 2026-10-03 19:43 ` Alan Maguire
  2026-10-03 20:27   ` bot+bpf-ci
  2026-10-03 19:43 ` [PATCH v3 bpf-next 2/7] resolve_btfids: Populate BTF link section Alan Maguire
                   ` (5 subsequent siblings)
  6 siblings, 1 reply; 20+ messages in thread
From: Alan Maguire @ 2026-10-03 19:43 UTC (permalink / raw)
  To: ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, Alan Maguire

From: Eduard Zingerman <eddyz87@gmail.com>

Extract .BTF.inline via the following logic:
- Traverse all types reachable from LOC_PARAM, LOC_PROTO, LOCSEC
  entries and mark them as having LOC color.
- Traverse all types reachable from the remaining types
  and mark them as having MAIN or SHARED.
- Create a new base BTF object and copy all MAIN/SHARED types there.
- Create a new .BTF.inline object with base set to the new base BTF
  object, and copy all LOC types there.

This way .BTF.inline can refer to types and strings in the new base,
and strings used only by .BTF.inline remain in it.

Additionally, all BTF_KIND_FUNC types that map to ELF symbols with the
same name are forced to be in the base BTF. Same for types referenced
from BTF id sets.

Regular types referenced only from .BTF.inline remain in the base BTF,
and sizes are 5 and 9.5Mb for vmlinux and vmlinux.inline respectively.

Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
 tools/bpf/resolve_btfids/Build        |   1 +
 tools/bpf/resolve_btfids/btf_colors.c | 139 ++++++++++++++++++++++
 tools/bpf/resolve_btfids/btf_colors.h |  20 ++++
 tools/bpf/resolve_btfids/main.c       | 160 +++++++++++++++++++++++++-
 tools/testing/selftests/bpf/Makefile  |   4 +-
 tools/testing/selftests/hid/Makefile  |   2 +
 6 files changed, 324 insertions(+), 2 deletions(-)
 create mode 100644 tools/bpf/resolve_btfids/btf_colors.c
 create mode 100644 tools/bpf/resolve_btfids/btf_colors.h

diff --git a/tools/bpf/resolve_btfids/Build b/tools/bpf/resolve_btfids/Build
index 077de3829c72..602e1508bf09 100644
--- a/tools/bpf/resolve_btfids/Build
+++ b/tools/bpf/resolve_btfids/Build
@@ -1,6 +1,7 @@
 hostprogs := resolve_btfids
 
 resolve_btfids-y += main.o
+resolve_btfids-y += btf_colors.o
 resolve_btfids-y += rbtree.o
 resolve_btfids-y += zalloc.o
 resolve_btfids-y += string.o
diff --git a/tools/bpf/resolve_btfids/btf_colors.c b/tools/bpf/resolve_btfids/btf_colors.c
new file mode 100644
index 000000000000..347ae85d6bc7
--- /dev/null
+++ b/tools/bpf/resolve_btfids/btf_colors.c
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <bpf/libbpf_internal.h>
+#include "btf_colors.h"
+
+/*
+ * Functions in this file mainly exist to refer to functions from libbpf_internal.h,
+ * which can't be included in main.c because of the u32 poison and pr_warn macro conflicts.
+ */
+
+/*
+ * Marks `root_id` and local types reachable from it with `color`.
+ * `colors` is indexed by source ID; worklist has room for every local type.
+ */
+void btf_mark_reachable(struct btf *btf, __u32 root_id, enum btf_color color,
+			__u8 *colors, __u32 *worklist)
+{
+	const struct btf *base = btf__base_btf(btf);
+	__u32 start_id = base ? btf__type_cnt(base) : 1;
+	__u32 pending = 0;
+
+	if (root_id < start_id || (colors[root_id] & color) == color)
+		return;
+	colors[root_id] |= color;
+	worklist[pending++] = root_id;
+	while (pending) {
+		const struct btf_type *t = btf__type_by_id(btf, worklist[--pending]);
+		struct btf_field_iter it;
+		__u32 *id;
+
+		btf_field_iter_init(&it, (struct btf_type *)t, BTF_FIELD_ITER_IDS);
+		while ((id = btf_field_iter_next(&it))) {
+			if (*id < start_id || (colors[*id] & color) == color)
+				continue;
+			colors[*id] |= color;
+			worklist[pending++] = *id;
+		}
+	}
+}
+
+static int cmp_loc(const void *a, const void *b)
+{
+	const struct btf_loc *la = a, *lb = b;
+
+	if (la->func != lb->func)
+		return la->func < lb->func ? -1 : 1;
+	if (la->offset != lb->offset)
+		return la->offset < lb->offset ? -1 : 1;
+	if (la->loc_proto != lb->loc_proto)
+		return la->loc_proto < lb->loc_proto ? -1 : 1;
+	return 0;
+}
+
+static void remap(struct btf *btf, const __u32 *id_map, __u32 src_start_id)
+{
+	const struct btf *base = btf__base_btf(btf);
+	__u32 start_id = base ? btf__type_cnt(base) : 1;
+	__u32 i, type_cnt = btf__type_cnt(btf);
+
+	for (i = start_id; i < type_cnt; i++) {
+		struct btf_type *t = (struct btf_type *)btf__type_by_id(btf, i);
+		struct btf_field_iter it;
+		__u32 *id;
+
+		btf_field_iter_init(&it, t, BTF_FIELD_ITER_IDS);
+		while ((id = btf_field_iter_next(&it))) {
+			/* Void and types in the original ancestor are unchanged. */
+			if (*id >= src_start_id)
+				*id = id_map[*id - src_start_id];
+		}
+		if (btf_is_locsec(t))
+			qsort(btf_locsec_locs(t), btf_vlen(t), sizeof(struct btf_loc), cmp_loc);
+	}
+}
+
+/*
+ * Split the `src` into `main_out` base and `inline_out`,
+ * according to `colors` array. Relative ordering remains
+ * the same as in `src`.
+ */
+int btf_split_by_color(struct btf *src, const __u8 *colors,
+		       struct btf **main_out, struct btf **inline_out)
+{
+	LIBBPF_OPTS(btf_new_opts, opts,
+		    .base_btf = (struct btf *)btf__base_btf(src),
+		    .add_layout = btf_header(src)->layout_len != 0,
+	);
+	struct btf *main_btf, *inline_btf = NULL;
+	__u32 start_id = opts.base_btf ? btf__type_cnt(opts.base_btf) : 1;
+	__u32 type_cnt = btf__type_cnt(src);
+	__u32 *id_map, i;
+	int err = -ENOMEM;
+
+	*main_out = NULL;
+	*inline_out = NULL;
+	id_map = malloc((type_cnt - start_id ?: 1) * sizeof(*id_map));
+	if (!id_map)
+		return -ENOMEM;
+	main_btf = btf__new_empty_opts(&opts);
+	if (!main_btf)
+		goto out;
+	btf__set_endianness(main_btf, btf__endianness(src));
+	/* Copy MAIN and SHARED marked types to `main_out`. */
+	for (i = start_id; i < type_cnt; i++) {
+		if (colors[i] == BTF_COLOR_LOC)
+			continue;
+		err = btf__add_type(main_btf, src, btf__type_by_id(src, i));
+		if (err < 0)
+			goto out;
+		id_map[i - start_id] = err;
+	}
+	/*
+	 * Copy LOC marked types to `inline_out`.
+	 * `main_btf` types count is stable at this point.
+	 */
+	inline_btf = btf__new_empty_split(main_btf);
+	if (!inline_btf) {
+		err = -ENOMEM;
+		goto out;
+	}
+	for (i = start_id; i < type_cnt; i++) {
+		if (colors[i] != BTF_COLOR_LOC)
+			continue;
+		err = btf__add_type(inline_btf, src, btf__type_by_id(src, i));
+		if (err < 0)
+			goto out;
+		id_map[i - start_id] = err;
+	}
+	remap(main_btf, id_map, start_id);
+	remap(inline_btf, id_map, start_id);
+	*main_out = main_btf;
+	*inline_out = inline_btf;
+	free(id_map);
+	return 0;
+out:
+	btf__free(inline_btf);
+	btf__free(main_btf);
+	free(id_map);
+	return err;
+}
diff --git a/tools/bpf/resolve_btfids/btf_colors.h b/tools/bpf/resolve_btfids/btf_colors.h
new file mode 100644
index 000000000000..9cea757cb356
--- /dev/null
+++ b/tools/bpf/resolve_btfids/btf_colors.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __RESOLVE_BTFIDS_BTF_COLORS_H
+#define __RESOLVE_BTFIDS_BTF_COLORS_H
+
+#include <bpf/btf.h>
+
+enum btf_color {
+	BTF_COLOR_NONE = 0,
+	BTF_COLOR_MAIN = 1,
+	BTF_COLOR_LOC = 2,
+	BTF_COLOR_SHARED = BTF_COLOR_MAIN | BTF_COLOR_LOC,
+};
+
+void btf_mark_reachable(struct btf *btf, __u32 root, enum btf_color color,
+			__u8 *colors, __u32 *worklist);
+
+int btf_split_by_color(struct btf *src, const __u8 *colors,
+		       struct btf **main_out, struct btf **inline_out);
+
+#endif
diff --git a/tools/bpf/resolve_btfids/main.c b/tools/bpf/resolve_btfids/main.c
index 37d7e7224207..dcd289d16faf 100644
--- a/tools/bpf/resolve_btfids/main.c
+++ b/tools/bpf/resolve_btfids/main.c
@@ -89,6 +89,7 @@
 #include <linux/limits.h>
 #include <bpf/btf.h>
 #include <bpf/libbpf.h>
+#include "btf_colors.h"
 #include <subcmd/parse-options.h>
 
 #define BTF_IDS_SECTION	".BTF_ids"
@@ -142,7 +143,9 @@ struct object {
 
 	struct btf *btf;
 	struct btf *base_btf;
+	struct btf *inline_btf;
 	bool distill_base;
+	bool extract_inline;
 
 	struct {
 		int		 fd;
@@ -154,6 +157,10 @@ struct object {
 		size_t		 strtabidx;
 		unsigned long	 idlist_addr;
 		int		 encoding;
+		const char	**func_symbols;
+		u32		 func_symbols_cnt;
+		u32		 func_symbols_cap;
+		bool		 func_symbols_collected;
 	} efile;
 
 	struct rb_root	sets;
@@ -572,6 +579,58 @@ static const char *find_name_by_addr(struct object *obj, Elf64_Addr addr)
 	return res ? res->name : NULL;
 }
 
+static int cmp_func_symbol(const void *a, const void *b)
+{
+	const char * const *name = a;
+	const char * const *other = b;
+
+	return strcmp(*name, *other);
+}
+
+static int collect_func_symbols(struct object *obj)
+{
+	Elf_Scn *scn;
+	GElf_Shdr sh;
+	int n, i;
+
+	if (obj->efile.symbols_shndx == -1)
+		return 0;
+
+	scn = elf_getscn(obj->efile.elf, obj->efile.symbols_shndx);
+	if (!scn || gelf_getshdr(scn, &sh) != &sh || !sh.sh_entsize)
+		return -EINVAL;
+	n = sh.sh_size / sh.sh_entsize;
+
+	for (i = 0; i < n; i++) {
+		GElf_Sym sym;
+		const char *name;
+
+		if (!gelf_getsym(obj->efile.symbols, i, &sym))
+			return -EINVAL;
+		if (GELF_ST_TYPE(sym.st_info) != STT_FUNC ||
+		    sym.st_shndx == SHN_UNDEF || !sym.st_name)
+			continue;
+		name = elf_strptr(obj->efile.elf, obj->efile.strtabidx, sym.st_name);
+		if (!name)
+			return -EINVAL;
+		if (ensure_mem(&obj->efile.func_symbols, &obj->efile.func_symbols_cap,
+			       obj->efile.func_symbols_cnt + 1))
+			return -ENOMEM;
+		obj->efile.func_symbols[obj->efile.func_symbols_cnt++] = name;
+	}
+
+	qsort(obj->efile.func_symbols, obj->efile.func_symbols_cnt,
+	      sizeof(*obj->efile.func_symbols), cmp_func_symbol);
+	obj->efile.func_symbols_collected = true;
+	return 0;
+}
+
+static bool has_func_symbol(const struct object *obj, const char *name)
+{
+	return bsearch(&name, obj->efile.func_symbols, obj->efile.func_symbols_cnt,
+		       sizeof(*obj->efile.func_symbols), cmp_func_symbol) != NULL;
+}
+
 static int symbols_collect(struct object *obj)
 {
 	Elf_Scn *scn = NULL;
@@ -1510,6 +1569,85 @@ static int btf2btf(struct object *obj)
 	return err;
 }
 
+static bool keep_in_base_btf(struct object *obj, const struct btf_type *t)
+{
+	const char *name;
+
+	switch (btf_kind(t)) {
+	case BTF_KIND_FUNC:
+		name = btf__name_by_offset(obj->btf, t->name_off);
+		return !obj->efile.func_symbols_collected || has_func_symbol(obj, name) ||
+		       btf_id__find(&obj->funcs, name);
+	case BTF_KIND_FUNC_PROTO:
+	case BTF_KIND_LOC_PARAM:
+	case BTF_KIND_LOC_PROTO:
+	case BTF_KIND_LOCSEC:
+		return false;
+	default:
+		/*
+		 * Ordinary types stay in base btf even if only inline records use them.
+		 * Should we relax this?
+		 */
+		return true;
+	}
+}
+
+/* Mark which types should go to .BTF.inline */
+static int color_btf(struct object *obj, __u8 *colors)
+{
+	struct btf *btf = obj->btf;
+	const struct btf *base = btf__base_btf(btf);
+	__u32 start_id = base ? btf__type_cnt(base) : 1;
+	__u32 type_cnt = btf__type_cnt(btf), i;
+	__u32 *worklist;
+
+	worklist = malloc((type_cnt - start_id ?: 1) * sizeof(*worklist));
+	if (!worklist)
+		return -ENOMEM;
+	/* First, mark all types reachable from LOC BTF entries. */
+	for (i = start_id; i < type_cnt; i++) {
+		const struct btf_type *t = btf__type_by_id(btf, i);
+
+		if (!btf_is_locsec(t) && !btf_is_loc_proto(t) && !btf_is_loc_param(t))
+			continue;
+		btf_mark_reachable(btf, i, BTF_COLOR_LOC, colors, worklist);
+	}
+	/* Next, traverse non-marked types converting some LOC markings to SHARED. */
+	for (i = start_id; i < type_cnt; i++) {
+		const struct btf_type *t = btf__type_by_id(btf, i);
+
+		if (colors[i] == BTF_COLOR_LOC && !keep_in_base_btf(obj, t))
+			continue;
+		btf_mark_reachable(btf, i, BTF_COLOR_MAIN, colors, worklist);
+	}
+	free(worklist);
+	return 0;
+}
+
+static int extract_inline_btf(struct object *obj)
+{
+	struct btf *btf = obj->btf, *main_btf, *inline_btf;
+	__u32 type_cnt = btf__type_cnt(btf);
+	__u8 *colors;
+	int err;
+
+	colors = calloc(type_cnt, sizeof(*colors));
+	if (!colors)
+		return -ENOMEM;
+	err = color_btf(obj, colors);
+	if (err)
+		goto out;
+	err = btf_split_by_color(btf, colors, &main_btf, &inline_btf);
+	if (!err) {
+		obj->btf = main_btf;
+		obj->inline_btf = inline_btf;
+		btf__free(btf);
+	}
+out:
+	free(colors);
+	return err;
+}
+
 /*
  * Sort types by name in ascending order resulting in all
  * anonymous types being placed before named types.
@@ -1605,6 +1743,14 @@ static int finalize_btf(struct object *obj)
 		goto out_err;
 	}
 
+	if (obj->extract_inline) {
+		err = extract_inline_btf(obj);
+		if (err) {
+			pr_err("FAILED to extract inline BTF: %s\n", strerror(-err));
+			goto out_err;
+		}
+	}
+
 	return 0;
 
 out_err:
@@ -1773,6 +1919,8 @@ int main(int argc, const char **argv)
 			    "turn warnings into errors"),
 		OPT_BOOLEAN(0, "distill_base", &obj.distill_base,
 			    "distill --btf_base and emit .BTF.base section data"),
+		OPT_BOOLEAN(0, "inline", &obj.extract_inline,
+			    "extract location BTF into a .BTF.inline file"),
 		OPT_STRING(0, "patch_btfids", &btfids_path, "file",
 			   "path to .BTF_ids section data blob to patch into ELF file"),
 		OPT_END()
@@ -1791,6 +1939,8 @@ int main(int argc, const char **argv)
 
 	if (elf_collect(&obj))
 		goto out;
+	if (obj.extract_inline && collect_func_symbols(&obj))
+		goto out;
 
 	/*
 	 * We did not find .BTF_ids section or symbols section,
@@ -1841,18 +1991,26 @@ int main(int argc, const char **argv)
 		if (err)
 			goto out;
 	}
+	if (obj.inline_btf) {
+		err = make_out_path(out_path, sizeof(out_path), obj.path, BTF_ELF_SEC ".inline");
+		err = err ?: dump_raw_btf(obj.inline_btf, out_path);
+		if (err)
+			goto out;
+	}
 
 	if (!(fatal_warnings && warnings))
 		err = 0;
 out:
-	btf__free(obj.base_btf);
+	btf__free(obj.inline_btf);
 	btf__free(obj.btf);
+	btf__free(obj.base_btf);
 	btf_id__free_all(&obj.structs);
 	btf_id__free_all(&obj.unions);
 	btf_id__free_all(&obj.typedefs);
 	btf_id__free_all(&obj.funcs);
 	btf_id__free_all(&obj.sets);
 	free(obj.addr_syms);
+	free(obj.efile.func_symbols);
 	if (obj.efile.elf) {
 		elf_end(obj.efile.elf);
 		close(obj.efile.fd);
diff --git a/tools/testing/selftests/bpf/Makefile b/tools/testing/selftests/bpf/Makefile
index afa589a27b15..00ad1b2833a3 100644
--- a/tools/testing/selftests/bpf/Makefile
+++ b/tools/testing/selftests/bpf/Makefile
@@ -343,7 +343,9 @@ else
 endif
 
 $(RESOLVE_BTFIDS): $(HOST_BPFOBJ) | $(HOST_BUILD_DIR)/resolve_btfids	\
-		       $(TOOLSDIR)/bpf/resolve_btfids/main.c	\
+		       $(TOOLSDIR)/bpf/resolve_btfids/main.c		\
+		       $(TOOLSDIR)/bpf/resolve_btfids/btf_colors.c	\
+		       $(TOOLSDIR)/bpf/resolve_btfids/btf_colors.h	\
 		       $(TOOLSDIR)/lib/rbtree.c			\
 		       $(TOOLSDIR)/lib/zalloc.c			\
 		       $(TOOLSDIR)/lib/string.c			\
diff --git a/tools/testing/selftests/hid/Makefile b/tools/testing/selftests/hid/Makefile
index 2f423de83147..d1b65fd37911 100644
--- a/tools/testing/selftests/hid/Makefile
+++ b/tools/testing/selftests/hid/Makefile
@@ -148,6 +148,8 @@ endif
 
 $(RESOLVE_BTFIDS): $(HOST_BPFOBJ) | $(HOST_BUILD_DIR)/resolve_btfids	\
 		       $(TOOLSDIR)/bpf/resolve_btfids/main.c	\
+		       $(TOOLSDIR)/bpf/resolve_btfids/btf_colors.c	\
+		       $(TOOLSDIR)/bpf/resolve_btfids/btf_colors.h	\
 		       $(TOOLSDIR)/lib/rbtree.c			\
 		       $(TOOLSDIR)/lib/zalloc.c			\
 		       $(TOOLSDIR)/lib/string.c			\
-- 
2.43.5


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

* [PATCH v3 bpf-next 2/7] resolve_btfids: Populate BTF link section
  2026-10-03 19:43 [PATCH v3 bpf-next 0/7] kbuild, bpf: Support inline info in BTF Alan Maguire
  2026-10-03 19:43 ` [PATCH v3 bpf-next 1/7] resolve_btfids: Build separate main and inline BTF objects Alan Maguire
@ 2026-10-03 19:43 ` Alan Maguire
  2026-10-03 20:27   ` bot+bpf-ci
  2026-10-03 19:43 ` [PATCH v3 bpf-next 3/7] kbuild, btf: Expose vmlinux inline BTF through sysfs Alan Maguire
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 20+ messages in thread
From: Alan Maguire @ 2026-10-03 19:43 UTC (permalink / raw)
  To: ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, Alan Maguire

Allow the final BTF patch pass to populate one or more BTF link
records.  A link identifies the BTF section to be delivered, its
carrier module, and the raw finalized BTF payload:

    --btf_link <section>:<module>:<raw-btf-file>

Use the raw file size and SHA-256 digest to populate the corresponding
<section>.link record.  This avoids reparsing BTF during final ELF
patching and supports both inline BTF and a future module-delivered
vmlinux BTF.

Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
Assisted-by: OpenAI Codex (GPT 5.6)
---
 tools/bpf/resolve_btfids/btf_colors.c |   6 +
 tools/bpf/resolve_btfids/btf_colors.h |   4 +
 tools/bpf/resolve_btfids/main.c       | 192 +++++++++++++++++++++++++-
 3 files changed, 198 insertions(+), 4 deletions(-)

diff --git a/tools/bpf/resolve_btfids/btf_colors.c b/tools/bpf/resolve_btfids/btf_colors.c
index 347ae85d6bc7..5d307d70e7f2 100644
--- a/tools/bpf/resolve_btfids/btf_colors.c
+++ b/tools/bpf/resolve_btfids/btf_colors.c
@@ -7,6 +7,12 @@
  * which can't be included in main.c because of the u32 poison and pr_warn macro conflicts.
  */
 
+void btf_sha256(const void *data, size_t len,
+		__u8 out[BTF_SHA256_DIGEST_LENGTH])
+{
+	libbpf_sha256(data, len, out);
+}
+
 /*
  * Marks `root_id` and local types reachable from it with `color`.
  * `colors` is indexed by source ID; worklist has room for every local type.
diff --git a/tools/bpf/resolve_btfids/btf_colors.h b/tools/bpf/resolve_btfids/btf_colors.h
index 9cea757cb356..56a85a242d6b 100644
--- a/tools/bpf/resolve_btfids/btf_colors.h
+++ b/tools/bpf/resolve_btfids/btf_colors.h
@@ -11,6 +11,10 @@ enum btf_color {
 	BTF_COLOR_SHARED = BTF_COLOR_MAIN | BTF_COLOR_LOC,
 };
 
+#define BTF_SHA256_DIGEST_LENGTH	32
+
+void btf_sha256(const void *data, size_t len,
+		__u8 out[BTF_SHA256_DIGEST_LENGTH]);
 void btf_mark_reachable(struct btf *btf, __u32 root, enum btf_color color,
 			__u8 *colors, __u32 *worklist);
 
diff --git a/tools/bpf/resolve_btfids/main.c b/tools/bpf/resolve_btfids/main.c
index dcd289d16faf..84cd9691f177 100644
--- a/tools/bpf/resolve_btfids/main.c
+++ b/tools/bpf/resolve_btfids/main.c
@@ -93,6 +93,7 @@
 #include <subcmd/parse-options.h>
 
 #define BTF_IDS_SECTION	".BTF_ids"
+#define BTF_LINK_MODULE_NAME_MAX	64
 #define BTF_ID_PREFIX	"__BTF_ID__"
 
 #define BTF_STRUCT	"struct"
@@ -157,6 +158,7 @@ struct object {
 		size_t		 strtabidx;
 		unsigned long	 idlist_addr;
 		int		 encoding;
+		int		 elf_class;
 		const char	**func_symbols;
 		u32		 func_symbols_cnt;
 		u32		 func_symbols_cap;
@@ -179,6 +181,19 @@ struct object {
 	u32 addr_syms_cap;
 };
 
+struct btf_link {
+	char *value;
+	char *section;
+	char *module;
+	char *btf_path;
+};
+
+struct btf_links {
+	struct btf_link *links;
+	u32 cnt;
+	u32 cap;
+};
+
 #define DECL_TAG_FASTCALL "bpf_fastcall"
 #define DECL_TAG_KFUNC "bpf_kfunc"
 
@@ -266,6 +281,49 @@ static int __ensure_mem(void **data, u32 *cap, u32 cnt, size_t elem_sz)
 #define ensure_mem(arr_ptr, cap_ptr, cnt) \
 	__ensure_mem((void **)(arr_ptr), (cap_ptr), (cnt), sizeof(**(arr_ptr)))
 
+static int parse_btf_link(const struct option *opt, const char *arg, int unset)
+{
+	struct btf_links *links = opt->value;
+	struct btf_link *link;
+	char *separator;
+
+	if (unset)
+		return -EINVAL;
+	if (__ensure_mem((void **)&links->links, &links->cap, links->cnt + 1,
+			 sizeof(*links->links)))
+		return -ENOMEM;
+	link = &links->links[links->cnt];
+	memset(link, 0, sizeof(*link));
+	link->value = strdup(arg);
+	if (!link->value)
+		return -ENOMEM;
+	link->section = link->value;
+	separator = strchr(link->section, ':');
+	if (!separator || separator == link->section)
+		goto err_value;
+	*separator++ = '\0';
+	link->module = separator;
+	separator = strchr(link->module, ':');
+	if (!separator || separator == link->module || !separator[1])
+		goto err_value;
+	*separator++ = '\0';
+	link->btf_path = separator;
+	links->cnt++;
+	return 0;
+err_value:
+	free(link->value);
+	return -EINVAL;
+}
+
+static void free_btf_links(struct btf_links *links)
+{
+	u32 i;
+
+	for (i = 0; i < links->cnt; i++)
+		free(links->links[i].value);
+	free(links->links);
+}
+
 static bool is_btf_id(const char *name)
 {
 	return name && !strncmp(name, BTF_ID_PREFIX, sizeof(BTF_ID_PREFIX) - 1);
@@ -487,6 +545,7 @@ static int elf_collect(struct object *obj)
 		return -1;
 	}
 	obj->efile.encoding = ehdr.e_ident[EI_DATA];
+	obj->efile.elf_class = ehdr.e_ident[EI_CLASS];
 
 	/*
 	 * Scan all the elf sections and look for save data
@@ -1036,6 +1095,110 @@ static int dump_raw_btf(struct btf *btf, const char *out_path)
 	return 0;
 }
 
+static int patch_btf_link(struct object *obj, const struct btf_link *link)
+{
+	void *raw_btf_data;
+	Elf_Scn *scn = NULL;
+	Elf_Data *data;
+	GElf_Shdr sh;
+	char section[128];
+	char *name;
+	size_t shdrstrndx, module_name_len;
+	int fd, len, err = -1;
+	struct stat st;
+	FILE *btf_file;
+	u32 raw_btf_size;
+	Elf *elf;
+	u8 *size;
+
+	if (stat(link->btf_path, &st) || !S_ISREG(st.st_mode) ||
+	    st.st_size <= 0 || st.st_size > UINT_MAX)
+		return -EINVAL;
+	raw_btf_size = st.st_size;
+	raw_btf_data = malloc(raw_btf_size);
+	if (!raw_btf_data)
+		return -ENOMEM;
+	btf_file = fopen(link->btf_path, "rb");
+	if (!btf_file)
+		goto out_data;
+	if (fread(raw_btf_data, raw_btf_size, 1, btf_file) != 1)
+		goto out_file;
+	fclose(btf_file);
+	if (obj->efile.elf_class == ELFCLASS32)
+		module_name_len = BTF_LINK_MODULE_NAME_MAX - 4;
+	else if (obj->efile.elf_class == ELFCLASS64)
+		module_name_len = BTF_LINK_MODULE_NAME_MAX - 8;
+	else
+		goto out_data;
+	if (strlen(link->module) >= module_name_len)
+		goto out_data;
+	len = snprintf(section, sizeof(section), "%s.link", link->section);
+	if (len < 0 || len >= sizeof(section))
+		goto out_data;
+
+	fd = open(obj->path, O_RDWR);
+	if (fd < 0)
+		goto out_data;
+	elf = elf_begin(fd, ELF_C_RDWR_MMAP, NULL);
+	if (!elf) {
+		err = -EINVAL;
+		goto out_close;
+	}
+	elf_flagelf(elf, ELF_C_SET, ELF_F_LAYOUT);
+	if (elf_getshdrstrndx(elf, &shdrstrndx))
+		goto out_elf;
+	while ((scn = elf_nextscn(elf, scn))) {
+		if (gelf_getshdr(scn, &sh) != &sh)
+			goto out_elf;
+		name = elf_strptr(elf, shdrstrndx, sh.sh_name);
+		if (name && !strcmp(name, section))
+			break;
+	}
+	if (!scn) {
+		pr_err("FAILED: section %s not found in %s\n", section, obj->path);
+		goto out_elf;
+	}
+	data = elf_getdata(scn, NULL);
+	if (sh.sh_type != SHT_PROGBITS || !data || !data->d_buf ||
+	    data->d_size != module_name_len +
+		BTF_SHA256_DIGEST_LENGTH + sizeof(u32)) {
+		pr_err("FAILED: section %s is not a writable BTF link\n", section);
+		goto out_elf;
+	}
+	memset(data->d_buf, 0, data->d_size);
+	memcpy(data->d_buf, link->module, strlen(link->module) + 1);
+	btf_sha256(raw_btf_data, raw_btf_size,
+		   (u8 *)data->d_buf + module_name_len);
+	size = (u8 *)data->d_buf + module_name_len + BTF_SHA256_DIGEST_LENGTH;
+	if (obj->efile.encoding == ELFDATA2LSB) {
+		size[0] = raw_btf_size;
+		size[1] = raw_btf_size >> 8;
+		size[2] = raw_btf_size >> 16;
+		size[3] = raw_btf_size >> 24;
+	} else if (obj->efile.encoding == ELFDATA2MSB) {
+		size[0] = raw_btf_size >> 24;
+		size[1] = raw_btf_size >> 16;
+		size[2] = raw_btf_size >> 8;
+		size[3] = raw_btf_size;
+	} else {
+		goto out_elf;
+	}
+	elf_flagdata(data, ELF_C_SET, ELF_F_DIRTY);
+	if (elf_update(elf, ELF_C_WRITE) >= 0)
+		err = 0;
+out_elf:
+	elf_end(elf);
+out_close:
+	close(fd);
+	free(raw_btf_data);
+	return err;
+out_file:
+	fclose(btf_file);
+out_data:
+	free(raw_btf_data);
+	return err;
+}
+
 static const struct btf_type *btf_type_skip_qualifiers(const struct btf *btf, s32 type_id)
 {
 	const struct btf_type *t = btf__type_by_id(btf, type_id);
@@ -1886,7 +2049,7 @@ static int patch_btfids(const char *btfids_path, const char *elf_path)
 
 static const char * const resolve_btfids_usage[] = {
 	"resolve_btfids [<options>] <ELF object>",
-	"resolve_btfids --patch_btfids <.BTF_ids file> <ELF object>",
+	"resolve_btfids --patch_btfids <.BTF_ids file> [--btf_link <section:module:btf-file>] <ELF object>",
 	NULL
 };
 
@@ -1904,6 +2067,7 @@ int main(int argc, const char **argv)
 		.sets     = RB_ROOT,
 	};
 	const char *btfids_path = NULL;
+	struct btf_links btf_links = {};
 	bool fatal_warnings = false;
 	bool resolve_btfids = true;
 	char out_path[PATH_MAX];
@@ -1921,6 +2085,8 @@ int main(int argc, const char **argv)
 			    "distill --btf_base and emit .BTF.base section data"),
 		OPT_BOOLEAN(0, "inline", &obj.extract_inline,
 			    "extract location BTF into a .BTF.inline file"),
+		OPT_CALLBACK(0, "btf_link", &btf_links, "section:module:btf-file",
+			     "patch a BTF link with --patch_btfids", parse_btf_link),
 		OPT_STRING(0, "patch_btfids", &btfids_path, "file",
 			   "path to .BTF_ids section data blob to patch into ELF file"),
 		OPT_END()
@@ -1931,12 +2097,30 @@ int main(int argc, const char **argv)
 			     PARSE_OPT_STOP_AT_NON_OPTION);
 	if (argc != 1)
 		usage_with_options(resolve_btfids_usage, btfid_options);
+	if (btf_links.cnt && !btfids_path) {
+		pr_err("--btf_link requires --patch_btfids\n");
+		goto out;
+	}
 
 	obj.path = argv[0];
 
-	if (btfids_path)
-		return patch_btfids(btfids_path, obj.path);
+	if (btfids_path) {
+		err = patch_btfids(btfids_path, obj.path);
+		if (err || !btf_links.cnt)
+			goto out;
+		if (elf_collect(&obj)) {
+			err = -EINVAL;
+			goto out;
+		}
+		for (u32 i = 0; i < btf_links.cnt; i++) {
+			struct btf_link *link = &btf_links.links[i];
 
+			err = patch_btf_link(&obj, link);
+			if (err)
+				break;
+		}
+		goto out;
+	}
 	if (elf_collect(&obj))
 		goto out;
 	if (obj.extract_inline && collect_func_symbols(&obj))
@@ -1997,10 +2181,10 @@ int main(int argc, const char **argv)
 		if (err)
 			goto out;
 	}
-
 	if (!(fatal_warnings && warnings))
 		err = 0;
 out:
+	free_btf_links(&btf_links);
 	btf__free(obj.inline_btf);
 	btf__free(obj.btf);
 	btf__free(obj.base_btf);
-- 
2.43.5


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

* [PATCH v3 bpf-next 3/7] kbuild, btf: Expose vmlinux inline BTF through sysfs
  2026-10-03 19:43 [PATCH v3 bpf-next 0/7] kbuild, bpf: Support inline info in BTF Alan Maguire
  2026-10-03 19:43 ` [PATCH v3 bpf-next 1/7] resolve_btfids: Build separate main and inline BTF objects Alan Maguire
  2026-10-03 19:43 ` [PATCH v3 bpf-next 2/7] resolve_btfids: Populate BTF link section Alan Maguire
@ 2026-10-03 19:43 ` Alan Maguire
  2026-10-03 19:56   ` sashiko-bot
  2026-10-03 20:41   ` bot+bpf-ci
  2026-10-03 19:43 ` [PATCH v3 bpf-next 4/7] btf: Preserve string relocation maps for split BTF Alan Maguire
                   ` (3 subsequent siblings)
  6 siblings, 2 replies; 20+ messages in thread
From: Alan Maguire @ 2026-10-03 19:43 UTC (permalink / raw)
  To: ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, Alan Maguire

Expose BTF inline-location information from vmlinux .BTF.inline
sections in /sys/kernel/btf as vmlinux.inline. Embedded vmlinux
inline BTF supports read-only mmap().

Provide runtime support for a module-delivered vmlinux inline BTF
carrier. Its link record binds the carrier module name, BTF size, and
SHA-256 digest to the running kernel before its data is made available
through a lazy vmlinux.inline sysfs attribute. The carrier is handled
before ordinary module BTF registration, so its incidental .BTF data is
not exposed.

Make CONFIG_DEBUG_INFO_BTF_INLINE tristate. When configured as a
module, retain vmlinux final split inline BTF for
btf_vmlinux_inline.ko instead of embedding it in vmlinux.

Reserve .BTF.inline.link in vmlinux and extend the final
resolve_btfids patch pass to bind btf_vmlinux_inline to the raw
vmlinux.BTF.inline payload. This preserves the normal built-in
configuration while allowing the large inline BTF payload to load on
demand.

Normal module inline BTF is handled separately, after its
references have been relocated against the final module BTF.

Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
Assisted-by: OpenAI Codex (GPT-5.6)
---
 Makefile                          |   1 +
 include/asm-generic/vmlinux.lds.h |  27 ++++++
 include/linux/btf.h               |  37 ++++++++
 include/linux/module.h            |   4 +
 kernel/bpf/Makefile               |   1 +
 kernel/bpf/btf.c                  | 150 +++++++++++++++++++++++-------
 kernel/bpf/btf_vmlinux_inline.c   |  29 ++++++
 kernel/bpf/sysfs_btf.c            | 145 ++++++++++++++++++++++++++---
 kernel/module/main.c              |   7 ++
 lib/Kconfig.debug                 |  19 ++++
 scripts/Makefile.btf              |   7 ++
 scripts/gen-btf.sh                |  34 ++++++-
 scripts/link-vmlinux.sh           |   8 +-
 scripts/package/kernel.spec       |  10 +-
 14 files changed, 428 insertions(+), 51 deletions(-)
 create mode 100644 kernel/bpf/btf_vmlinux_inline.c

diff --git a/Makefile b/Makefile
index 6b8b812f23b9..b3053dd3c105 100644
--- a/Makefile
+++ b/Makefile
@@ -1769,6 +1769,7 @@ endif # CONFIG_MODULES
 CLEAN_FILES += vmlinux.symvers modules-only.symvers \
 	       modules.builtin modules.builtin.modinfo modules.nsdeps \
 	       modules.builtin.ranges vmlinux.o.map vmlinux.unstripped \
+	       vmlinux.BTF.inline \
 	       vmlinux.thinlto-index builtin.order \
 	       compile_commands.json rust/test \
 	       rust-project.json .vmlinux.objs .vmlinux.export.c \
diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
index b2988aa12f66..863f02660bc3 100644
--- a/include/asm-generic/vmlinux.lds.h
+++ b/include/asm-generic/vmlinux.lds.h
@@ -675,12 +675,39 @@
 /*
  * .BTF
  */
+#ifdef CONFIG_DEBUG_INFO_BTF_INLINE
+#define BTF_INLINE							\
+	. = ALIGN(PAGE_SIZE);						\
+	.BTF.inline : AT(ADDR(.BTF.inline) - LOAD_OFFSET) {		\
+		BOUNDED_SECTION_BY(.BTF.inline, _BTF_inline)		\
+	}
+#else
+#define BTF_INLINE
+#endif
+
+#ifdef CONFIG_DEBUG_INFO_BTF_INLINE_MODULE
+/* __MODULE_NAME_LEN + SHA-256 digest + u32 BTF size */
+#define BTF_LINK_SIZE	(64 - __SIZEOF_LONG__ + 32 + 4)
+#define BTF_INLINE_LINK						\
+	. = ALIGN(PAGE_SIZE);					\
+	.BTF.inline.link : AT(ADDR(.BTF.inline.link) - LOAD_OFFSET) { \
+		__start_BTF_inline_link = .;				\
+		BYTE(0)						\
+		. += BTF_LINK_SIZE - 1;					\
+		__stop_BTF_inline_link = .;				\
+	}
+#else
+#define BTF_INLINE_LINK
+#endif
+
 #ifdef CONFIG_DEBUG_INFO_BTF
 #define BTF								\
 	. = ALIGN(PAGE_SIZE);						\
 	.BTF : AT(ADDR(.BTF) - LOAD_OFFSET) {				\
 		BOUNDED_SECTION_BY(.BTF, _BTF)				\
 	}								\
+	BTF_INLINE							\
+	BTF_INLINE_LINK						\
 	. = ALIGN(PAGE_SIZE);						\
 	.BTF_ids : AT(ADDR(.BTF_ids) - LOAD_OFFSET) {			\
 		*(.BTF_ids)						\
diff --git a/include/linux/btf.h b/include/linux/btf.h
index 4b63bb91550a..f731c7a5f14c 100644
--- a/include/linux/btf.h
+++ b/include/linux/btf.h
@@ -5,6 +5,7 @@
 #define _LINUX_BTF_H 1
 
 #include <linux/types.h>
+#include <linux/module.h>
 #include <linux/bpfptr.h>
 #include <linux/bsearch.h>
 #include <linux/btf_ids.h>
@@ -118,6 +119,19 @@ struct btf_show;
 struct btf_id_set;
 struct bpf_prog;
 
+#define BTF_LINK_SHA256_LEN	32
+
+/*
+ * A .BTF.link or .BTF.inline.link section identifies the module which
+ * carries a vmlinux BTF section.  The module name is NUL terminated and the
+ * field is padded to __MODULE_NAME_LEN.
+ */
+struct btf_link {
+	char module_name[__MODULE_NAME_LEN];
+	u8 sha256[BTF_LINK_SHA256_LEN];
+	u32 btf_size;
+} __packed;
+
 typedef int (*btf_kfunc_filter_t)(const struct bpf_prog *prog, u32 kfunc_id);
 
 struct btf_kfunc_id_set {
@@ -624,6 +638,29 @@ bool btf_types_are_same(const struct btf *btf1, u32 id1,
 			const struct btf *btf2, u32 id2);
 int btf_check_iter_arg(struct btf *btf, const struct btf_type *func, int arg_idx);
 
+#if IS_ENABLED(CONFIG_SYSFS)
+struct bin_attribute *sysfs_btf_add(const char *name, void *data, size_t data_size,
+				    bool mmap, const char *lazy_module_name);
+bool sysfs_btf_update(struct bin_attribute *attr, void *data);
+void sysfs_btf_remove(struct bin_attribute *attr);
+#else
+static inline struct bin_attribute *
+sysfs_btf_add(const char *name, void *data, size_t data_size, bool mmap,
+	      const char *lazy_module_name)
+{
+	return NULL;
+}
+
+static inline bool sysfs_btf_update(struct bin_attribute *attr, void *data)
+{
+	return false;
+}
+
+static inline void sysfs_btf_remove(struct bin_attribute *attr)
+{
+}
+#endif
+
 static inline bool btf_type_is_struct_ptr(struct btf *btf, const struct btf_type *t)
 {
 	if (!btf_type_is_ptr(t))
diff --git a/include/linux/module.h b/include/linux/module.h
index 96cc98568eea..a6abde12a458 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -503,6 +503,10 @@ struct module {
 	void *btf_data;
 	void *btf_base_data;
 #endif
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF_INLINE)
+	unsigned int btf_inline_data_size;
+	void *btf_inline_data;
+#endif
 #ifdef CONFIG_JUMP_LABEL
 	struct jump_entry *jump_entries;
 	unsigned int num_jump_entries;
diff --git a/kernel/bpf/Makefile b/kernel/bpf/Makefile
index c1f9b0d3468d..37e62ef45079 100644
--- a/kernel/bpf/Makefile
+++ b/kernel/bpf/Makefile
@@ -42,6 +42,7 @@ obj-$(CONFIG_BPF_SYSCALL) += reuseport_array.o
 endif
 ifeq ($(CONFIG_SYSFS),y)
 obj-$(CONFIG_DEBUG_INFO_BTF) += sysfs_btf.o
+obj-$(CONFIG_DEBUG_INFO_BTF_INLINE) += btf_vmlinux_inline.o
 endif
 ifeq ($(CONFIG_BPF_JIT),y)
 obj-$(CONFIG_BPF_SYSCALL) += bpf_struct_ops.o
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 8cc17a1cd25c..7aef2d662263 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -25,11 +25,12 @@
 #include <linux/skmsg.h>
 #include <linux/perf_event.h>
 #include <linux/bsearch.h>
-#include <linux/kobject.h>
 #include <linux/string.h>
-#include <linux/sysfs.h>
 #include <linux/overflow.h>
 #include <linux/bitops.h>
+#include <linux/unaligned.h>
+#include <linux/vmalloc.h>
+#include <crypto/sha2.h>
 
 #include <net/netfilter/nf_bpf_link.h>
 
@@ -9005,21 +9006,122 @@ static DEFINE_MUTEX(btf_module_mutex);
 
 static void purge_cand_cache(struct btf *btf);
 
+#if IS_MODULE(CONFIG_DEBUG_INFO_BTF_INLINE)
+extern char __start_BTF_inline_link[];
+extern char __stop_BTF_inline_link[];
+
+static struct bin_attribute *vmlinux_inline_attr;
+static const struct btf_link *vmlinux_inline_link;
+
+static const struct btf_link *btf_parse_link(void *start, unsigned int size)
+{
+	const struct btf_link *link = start;
+	unsigned int module_name_len;
+
+	if (!link || size != sizeof(*link))
+		return NULL;
+
+	if (link->btf_size == 0)
+		return 0;
+	module_name_len = strnlen(link->module_name, sizeof(link->module_name));
+	if (!module_name_len || module_name_len == sizeof(link->module_name) ||
+	    memchr_inv(link->module_name + module_name_len + 1, 0,
+		       sizeof(link->module_name) - module_name_len - 1))
+		return NULL;
+	return link;
+}
+
+static bool btf_verify_link(const struct btf_link *link, const struct module *mod,
+			    const void *data, size_t data_size)
+{
+	u8 actual_digest[BTF_LINK_SHA256_LEN];
+
+	if (!link || data_size != link->btf_size) {
+		pr_warn(" BTF link from module [%s] missing or has unexpected size\n", mod->name);
+		return false;
+	}
+	sha256(data, data_size, actual_digest);
+	if (memcmp(actual_digest, link->sha256, sizeof(actual_digest))) {
+		pr_warn("BTF link from module [%s] has unexpected digest\n", mod->name);
+		return false;
+	}
+
+	return true;
+}
+
+static void btf_vmlinux_inline_lazy_init(void)
+{
+	vmlinux_inline_link = btf_parse_link(__start_BTF_inline_link,
+					     __stop_BTF_inline_link -
+					     __start_BTF_inline_link);
+	if (!vmlinux_inline_link) {
+		pr_warn("invalid vmlinux inline BTF link\n");
+		return;
+	}
+	vmlinux_inline_attr = sysfs_btf_add("vmlinux.inline", NULL,
+					    vmlinux_inline_link->btf_size, true,
+					    vmlinux_inline_link->module_name);
+	if (IS_ERR(vmlinux_inline_attr)) {
+		pr_warn("failed to register vmlinux inline BTF in sysfs: %ld\n",
+			PTR_ERR(vmlinux_inline_attr));
+		vmlinux_inline_attr = NULL;
+	}
+}
+
+static int btf_module_handle_vmlinux_inline(const struct module *mod)
+{
+	void *data = NULL;
+
+	if (!vmlinux_inline_link || strcmp(mod->name, vmlinux_inline_link->module_name))
+		return -ENOENT;
+
+	data = vmalloc_user(mod->btf_inline_data_size);
+	if (!data)
+		return -ENOMEM;
+	memcpy(data, mod->btf_inline_data, mod->btf_inline_data_size);
+
+	if (!btf_verify_link(vmlinux_inline_link, mod, data, mod->btf_inline_data_size) ||
+	    !sysfs_btf_update(vmlinux_inline_attr, data)) {
+		kvfree(data);
+		return -EINVAL;
+	}
+	return 0;
+}
+
+#else
+static int btf_module_handle_vmlinux_inline(const struct module *mod)
+{
+	return -ENOENT;
+}
+
+static void btf_vmlinux_inline_lazy_init(void)
+{
+}
+
+#endif /* IS_MODULE(CONFIG_DEBUG_INFO_BTF_INLINE) */
+
 static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			     void *module)
 {
 	struct btf_module *btf_mod, *tmp;
 	struct module *mod = module;
+	struct bin_attribute *attr;
 	struct btf *btf;
 	int err = 0;
 
-	if (mod->btf_data_size == 0 ||
-	    (op != MODULE_STATE_COMING && op != MODULE_STATE_LIVE &&
-	     op != MODULE_STATE_GOING))
+	if (op != MODULE_STATE_COMING && op != MODULE_STATE_LIVE &&
+	    op != MODULE_STATE_GOING)
 		goto out;
 
 	switch (op) {
 	case MODULE_STATE_COMING:
+		err = btf_module_handle_vmlinux_inline(mod);
+		if (err != -ENOENT) {
+			err = 0;
+			break;
+		}
+		if (!mod->btf_data_size)
+			break;
 		btf_mod = kzalloc_obj(*btf_mod);
 		if (!btf_mod) {
 			err = -ENOMEM;
@@ -9052,34 +9154,16 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 		list_add(&btf_mod->list, &btf_modules);
 		mutex_unlock(&btf_module_mutex);
 
-		if (IS_ENABLED(CONFIG_SYSFS)) {
-			struct bin_attribute *attr;
-
-			attr = kzalloc_obj(*attr);
-			if (!attr)
-				goto out;
-
-			sysfs_bin_attr_init(attr);
-			attr->attr.name = btf->name;
-			attr->attr.mode = 0444;
-			attr->size = btf->data_size;
-			attr->private = btf->data;
-			attr->read = sysfs_bin_attr_simple_read;
-
-			err = sysfs_create_bin_file(btf_kobj, attr);
-			if (err) {
-				pr_warn("failed to register module [%s] BTF in sysfs: %d\n",
-					mod->name, err);
-				kfree(attr);
-				err = 0;
-				goto out;
-			}
-
-			btf_mod->sysfs_attr = attr;
+		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;
 		break;
 	case MODULE_STATE_LIVE:
+		if (!mod->btf_data_size)
+			break;
 		mutex_lock(&btf_module_mutex);
 		list_for_each_entry_safe(btf_mod, tmp, &btf_modules, list) {
 			if (btf_mod->module != module)
@@ -9091,6 +9175,8 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 		mutex_unlock(&btf_module_mutex);
 		break;
 	case MODULE_STATE_GOING:
+		if (!mod->btf_data_size)
+			break;
 		mutex_lock(&btf_module_mutex);
 		list_for_each_entry_safe(btf_mod, tmp, &btf_modules, list) {
 			if (btf_mod->module != module)
@@ -9105,10 +9191,9 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			btf_free_id(btf_mod->btf);
 			list_del(&btf_mod->list);
 			if (btf_mod->sysfs_attr)
-				sysfs_remove_bin_file(btf_kobj, btf_mod->sysfs_attr);
+				sysfs_btf_remove(btf_mod->sysfs_attr);
 			purge_cand_cache(btf_mod->btf);
 			btf_put(btf_mod->btf);
-			kfree(btf_mod->sysfs_attr);
 			kfree(btf_mod);
 			break;
 		}
@@ -9125,6 +9210,7 @@ static struct notifier_block btf_module_nb = {
 
 static int __init btf_module_init(void)
 {
+	btf_vmlinux_inline_lazy_init();
 	register_module_notifier(&btf_module_nb);
 	return 0;
 }
diff --git a/kernel/bpf/btf_vmlinux_inline.c b/kernel/bpf/btf_vmlinux_inline.c
new file mode 100644
index 000000000000..d5c599d9f729
--- /dev/null
+++ b/kernel/bpf/btf_vmlinux_inline.c
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026, Oracle and/or its affiliates. */
+/*
+ * Provide kernel BTF inline function information for use by BPF tools.
+ */
+#include <linux/btf.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+
+#if IS_BUILTIN(CONFIG_DEBUG_INFO_BTF_INLINE)
+extern char __start_BTF_inline[];
+extern char __stop_BTF_inline[];
+#endif
+
+static int __init btf_vmlinux_inline_init(void)
+{
+#if IS_BUILTIN(CONFIG_DEBUG_INFO_BTF_INLINE)
+	size_t data_size = __stop_BTF_inline - __start_BTF_inline;
+
+	if (data_size)
+		sysfs_btf_add("vmlinux.inline", __start_BTF_inline, data_size, true, NULL);
+#endif
+	return 0;
+}
+subsys_initcall(btf_vmlinux_inline_init);
+
+MODULE_DESCRIPTION("BTF inline information for vmlinux");
+MODULE_LICENSE("GPL");
diff --git a/kernel/bpf/sysfs_btf.c b/kernel/bpf/sysfs_btf.c
index 9cbe15ce3540..a3c54b5fe294 100644
--- a/kernel/bpf/sysfs_btf.c
+++ b/kernel/bpf/sysfs_btf.c
@@ -6,8 +6,10 @@
 #include <linux/module.h>
 #include <linux/kobject.h>
 #include <linux/init.h>
+#include <linux/slab.h>
 #include <linux/sysfs.h>
 #include <linux/mm.h>
+#include <linux/vmalloc.h>
 #include <linux/io.h>
 #include <linux/btf.h>
 
@@ -15,16 +17,12 @@
 extern char __start_BTF[];
 extern char __stop_BTF[];
 
-static int btf_sysfs_vmlinux_mmap(struct file *filp, struct kobject *kobj,
-				  const struct bin_attribute *attr,
-				  struct vm_area_struct *vma)
+static int btf_sysfs_mmap_check(void *data, size_t size, struct vm_area_struct *vma)
 {
-	unsigned long pages = PAGE_ALIGN(attr->size) >> PAGE_SHIFT;
+	unsigned long pages = PAGE_ALIGN(size) >> PAGE_SHIFT;
 	size_t vm_size = vma->vm_end - vma->vm_start;
-	phys_addr_t addr = __pa_symbol(__start_BTF);
-	unsigned long pfn = addr >> PAGE_SHIFT;
 
-	if (attr->private != __start_BTF || !PAGE_ALIGNED(addr))
+	if (!data || !PAGE_ALIGNED((unsigned long)data))
 		return -EINVAL;
 
 	if (vma->vm_pgoff)
@@ -33,24 +31,149 @@ static int btf_sysfs_vmlinux_mmap(struct file *filp, struct kobject *kobj,
 	if (vma->vm_flags & (VM_WRITE | VM_EXEC | VM_MAYSHARE))
 		return -EACCES;
 
-	if (pfn + pages < pfn)
-		return -EINVAL;
-
 	if ((vm_size >> PAGE_SHIFT) > pages)
 		return -EINVAL;
 
 	vm_flags_mod(vma, VM_DONTDUMP, VM_MAYEXEC | VM_MAYWRITE);
+	return 0;
+}
+
+static int btf_sysfs_mmap_direct(struct file *filp, struct kobject *kobj,
+				 const struct bin_attribute *attr,
+				 struct vm_area_struct *vma)
+{
+	void *data = READ_ONCE(attr->private);
+	phys_addr_t addr;
+	unsigned long pfn;
+	size_t vm_size = vma->vm_end - vma->vm_start;
+	int err;
+
+	err = btf_sysfs_mmap_check(data, attr->size, vma);
+	if (err)
+		return err;
+	if (is_vmalloc_addr(data))
+		return remap_vmalloc_range(vma, data, 0);
+
+	addr = __pa_symbol(data);
+	pfn = addr >> PAGE_SHIFT;
+	if (pfn + (PAGE_ALIGN(attr->size) >> PAGE_SHIFT) < pfn)
+		return -EINVAL;
+
 	return remap_pfn_range(vma, vma->vm_start, pfn, vm_size, vma->vm_page_prot);
 }
 
 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_vmlinux_mmap,
+	.mmap = btf_sysfs_mmap_direct,
 };
 
 struct kobject *btf_kobj;
 
+struct btf_sysfs_entry {
+	struct bin_attribute attr;
+	char *module_name;
+};
+
+static void *btf_sysfs_lazy_data(const struct bin_attribute *attr)
+{
+	struct btf_sysfs_entry *entry = container_of(attr, struct btf_sysfs_entry, attr);
+	void *data;
+
+	data = smp_load_acquire(&attr->private);
+	if (!data) {
+		request_module("%s", entry->module_name);
+		data = smp_load_acquire(&attr->private);
+	}
+	return data;
+}
+
+static ssize_t btf_sysfs_read_lazy(struct file *filp, struct kobject *kobj,
+				   const struct bin_attribute *attr, char *buf,
+				   loff_t off, size_t count)
+{
+	void *data = btf_sysfs_lazy_data(attr);
+
+	if (!data)
+		return -ENODEV;
+
+	return memory_read_from_buffer(buf, count, &off, data, attr->size);
+}
+
+static int btf_sysfs_mmap_lazy(struct file *filp, struct kobject *kobj,
+			       const struct bin_attribute *attr,
+			       struct vm_area_struct *vma)
+{
+	void *data = btf_sysfs_lazy_data(attr);
+
+	if (!data)
+		return -ENODEV;
+
+	return btf_sysfs_mmap_direct(filp, kobj, attr, vma);
+}
+
+struct bin_attribute *sysfs_btf_add(const char *name, void *data, size_t data_size,
+				    bool mmap, const char *lazy_module_name)
+{
+	struct btf_sysfs_entry *entry;
+	struct bin_attribute *attr;
+	int err;
+
+	entry = kzalloc_obj(*entry);
+	if (!entry)
+		return ERR_PTR(-ENOMEM);
+
+	attr = &entry->attr;
+	sysfs_bin_attr_init(attr);
+	attr->attr.mode = 0444;
+	attr->size = data_size;
+	attr->private = data;
+	attr->read = lazy_module_name ? btf_sysfs_read_lazy : sysfs_bin_attr_simple_read;
+	if (mmap)
+		attr->mmap = lazy_module_name ? btf_sysfs_mmap_lazy : btf_sysfs_mmap_direct;
+	attr->attr.name = kstrdup(name, GFP_KERNEL);
+	if (!attr->attr.name) {
+		err = -ENOMEM;
+		goto err_free;
+	}
+	if (lazy_module_name) {
+		entry->module_name = kstrdup(lazy_module_name, GFP_KERNEL);
+		if (!entry->module_name) {
+			err = -ENOMEM;
+			goto err_free;
+		}
+	}
+
+	err = sysfs_create_bin_file(btf_kobj, attr);
+	if (err) {
+		pr_warn("failed to register [%s] BTF in sysfs: %d\n", name, err);
+		goto err_free;
+	}
+
+	return attr;
+
+err_free:
+	kfree(entry->module_name);
+	kfree(attr->attr.name);
+	kfree(entry);
+	return ERR_PTR(err);
+}
+
+bool sysfs_btf_update(struct bin_attribute *attr, void *data)
+{
+	return attr && !cmpxchg(&attr->private, NULL, data);
+}
+
+void sysfs_btf_remove(struct bin_attribute *attr)
+{
+	struct btf_sysfs_entry *entry = container_of(attr, struct btf_sysfs_entry, attr);
+
+	sysfs_remove_bin_file(btf_kobj, attr);
+	kfree(entry->module_name);
+	kfree(attr->attr.name);
+	kfree(entry);
+}
+
 static int __init btf_vmlinux_init(void)
 {
 	bin_attr_btf_vmlinux.private = __start_BTF;
diff --git a/kernel/module/main.c b/kernel/module/main.c
index c1b34dc1e89a..5b9eb56164b2 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2723,6 +2723,10 @@ static int find_module_sections(struct module *mod, struct load_info *info)
 	mod->btf_base_data = any_section_objs(info, ".BTF.base", 1,
 					      &mod->btf_base_data_size);
 #endif
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF_INLINE)
+	mod->btf_inline_data = any_section_objs(info, ".BTF.inline", 1,
+						&mod->btf_inline_data_size);
+#endif
 #ifdef CONFIG_JUMP_LABEL
 	mod->jump_entries = section_objs(info, "__jump_table",
 					sizeof(*mod->jump_entries),
@@ -3176,6 +3180,9 @@ static noinline int do_init_module(struct module *mod)
 	/* .BTF is not SHF_ALLOC and will get removed, so sanitize pointers */
 	mod->btf_data = NULL;
 	mod->btf_base_data = NULL;
+#endif
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF_INLINE)
+	mod->btf_inline_data = NULL;
 #endif
 	/*
 	 * We want to free module_init, but be aware that kallsyms may be
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 134b15a44625..24abfff1ddc3 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -425,6 +425,12 @@ config PAHOLE_HAS_LANG_EXCLUDE
 	  otherwise it would emit malformed kernel and module binaries when
 	  using DEBUG_INFO_BTF_MODULES.
 
+config PAHOLE_HAS_INLINE
+	def_bool PAHOLE_VERSION >= 132
+	help
+	  Support for the "inline" BTF feature is available. It encodes
+	  information about inline sites and how to retrieve their parameters.
+
 config DEBUG_INFO_BTF_MODULES
 	bool "Generate BTF type information for kernel modules"
 	default y
@@ -432,6 +438,19 @@ config DEBUG_INFO_BTF_MODULES
 	help
 	  Generate compact split BTF type information for kernel modules.
 
+config DEBUG_INFO_BTF_INLINE
+	tristate "Provide information about inline sites in BTF"
+	default n
+	depends on DEBUG_INFO_BTF && PAHOLE_HAS_INLINE && SYSFS
+	select DEBUG_INFO_BTF_MODULES if MODULES
+	select CRYPTO_LIB_SHA256
+	help
+	  Generate information about inline sites in .BTF.inline sections.
+	  These sections contain split BTF relative to the kernel or module BTF
+	  and are made available in /sys/kernel/btf with a ".inline" suffix.
+	  The information describes inline locations and how to retrieve their
+	  associated parameters.
+
 config MODULE_ALLOW_BTF_MISMATCH
 	bool "Allow loading modules with non-matching BTF type info"
 	depends on DEBUG_INFO_BTF_MODULES
diff --git a/scripts/Makefile.btf b/scripts/Makefile.btf
index a1812985a61a..98012344a81e 100644
--- a/scripts/Makefile.btf
+++ b/scripts/Makefile.btf
@@ -22,7 +22,14 @@ endif
 
 pahole-flags-$(CONFIG_PAHOLE_HAS_LANG_EXCLUDE)		+= --lang_exclude=rust
 
+btf-inline := $(CONFIG_DEBUG_INFO_BTF_INLINE)
+ifneq ($(btf-inline),)
+btf-inline-feat := inline
+pahole-flags-$(call test-ge, $(pahole-ver), 132) += --btf_features=$(btf-inline-feat)
+endif
+
 export PAHOLE_FLAGS := $(pahole-flags-y)
+export BTF_INLINE := $(btf-inline)
 
 resolve-btfids-flags-y :=
 resolve-btfids-flags-$(CONFIG_WERROR) += --fatal_warnings
diff --git a/scripts/gen-btf.sh b/scripts/gen-btf.sh
index 8ca96eb10a69..e638291753d0 100755
--- a/scripts/gen-btf.sh
+++ b/scripts/gen-btf.sh
@@ -68,6 +68,10 @@ esac
 
 gen_btf_data()
 {
+	inline=""
+	if [ -n "${BTF_INLINE}" ] && [ -z "${BTF_BASE}" ]; then
+		inline="--inline"
+	fi
 	btf1="${ELF_FILE}.BTF.1"
 	${PAHOLE} -J ${PAHOLE_FLAGS}			\
 		${BTF_BASE:+--btf_base ${BTF_BASE}}	\
@@ -76,6 +80,7 @@ gen_btf_data()
 
 	${RESOLVE_BTFIDS} ${RESOLVE_BTFIDS_FLAGS}	\
 		${BTF_BASE:+--btf_base ${BTF_BASE}}	\
+		${inline}				\
 		--btf ${btf1} "${ELF_FILE}"
 }
 
@@ -83,14 +88,28 @@ gen_btf_o()
 {
 	btf_data=${ELF_FILE}.btf.o
 
-	# Create ${btf_data} which contains just .BTF section but no symbols. Add
+	# 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.
 	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}
-	${OBJCOPY} --only-section=.BTF --strip-all ${btf_data}
+	ONLY_SEC="--only-section=.BTF"
+	btf_inline=${ELF_FILE}.BTF.inline
+	if [ "${BTF_INLINE}" = "m" ]; then
+		if [ -f "${btf_inline}" ]; then
+			cp "${btf_inline}" "${objtree}/vmlinux.BTF.inline"
+		else
+			rm -f "${objtree}/vmlinux.BTF.inline"
+		fi
+	fi
+	if [ "${BTF_INLINE}" = "y" ] && [ -f "${btf_inline}" ]; then
+		${OBJCOPY} --add-section .BTF.inline=${btf_inline} \
+			--set-section-flags .BTF.inline=alloc,readonly ${btf_data}
+		ONLY_SEC="${ONLY_SEC} --only-section=.BTF.inline"
+	fi
+	${OBJCOPY} ${ONLY_SEC} --strip-all ${btf_data}
 
 	# Change e_type to ET_REL so that it can be used to link final vmlinux.
 	# GNU ld 2.35+ and lld do not allow an ET_EXEC input.
@@ -111,6 +130,15 @@ embed_btf_data()
 	if [ -f "${btf_base}" ]; then
 		${OBJCOPY} --add-section .BTF.base=${btf_base} ${ELF_FILE}
 	fi
+	btf_inline=""
+	case "${ELF_FILE}" in
+	*/btf_vmlinux_inline.ko)
+		btf_inline=${BTF_BASE}.BTF.inline
+		;;
+	esac
+	if [ -n "${BTF_INLINE}" ] && [ -f "${btf_inline}" ]; then
+		${OBJCOPY} --add-section .BTF.inline=${btf_inline} ${ELF_FILE}
+	fi
 	btf_ids="${ELF_FILE}.BTF_ids"
 	if [ -f "${btf_ids}" ]; then
 		${RESOLVE_BTFIDS} --patch_btfids ${btf_ids} ${ELF_FILE}
@@ -121,6 +149,8 @@ cleanup()
 {
 	rm -f "${ELF_FILE}.BTF.1"
 	rm -f "${ELF_FILE}.BTF"
+	rm -f "${ELF_FILE}.BTF.inline"
+	rm -f "${ELF_FILE}.BTF.inline.link"
 	if [ "${BTFGEN_MODE}" = "module" ]; then
 		rm -f "${ELF_FILE}.BTF.base"
 		rm -f "${ELF_FILE}.BTF_ids"
diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
index ab0b8125c8cb..5424caf5135b 100755
--- a/scripts/link-vmlinux.sh
+++ b/scripts/link-vmlinux.sh
@@ -289,7 +289,13 @@ vmlinux_link "${VMLINUX}"
 
 if is_enabled CONFIG_DEBUG_INFO_BTF; then
 	info BTFIDS ${VMLINUX}
-	${RESOLVE_BTFIDS} --patch_btfids ${btfids_vmlinux} ${VMLINUX}
+	if [ "${BTF_INLINE}" = "m" ]; then
+		${RESOLVE_BTFIDS} --patch_btfids ${btfids_vmlinux} \
+			--btf_link .BTF.inline:btf_vmlinux_inline:vmlinux.BTF.inline \
+			${VMLINUX}
+	else
+		${RESOLVE_BTFIDS} --patch_btfids ${btfids_vmlinux} ${VMLINUX}
+	fi
 fi
 
 mksysmap "${VMLINUX}" System.map
diff --git a/scripts/package/kernel.spec b/scripts/package/kernel.spec
index 46e80970f723..8f0933cbbe45 100644
--- a/scripts/package/kernel.spec
+++ b/scripts/package/kernel.spec
@@ -68,13 +68,13 @@ This package provides debug information for the kernel image and modules from th
 %undefine _debugsource_packages
 %undefine _debuginfo_subpackages
 
-# Preserve .BTF and .BTF.base sections in kernel modules during debuginfo
-# stripping. find-debuginfo.sh uses eu-strip which removes non-allocated ELF
-# sections like .BTF by default. .BTF.base is required for BTF distillation
-# support; without it, module BTF validation fails.
+# Preserve .BTF,  .BTF.base and .BTF.inline sections in kernel modules during
+# debuginfo stripping. find-debuginfo.sh uses eu-strip which removes
+# non-allocated ELF sections like .BTF by default. .BTF.base is required for
+# BTF distillation support; without it, module BTF validation fails.
 %global with_keep_section %(%{__find_debuginfo} --help 2>&1 | grep -c keep-section)
 %if %{with_keep_section}
-%global _find_debuginfo_opts -r --keep-section .BTF --keep-section .BTF.base
+%global _find_debuginfo_opts -r --keep-section .BTF --keep-section .BTF.base --keep-section .BTF.inline
 %else
 %global _find_debuginfo_opts -r
 %endif
-- 
2.43.5


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

* [PATCH v3 bpf-next 4/7] btf: Preserve string relocation maps for split BTF
  2026-10-03 19:43 [PATCH v3 bpf-next 0/7] kbuild, bpf: Support inline info in BTF Alan Maguire
                   ` (2 preceding siblings ...)
  2026-10-03 19:43 ` [PATCH v3 bpf-next 3/7] kbuild, btf: Expose vmlinux inline BTF through sysfs Alan Maguire
@ 2026-10-03 19:43 ` Alan Maguire
  2026-10-03 19:43 ` [PATCH v3 bpf-next 5/7] btf: Relocate and expose module inline BTF Alan Maguire
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 20+ messages in thread
From: Alan Maguire @ 2026-10-03 19:43 UTC (permalink / raw)
  To: ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, Alan Maguire

Make btf_relocate() optionally return the string-offset map it builds
while relocating a BTF relative to a distilled base. A subsequent split
BTF object can use that map to rebase references to base strings.

Also account for the accumulated base string offset when assigning a
new BTF base. This is required when the base is itself split.

Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
Assisted-by: OpenAI Codex (GPT-5.6)
---
 include/linux/btf.h             | 5 +++--
 kernel/bpf/btf.c                | 4 ++--
 tools/lib/bpf/btf.c             | 2 +-
 tools/lib/bpf/btf_relocate.c    | 7 ++++++-
 tools/lib/bpf/libbpf_internal.h | 3 ++-
 5 files changed, 14 insertions(+), 7 deletions(-)

diff --git a/include/linux/btf.h b/include/linux/btf.h
index f731c7a5f14c..b66e20a18c54 100644
--- a/include/linux/btf.h
+++ b/include/linux/btf.h
@@ -608,7 +608,8 @@ struct btf_field_iter {
 #ifdef CONFIG_BPF_SYSCALL
 const struct btf_type *btf_type_by_id(const struct btf *btf, u32 type_id);
 void btf_set_base_btf(struct btf *btf, const struct btf *base_btf);
-int btf_relocate(struct btf *btf, const struct btf *base_btf, __u32 **map_ids);
+int btf_relocate(struct btf *btf, const struct btf *base_btf, __u32 **map_ids,
+		 __u32 **map_strs);
 int btf_field_iter_init(struct btf_field_iter *it, struct btf_type *t,
 			enum btf_field_iter_kind iter_kind);
 __u32 *btf_field_iter_next(struct btf_field_iter *it);
@@ -682,7 +683,7 @@ static inline void btf_set_base_btf(struct btf *btf, const struct btf *base_btf)
 }
 
 static inline int btf_relocate(void *log, struct btf *btf, const struct btf *base_btf,
-			       __u32 **map_ids)
+			       __u32 **map_ids, __u32 **map_strs)
 {
 	return -EOPNOTSUPP;
 }
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 7aef2d662263..4242e00a3f7e 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -1945,7 +1945,7 @@ void btf_set_base_btf(struct btf *btf, const struct btf *base_btf)
 {
 	btf->base_btf = (struct btf *)base_btf;
 	btf->start_id = btf_nr_types(base_btf);
-	btf->start_str_off = base_btf->hdr.str_len;
+	btf->start_str_off = base_btf->start_str_off + base_btf->hdr.str_len;
 }
 
 static int env_resolve_init(struct btf_verifier_env *env)
@@ -6984,7 +6984,7 @@ static struct btf *btf_parse_module(const char *module_name, const void *data,
 		goto errout;
 
 	if (base_btf != vmlinux_btf) {
-		err = btf_relocate(btf, vmlinux_btf, &btf->base_id_map);
+		err = btf_relocate(btf, vmlinux_btf, &btf->base_id_map, NULL);
 		if (err)
 			goto errout;
 		btf_free(base_btf);
diff --git a/tools/lib/bpf/btf.c b/tools/lib/bpf/btf.c
index 512b3962f75f..d2ff6f5d6d5b 100644
--- a/tools/lib/bpf/btf.c
+++ b/tools/lib/bpf/btf.c
@@ -6640,7 +6640,7 @@ void btf_set_base_btf(struct btf *btf, const struct btf *base_btf)
 
 int btf__relocate(struct btf *btf, const struct btf *base_btf)
 {
-	int err = btf_relocate(btf, base_btf, NULL);
+	int err = btf_relocate(btf, base_btf, NULL, NULL);
 
 	if (!err)
 		btf->owns_base = false;
diff --git a/tools/lib/bpf/btf_relocate.c b/tools/lib/bpf/btf_relocate.c
index df5fa4bd87d6..e55dd75f6c95 100644
--- a/tools/lib/bpf/btf_relocate.c
+++ b/tools/lib/bpf/btf_relocate.c
@@ -441,7 +441,8 @@ static int btf_relocate_rewrite_strs(struct btf_relocate *r, __u32 i)
 /* If successful, output of relocation is updated BTF with base BTF pointing
  * at base_btf, and type ids, strings adjusted accordingly.
  */
-int btf_relocate(struct btf *btf, const struct btf *base_btf, __u32 **id_map)
+int btf_relocate(struct btf *btf, const struct btf *base_btf, __u32 **id_map,
+		 __u32 **str_map)
 {
 	unsigned int nr_types = btf__type_cnt(btf);
 	const struct btf_header *dist_base_hdr;
@@ -512,6 +513,10 @@ int btf_relocate(struct btf *btf, const struct btf *base_btf, __u32 **id_map)
 		*id_map = r.id_map;
 		r.id_map = NULL;
 	}
+	if (str_map) {
+		*str_map = r.str_map;
+		r.str_map = NULL;
+	}
 err_out:
 	free(r.id_map);
 	free(r.str_map);
diff --git a/tools/lib/bpf/libbpf_internal.h b/tools/lib/bpf/libbpf_internal.h
index 546f65b95cf4..4e45dfacb548 100644
--- a/tools/lib/bpf/libbpf_internal.h
+++ b/tools/lib/bpf/libbpf_internal.h
@@ -249,7 +249,8 @@ const char *btf_kind_str(const struct btf_type *t);
 const struct btf_type *skip_mods_and_typedefs(const struct btf *btf, __u32 id, __u32 *res_id);
 const struct btf_header *btf_header(const struct btf *btf);
 void btf_set_base_btf(struct btf *btf, const struct btf *base_btf);
-int btf_relocate(struct btf *btf, const struct btf *base_btf, __u32 **id_map);
+int btf_relocate(struct btf *btf, const struct btf *base_btf, __u32 **id_map,
+		 __u32 **str_map);
 bool btf_type_is_traceable_func(const struct btf *btf, const struct btf_type *t);
 
 static inline enum btf_func_linkage btf_func_linkage(const struct btf_type *t)
-- 
2.43.5


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

* [PATCH v3 bpf-next 5/7] btf: Relocate and expose module inline BTF
  2026-10-03 19:43 [PATCH v3 bpf-next 0/7] kbuild, bpf: Support inline info in BTF Alan Maguire
                   ` (3 preceding siblings ...)
  2026-10-03 19:43 ` [PATCH v3 bpf-next 4/7] btf: Preserve string relocation maps for split BTF Alan Maguire
@ 2026-10-03 19:43 ` Alan Maguire
  2026-10-03 19:59   ` sashiko-bot
  2026-10-03 20:27   ` bot+bpf-ci
  2026-10-03 19:43 ` [PATCH v3 bpf-next 6/7] selftests/bpf: Test BTF sysfs inline representations Alan Maguire
  2026-10-03 19:43 ` [PATCH v3 bpf-next 7/7] selftests/bpf: Add a test verifying inline information Alan Maguire
  6 siblings, 2 replies; 20+ messages in thread
From: Alan Maguire @ 2026-10-03 19:43 UTC (permalink / raw)
  To: ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, Alan Maguire

Module .BTF.inline is split relative to its module BTF. For external
modules, module BTF can instead be split relative to a distilled
.BTF.base and is relocated against vmlinux when the module loads.

Parse ordinary module inline BTF relative to the pre-relocation
module BTF. Retain the module relocation ID and string maps, then
rebase inline type IDs and string offsets onto the final
vmlinux/module BTF space before exposing the result through sysfs.

Malformed or unrelocatable optional inline BTF is warned about and
omitted without preventing normal module BTF registration. The
btf_vmlinux_inline carrier remains on its existing hash-checked,
vmalloc-backed publishing path, preserving mmap support for
vmlinux.inline when delivered by a module.

Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
Assisted-by: OpenAI Codex (GPT-5.6)
---
 kernel/bpf/btf.c   | 244 ++++++++++++++++++++++++++++++++++++---------
 scripts/gen-btf.sh |   7 +-
 2 files changed, 201 insertions(+), 50 deletions(-)

diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 4242e00a3f7e..a9c46c731602 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -6912,15 +6912,137 @@ __u32 btf_relocate_id(const struct btf *btf, __u32 id)
 
 #ifdef CONFIG_DEBUG_INFO_BTF_MODULES
 
+static struct btf *btf_parse_split(struct btf_verifier_env *env,
+				   const char *name, const void *data,
+				   unsigned int data_size, struct btf *base_btf)
+{
+	struct btf *btf;
+	int err;
+
+	btf = kzalloc_obj(*btf, GFP_KERNEL | __GFP_NOWARN);
+	if (!btf)
+		return ERR_PTR(-ENOMEM);
+	env->btf = btf;
+
+	btf_set_base_btf(btf, base_btf);
+	btf->kernel_btf = true;
+	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_size = data_size;
+
+	err = btf_parse_hdr(env);
+	if (err)
+		goto errout;
+
+	btf->nohdr_data = btf->data + btf->hdr.hdr_len;
+
+	err = btf_parse_str_sec(env);
+	if (err)
+		goto errout;
+
+	err = btf_check_all_metas(env);
+	if (err)
+		goto errout;
+
+	err = btf_check_modifier_chain_length(env, btf, btf_nr_types(base_btf));
+	if (err)
+		goto errout;
+
+	return btf;
+
+errout:
+	btf_free(btf);
+	return ERR_PTR(err);
+}
+
+static int btf_rebase_inline(struct btf *inline_btf,
+			     const struct btf *module_btf,
+			     const u32 *module_id_map,
+			     const u32 *module_str_map,
+			     u32 old_module_type_cnt)
+{
+	u32 old_start_id = inline_btf->start_id;
+	u32 old_start_str_off = inline_btf->start_str_off;
+	u32 old_module_start_str_off = old_start_str_off - module_btf->hdr.str_len;
+	u32 new_start_id = btf_nr_types(module_btf);
+	u32 new_start_str_off = module_btf->start_str_off + module_btf->hdr.str_len;
+	s64 id_delta = (s64)new_start_id - old_start_id;
+	s64 str_delta = (s64)new_start_str_off - old_start_str_off;
+	u32 i;
+
+	/*
+	 * The inline BTF was parsed relative to the original module BTF. Its
+	 * base IDs must therefore use the map generated when that BTF was
+	 * relocated, while IDs for inline-local types only move by the change
+	 * in the module BTF's starting ID.
+	 */
+	for (i = 0; i < inline_btf->nr_types; i++) {
+		struct btf_field_iter it;
+		struct btf_type *t = inline_btf->types[i];
+		u32 *id, *str_off;
+		int err;
+
+		err = btf_field_iter_init(&it, t, BTF_FIELD_ITER_IDS);
+		if (err)
+			return err;
+		while ((id = btf_field_iter_next(&it))) {
+			if (!*id)
+				continue;
+			if (*id < old_module_type_cnt) {
+				if (module_id_map)
+					*id = module_id_map[*id];
+			} else if (*id >= old_start_id) {
+				*id += id_delta;
+			} else {
+				return -EINVAL;
+			}
+		}
+
+		err = btf_field_iter_init(&it, t, BTF_FIELD_ITER_STRS);
+		if (err)
+			return err;
+		while ((str_off = btf_field_iter_next(&it))) {
+			if (!*str_off)
+				continue;
+			if (*str_off < old_module_start_str_off) {
+				/* Vmlinux strings retain their offsets for in-tree modules. */
+				if (!module_id_map)
+					continue;
+				if (!module_str_map || !module_str_map[*str_off])
+					return -EINVAL;
+				*str_off = module_str_map[*str_off];
+				continue;
+			}
+			*str_off += str_delta;
+		}
+	}
+
+	btf_set_base_btf(inline_btf, module_btf);
+	btf_check_sorted(inline_btf);
+	return 0;
+}
+
 static struct btf *btf_parse_module(const char *module_name, const void *data,
 				    unsigned int data_size, void *base_data,
-				    unsigned int base_data_size)
+				    unsigned int base_data_size,
+				    const void *inline_data,
+				    unsigned int inline_data_size,
+				    void **relocated_inline_data)
 {
-	struct btf *btf = NULL, *vmlinux_btf, *base_btf = NULL;
+	struct btf *btf = NULL, *inline_btf = NULL, *vmlinux_btf, *base_btf = NULL;
 	struct btf_verifier_env *env = NULL;
 	struct bpf_verifier_log *log;
+	u32 old_module_type_cnt;
+	u32 *module_str_map = NULL;
 	int err = 0;
 
+	*relocated_inline_data = NULL;
 	vmlinux_btf = bpf_get_btf_vmlinux();
 	if (IS_ERR(vmlinux_btf))
 		return vmlinux_btf;
@@ -6944,67 +7066,66 @@ static struct btf *btf_parse_module(const char *module_name, const void *data,
 		base_btf = vmlinux_btf;
 	}
 
-	btf = kzalloc_obj(*btf, GFP_KERNEL | __GFP_NOWARN);
-	if (!btf) {
-		err = -ENOMEM;
+	btf = btf_parse_split(env, module_name, data, data_size, base_btf);
+	if (IS_ERR(btf)) {
+		err = PTR_ERR(btf);
+		btf = NULL;
 		goto errout;
 	}
-	env->btf = btf;
-
-	btf->base_btf = base_btf;
-	btf->start_id = base_btf->nr_types;
-	btf->start_str_off = base_btf->hdr.str_len;
-	btf->kernel_btf = true;
-	btf->named_start_id = 0;
-	strscpy(btf->name, module_name);
 
-	btf->data = kvmemdup(data, data_size, GFP_KERNEL | __GFP_NOWARN);
-	if (!btf->data) {
-		err = -ENOMEM;
-		goto errout;
+	if (inline_data_size) {
+		inline_btf = btf_parse_split(env, module_name, inline_data,
+					     inline_data_size, btf);
+		if (IS_ERR(inline_btf)) {
+			pr_warn("failed to validate module [%s] inline BTF: %ld\n",
+				module_name, PTR_ERR(inline_btf));
+			inline_btf = NULL;
+		}
 	}
-	btf->data_size = data_size;
-
-	err = btf_parse_hdr(env);
-	if (err)
-		goto errout;
-
-	btf->nohdr_data = btf->data + btf->hdr.hdr_len;
-
-	err = btf_parse_str_sec(env);
-	if (err)
-		goto errout;
-
-	err = btf_check_all_metas(env);
-	if (err)
-		goto errout;
-
-	err = btf_check_modifier_chain_length(env, btf, btf_nr_types(base_btf));
-	if (err)
-		goto errout;
 
+	old_module_type_cnt = btf_nr_types(btf);
 	if (base_btf != vmlinux_btf) {
-		err = btf_relocate(btf, vmlinux_btf, &btf->base_id_map, NULL);
+		err = btf_relocate(btf, vmlinux_btf, &btf->base_id_map,
+				   &module_str_map);
 		if (err)
 			goto errout;
 		btf_free(base_btf);
 		base_btf = vmlinux_btf;
 	}
 
-	btf_verifier_env_free(env);
+	if (inline_btf) {
+		err = btf_rebase_inline(inline_btf, btf, btf->base_id_map,
+					module_str_map, old_module_type_cnt);
+		if (err) {
+			pr_warn("failed to relocate module [%s] inline BTF: %d\n",
+				module_name, err);
+			btf_free(inline_btf);
+		} else {
+			*relocated_inline_data = inline_btf->data;
+			inline_btf->data = NULL;
+			btf_free(inline_btf);
+		}
+	}
+
+	/*
+	 * With a distilled base, btf_relocate() replaces the base BTF and
+	 * rewrites string offsets. Check ordering only after that final BTF
+	 * view has been established, so named_start_id describes the BTF used
+	 * by name lookups.
+	 */
 	btf_check_sorted(btf);
+	btf_verifier_env_free(env);
+	kvfree(module_str_map);
 	refcount_set(&btf->refcnt, 1);
 	return btf;
 
 errout:
+	kvfree(module_str_map);
 	btf_verifier_env_free(env);
+	btf_free(inline_btf);
 	if (!IS_ERR(base_btf) && base_btf != vmlinux_btf)
 		btf_free(base_btf);
-	if (btf) {
-		kvfree(btf->data);
-		kvfree(btf->types);
-		kfree(btf);
-	}
+	btf_free(btf);
 	return ERR_PTR(err);
 }
 
@@ -8998,6 +9119,8 @@ struct btf_module {
 	struct module *module;
 	struct btf *btf;
 	struct bin_attribute *sysfs_attr;
+	struct bin_attribute *sysfs_inline_attr;
+	void *btf_inline_data;
 	int flags;
 };
 
@@ -9072,7 +9195,8 @@ static int btf_module_handle_vmlinux_inline(const struct module *mod)
 {
 	void *data = NULL;
 
-	if (!vmlinux_inline_link || strcmp(mod->name, vmlinux_inline_link->module_name))
+	if (!vmlinux_inline_link ||
+	    strcmp(mod->name, vmlinux_inline_link->module_name))
 		return -ENOENT;
 
 	data = vmalloc_user(mod->btf_inline_data_size);
@@ -9107,6 +9231,8 @@ 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;
+	unsigned int inline_data_size = 0;
 	int err = 0;
 
 	if (op != MODULE_STATE_COMING && op != MODULE_STATE_LIVE &&
@@ -9120,6 +9246,12 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			err = 0;
 			break;
 		}
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF_INLINE)
+		if (mod->btf_inline_data_size) {
+			inline_data = mod->btf_inline_data;
+			inline_data_size = mod->btf_inline_data_size;
+		}
+#endif
 		if (!mod->btf_data_size)
 			break;
 		btf_mod = kzalloc_obj(*btf_mod);
@@ -9128,7 +9260,9 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			goto out;
 		}
 		btf = btf_parse_module(mod->name, mod->btf_data, mod->btf_data_size,
-				       mod->btf_base_data, mod->btf_base_data_size);
+				       mod->btf_base_data, mod->btf_base_data_size,
+				       inline_data, inline_data_size,
+				       &relocated_inline_data);
 		if (IS_ERR(btf)) {
 			kfree(btf_mod);
 			if (!IS_ENABLED(CONFIG_MODULE_ALLOW_BTF_MISMATCH)) {
@@ -9143,6 +9277,7 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 		err = btf_alloc_id(btf);
 		if (err) {
 			btf_free(btf);
+			kvfree(relocated_inline_data);
 			kfree(btf_mod);
 			goto out;
 		}
@@ -9151,6 +9286,7 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 		mutex_lock(&btf_module_mutex);
 		btf_mod->module = module;
 		btf_mod->btf = btf;
+		btf_mod->btf_inline_data = relocated_inline_data;
 		list_add(&btf_mod->list, &btf_modules);
 		mutex_unlock(&btf_module_mutex);
 
@@ -9160,6 +9296,21 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			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;
+		}
 		break;
 	case MODULE_STATE_LIVE:
 		if (!mod->btf_data_size)
@@ -9192,6 +9343,9 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			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);
 			purge_cand_cache(btf_mod->btf);
 			btf_put(btf_mod->btf);
 			kfree(btf_mod);
diff --git a/scripts/gen-btf.sh b/scripts/gen-btf.sh
index e638291753d0..b17f1189298a 100755
--- a/scripts/gen-btf.sh
+++ b/scripts/gen-btf.sh
@@ -68,10 +68,7 @@ esac
 
 gen_btf_data()
 {
-	inline=""
-	if [ -n "${BTF_INLINE}" ] && [ -z "${BTF_BASE}" ]; then
-		inline="--inline"
-	fi
+	inline=${BTF_INLINE:+--inline}
 	btf1="${ELF_FILE}.BTF.1"
 	${PAHOLE} -J ${PAHOLE_FLAGS}			\
 		${BTF_BASE:+--btf_base ${BTF_BASE}}	\
@@ -130,7 +127,7 @@ embed_btf_data()
 	if [ -f "${btf_base}" ]; then
 		${OBJCOPY} --add-section .BTF.base=${btf_base} ${ELF_FILE}
 	fi
-	btf_inline=""
+	btf_inline=${ELF_FILE}.BTF.inline
 	case "${ELF_FILE}" in
 	*/btf_vmlinux_inline.ko)
 		btf_inline=${BTF_BASE}.BTF.inline
-- 
2.43.5


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

* [PATCH v3 bpf-next 6/7] selftests/bpf: Test BTF sysfs inline representations
  2026-10-03 19:43 [PATCH v3 bpf-next 0/7] kbuild, bpf: Support inline info in BTF Alan Maguire
                   ` (4 preceding siblings ...)
  2026-10-03 19:43 ` [PATCH v3 bpf-next 5/7] btf: Relocate and expose module inline BTF Alan Maguire
@ 2026-10-03 19:43 ` Alan Maguire
  2026-10-03 19:54   ` sashiko-bot
  2026-10-03 19:43 ` [PATCH v3 bpf-next 7/7] selftests/bpf: Add a test verifying inline information Alan Maguire
  6 siblings, 1 reply; 20+ messages in thread
From: Alan Maguire @ 2026-10-03 19:43 UTC (permalink / raw)
  To: ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, Alan Maguire

Load vmlinux, vmlinux.inline and all module and module.inline
entries in /sys/fs/btf; this will allow us to sanity-check

- kernel and its inline representations
- in-tree modules and their inline multi-split BTF representations
- out-of-tree modules and the kernel relocation done for
  module and module inline BTF

Since bpf_testmod.ko is built as an out-of-tree module, the
combination of vmlinux, normal in-tree module BTF and out-of-tree
testmod ensures we sanity check relocations for each case.

Also test mmap for vmlinux.inline.

Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
 .../selftests/bpf/prog_tests/btf_sysfs.c      | 76 +++++++++++++++++++
 1 file changed, 76 insertions(+)

diff --git a/tools/testing/selftests/bpf/prog_tests/btf_sysfs.c b/tools/testing/selftests/bpf/prog_tests/btf_sysfs.c
index 3923e64c4c1d..069c4483b3a2 100644
--- a/tools/testing/selftests/bpf/prog_tests/btf_sysfs.c
+++ b/tools/testing/selftests/bpf/prog_tests/btf_sysfs.c
@@ -3,11 +3,16 @@
 
 #include <test_progs.h>
 #include <bpf/btf.h>
+#include <dirent.h>
+#include <limits.h>
 #include <sys/stat.h>
 #include <sys/mman.h>
 #include <fcntl.h>
 #include <unistd.h>
 
+#define BTF_SYSFS_DIR		"/sys/kernel/btf"
+#define BTF_INLINE_SUFFIX	".inline"
+
 static void test_btf_mmap_sysfs(const char *path, struct btf *base)
 {
 	struct stat st;
@@ -75,7 +80,78 @@ static void test_btf_mmap_sysfs(const char *path, struct btf *base)
 		close(fd);
 }
 
+static void test_btf_inline_sysfs_all(void)
+{
+	struct btf *vmlinux_btf;
+	struct dirent *dentry;
+	DIR *dir;
+	int err = 0;
+
+	dir = opendir(BTF_SYSFS_DIR);
+	if (!ASSERT_OK_PTR(dir, "open_btf_sysfs"))
+		return;
+
+	vmlinux_btf = btf__parse(BTF_SYSFS_DIR "/vmlinux", NULL);
+	if (!ASSERT_OK_PTR(vmlinux_btf, "parse_vmlinux_btf")) {
+		closedir(dir);
+		return;
+	}
+
+	while ((dentry = readdir(dir)) != NULL) {
+		struct btf *base_btf = NULL, *module_btf = NULL, *inline_btf = NULL;
+		char btf_path[PATH_MAX], inline_path[PATH_MAX];
+		struct stat st;
+
+		/* Skip ".", ".." and "foo.inline" */
+		if (strstr(dentry->d_name, "."))
+			continue;
+
+		if (strcmp(dentry->d_name, "vmlinux") == 0)
+			base_btf = vmlinux_btf;
+
+		if (snprintf(btf_path, sizeof(btf_path), "%s/%s",
+			     BTF_SYSFS_DIR, dentry->d_name) >= sizeof(btf_path) ||
+		    snprintf(inline_path, sizeof(inline_path), "%s/%s%s",
+			     BTF_SYSFS_DIR, dentry->d_name, BTF_INLINE_SUFFIX) >=
+			     sizeof(inline_path)) {
+			ASSERT_FAIL("BTF sysfs path is too long\n");
+			break;
+		}
+
+		if (!base_btf) {
+			module_btf = btf__parse_split(btf_path, vmlinux_btf);
+			err = libbpf_get_error(module_btf);
+			if (err) {
+				/* A module can be unloaded while its sysfs entry is iterated. */
+				if (err == -ENOENT)
+					continue;
+				ASSERT_OK(err, "parse_module_btf");
+				continue;
+			}
+			base_btf = module_btf;
+		}
+		if (stat(inline_path, &st)) {
+			err = errno;
+			if (err == ENOENT)
+				continue;
+			ASSERT_OK(err, "stat_inline_btf");
+		}
+		if (base_btf == vmlinux_btf)
+			test_btf_mmap_sysfs(inline_path, base_btf);
+		inline_btf = btf__parse_split(inline_path, base_btf);
+		err = libbpf_get_error(inline_btf);
+		if (!err)
+			btf__free(inline_btf);
+		ASSERT_OK(err, "parse_inline_btf");
+		btf__free(module_btf);
+	}
+	closedir(dir);
+
+	btf__free(vmlinux_btf);
+}
+
 void test_btf_sysfs(void)
 {
 	test_btf_mmap_sysfs("/sys/kernel/btf/vmlinux", NULL);
+	test_btf_inline_sysfs_all();
 }
-- 
2.43.5


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

* [PATCH v3 bpf-next 7/7] selftests/bpf: Add a test verifying inline information
  2026-10-03 19:43 [PATCH v3 bpf-next 0/7] kbuild, bpf: Support inline info in BTF Alan Maguire
                   ` (5 preceding siblings ...)
  2026-10-03 19:43 ` [PATCH v3 bpf-next 6/7] selftests/bpf: Test BTF sysfs inline representations Alan Maguire
@ 2026-10-03 19:43 ` Alan Maguire
  2026-10-03 19:54   ` sashiko-bot
  6 siblings, 1 reply; 20+ messages in thread
From: Alan Maguire @ 2026-10-03 19:43 UTC (permalink / raw)
  To: ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, Alan Maguire

For bpf_testmod verify that we have inline info for an
__always_inline'd function and it matches reasonable
expectations (a single location parameter encoded in a
register given that it cannot be compile-time optimized).

Also verify that the offset of the LOCSEC descriptor makes
sense, i.e. that it is in the range of the function where
it was inlined.

Because bpf_testmod is treated as an out-of-tree module,
the inline information will be in btf_testmod.inline which
is relocated using bpf_testmod.ko .BTF.base.

Test is skipped if pahole does not encode inline info.

Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
 .../selftests/bpf/prog_tests/btf_inline.c     | 110 ++++++++++++++++++
 .../selftests/bpf/test_kmods/bpf_testmod.c    |   2 +-
 tools/testing/selftests/bpf/trace_helpers.c   |  20 ++++
 tools/testing/selftests/bpf/trace_helpers.h   |   1 +
 4 files changed, 132 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/btf_inline.c

diff --git a/tools/testing/selftests/bpf/prog_tests/btf_inline.c b/tools/testing/selftests/bpf/prog_tests/btf_inline.c
new file mode 100644
index 000000000000..399b931ace7b
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/btf_inline.c
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026, Oracle and/or its affiliates. */
+
+#include <test_progs.h>
+#include <bpf/btf.h>
+#include <bpf/libbpf.h>
+
+#define BTF_SYSFS_DIR		"/sys/kernel/btf"
+#define BTF_INLINE_SUFFIX	".inline"
+
+/*
+ * For a specific inline site, verify we have the right function,
+ * loc proto and loc param representation and that the offset is
+ * reasonable given the caller where it was inlined.
+ *
+ * Because bpf_testmod is compiled "out-of-tree" we have inline
+ * information in the split BTF directly rather than in btf_testmod.inline.
+ */
+void test_btf_inline(void)
+{
+	struct btf *inline_btf = NULL, *btf = NULL, *vmlinux_btf = NULL;
+	const char *inline_caller = "bpf_testmod_uprobe_write";
+	const char *inline_func = "testmod_register_uprobe";
+	bool skip = false, found_loc = false;
+	const struct btf_loc_param *lp;
+	int locsec_id, func_id, n, i;
+	long caller_addr, base_addr;
+	const struct btf_type *t;
+	struct btf_loc *l;
+	const __u32 *p;
+	int err = 0;
+
+	if (!env.has_testmod) {
+		test__skip();
+		return;
+	}
+
+	base_addr = module_get_base_addr("bpf_testmod");
+	if (!ASSERT_NEQ(base_addr, 0, "base_addr_nonzero"))
+		return;
+
+	load_kallsyms();
+	caller_addr = ksym_get_addr(inline_caller);
+	if (!ASSERT_NEQ(caller_addr, 0, "caller_addr_nonzero"))
+		return;
+
+	if (!ASSERT_GT(caller_addr, base_addr, "caller_addr_gt_base_addr"))
+		return;
+	caller_addr -= base_addr;
+
+	vmlinux_btf = btf__load_vmlinux_btf();
+	if (!ASSERT_OK_PTR(vmlinux_btf, "vmlinux_btf"))
+		return;
+
+	btf = btf__parse_split(BTF_SYSFS_DIR "/bpf_testmod", vmlinux_btf);
+	if (!ASSERT_OK_PTR(btf, "bpf_testmod_btf"))
+		goto out;
+
+	inline_btf = btf__parse_split(BTF_SYSFS_DIR "/bpf_testmod" BTF_INLINE_SUFFIX,
+				      btf);
+	err = libbpf_get_error(inline_btf);
+	/* pahole may not have inline BTF feature support. */
+	if (err == -ENOENT) {
+		skip = true;
+		goto out;
+	}
+	locsec_id = btf__find_by_name_kind(inline_btf, "inline.text", BTF_KIND_LOCSEC);
+	if (locsec_id < 0) {
+		skip = true;
+		goto out;
+	}
+	func_id = btf__find_by_name_kind(inline_btf, inline_func, BTF_KIND_FUNC);
+	if (!ASSERT_GT(func_id, 0, "inline_caller_func"))
+		goto out;
+	t = btf__type_by_id(inline_btf, locsec_id);
+	n = btf_vlen(t);
+	for (i = 0, l = btf_locsec_locs(t); i < n; i++, l++) {
+		if (l->func == func_id) {
+			found_loc = true;
+			break;
+		}
+	}
+	if (!ASSERT_TRUE(found_loc, "found_loc"))
+		goto out;
+	if (!ASSERT_GT(l->loc_proto, 0, "loc_proto_id"))
+		goto out;
+	if (!ASSERT_GT(l->offset, 0, "loc_offset"))
+		goto out;
+	t = btf__type_by_id(inline_btf, l->loc_proto);
+	if (!ASSERT_OK_PTR(t, "loc_proto_ptr"))
+		goto out;
+	if (!ASSERT_EQ(btf_vlen(t), 1, "loc_proto_one_param"))
+		goto out;
+	p = btf_loc_proto_params(t);
+	t = btf__type_by_id(inline_btf, *p);
+	lp = btf_loc_param(t);
+	if (!ASSERT_EQ(lp->flags, BTF_LOC_PARAM_REG, "param_is_reg"))
+		goto out;
+	if (!ASSERT_GT(l->offset, caller_addr, "inline_gt_caller"))
+		goto out;
+	/* simple sanity test to roughly ensure inline site still in function */
+	if (ASSERT_LT(l->offset, caller_addr + 256, "inline_in_caller"))
+		goto out;
+out:
+	btf__free(inline_btf);
+	btf__free(btf);
+	btf__free(vmlinux_btf);
+	if (skip)
+		test__skip();
+}
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
index 93847ca6293b..8b48407ce086 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
@@ -802,7 +802,7 @@ static struct testmod_uprobe uprobe = {
 	.consumer.ret_handler = uprobe_ret_handler,
 };
 
-static int testmod_register_uprobe(loff_t offset)
+static __always_inline int testmod_register_uprobe(loff_t offset)
 {
 	int err = -EBUSY;
 
diff --git a/tools/testing/selftests/bpf/trace_helpers.c b/tools/testing/selftests/bpf/trace_helpers.c
index 679008b310d9..bf4cb720e7b8 100644
--- a/tools/testing/selftests/bpf/trace_helpers.c
+++ b/tools/testing/selftests/bpf/trace_helpers.c
@@ -244,6 +244,26 @@ int kallsyms_find(const char *sym, unsigned long long *addr)
 	return err;
 }
 
+long module_get_base_addr(const char *module)
+{
+	unsigned long long addr, base_addr = 0;
+	char name[500];
+	FILE *f;
+
+	f = fopen("/proc/modules", "r");
+	if (!f)
+		return 0;
+
+	while (fscanf(f, "%s %*s %*s %*s %*s 0x%llx", name, &addr) > 0) {
+		if (strcmp(module, name) == 0) {
+			base_addr = addr;
+			break;
+		}
+	}
+	fclose(f);
+	return base_addr;
+}
+
 #ifdef PROCMAP_QUERY
 int env_verbosity __weak = 0;
 
diff --git a/tools/testing/selftests/bpf/trace_helpers.h b/tools/testing/selftests/bpf/trace_helpers.h
index 01c8ecc45627..473508306f79 100644
--- a/tools/testing/selftests/bpf/trace_helpers.h
+++ b/tools/testing/selftests/bpf/trace_helpers.h
@@ -38,6 +38,7 @@ typedef int (*ksym_search_cmp_t)(const void *p1, const struct ksym *p2);
 int load_kallsyms(void);
 struct ksym *ksym_search(long key);
 long ksym_get_addr(const char *name);
+long module_get_base_addr(const char *module);
 
 struct ksyms *load_kallsyms_local(void);
 struct ksym *ksym_search_local(struct ksyms *ksyms, long key);
-- 
2.43.5


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

* Re: [PATCH v3 bpf-next 7/7] selftests/bpf: Add a test verifying inline information
  2026-10-03 19:43 ` [PATCH v3 bpf-next 7/7] selftests/bpf: Add a test verifying inline information Alan Maguire
@ 2026-10-03 19:54   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-03 19:54 UTC (permalink / raw)
  To: Alan Maguire; +Cc: linux-modules, Petr Pavlu, bpf

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] bpf: null pointer dereference in test_btf_inline() on error path
- [Medium] bpf: invalid pointer dereference in test_btf_inline() due to missing NULL check
- [Medium] bpf: uninitialized variable usage in module_get_base_addr()
- [Low] bpf: inverted assertion logic in test_btf_inline() breaks failure-abort pattern
--

commit 0f45ec42b3956956fce6d91dc0397c28034b638d
Author: Alan Maguire <alan.maguire@oracle.com>

selftests/bpf: Add a test verifying inline information

This commit adds a selftest for bpf_testmod to verify that inline information
is correctly generated and matches expectations for an always-inline function.
It validates the LOCSEC descriptor offset and skips the test if pahole does
not encode inline information.

> diff --git a/tools/testing/selftests/bpf/prog_tests/btf_inline.c b/tools/testing/selftests/bpf/prog_tests/btf_inline.c
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/btf_inline.c
[ ... ]
> +void test_btf_inline(void)
> +{
[ ... ]
> +	inline_btf = btf__parse_split(BTF_SYSFS_DIR "/bpf_testmod" BTF_INLINE_SUFFIX,
> +				      btf);
> +	err = libbpf_get_error(inline_btf);
> +	/* pahole may not have inline BTF feature support. */
> +	if (err == -ENOENT) {
> +		skip = true;
> +		goto out;
> +	}
> +	locsec_id = btf__find_by_name_kind(inline_btf, "inline.text", BTF_KIND_LOCSEC);

[Severity: Medium]
What happens if btf__parse_split() fails with an error other than -ENOENT,
such as -ENOMEM?

If inline_btf is an error pointer or NULL, the condition checking for -ENOENT
evaluates to false, and the invalid inline_btf pointer is passed directly to
btf__find_by_name_kind().

Can this cause a null pointer dereference when libbpf attempts to access the
BTF struct?

[ ... ]
> +	t = btf__type_by_id(inline_btf, l->loc_proto);
> +	if (!ASSERT_OK_PTR(t, "loc_proto_ptr"))
> +		goto out;
> +	if (!ASSERT_EQ(btf_vlen(t), 1, "loc_proto_one_param"))
> +		goto out;
> +	p = btf_loc_proto_params(t);
> +	t = btf__type_by_id(inline_btf, *p);
> +	lp = btf_loc_param(t);
> +	if (!ASSERT_EQ(lp->flags, BTF_LOC_PARAM_REG, "param_is_reg"))

[Severity: Medium]
Does the second call to btf__type_by_id() require a check to ensure 't' is
not NULL before calculating the location param?

If *p is an invalid type ID, t would be NULL, and passing it to
btf_loc_param() calculates an offset from a NULL pointer. Could this cause a
segmentation fault when lp->flags is accessed?

> +		goto out;
> +	if (!ASSERT_GT(l->offset, caller_addr, "inline_gt_caller"))
> +		goto out;
> +	/* simple sanity test to roughly ensure inline site still in function */
> +	if (ASSERT_LT(l->offset, caller_addr + 256, "inline_in_caller"))
> +		goto out;

[Severity: Low]
Is the logic inverted on this ASSERT_LT() check?

The other assertions use the ! operator to jump to 'out' on failure. Because
ASSERT_LT() returns true when the check succeeds, it looks like this will jump
to 'out' on success instead of on failure. While this might be benign right
now as the last statement, will it bypass any future checks appended here?


> diff --git a/tools/testing/selftests/bpf/trace_helpers.c b/tools/testing/selftests/bpf/trace_helpers.c
> --- a/tools/testing/selftests/bpf/trace_helpers.c
> +++ b/tools/testing/selftests/bpf/trace_helpers.c
[ ... ]
> +long module_get_base_addr(const char *module)
> +{
[ ... ]
> +	while (fscanf(f, "%s %*s %*s %*s %*s 0x%llx", name, &addr) > 0) {
> +		if (strcmp(module, name) == 0) {
> +			base_addr = addr;
> +			break;
> +		}
> +	}

[Severity: Medium]
Can fscanf() return 1 if it successfully parses the module name but fails to
parse the address field?

If the line format in /proc/modules is unexpected or malformed, and fscanf()
returns 1, the check > 0 will pass, but the addr variable will remain
uninitialized. If the module name matches, could this return uninitialized
stack memory as the base address? Would it be safer to check that fscanf
returns exactly 2?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261003194323.817885-1-alan.maguire@oracle.com?part=7

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

* Re: [PATCH v3 bpf-next 6/7] selftests/bpf: Test BTF sysfs inline representations
  2026-10-03 19:43 ` [PATCH v3 bpf-next 6/7] selftests/bpf: Test BTF sysfs inline representations Alan Maguire
@ 2026-10-03 19:54   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-03 19:54 UTC (permalink / raw)
  To: Alan Maguire; +Cc: bpf, linux-modules, Petr Pavlu

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] bpf: module_btf memory leak when .inline sysfs entry is missing
- [Low] bpf: missing continue statement after unexpected stat() failure leads to cascading failures
- [Low] bpf: partial memory leak on unexpected mmap success in btf sysfs test
--

commit de628b5a108fca7fc52dbae8ef2aa0fc5fbd85bd
Author: Alan Maguire <alan.maguire@oracle.com>

selftests/bpf: Test BTF sysfs inline representations

This commit tests BTF sysfs inline representations by loading vmlinux,
vmlinux.inline, and all module/module.inline entries in /sys/fs/btf. This
verifies the kernel and module inline representations and sanity-checks
relocations.

There is a minor memory leak on an error path in test_btf_mmap_sysfs()
when the mmap with an invalid size unexpectedly succeeds:

tools/testing/selftests/bpf/prog_tests/btf_sysfs.c:test_btf_mmap_sysfs() {
    ...
	raw_data = mmap(NULL, end + 1, PROT_READ, MAP_PRIVATE, fd, 0);
	if (!ASSERT_EQ(raw_data, MAP_FAILED, "mmap_btf_invalid_size"))
		goto cleanup;
    ...
cleanup:
	btf__free(btf);
	if (raw_data && raw_data != MAP_FAILED)
		munmap(raw_data, btf_size);
    ...
}

[Severity: Low]
Does this leak the final page allocated by the end + 1 size?

If the oversized mmap unexpectedly succeeds, it allocates an extra page, but
the cleanup label only unmaps up to btf_size.

> diff --git a/tools/testing/selftests/bpf/prog_tests/btf_sysfs.c b/tools/testing/selftests/bpf/prog_tests/btf_sysfs.c
> index 3923e64c4c1d0..069c4483b3a21 100644
> --- a/tools/testing/selftests/bpf/prog_tests/btf_sysfs.c
> +++ b/tools/testing/selftests/bpf/prog_tests/btf_sysfs.c

[ ... ]

> @@ -75,7 +80,78 @@ static void test_btf_mmap_sysfs(const char *path, struct btf *base)
>  		close(fd);
>  }
>  
> +static void test_btf_inline_sysfs_all(void)
> +{

[ ... ]

> +		if (!base_btf) {
> +			module_btf = btf__parse_split(btf_path, vmlinux_btf);
> +			err = libbpf_get_error(module_btf);
> +			if (err) {
> +				/* A module can be unloaded while its sysfs entry is iterated. */
> +				if (err == -ENOENT)
> +					continue;
> +				ASSERT_OK(err, "parse_module_btf");
> +				continue;
> +			}
> +			base_btf = module_btf;
> +		}
> +		if (stat(inline_path, &st)) {
> +			err = errno;
> +			if (err == ENOENT)
> +				continue;

[Severity: Medium]
Does this early continue leak the memory allocated for module_btf?

When an .inline file does not exist, the continue statement bypasses the
btf__free(module_btf) call at the bottom of the loop. Since most modules
might lack this representation, this could leak memory for every module.

> +			ASSERT_OK(err, "stat_inline_btf");
> +		}

[Severity: Low]
Should there be a continue statement here to avoid cascading failures?

If stat() fails with an error other than ENOENT, the code asserts but falls
through to parse the nonexistent or inaccessible inline file instead of
jumping to the next iteration.

> +		if (base_btf == vmlinux_btf)
> +			test_btf_mmap_sysfs(inline_path, base_btf);
> +		inline_btf = btf__parse_split(inline_path, base_btf);
> +		err = libbpf_get_error(inline_btf);
> +		if (!err)
> +			btf__free(inline_btf);
> +		ASSERT_OK(err, "parse_inline_btf");
> +		btf__free(module_btf);
> +	}
> +	closedir(dir);
> +
> +	btf__free(vmlinux_btf);
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261003194323.817885-1-alan.maguire@oracle.com?part=6

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

* Re: [PATCH v3 bpf-next 3/7] kbuild, btf: Expose vmlinux inline BTF through sysfs
  2026-10-03 19:43 ` [PATCH v3 bpf-next 3/7] kbuild, btf: Expose vmlinux inline BTF through sysfs Alan Maguire
@ 2026-10-03 19:56   ` sashiko-bot
  2026-10-06 18:12     ` Alan Maguire
  2026-10-03 20:41   ` bot+bpf-ci
  1 sibling, 1 reply; 20+ messages in thread
From: sashiko-bot @ 2026-10-03 19:56 UTC (permalink / raw)
  To: Alan Maguire; +Cc: linux-modules, Petr Pavlu, bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] bpf: btf: Unvalidated zero-size allocation triggers WARN_ON_ONCE in vmalloc_user
- [Low] bpf: btf: Linkage fails for sha256 when CONFIG_DEBUG_INFO_BTF_INLINE=m
--

commit de2bb52fc001c93d461a0839a26e550f67f5ecc2
Author: Alan Maguire <alan.maguire@oracle.com>

kbuild, btf: Expose vmlinux inline BTF through sysfs

This commit introduces support for embedded and module-delivered vmlinux
inline BTF payloads, making them available via sysfs. It also sets up deferred
binding for a module-provided .BTF.inline section.

> diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
> index 134b15a44625e..24abfff1ddc3c 100644
> --- a/lib/Kconfig.debug
> +++ b/lib/Kconfig.debug
> @@ -432,6 +438,19 @@ config DEBUG_INFO_BTF_MODULES
>  	help
>  	  Generate compact split BTF type information for kernel modules.
>  
> +config DEBUG_INFO_BTF_INLINE
> +	tristate "Provide information about inline sites in BTF"
> +	default n
> +	depends on DEBUG_INFO_BTF && PAHOLE_HAS_INLINE && SYSFS
> +	select DEBUG_INFO_BTF_MODULES if MODULES
> +	select CRYPTO_LIB_SHA256

[Severity: Low]
Does this cause a linkage failure when CONFIG_DEBUG_INFO_BTF_INLINE is built
as a module?

Because this is a tristate, selecting CRYPTO_LIB_SHA256 here means it will 
evaluate to 'm' if the inline feature is configured as a module. 

However, the crypto library is called directly from the built-in core kernel
object kernel/bpf/btf.c:

kernel/bpf/btf.c:btf_verify_link() {
    ...
    sha256(data, data_size, actual_digest);
    ...
}

A built-in object cannot resolve a symbol exported by a loadable module,
which will result in an 'undefined reference to sha256' error at build time.

> +	help
> +	  Generate information about inline sites in .BTF.inline sections.

[ ... ]

> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> index 0630675377aaa..732ba06846675 100644
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c

[ ... ]

> @@ -9015,21 +9016,122 @@ static DEFINE_MUTEX(btf_module_mutex);
> +static int btf_module_handle_vmlinux_inline(const struct module *mod)
> +{
> +	void *data = NULL;
> +
> +	if (!vmlinux_inline_link || strcmp(mod->name, vmlinux_inline_link->module_name))
> +		return -ENOENT;
> +
> +	data = vmalloc_user(mod->btf_inline_data_size);

[Severity: Medium]
Could this trigger a WARN_ON_ONCE splat in the memory allocation paths?

If a user with CAP_SYS_MODULE loads a module matching
vmlinux_inline_link->module_name, but the module has no .BTF.inline section
(or its size is exactly 0), mod->btf_inline_data_size will be set to 0.

Calling vmalloc_user(0) here happens before the size is validated against 
link->btf_size, which causes a kernel warning in __vmalloc_node_range_noprof.

> +	if (!data)
> +		return -ENOMEM;
> +	memcpy(data, mod->btf_inline_data, mod->btf_inline_data_size);
> +
> +	if (!btf_verify_link(vmlinux_inline_link, mod, data, mod->btf_inline_data_size) ||
> +	    !sysfs_btf_update(vmlinux_inline_attr, data)) {
> +		kvfree(data);
> +		return -EINVAL;
> +	}
> +	return 0;
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261003194323.817885-1-alan.maguire@oracle.com?part=3

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

* Re: [PATCH v3 bpf-next 5/7] btf: Relocate and expose module inline BTF
  2026-10-03 19:43 ` [PATCH v3 bpf-next 5/7] btf: Relocate and expose module inline BTF Alan Maguire
@ 2026-10-03 19:59   ` sashiko-bot
  2026-10-06 18:56     ` Alan Maguire
  2026-10-03 20:27   ` bot+bpf-ci
  1 sibling, 1 reply; 20+ messages in thread
From: sashiko-bot @ 2026-10-03 19:59 UTC (permalink / raw)
  To: Alan Maguire; +Cc: bpf, Petr Pavlu, linux-modules

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] bpf: kernel panic on NULL pointer dereference in btf_parse_module() error path
--

commit 7871f353137bb8eca880dde9f21fa3041a6f681f
Author: Alan Maguire <alan.maguire@oracle.com>

btf: Relocate and expose module inline BTF

This commit introduces parsing of module inline BTF data, rebases inline type
IDs on top of the relocated module BTF, and exposes the result via sysfs.
Malformed inline BTF is warned about and skipped without preventing normal
module BTF registration.

> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> index c535e636d41c1..28b7c9786fdce 100644
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c
[ ... ]
> @@ -6954,67 +7076,66 @@ static struct btf *btf_parse_module(const char *module_name, const void *data,
>  		base_btf = vmlinux_btf;
>  	}
>  
> -	btf = kzalloc_obj(*btf, GFP_KERNEL | __GFP_NOWARN);
> -	if (!btf) {
> -		err = -ENOMEM;
> +	btf = btf_parse_split(env, module_name, data, data_size, base_btf);
> +	if (IS_ERR(btf)) {
> +		err = PTR_ERR(btf);
> +		btf = NULL;
>  		goto errout;
>  	}
[ ... ]
>  errout:
> +	kvfree(module_str_map);
>  	btf_verifier_env_free(env);
> +	btf_free(inline_btf);
>  	if (!IS_ERR(base_btf) && base_btf != vmlinux_btf)
>  		btf_free(base_btf);
> -	if (btf) {
> -		kvfree(btf->data);
> -		kvfree(btf->types);
> -		kfree(btf);
> -	}
> +	btf_free(btf);

[Severity: High]
Will this cause a kernel panic if btf_parse_split() fails?

In the error path above, if btf_parse_split() returns an error, btf is
explicitly set to NULL and execution jumps to errout. At this point,
inline_btf is also initialized to NULL.

The errout path then calls btf_free() unconditionally for both inline_btf
and btf. Looking at btf_free():

static void btf_free(struct btf *btf)
{
        btf_free_struct_meta_tab(btf);
        ...
}

And btf_free_struct_meta_tab() unconditionally dereferences the pointer:

static void btf_free_struct_meta_tab(struct btf *btf)
{
        struct btf_struct_metas *tab = btf->struct_meta_tab;
        ...
}

Does this result in an immediate NULL pointer dereference when
btf_free() is called on the NULL btf or inline_btf pointers?

>  	return ERR_PTR(err);
>  }

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261003194323.817885-1-alan.maguire@oracle.com?part=5

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

* Re: [PATCH v3 bpf-next 5/7] btf: Relocate and expose module inline BTF
  2026-10-03 19:43 ` [PATCH v3 bpf-next 5/7] btf: Relocate and expose module inline BTF Alan Maguire
  2026-10-03 19:59   ` sashiko-bot
@ 2026-10-03 20:27   ` bot+bpf-ci
  1 sibling, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-10-03 20:27 UTC (permalink / raw)
  To: alan.maguire, ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, alan.maguire, ast, andrii, daniel, martin.lau, eddyz87,
	yonghong.song, mason, ihor.solodrai

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

> btf: Relocate and expose module inline BTF

TL;DR: Nits only, no bugs: in module inline BTF relocation, the
old_module_type_cnt parameter looks redundant, and the final
btf_set_base_btf()/btf_check_sorted() calls appear unneeded.

> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c

[ ... ]

> @@ -6922,15 +6922,137 @@ __u32 btf_relocate_id(const struct btf *btf, __u32 id)
> +static int btf_rebase_inline(struct btf *inline_btf,
> +			     const struct btf *module_btf,
> +			     const u32 *module_id_map,
> +			     const u32 *module_str_map,
> +			     u32 old_module_type_cnt)
> +{
> +	u32 old_start_id = inline_btf->start_id;
> +	u32 old_start_str_off = inline_btf->start_str_off;

[ ... ]

> +		while ((id = btf_field_iter_next(&it))) {
> +			if (!*id)
> +				continue;
> +			if (*id < old_module_type_cnt) {
> +				if (module_id_map)
> +					*id = module_id_map[*id];
> +			} else if (*id >= old_start_id) {
> +				*id += id_delta;
> +			} else {
> +				return -EINVAL;
> +			}
> +		}

This isn't a bug, but is the old_module_type_cnt parameter needed?

The only caller passes old_module_type_cnt = btf_nr_types(btf), sampled
before btf_relocate().  inline_btf was parsed by btf_parse_split() against
that same btf, and btf_set_base_btf() sets its start_id to
btf_nr_types(base_btf).

That makes old_module_type_cnt equal to old_start_id, so the final
"else return -EINVAL" branch above does not look reachable.  Could
old_start_id be used for both checks and the parameter dropped?

[ ... ]

> +	btf_set_base_btf(inline_btf, module_btf);
> +	btf_check_sorted(inline_btf);
> +	return 0;
> +}

[ ... ]

> @@ -6954,67 +7076,66 @@ static struct btf *btf_parse_module(const char *module_name, const void *data,

[ ... ]

> +	if (inline_btf) {
> +		err = btf_rebase_inline(inline_btf, btf, btf->base_id_map,
> +					module_str_map, old_module_type_cnt);
> +		if (err) {
> +			pr_warn("failed to relocate module [%s] inline BTF: %d\n",
> +				module_name, err);
> +			btf_free(inline_btf);
> +		} else {
> +			*relocated_inline_data = inline_btf->data;
> +			inline_btf->data = NULL;
> +			btf_free(inline_btf);
> +		}
> +	}

This isn't a bug, but are the btf_set_base_btf() and btf_check_sorted()
calls at the end of btf_rebase_inline() needed?

On success, btf_parse_module() only takes inline_btf->data and then calls
btf_free(inline_btf) right away, so the updated start_id, start_str_off and
named_start_id are never read.  btf_check_sorted() also runs strcmp() across
the inline types and the result is discarded.


---
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/37149955093

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

* Re: [PATCH v3 bpf-next 1/7] resolve_btfids: Build separate main and inline BTF objects
  2026-10-03 19:43 ` [PATCH v3 bpf-next 1/7] resolve_btfids: Build separate main and inline BTF objects Alan Maguire
@ 2026-10-03 20:27   ` bot+bpf-ci
  2026-10-06 17:30     ` Alan Maguire
  0 siblings, 1 reply; 20+ messages in thread
From: bot+bpf-ci @ 2026-10-03 20:27 UTC (permalink / raw)
  To: alan.maguire, ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, alan.maguire, ast, andrii, daniel, martin.lau, eddyz87,
	yonghong.song, mason, ihor.solodrai

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

> resolve_btfids: Build separate main and inline BTF objects

TL;DR: In resolve_btfids --inline, extract_inline_btf() may free a base BTF
that the new main BTF still points to, causing a possible use-after-free for
modules with .BTF.base; in-tree builds seem unaffected.

> diff --git a/tools/bpf/resolve_btfids/main.c b/tools/bpf/resolve_btfids/main.c
> index 37d7e7224207..dcd289d16faf 100644
> --- a/tools/bpf/resolve_btfids/main.c
> +++ b/tools/bpf/resolve_btfids/main.c

[ ... ]

> @@ -1510,6 +1569,85 @@ static int btf2btf(struct object *obj)
>  	return err;
>  }
>

[ ... ]

> +static int extract_inline_btf(struct object *obj)
> +{
> +	struct btf *btf = obj->btf, *main_btf, *inline_btf;
> +	__u32 type_cnt = btf__type_cnt(btf);
> +	__u8 *colors;
> +	int err;
> +
> +	colors = calloc(type_cnt, sizeof(*colors));
> +	if (!colors)
> +		return -ENOMEM;
> +	err = color_btf(obj, colors);
> +	if (err)
> +		goto out;
> +	err = btf_split_by_color(btf, colors, &main_btf, &inline_btf);
> +	if (!err) {
> +		obj->btf = main_btf;
> +		obj->inline_btf = inline_btf;
> +		btf__free(btf);
                 ^^^^

Can this btf__free(btf) free a base BTF that the new obj->btf still points
to?

btf_split_by_color() creates main_btf with .base_btf set to
btf__base_btf(src), and btf_new_empty() stores that pointer without taking
ownership.  btf__free() on the source also frees its base when the source
owns it:

    if (btf->owns_base)
        btf__free(btf->base_btf);

libbpf sets owns_base in btf_parse_elf() when the ELF passed to
btf__parse_split() has a .BTF.base section and no base_btf was given.

This looks reachable with "resolve_btfids --inline mod.ko" (no --btf_base,
no --btf) on an out-of-tree module that already has .BTF, .BTF.base and
.BTF_ids:

load_btf()
  btf__parse_split(obj->path, NULL)
    owns_base = true, obj->base_btf stays NULL
finalize_btf()
  distill is skipped because obj->base_btf is NULL
  sort_btf_by_name()
  extract_inline_btf()
    btf__free(btf)
      also frees the distilled base that main_btf->base_btf points to
main()
  symbols_resolve()
    btf__type_by_id(obj->btf, ...)
      btf_type_by_id(btf->base_btf, type_id)
        reads freed memory

Without --inline the same command works, because obj->btf keeps owning its
base until main() frees it.

The in-tree build does not appear to be affected, since gen-btf.sh passes a
raw --btf file together with --btf_base, which leaves owns_base false.  I did
not find a fix for this later in the series.

Should extract_inline_btf() keep the original BTF alive, for example in a
field of struct object that main() frees after obj->btf?  Alternatively,
could it reject or handle the case where btf__base_btf(obj->btf) is set but
obj->base_btf is NULL?

> +	}
> +out:
> +	free(colors);
> +	return err;
> +}

[ ... ]


---
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/37149955093

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

* Re: [PATCH v3 bpf-next 2/7] resolve_btfids: Populate BTF link section
  2026-10-03 19:43 ` [PATCH v3 bpf-next 2/7] resolve_btfids: Populate BTF link section Alan Maguire
@ 2026-10-03 20:27   ` bot+bpf-ci
  0 siblings, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-10-03 20:27 UTC (permalink / raw)
  To: alan.maguire, ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, alan.maguire, ast, andrii, daniel, martin.lau, eddyz87,
	yonghong.song, mason, ihor.solodrai

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

> resolve_btfids: Populate BTF link section

TL;DR: patch_btf_link() and main() in resolve_btfids mostly fail silently; a
kbuild vmlinux link step could stop with only "Error 234". Should pr_err()
messages be added, as patch_btfids() does?

> diff --git a/tools/bpf/resolve_btfids/main.c b/tools/bpf/resolve_btfids/main.c
> index dcd289d16faf..84cd9691f177 100644
> --- a/tools/bpf/resolve_btfids/main.c
> +++ b/tools/bpf/resolve_btfids/main.c

[ ... ]

> @@ -1036,6 +1095,110 @@ static int dump_raw_btf(struct btf *btf, const char *out_path)
>  	return 0;
>  }
>  
> +static int patch_btf_link(struct object *obj, const struct btf_link *link)
> +{
> [ ... ]
> +	if (stat(link->btf_path, &st) || !S_ISREG(st.st_mode) ||
> +	    st.st_size <= 0 || st.st_size > UINT_MAX)
> +		return -EINVAL;
> +	raw_btf_size = st.st_size;
> +	raw_btf_data = malloc(raw_btf_size);
> +	if (!raw_btf_data)
> +		return -ENOMEM;
> +	btf_file = fopen(link->btf_path, "rb");
> +	if (!btf_file)
> +		goto out_data;

Should patch_btf_link() print a message when it fails?

Nearly every error path here returns without any diagnostic: the stat() and
S_ISREG() check, malloc(), fopen(), fread(), an unknown ELF class, a module
name that is too long, a section name that does not fit in section[], open(),
elf_begin(), elf_getshdrstrndx(), gelf_getshdr(), an unknown EI_DATA and
elf_update().  Only "section %s not found" and "section %s is not a writable
BTF link" call pr_err().

The sibling patch_btfids() calls pr_err() with strerror(errno) or
elf_errmsg() on the corresponding failures (open, elf_begin,
elf_getshdrstrndx, gelf_getshdr, stat, fopen, fread, elf_update), and
pr_err() prints regardless of verbosity.

> [ ... ]

> +	if (!scn) {
> +		pr_err("FAILED: section %s not found in %s\n", section, obj->path);
> +		goto out_elf;
> +	}
> [ ... ]
> +	if (elf_update(elf, ELF_C_WRITE) >= 0)
> +		err = 0;
> +out_elf:
> [ ... ]

[ ... ]

> @@ -1931,12 +2097,30 @@ int main(int argc, const char **argv)
> [ ... ]
> -	if (btfids_path)
> -		return patch_btfids(btfids_path, obj.path);
> +	if (btfids_path) {
> +		err = patch_btfids(btfids_path, obj.path);
> +		if (err || !btf_links.cnt)
> +			goto out;
> +		if (elf_collect(&obj)) {
> +			err = -EINVAL;
> +			goto out;
> +		}
> +		for (u32 i = 0; i < btf_links.cnt; i++) {
> +			struct btf_link *link = &btf_links.links[i];
>  
> +			err = patch_btf_link(&obj, link);
> +			if (err)
> +				break;
> +		}
> +		goto out;
> +	}

main() does not print anything either when patch_btf_link() fails:

    err = patch_btf_link(&obj, link);
    if (err)
        break;
    ...
    goto out;

so resolve_btfids exits with status 234 (-EINVAL) or 255 (-1) and no
message.

This looks reachable from kbuild.  f61c6417ac15 ("kbuild, btf: Expose
vmlinux inline BTF through sysfs") makes scripts/link-vmlinux.sh always pass:

    --btf_link .BTF.inline:btf_vmlinux_inline:vmlinux.BTF.inline

when BTF_INLINE=m, while scripts/gen-btf.sh explicitly handles a missing
${ELF_FILE}.BTF.inline by running:

    rm -f "${objtree}/vmlinux.BTF.inline"

In that case, or if writing the output fails, or if a module name is 56
characters or more on a 64-bit target, the vmlinux link step stops after
"BTFIDS vmlinux" with only make's "Error 234" and nothing that says which
check failed.

Could pr_err() messages be added, as patch_btfids() does, naming
link->btf_path, link->module, the section and obj->path?


---
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/37149955093

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

* Re: [PATCH v3 bpf-next 3/7] kbuild, btf: Expose vmlinux inline BTF through sysfs
  2026-10-03 19:43 ` [PATCH v3 bpf-next 3/7] kbuild, btf: Expose vmlinux inline BTF through sysfs Alan Maguire
  2026-10-03 19:56   ` sashiko-bot
@ 2026-10-03 20:41   ` bot+bpf-ci
  2026-10-06 18:42     ` Alan Maguire
  1 sibling, 1 reply; 20+ messages in thread
From: bot+bpf-ci @ 2026-10-03 20:41 UTC (permalink / raw)
  To: alan.maguire, ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, alan.maguire, ast, andrii, daniel, martin.lau, eddyz87,
	yonghong.song, mason, ihor.solodrai

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

> kbuild, btf: Expose vmlinux inline BTF through sysfs

TL;DR:
- Module load may fail with -ENOENT for modules without .BTF or with BTF
  mismatch allowed
- mmap of /sys/kernel/btf/vmlinux may return -EINVAL on arm64
- __SIZEOF_LONG__ may break 32-bit builds
- Nits: .gitignore, NULL return

> diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
> index b2988aa12f66..863f02660bc3 100644
> --- a/include/asm-generic/vmlinux.lds.h
> +++ b/include/asm-generic/vmlinux.lds.h

[ ... ]

> @@ -675,12 +675,39 @@
> +#ifdef CONFIG_DEBUG_INFO_BTF_INLINE_MODULE
> +/* __MODULE_NAME_LEN + SHA-256 digest + u32 BTF size */
> +#define BTF_LINK_SIZE	(64 - __SIZEOF_LONG__ + 32 + 4)
> +#define BTF_INLINE_LINK						\
> +	. = ALIGN(PAGE_SIZE);					\
> +	.BTF.inline.link : AT(ADDR(.BTF.inline.link) - LOAD_OFFSET) { \
> +		__start_BTF_inline_link = .;				\
> +		BYTE(0)						\
> +		. += BTF_LINK_SIZE - 1;					\

Is __SIZEOF_LONG__ reliable in a linker script?

The lds is preprocessed with KBUILD_CPPFLAGS only.  On x86, -m32 is added
only to KBUILD_CFLAGS and KBUILD_AFLAGS, and with clang KBUILD_CPPFLAGS
carries --target=x86_64-linux-gnu for all of SRCARCH=x86.  So for a 32-bit
x86 build with clang, or with a gcc whose default target is x86_64,
__SIZEOF_LONG__ is 8 while the lds is preprocessed, and BTF_LINK_SIZE
becomes 64 - 8 + 32 + 4 = 92.

On a 32-bit kernel __MODULE_NAME_LEN is 64 - sizeof(unsigned long) = 60, so
sizeof(struct btf_link) is 96.

With CONFIG_DEBUG_INFO_BTF_INLINE=m, link-vmlinux.sh runs:

  resolve_btfids --btf_link .BTF.inline:btf_vmlinux_inline:vmlinux.BTF.inline

patch_btf_link() uses module_name_len = BTF_LINK_MODULE_NAME_MAX - 4 for
ELFCLASS32 and rejects the section if

  data->d_size != module_name_len + BTF_SHA256_DIGEST_LENGTH + sizeof(u32)

Since 92 != 96, it prints "FAILED: section .BTF.inline.link is not a
writable BTF link", returns -1, and the vmlinux link fails.  Even if the
build got past that, btf_parse_link() checks size != sizeof(*link) and would
reject the record.

riscv32 looks the same: -mabi=ilp32 is only in KBUILD_CFLAGS and
KBUILD_AFLAGS, and the clang target is riscv64-linux-gnu.

Could this use something tied to Kconfig instead, such as #ifdef
CONFIG_64BIT, or BITS_PER_LONG / 8 from asm/bitsperlong.h, rather than the
compiler's default ABI macro?

[ ... ]

> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> index 0630675377aa..732ba0684667 100644
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c

[ ... ]

> @@ -9015,21 +9016,122 @@ static DEFINE_MUTEX(btf_module_mutex);
> +static const struct btf_link *btf_parse_link(void *start, unsigned int size)
> +{
> +	const struct btf_link *link = start;
> +	unsigned int module_name_len;
> +
> +	if (!link || size != sizeof(*link))
> +		return NULL;
> +
> +	if (link->btf_size == 0)
> +		return 0;

this isn't a bug, but btf_parse_link() returns a pointer and this path
returns a plain integer 0 where the other failure paths return NULL.  sparse
reports "Using plain integer as NULL pointer" here.

The only caller, btf_vmlinux_inline_lazy_init(), handles this the same way
as the other failures and prints "invalid vmlinux inline BTF link".  Could
this return NULL, or be folded into the check above?

[ ... ]

>  static int btf_module_notify(struct notifier_block *nb, unsigned long op,
>  			     void *module)
>  {
> [ ... ]
>  	int err = 0;
>
> -	if (mod->btf_data_size == 0 ||
> -	    (op != MODULE_STATE_COMING && op != MODULE_STATE_LIVE &&
> -	     op != MODULE_STATE_GOING))
> +	if (op != MODULE_STATE_COMING && op != MODULE_STATE_LIVE &&
> +	    op != MODULE_STATE_GOING)
>  		goto out;
>
>  	switch (op) {
>  	case MODULE_STATE_COMING:
> +		err = btf_module_handle_vmlinux_inline(mod);
> +		if (err != -ENOENT) {
> +			err = 0;
> +			break;
> +		}
> +		if (!mod->btf_data_size)
> +			break;
>  		btf_mod = kzalloc_obj(*btf_mod);

Can this reject every module that has no .BTF section?

btf_module_handle_vmlinux_inline() returns -ENOENT for any module that is
not the vmlinux inline carrier, and the
!IS_MODULE(CONFIG_DEBUG_INFO_BTF_INLINE) stub returns -ENOENT
unconditionally.  That value stays in err.

The old code returned early with err == 0 when mod->btf_data_size was 0.
Now a module without .BTF hits:

		if (!mod->btf_data_size)
			break;

and btf_module_notify() returns notifier_from_errno(-ENOENT) from the out:
label.

The same leftover err is returned when btf_parse_module() fails with
CONFIG_MODULE_ALLOW_BTF_MISMATCH=y:

		if (IS_ERR(btf)) {
			kfree(btf_mod);
			if (!IS_ENABLED(CONFIG_MODULE_ALLOW_BTF_MISMATCH)) {
				...
			} else {
				pr_warn_once("Kernel module BTF mismatch detected, ...");
			}
			goto out;
		}

That branch never resets err, so a mismatched module, which that option
exists to let load, now fails with -ENOENT as well.

notifier_from_errno() sets NOTIFY_STOP_MASK, notifier_call_chain_robust()
rolls back the COMING chain, prepare_coming_module() returns
notifier_to_errno() == -ENOENT, and load_module() goes to bug_cleanup.  The
module load then fails with "No such file or directory".

Modules without .BTF are a normal case.  scripts/Makefile.modfinal prints
"Skipping BTF generation for %s due to unavailability of vmlinux" for
external/DKMS modules, and the kernel.spec comment changed in this same
commit says eu-strip removes .BTF when find-debuginfo has no --keep-section.
Before this patch both cases loaded fine.

The problem is still there at the end of the series (e1c2367cf1b5);
3357b172196f ("btf: Relocate and expose module inline BTF") keeps the same
COMING prologue.

Would resetting err to 0 when the helper returns -ENOENT work?

	err = btf_module_handle_vmlinux_inline(mod);
	if (err != -ENOENT) {
		err = 0;
		break;
	}
	err = 0;

[ ... ]

> diff --git a/kernel/bpf/sysfs_btf.c b/kernel/bpf/sysfs_btf.c
> index 9cbe15ce3540..a3c54b5fe294 100644
> --- a/kernel/bpf/sysfs_btf.c
> +++ b/kernel/bpf/sysfs_btf.c

[ ... ]

> @@ -33,24 +31,149 @@ static int btf_sysfs_vmlinux_mmap(struct file *filp, struct kobject *kobj,
> +static int btf_sysfs_mmap_direct(struct file *filp, struct kobject *kobj,
> +				 const struct bin_attribute *attr,
> +				 struct vm_area_struct *vma)
> +{
> +	void *data = READ_ONCE(attr->private);
> [ ... ]
> +	err = btf_sysfs_mmap_check(data, attr->size, vma);
> +	if (err)
> +		return err;
> +	if (is_vmalloc_addr(data))
> +		return remap_vmalloc_range(vma, data, 0);
> +
> +	addr = __pa_symbol(data);
> [ ... ]
>  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_vmlinux_mmap,
> +	.mmap = btf_sysfs_mmap_direct,
>  };

Does this break mmap() of /sys/kernel/btf/vmlinux on arm64?

bin_attr_btf_vmlinux now uses btf_sysfs_mmap_direct(), which chooses between
remap_vmalloc_range() and remap_pfn_range() based on is_vmalloc_addr(data).
On arm64 the kernel image lives inside the vmalloc range:

  arch/arm64/include/asm/memory.h:  #define KIMAGE_VADDR (MODULES_END)
  arch/arm64/include/asm/pgtable.h: #define VMALLOC_START (MODULES_END)

So is_vmalloc_addr(__start_BTF) returns true, and the call goes to
remap_vmalloc_range() -> remap_vmalloc_range_partial().  There,
find_vm_area() finds the vm_struct that declare_kernel_vmas() registered for
the image rodata segment:

  declare_vma(&vmlinux_seg[1], __start_rodata, __inittext_begin, VM_NO_GUARD);
  ...
  vma->flags = VM_MAP | vm_flags;

That vm_struct has no VM_USERMAP, so this check:

  if (!(area->flags & (VM_USERMAP | VM_DMA_COHERENT)))
          return -EINVAL;

fails and mmap() returns -EINVAL.  Before this patch,
btf_sysfs_vmlinux_mmap() always used __pa_symbol(__start_BTF) plus
remap_pfn_range(), which worked on arm64 since .BTF sits inside RO_DATA,
between __start_rodata and __inittext_begin.

The same applies to the built-in /sys/kernel/btf/vmlinux.inline, which the
commit message says supports read-only mmap().  The selftest
test_btf_mmap_sysfs("/sys/kernel/btf/vmlinux") would fail on arm64 as well.

Would it be better to use remap_vmalloc_range() only for the lazily
populated vmalloc_user() entries, for example from btf_sysfs_mmap_lazy() or
by recording the backing type in struct btf_sysfs_entry, and keep the
__pa_symbol()/remap_pfn_range() path for kernel image data instead of
testing the address range?

[ ... ]

> diff --git a/scripts/gen-btf.sh b/scripts/gen-btf.sh
> index 8ca96eb10a69..e638291753d0 100755
> --- a/scripts/gen-btf.sh
> +++ b/scripts/gen-btf.sh

[ ... ]

> @@ -83,14 +88,28 @@ gen_btf_o()
> +	btf_inline=${ELF_FILE}.BTF.inline
> +	if [ "${BTF_INLINE}" = "m" ]; then
> +		if [ -f "${btf_inline}" ]; then
> +			cp "${btf_inline}" "${objtree}/vmlinux.BTF.inline"
> +		else
> +			rm -f "${objtree}/vmlinux.BTF.inline"
> +		fi
> +	fi

With CONFIG_DEBUG_INFO_BTF_INLINE=m, this leaves a long-lived top-level
build artifact, vmlinux.BTF.inline.  link-vmlinux.sh reads it for
--btf_link, and Makefile.modfinal reads it later to embed .BTF.inline into
btf_vmlinux_inline.ko.

The patch adds it to CLEAN_FILES in the top-level Makefile but not to
.gitignore, where the other top-level vmlinux artifacts are listed
(/vmlinux, /vmlinux.32, /vmlinux.map, /vmlinux.symvers,
/vmlinux.thinlto-index, /vmlinux.unstripped).  In an in-tree build,
"git check-ignore -v vmlinux.BTF.inline" returns nothing, so the file shows
up as untracked in git status.

Should /vmlinux.BTF.inline be added to .gitignore next to
/vmlinux.unstripped?  The later commits in the series (f61c6417ac15 through
e1c2367cf1b5) do not touch .gitignore.


---
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/37149955093

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

* Re: [PATCH v3 bpf-next 1/7] resolve_btfids: Build separate main and inline BTF objects
  2026-10-03 20:27   ` bot+bpf-ci
@ 2026-10-06 17:30     ` Alan Maguire
  0 siblings, 0 replies; 20+ messages in thread
From: Alan Maguire @ 2026-10-06 17:30 UTC (permalink / raw)
  To: bot+bpf-ci, ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, martin.lau, mason

On 03/10/2026 21:27, bot+bpf-ci@kernel.org wrote:
>> resolve_btfids: Build separate main and inline BTF objects
> 
> TL;DR: In resolve_btfids --inline, extract_inline_btf() may free a base BTF
> that the new main BTF still points to, causing a possible use-after-free for
> modules with .BTF.base; in-tree builds seem unaffected.
> 
>> diff --git a/tools/bpf/resolve_btfids/main.c b/tools/bpf/resolve_btfids/main.c
>> index 37d7e7224207..dcd289d16faf 100644
>> --- a/tools/bpf/resolve_btfids/main.c
>> +++ b/tools/bpf/resolve_btfids/main.c
> 
> [ ... ]
> 
>> @@ -1510,6 +1569,85 @@ static int btf2btf(struct object *obj)
>>  	return err;
>>  }
>>
> 
> [ ... ]
> 
>> +static int extract_inline_btf(struct object *obj)
>> +{
>> +	struct btf *btf = obj->btf, *main_btf, *inline_btf;
>> +	__u32 type_cnt = btf__type_cnt(btf);
>> +	__u8 *colors;
>> +	int err;
>> +
>> +	colors = calloc(type_cnt, sizeof(*colors));
>> +	if (!colors)
>> +		return -ENOMEM;
>> +	err = color_btf(obj, colors);
>> +	if (err)
>> +		goto out;
>> +	err = btf_split_by_color(btf, colors, &main_btf, &inline_btf);
>> +	if (!err) {
>> +		obj->btf = main_btf;
>> +		obj->inline_btf = inline_btf;
>> +		btf__free(btf);
>                  ^^^^
> 
> Can this btf__free(btf) free a base BTF that the new obj->btf still points
> to?
> 
> btf_split_by_color() creates main_btf with .base_btf set to
> btf__base_btf(src), and btf_new_empty() stores that pointer without taking
> ownership.  btf__free() on the source also frees its base when the source
> owns it:
> 
>     if (btf->owns_base)
>         btf__free(btf->base_btf);
> 
> libbpf sets owns_base in btf_parse_elf() when the ELF passed to
> btf__parse_split() has a .BTF.base section and no base_btf was given.
> 
> This looks reachable with "resolve_btfids --inline mod.ko" (no --btf_base,
> no --btf) on an out-of-tree module that already has .BTF, .BTF.base and
> .BTF_ids:
> 
> load_btf()
>   btf__parse_split(obj->path, NULL)
>     owns_base = true, obj->base_btf stays NULL
> finalize_btf()
>   distill is skipped because obj->base_btf is NULL
>   sort_btf_by_name()
>   extract_inline_btf()
>     btf__free(btf)
>       also frees the distilled base that main_btf->base_btf points to
> main()
>   symbols_resolve()
>     btf__type_by_id(obj->btf, ...)
>       btf_type_by_id(btf->base_btf, type_id)
>         reads freed memory
> 
> Without --inline the same command works, because obj->btf keeps owning its
> base until main() frees it.
> 
> The in-tree build does not appear to be affected, since gen-btf.sh passes a
> raw --btf file together with --btf_base, which leaves owns_base false.  I did
> not find a fix for this later in the series.
> 
> Should extract_inline_btf() keep the original BTF alive, for example in a
> field of struct object that main() frees after obj->btf?  Alternatively,
> could it reject or handle the case where btf__base_btf(obj->btf) is set but
> obj->base_btf is NULL?
> 
>> +	}
>> +out:
>> +	free(colors);
>> +	return err;
>> +}
> 
> [ ... ]
> 
>

legit issue; we need to keep around obj.orig_btf for after coloring to be
safe. Also worth centralizing all the freeing of btfs in the main out:
path to remove a bunch of goto labels and adding a comment to note the
importance of btf__free() order. Will fix..


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

* Re: [PATCH v3 bpf-next 3/7] kbuild, btf: Expose vmlinux inline BTF through sysfs
  2026-10-03 19:56   ` sashiko-bot
@ 2026-10-06 18:12     ` Alan Maguire
  0 siblings, 0 replies; 20+ messages in thread
From: Alan Maguire @ 2026-10-06 18:12 UTC (permalink / raw)
  To: sashiko-reviews; +Cc: linux-modules, Petr Pavlu, bpf

On 03/10/2026 20:56, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [Medium] bpf: btf: Unvalidated zero-size allocation triggers WARN_ON_ONCE in vmalloc_user
> - [Low] bpf: btf: Linkage fails for sha256 when CONFIG_DEBUG_INFO_BTF_INLINE=m
> --
> 
> commit de2bb52fc001c93d461a0839a26e550f67f5ecc2
> Author: Alan Maguire <alan.maguire@oracle.com>
> 
> kbuild, btf: Expose vmlinux inline BTF through sysfs
> 
> This commit introduces support for embedded and module-delivered vmlinux
> inline BTF payloads, making them available via sysfs. It also sets up deferred
> binding for a module-provided .BTF.inline section.
> 
>> diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
>> index 134b15a44625e..24abfff1ddc3c 100644
>> --- a/lib/Kconfig.debug
>> +++ b/lib/Kconfig.debug
>> @@ -432,6 +438,19 @@ config DEBUG_INFO_BTF_MODULES
>>  	help
>>  	  Generate compact split BTF type information for kernel modules.
>>  
>> +config DEBUG_INFO_BTF_INLINE
>> +	tristate "Provide information about inline sites in BTF"
>> +	default n
>> +	depends on DEBUG_INFO_BTF && PAHOLE_HAS_INLINE && SYSFS
>> +	select DEBUG_INFO_BTF_MODULES if MODULES
>> +	select CRYPTO_LIB_SHA256
> 
> [Severity: Low]
> Does this cause a linkage failure when CONFIG_DEBUG_INFO_BTF_INLINE is built
> as a module?
> 
> Because this is a tristate, selecting CRYPTO_LIB_SHA256 here means it will 
> evaluate to 'm' if the inline feature is configured as a module. 
> 
> However, the crypto library is called directly from the built-in core kernel
> object kernel/bpf/btf.c:
> 
> kernel/bpf/btf.c:btf_verify_link() {
>     ...
>     sha256(data, data_size, actual_digest);
>     ...
> }
> 
> A built-in object cannot resolve a symbol exported by a loadable module,
> which will result in an 'undefined reference to sha256' error at build time.
>

Given that CONFIG_BPF (not tristate) also selects CRYPTO_LIB_SHA256
(making it y) we can simply drop the above select.

 
>> +	help
>> +	  Generate information about inline sites in .BTF.inline sections.
> 
> [ ... ]
> 
>> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
>> index 0630675377aaa..732ba06846675 100644
>> --- a/kernel/bpf/btf.c
>> +++ b/kernel/bpf/btf.c
> 
> [ ... ]
> 
>> @@ -9015,21 +9016,122 @@ static DEFINE_MUTEX(btf_module_mutex);
>> +static int btf_module_handle_vmlinux_inline(const struct module *mod)
>> +{
>> +	void *data = NULL;
>> +
>> +	if (!vmlinux_inline_link || strcmp(mod->name, vmlinux_inline_link->module_name))
>> +		return -ENOENT;
>> +
>> +	data = vmalloc_user(mod->btf_inline_data_size);
> 
> [Severity: Medium]
> Could this trigger a WARN_ON_ONCE splat in the memory allocation paths?
> 
> If a user with CAP_SYS_MODULE loads a module matching
> vmlinux_inline_link->module_name, but the module has no .BTF.inline section
> (or its size is exactly 0), mod->btf_inline_data_size will be set to 0.
> 
> Calling vmalloc_user(0) here happens before the size is validated against 
> link->btf_size, which causes a kernel warning in __vmalloc_node_range_noprof.

yeah, we need size checks here prior to vmalloc_user().



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

* Re: [PATCH v3 bpf-next 3/7] kbuild, btf: Expose vmlinux inline BTF through sysfs
  2026-10-03 20:41   ` bot+bpf-ci
@ 2026-10-06 18:42     ` Alan Maguire
  0 siblings, 0 replies; 20+ messages in thread
From: Alan Maguire @ 2026-10-06 18:42 UTC (permalink / raw)
  To: bot+bpf-ci, ast, andrii, daniel, eddyz87
  Cc: jolsa, ihor.solodrai, yonghong.song, song, qmo, martin.lau,
	memxor, emil, mcgrof, petr.pavlu, tj, kees, nathan, arnd,
	puranjay, yatsenko, atenart, ojeda, linux-modules, arnaldo.melo,
	bpf, martin.lau, mason



On 03/10/2026 21:41, bot+bpf-ci@kernel.org wrote:
>> kbuild, btf: Expose vmlinux inline BTF through sysfs
> 
> TL;DR:
> - Module load may fail with -ENOENT for modules without .BTF or with BTF
>   mismatch allowed
> - mmap of /sys/kernel/btf/vmlinux may return -EINVAL on arm64
> - __SIZEOF_LONG__ may break 32-bit builds
> - Nits: .gitignore, NULL return
> 
>> diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
>> index b2988aa12f66..863f02660bc3 100644
>> --- a/include/asm-generic/vmlinux.lds.h
>> +++ b/include/asm-generic/vmlinux.lds.h
> 
> [ ... ]
> 
>> @@ -675,12 +675,39 @@
>> +#ifdef CONFIG_DEBUG_INFO_BTF_INLINE_MODULE
>> +/* __MODULE_NAME_LEN + SHA-256 digest + u32 BTF size */
>> +#define BTF_LINK_SIZE	(64 - __SIZEOF_LONG__ + 32 + 4)
>> +#define BTF_INLINE_LINK						\
>> +	. = ALIGN(PAGE_SIZE);					\
>> +	.BTF.inline.link : AT(ADDR(.BTF.inline.link) - LOAD_OFFSET) { \
>> +		__start_BTF_inline_link = .;				\
>> +		BYTE(0)						\
>> +		. += BTF_LINK_SIZE - 1;					\
> 
> Is __SIZEOF_LONG__ reliable in a linker script?
> 
> The lds is preprocessed with KBUILD_CPPFLAGS only.  On x86, -m32 is added
> only to KBUILD_CFLAGS and KBUILD_AFLAGS, and with clang KBUILD_CPPFLAGS
> carries --target=x86_64-linux-gnu for all of SRCARCH=x86.  So for a 32-bit
> x86 build with clang, or with a gcc whose default target is x86_64,
> __SIZEOF_LONG__ is 8 while the lds is preprocessed, and BTF_LINK_SIZE
> becomes 64 - 8 + 32 + 4 = 92.
> 
> On a 32-bit kernel __MODULE_NAME_LEN is 64 - sizeof(unsigned long) = 60, so
> sizeof(struct btf_link) is 96.
> 
> With CONFIG_DEBUG_INFO_BTF_INLINE=m, link-vmlinux.sh runs:
> 
>   resolve_btfids --btf_link .BTF.inline:btf_vmlinux_inline:vmlinux.BTF.inline
> 
> patch_btf_link() uses module_name_len = BTF_LINK_MODULE_NAME_MAX - 4 for
> ELFCLASS32 and rejects the section if
> 
>   data->d_size != module_name_len + BTF_SHA256_DIGEST_LENGTH + sizeof(u32)
> 
> Since 92 != 96, it prints "FAILED: section .BTF.inline.link is not a
> writable BTF link", returns -1, and the vmlinux link fails.  Even if the
> build got past that, btf_parse_link() checks size != sizeof(*link) and would
> reject the record.
> 
> riscv32 looks the same: -mabi=ilp32 is only in KBUILD_CFLAGS and
> KBUILD_AFLAGS, and the clang target is riscv64-linux-gnu.
> 
> Could this use something tied to Kconfig instead, such as #ifdef
> CONFIG_64BIT, or BITS_PER_LONG / 8 from asm/bitsperlong.h, rather than the
> compiler's default ABI macro?
> 
> [ ... ]
> 

yeah CONFIG_64BIT probably safest approach, will fix.

>> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
>> index 0630675377aa..732ba0684667 100644
>> --- a/kernel/bpf/btf.c
>> +++ b/kernel/bpf/btf.c
> 
> [ ... ]
> 
>> @@ -9015,21 +9016,122 @@ static DEFINE_MUTEX(btf_module_mutex);
>> +static const struct btf_link *btf_parse_link(void *start, unsigned int size)
>> +{
>> +	const struct btf_link *link = start;
>> +	unsigned int module_name_len;
>> +
>> +	if (!link || size != sizeof(*link))
>> +		return NULL;
>> +
>> +	if (link->btf_size == 0)
>> +		return 0;
> 
> this isn't a bug, but btf_parse_link() returns a pointer and this path
> returns a plain integer 0 where the other failure paths return NULL.  sparse
> reports "Using plain integer as NULL pointer" here.
> 
> The only caller, btf_vmlinux_inline_lazy_init(), handles this the same way
> as the other failures and prints "invalid vmlinux inline BTF link".  Could
> this return NULL, or be folded into the check above?
> 
> [ ... ]
> 
>>  static int btf_module_notify(struct notifier_block *nb, unsigned long op,
>>  			     void *module)
>>  {
>> [ ... ]
>>  	int err = 0;
>>
>> -	if (mod->btf_data_size == 0 ||
>> -	    (op != MODULE_STATE_COMING && op != MODULE_STATE_LIVE &&
>> -	     op != MODULE_STATE_GOING))
>> +	if (op != MODULE_STATE_COMING && op != MODULE_STATE_LIVE &&
>> +	    op != MODULE_STATE_GOING)
>>  		goto out;
>>
>>  	switch (op) {
>>  	case MODULE_STATE_COMING:
>> +		err = btf_module_handle_vmlinux_inline(mod);
>> +		if (err != -ENOENT) {
>> +			err = 0;
>> +			break;
>> +		}
>> +		if (!mod->btf_data_size)
>> +			break;
>>  		btf_mod = kzalloc_obj(*btf_mod);
> 
> Can this reject every module that has no .BTF section?
> 
> btf_module_handle_vmlinux_inline() returns -ENOENT for any module that is
> not the vmlinux inline carrier, and the
> !IS_MODULE(CONFIG_DEBUG_INFO_BTF_INLINE) stub returns -ENOENT
> unconditionally.  That value stays in err.
> 
> The old code returned early with err == 0 when mod->btf_data_size was 0.
> Now a module without .BTF hits:
> 
> 		if (!mod->btf_data_size)
> 			break;
> 
> and btf_module_notify() returns notifier_from_errno(-ENOENT) from the out:
> label.
> 
> The same leftover err is returned when btf_parse_module() fails with
> CONFIG_MODULE_ALLOW_BTF_MISMATCH=y:
> 
> 		if (IS_ERR(btf)) {
> 			kfree(btf_mod);
> 			if (!IS_ENABLED(CONFIG_MODULE_ALLOW_BTF_MISMATCH)) {
> 				...
> 			} else {
> 				pr_warn_once("Kernel module BTF mismatch detected, ...");
> 			}
> 			goto out;
> 		}
> 
> That branch never resets err, so a mismatched module, which that option
> exists to let load, now fails with -ENOENT as well.
> 
> notifier_from_errno() sets NOTIFY_STOP_MASK, notifier_call_chain_robust()
> rolls back the COMING chain, prepare_coming_module() returns
> notifier_to_errno() == -ENOENT, and load_module() goes to bug_cleanup.  The
> module load then fails with "No such file or directory".
> 
> Modules without .BTF are a normal case.  scripts/Makefile.modfinal prints
> "Skipping BTF generation for %s due to unavailability of vmlinux" for
> external/DKMS modules, and the kernel.spec comment changed in this same
> commit says eu-strip removes .BTF when find-debuginfo has no --keep-section.
> Before this patch both cases loaded fine.
> 
> The problem is still there at the end of the series (e1c2367cf1b5);
> 3357b172196f ("btf: Relocate and expose module inline BTF") keeps the same
> COMING prologue.
> 
> Would resetting err to 0 when the helper returns -ENOENT work?
> 
> 	err = btf_module_handle_vmlinux_inline(mod);
> 	if (err != -ENOENT) {
> 		err = 0;
> 		break;
> 	}
> 	err = 0;
> 
> [ ... ]
> 

Yeah we don't need to keep err here so we can just check
the return value; if not -ENOENT, break.


>> diff --git a/kernel/bpf/sysfs_btf.c b/kernel/bpf/sysfs_btf.c
>> index 9cbe15ce3540..a3c54b5fe294 100644
>> --- a/kernel/bpf/sysfs_btf.c
>> +++ b/kernel/bpf/sysfs_btf.c
> 
> [ ... ]
> 
>> @@ -33,24 +31,149 @@ static int btf_sysfs_vmlinux_mmap(struct file *filp, struct kobject *kobj,
>> +static int btf_sysfs_mmap_direct(struct file *filp, struct kobject *kobj,
>> +				 const struct bin_attribute *attr,
>> +				 struct vm_area_struct *vma)
>> +{
>> +	void *data = READ_ONCE(attr->private);
>> [ ... ]
>> +	err = btf_sysfs_mmap_check(data, attr->size, vma);
>> +	if (err)
>> +		return err;
>> +	if (is_vmalloc_addr(data))
>> +		return remap_vmalloc_range(vma, data, 0);
>> +
>> +	addr = __pa_symbol(data);
>> [ ... ]
>>  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_vmlinux_mmap,
>> +	.mmap = btf_sysfs_mmap_direct,
>>  };
> 
> Does this break mmap() of /sys/kernel/btf/vmlinux on arm64?
> 
> bin_attr_btf_vmlinux now uses btf_sysfs_mmap_direct(), which chooses between
> remap_vmalloc_range() and remap_pfn_range() based on is_vmalloc_addr(data).
> On arm64 the kernel image lives inside the vmalloc range:
> 
>   arch/arm64/include/asm/memory.h:  #define KIMAGE_VADDR (MODULES_END)
>   arch/arm64/include/asm/pgtable.h: #define VMALLOC_START (MODULES_END)
> 
> So is_vmalloc_addr(__start_BTF) returns true, and the call goes to
> remap_vmalloc_range() -> remap_vmalloc_range_partial().  There,
> find_vm_area() finds the vm_struct that declare_kernel_vmas() registered for
> the image rodata segment:
> 
>   declare_vma(&vmlinux_seg[1], __start_rodata, __inittext_begin, VM_NO_GUARD);
>   ...
>   vma->flags = VM_MAP | vm_flags;
> 
> That vm_struct has no VM_USERMAP, so this check:
> 
>   if (!(area->flags & (VM_USERMAP | VM_DMA_COHERENT)))
>           return -EINVAL;
> 
> fails and mmap() returns -EINVAL.  Before this patch,
> btf_sysfs_vmlinux_mmap() always used __pa_symbol(__start_BTF) plus
> remap_pfn_range(), which worked on arm64 since .BTF sits inside RO_DATA,
> between __start_rodata and __inittext_begin.
> 
> The same applies to the built-in /sys/kernel/btf/vmlinux.inline, which the
> commit message says supports read-only mmap().  The selftest
> test_btf_mmap_sysfs("/sys/kernel/btf/vmlinux") would fail on arm64 as well.
> 
> Would it be better to use remap_vmalloc_range() only for the lazily
> populated vmalloc_user() entries, for example from btf_sysfs_mmap_lazy() or
> by recording the backing type in struct btf_sysfs_entry, and keep the
> __pa_symbol()/remap_pfn_range() path for kernel image data instead of
> testing the address range?
>

yep, this is a good catch, will do this/retest on aarch64.
 
> [ ... ]
> 
>> diff --git a/scripts/gen-btf.sh b/scripts/gen-btf.sh
>> index 8ca96eb10a69..e638291753d0 100755
>> --- a/scripts/gen-btf.sh
>> +++ b/scripts/gen-btf.sh
> 
> [ ... ]
> 
>> @@ -83,14 +88,28 @@ gen_btf_o()
>> +	btf_inline=${ELF_FILE}.BTF.inline
>> +	if [ "${BTF_INLINE}" = "m" ]; then
>> +		if [ -f "${btf_inline}" ]; then
>> +			cp "${btf_inline}" "${objtree}/vmlinux.BTF.inline"
>> +		else
>> +			rm -f "${objtree}/vmlinux.BTF.inline"
>> +		fi
>> +	fi
> 
> With CONFIG_DEBUG_INFO_BTF_INLINE=m, this leaves a long-lived top-level
> build artifact, vmlinux.BTF.inline.  link-vmlinux.sh reads it for
> --btf_link, and Makefile.modfinal reads it later to embed .BTF.inline into
> btf_vmlinux_inline.ko.
> 
> The patch adds it to CLEAN_FILES in the top-level Makefile but not to
> .gitignore, where the other top-level vmlinux artifacts are listed
> (/vmlinux, /vmlinux.32, /vmlinux.map, /vmlinux.symvers,
> /vmlinux.thinlto-index, /vmlinux.unstripped).  In an in-tree build,
> "git check-ignore -v vmlinux.BTF.inline" returns nothing, so the file shows
> up as untracked in git status.
> 
> Should /vmlinux.BTF.inline be added to .gitignore next to
> /vmlinux.unstripped?  The later commits in the series (f61c6417ac15 through
> e1c2367cf1b5) do not touch .gitignore.
>

yep, will do.


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

* Re: [PATCH v3 bpf-next 5/7] btf: Relocate and expose module inline BTF
  2026-10-03 19:59   ` sashiko-bot
@ 2026-10-06 18:56     ` Alan Maguire
  0 siblings, 0 replies; 20+ messages in thread
From: Alan Maguire @ 2026-10-06 18:56 UTC (permalink / raw)
  To: sashiko-reviews; +Cc: bpf, Petr Pavlu, linux-modules

On 03/10/2026 20:59, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [High] bpf: kernel panic on NULL pointer dereference in btf_parse_module() error path
> --
> 
> commit 7871f353137bb8eca880dde9f21fa3041a6f681f
> Author: Alan Maguire <alan.maguire@oracle.com>
> 
> btf: Relocate and expose module inline BTF
> 
> This commit introduces parsing of module inline BTF data, rebases inline type
> IDs on top of the relocated module BTF, and exposes the result via sysfs.
> Malformed inline BTF is warned about and skipped without preventing normal
> module BTF registration.
> 
>> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
>> index c535e636d41c1..28b7c9786fdce 100644
>> --- a/kernel/bpf/btf.c
>> +++ b/kernel/bpf/btf.c
> [ ... ]
>> @@ -6954,67 +7076,66 @@ static struct btf *btf_parse_module(const char *module_name, const void *data,
>>  		base_btf = vmlinux_btf;
>>  	}
>>  
>> -	btf = kzalloc_obj(*btf, GFP_KERNEL | __GFP_NOWARN);
>> -	if (!btf) {
>> -		err = -ENOMEM;
>> +	btf = btf_parse_split(env, module_name, data, data_size, base_btf);
>> +	if (IS_ERR(btf)) {
>> +		err = PTR_ERR(btf);
>> +		btf = NULL;
>>  		goto errout;
>>  	}
> [ ... ]
>>  errout:
>> +	kvfree(module_str_map);
>>  	btf_verifier_env_free(env);
>> +	btf_free(inline_btf);
>>  	if (!IS_ERR(base_btf) && base_btf != vmlinux_btf)
>>  		btf_free(base_btf);
>> -	if (btf) {
>> -		kvfree(btf->data);
>> -		kvfree(btf->types);
>> -		kfree(btf);
>> -	}
>> +	btf_free(btf);
> 
> [Severity: High]
> Will this cause a kernel panic if btf_parse_split() fails?
> 
> In the error path above, if btf_parse_split() returns an error, btf is
> explicitly set to NULL and execution jumps to errout. At this point,
> inline_btf is also initialized to NULL.
> 
> The errout path then calls btf_free() unconditionally for both inline_btf
> and btf. Looking at btf_free():
> 
> static void btf_free(struct btf *btf)
> {
>         btf_free_struct_meta_tab(btf);
>         ...
> }
> 
> And btf_free_struct_meta_tab() unconditionally dereferences the pointer:
> 
> static void btf_free_struct_meta_tab(struct btf *btf)
> {
>         struct btf_struct_metas *tab = btf->struct_meta_tab;
>         ...
> }
> 
> Does this result in an immediate NULL pointer dereference when
> btf_free() is called on the NULL btf or inline_btf pointers?
> 
>>  	return ERR_PTR(err);
>>  }
> 

we need non-null guards on the btf_free()s alright, good catch.

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

end of thread, other threads:[~2026-10-06 18:56 UTC | newest]

Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-03 19:43 [PATCH v3 bpf-next 0/7] kbuild, bpf: Support inline info in BTF Alan Maguire
2026-10-03 19:43 ` [PATCH v3 bpf-next 1/7] resolve_btfids: Build separate main and inline BTF objects Alan Maguire
2026-10-03 20:27   ` bot+bpf-ci
2026-10-06 17:30     ` Alan Maguire
2026-10-03 19:43 ` [PATCH v3 bpf-next 2/7] resolve_btfids: Populate BTF link section Alan Maguire
2026-10-03 20:27   ` bot+bpf-ci
2026-10-03 19:43 ` [PATCH v3 bpf-next 3/7] kbuild, btf: Expose vmlinux inline BTF through sysfs Alan Maguire
2026-10-03 19:56   ` sashiko-bot
2026-10-06 18:12     ` Alan Maguire
2026-10-03 20:41   ` bot+bpf-ci
2026-10-06 18:42     ` Alan Maguire
2026-10-03 19:43 ` [PATCH v3 bpf-next 4/7] btf: Preserve string relocation maps for split BTF Alan Maguire
2026-10-03 19:43 ` [PATCH v3 bpf-next 5/7] btf: Relocate and expose module inline BTF Alan Maguire
2026-10-03 19:59   ` sashiko-bot
2026-10-06 18:56     ` Alan Maguire
2026-10-03 20:27   ` bot+bpf-ci
2026-10-03 19:43 ` [PATCH v3 bpf-next 6/7] selftests/bpf: Test BTF sysfs inline representations Alan Maguire
2026-10-03 19:54   ` sashiko-bot
2026-10-03 19:43 ` [PATCH v3 bpf-next 7/7] selftests/bpf: Add a test verifying inline information Alan Maguire
2026-10-03 19:54   ` sashiko-bot

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