mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory
@ 2026-09-23  5:39 Jay Wang
  2026-09-23  5:39 ` [PATCH bpf-next 1/6] bpf: pass the vmlinux BTF to btf_parse_module() and let it adopt the data Jay Wang
                   ` (6 more replies)
  0 siblings, 7 replies; 11+ messages in thread
From: Jay Wang @ 2026-09-23  5:39 UTC (permalink / raw)
  To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Kumar Kartikeya Dwivedi
  Cc: Alan Maguire, Martin KaFai Lau, Yonghong Song, Nathan Chancellor,
	Nicolas Schier, linux-kbuild, Luis Chamberlain, Petr Pavlu,
	linux-modules, Arnd Bergmann, linux-kernel,
	Hazem Mohamed Abuelfotoh, Bjoern Doebel, jay.wang.upstream

Based on and tested against bpf-next commit 91f8613d95ad ("bpf: Drop
duplicate check_app_limited in tcp_bpf_push").

This series makes CONFIG_DEBUG_INFO_BTF a tristate, so that it can be
set to =m.  With =m the vmlinux BTF is carried by a module, btf_vmlinux,
that the kernel loads the first time anything needs the BTF.  On a
system where nothing does, that saves ~5.4 MB of RAM with a distribution
config; on a system that uses BTF, nothing differs from =y.  =y itself
is untouched.

Problem
-------

The vmlinux BTF that CONFIG_DEBUG_INFO_BTF=y builds into the kernel
image takes ~5.4 MB of memory, resident from boot whether anything uses
it or not.  On small instances that is not negligible.

A distribution cannot simply turn it off for the users who do not need
it: it ships one kernel build for all its users, and BTF is not debug
info anymore.  CO-RE, fentry/fexit, kfuncs, struct_ops, sched_ext and
bpf-lsm all depend on it, so =n takes those away from everyone who does
use them. 

Hence this series adds CONFIG_DEBUG_INFO_BTF=m: the BTF becomes an
on-demand module.
Users who never use BTF get the memory back; for users who do, the first
use loads it automatically and everything works as with =y.

Approach
--------

Do not remove anything, defer it.  The BTF is generated exactly as
before, but with =m it is not part of the kernel image: it is packed
into a module, btf_vmlinux.ko, which the kernel loads itself the first
time anything needs the BTF.  Once loaded, the BTF stays, and nothing
differs from =y.

Making that work ran into five problems.  The first three are existing
components that rely on the vmlinux BTF being present from boot, which
a loadable module cannot provide; the last two follow from the BTF no
longer being part of the image.

1. Verifier.  Problem: bpf_check() fetched the vmlinux BTF for every
   program, so the first socket filter at boot would have loaded the
   module on every system.  Solution: fetch the BTF only where kernel
   types enter a program (attach_btf, kfunc calls, ksyms, map pointer
   access, helpers that take or return kernel pointers).  A program
   using none of these never touches it.

2. Initcall registrations.  Problem: kfunc, dtor kfunc and struct_ops
   registrations run from initcalls and need the parsed BTF, which
   would again pull it in at boot.  Solution: queue them and apply the
   queue when the BTF is parsed, before it is published, so no program
   can ever see a vmlinux BTF that lacks its kfuncs or struct_ops.

3. Module BTF.  Problem: module BTF is split BTF against the vmlinux
   BTF and was parsed at module load.  A module loaded before the
   vmlinux BTF cannot be parsed yet, and the module notifier cannot
   load btf_vmlinux (that would nest a module load inside a module
   load).  Solution: keep the module's BTF aside, expose it in
   /sys/kernel/btf right away, and parse and register it, together
   with the module's own kfuncs and struct_ops, once the vmlinux BTF
   arrives.  Module BTF thus works regardless of load order.

4. Trust.  Problem: the verifier treats the BTF as the description of
   the kernel's types, so a carrier from a different build must be
   refused even if vermagic lets it load.  Solution: link the size and
   SHA-256 of the BTF into the image and check them when the module
   loads.  The linked-in size also lets /sys/kernel/btf/vmlinux report
   its final size before the load, which tooling expects.

5. Tooling.  Problem: pahole --btf_base for module BTF, bpftool and
   external module builds read .BTF from the vmlinux ELF.  Solution:
   keep .BTF in the ELF as a non-loadable section, so it is in the file
   but not in the image.

CONFIG_BPF_PRELOAD is made unavailable with =m: its preloaded programs
attach through the vmlinux BTF, so every bpffs mount, which systemd does
at boot, would load it and defeat the point.

Relation to the inline BTF series
---------------------------------

Alan's inline BTF series [2] adds inline function information to BTF,
which is even larger than the BTF itself, and its cover letter leaves
delivering that information on demand, through a module and sysfs, to a
follow-up.  When Alexei suggested taking the same route for the vmlinux
BTF, Alan pointed out where the difficulty would lie [3].

However, that approach cannot be used directly for the vmlinux BTF,
because of what depends on the data:

1. Boot-time consumers.  Nothing needs inline information at boot, so
   loading it late only concerns the sysfs file.  The core vmlinux BTF
   is needed at boot by the verifier, by kfunc and struct_ops
   registrations, and by every module's BTF.  Therefore we need to make
   each of those wait for the BTF instead of expecting it at boot: the
   verifier fetches it only when a program brings kernel types in, and
   the registrations are queued and replayed once it is parsed.  That
   is most of this series.

2. Module BTF.  Module BTF is split against the vmlinux BTF, so a module
   loaded before it has nothing to be parsed against, and the module
   notifier cannot load btf_vmlinux itself.  Therefore we need to keep
   the module's BTF aside, expose it in sysfs as raw bytes right away,
   and parse and register it once the vmlinux BTF arrives.  Alan named
   this as the hard part; it works here regardless of whether the module
   loads before or after the vmlinux BTF.

The btf_vmlinux module added here could carry the inline information as
well when that follow-up comes, so both would share one mechanism.

Patches Structure
-----------------

Patch 1 (refactor, no functional change): btf_parse_module() takes the
vmlinux BTF as an argument and can adopt an existing copy of the module
BTF data instead of duplicating it.  Needed so that module BTF kept
aside at load time can be parsed later without moving the buffer the
sysfs file points at.

Patch 2 (refactor, no functional change): splits the kfunc, dtor kfunc
and struct_ops registration functions into "find the BTF for the owner"
and "add the registration to this BTF", so the second half can be
replayed on a queued registration.

Patch 3 (verifier): stops fetching the vmlinux BTF up front in
bpf_check() and fetches it where kernel types enter a program instead.
Adds bpf_peek_btf_vmlinux() for the helpers that run in program
context and cannot load anything.  With =y the BTF is parsed at boot
anyway, so this is invisible there.

Patch 4 (carrier): the runtime side of taking the vmlinux BTF from the
btf_vmlinux module: request it on first use, copy it out of the module
in the BTF module notifier after checking size and SHA-256 against
.BTF.meta, and serve /sys/kernel/btf/vmlinux from the copy with its
size known from boot.  All under IS_MODULE(CONFIG_DEBUG_INFO_BTF), so
unreachable until patch 6.

Patch 5 (deferrals): queues kfunc, dtor kfunc and struct_ops
registrations made before their BTF is available and applies them when
it is; keeps the BTF of modules loaded before the vmlinux BTF and parses
and registers it when the vmlinux BTF arrives.  Also unreachable until
patch 6.

Patch 6 (kbuild and Kconfig): makes CONFIG_DEBUG_INFO_BTF a tristate;
with =m links .BTF into vmlinux as a non-loadable section, emits
.BTF.meta, builds btf_vmlinux.ko with the vmlinux .BTF as its payload,
makes the Makefile and #ifdef sites that must hold for both =y and =m
do so, excludes CONFIG_BPF_PRELOAD, and documents the option.

Patches 1-3 are independently useful cleanups; 4 and 5 are dead code
until 6 flips the switch, which keeps each bisect step building and
behaving as before.

Testing
-------

Tested with 1 GiB of memory, same tree, =y against =m, both with
CONFIG_DEBUG_INFO_BTF_MODULES=y.  The on-demand behaviour is easy to
see by hand on an =m kernel:

  # lsmod | grep btf_vmlinux
      -> nothing: the BTF is not loaded at boot.
  # ls -la /sys/kernel/btf/vmlinux
      -> the file exists with its final size (from .BTF.meta), although
         the BTF behind it is not loaded yet.
  # modprobe ext4 nf_conntrack
  # ls /sys/kernel/btf/
      -> ext4, nf_conntrack, ... appear immediately, although their
         BTF is only kept aside, not parsed: there is no vmlinux BTF to
         parse it against yet.
  # lsmod | grep btf_vmlinux
      -> still nothing: loading modules does not load the vmlinux BTF.
  # cat /sys/kernel/btf/ext4 > /dev/null
  # lsmod | grep btf_vmlinux
      -> still nothing: a module's BTF file is served from the raw copy,
         reading it does not need the vmlinux BTF.
  # grep VmallocUsed /proc/meminfo
      -> baseline.
  # cat /sys/kernel/btf/vmlinux > /dev/null
  # lsmod | grep btf_vmlinux
      -> btf_vmlinux ... [permanent]: the first use loaded it, and it
         cannot be unloaded.
  # grep VmallocUsed /proc/meminfo
      -> up by ~5.5 MB: the BTF copy, allocated only now.
  # bpftool btf list
      -> vmlinux and every loaded module now have BTF ids; the modules
         loaded before were parsed and registered on the way.

Any BPF program that uses kernel types (kprobe with
bpf_get_current_task_btf(), a kfunc call, fentry, ...) triggers the
same load; a plain socket filter does not.

Results:

 - MemTotal is ~5.4 MB higher with =m while the BTF is unused, which is
   the size of the .BTF section.  Once the BTF is in use, MemFree is the
   same within run-to-run noise.
 - Modules loaded before the trigger (ext4, nf_conntrack, which
   registers kfuncs from its init, xfrm_interface) get BTF ids once the
   BTF is loaded; a struct_ops map for tcp_congestion_ops and a syscall
   program calling bpf_task_from_pid() work; nf_nat loaded afterwards
   takes the usual path.
 - stat() of /sys/kernel/btf/vmlinux reports the final size before the
   load; fstat/read/mmap agree afterwards.
 - A carrier with one byte of .BTF changed is refused with -EINVAL.
 - lockdep and kmemleak kernels are clean in all trigger orders.
 - =y and =n build and behave as before; =m without module BTF works.

[1] https://lore.kernel.org/all/20260917000201.25581-2-wanjay@amazon.com/
[2] https://lore.kernel.org/bpf/20260916074118.1007116-1-alan.maguire@oracle.com/
[3] https://lore.kernel.org/all/33592fca-88a8-44aa-8d94-40e1e604554e@oracle.com/

Jay Wang (6):
  bpf: pass the vmlinux BTF to btf_parse_module() and let it adopt the
    data
  bpf: split the kfunc, dtor kfunc and struct_ops registration bodies
  bpf: fetch the vmlinux BTF where kernel types enter a program
  bpf: take the vmlinux BTF from the btf_vmlinux module
  bpf: defer registrations until the vmlinux BTF is available
  kbuild, bpf: allow building the vmlinux BTF as a module

 Documentation/bpf/btf.rst         |  35 ++
 Makefile                          |   8 +-
 include/asm-generic/vmlinux.lds.h |  30 +-
 include/linux/bpf.h               |   1 +
 include/linux/btf.h               |   7 +
 include/linux/btf_ids.h           |   2 +-
 include/linux/compiler_types.h    |   2 +-
 include/linux/module.h            |   2 +-
 include/trace/trace_events.h      |   2 +-
 kernel/bpf/Makefile               |   6 +-
 kernel/bpf/bpf_struct_ops.c       |   3 +-
 kernel/bpf/btf.c                  | 782 ++++++++++++++++++++++++++----
 kernel/bpf/btf_vmlinux.c          |  23 +
 kernel/bpf/preload/Kconfig        |   4 +
 kernel/bpf/syscall.c              |   6 +
 kernel/bpf/sysfs_btf.c            |  80 ++-
 kernel/bpf/verifier.c             | 104 +++-
 kernel/module/main.c              |   4 +-
 kernel/trace/bpf_trace.c          |   3 +-
 kernel/trace/trace_syscalls.c     |   6 +-
 lib/Kconfig.debug                 |  13 +-
 net/netfilter/Makefile            |   6 +-
 net/xfrm/Makefile                 |   4 +-
 scripts/Makefile.modfinal         |  14 +-
 scripts/gen-btf.sh                |  93 +++-
 scripts/link-vmlinux.sh           |  25 +-
 26 files changed, 1121 insertions(+), 144 deletions(-)
 create mode 100644 kernel/bpf/btf_vmlinux.c


base-commit: 91f8613d95ad8cd99d8baf094806d1ef98bc6380
-- 
2.47.3


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

* [PATCH bpf-next 1/6] bpf: pass the vmlinux BTF to btf_parse_module() and let it adopt the data
  2026-09-23  5:39 [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Jay Wang
@ 2026-09-23  5:39 ` Jay Wang
  2026-09-23  5:39 ` [PATCH bpf-next 2/6] bpf: split the kfunc, dtor kfunc and struct_ops registration bodies Jay Wang
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Jay Wang @ 2026-09-23  5:39 UTC (permalink / raw)
  To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Kumar Kartikeya Dwivedi
  Cc: Alan Maguire, Martin KaFai Lau, Yonghong Song, Nathan Chancellor,
	Nicolas Schier, linux-kbuild, Luis Chamberlain, Petr Pavlu,
	linux-modules, Arnd Bergmann, linux-kernel,
	Hazem Mohamed Abuelfotoh, Bjoern Doebel, jay.wang.upstream

Make btf_parse_module() take the vmlinux BTF as an argument instead of
fetching it with bpf_get_btf_vmlinux(), and add a data_owned flag: when
set, the passed .BTF data is an already kvmalloc()ed copy that the new
btf takes ownership of on success (on failure the caller keeps it).

Factor the sysfs file creation out of the module notifier into
btf_module_sysfs_add() and the teardown into btf_module_free(), and set
btf_mod->module right after the allocation rather than under the mutex.

No functional change.  This prepares for CONFIG_DEBUG_INFO_BTF=m, where a
module can be loaded before the vmlinux BTF is available: its .BTF is
then copied and exposed in sysfs first and parsed later, at which point
the parser must take the copy as is so that the sysfs file keeps
pointing at valid data.

Signed-off-by: Jay Wang <wanjay@amazon.com>
---
 kernel/bpf/btf.c | 105 +++++++++++++++++++++++++++++------------------
 1 file changed, 64 insertions(+), 41 deletions(-)

diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 4a1fa4fbdf4e..a6634237dc89 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -6516,16 +6516,20 @@ __u32 btf_relocate_id(const struct btf *btf, __u32 id)
 
 #ifdef CONFIG_DEBUG_INFO_BTF_MODULES
 
-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)
+/*
+ * Parse split module BTF against @vmlinux_btf.  @data is the module's .BTF
+ * section; if @data_owned, it is an already kvmalloc()ed copy that the new
+ * btf takes ownership of on success (on failure the caller keeps it).
+ */
+static struct btf *btf_parse_module(const char *module_name, struct btf *vmlinux_btf,
+				    void *data, unsigned int data_size, bool data_owned,
+				    void *base_data, unsigned int base_data_size)
 {
-	struct btf *btf = NULL, *vmlinux_btf, *base_btf = NULL;
+	struct btf *btf = NULL, *base_btf = NULL;
 	struct btf_verifier_env *env = NULL;
 	struct bpf_verifier_log *log;
 	int err = 0;
 
-	vmlinux_btf = bpf_get_btf_vmlinux();
 	if (IS_ERR(vmlinux_btf))
 		return vmlinux_btf;
 	if (!vmlinux_btf)
@@ -6562,7 +6566,10 @@ static struct btf *btf_parse_module(const char *module_name, const void *data,
 	btf->named_start_id = 0;
 	strscpy(btf->name, module_name);
 
-	btf->data = kvmemdup(data, data_size, GFP_KERNEL | __GFP_NOWARN);
+	if (data_owned)
+		btf->data = data;
+	else
+		btf->data = kvmemdup(data, data_size, GFP_KERNEL | __GFP_NOWARN);
 	if (!btf->data) {
 		err = -ENOMEM;
 		goto errout;
@@ -6605,7 +6612,8 @@ static struct btf *btf_parse_module(const char *module_name, const void *data,
 	if (!IS_ERR(base_btf) && base_btf != vmlinux_btf)
 		btf_free(base_btf);
 	if (btf) {
-		kvfree(btf->data);
+		if (!data_owned)
+			kvfree(btf->data);
 		kvfree(btf->types);
 		kfree(btf);
 	}
@@ -8610,6 +8618,48 @@ static DEFINE_MUTEX(btf_module_mutex);
 
 static void purge_cand_cache(struct btf *btf);
 
+static int btf_module_sysfs_add(struct btf_module *btf_mod, const char *name,
+				void *data, size_t data_size)
+{
+	struct bin_attribute *attr;
+	int err;
+
+	if (!IS_ENABLED(CONFIG_SYSFS))
+		return 0;
+
+	attr = kzalloc_obj(*attr);
+	if (!attr)
+		return -ENOMEM;
+
+	sysfs_bin_attr_init(attr);
+	attr->attr.name = name;
+	attr->attr.mode = 0444;
+	attr->size = data_size;
+	attr->private = 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",
+			name, err);
+		kfree(attr);
+		return err;
+	}
+
+	btf_mod->sysfs_attr = attr;
+	return 0;
+}
+
+static void btf_module_free(struct btf_module *btf_mod)
+{
+	if (btf_mod->sysfs_attr)
+		sysfs_remove_bin_file(btf_kobj, btf_mod->sysfs_attr);
+	purge_cand_cache(btf_mod->btf);
+	btf_put(btf_mod->btf);
+	kfree(btf_mod->sysfs_attr);
+	kfree(btf_mod);
+}
+
 static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			     void *module)
 {
@@ -8630,7 +8680,10 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			err = -ENOMEM;
 			goto out;
 		}
-		btf = btf_parse_module(mod->name, mod->btf_data, mod->btf_data_size,
+		btf_mod->module = module;
+
+		btf = btf_parse_module(mod->name, bpf_get_btf_vmlinux(),
+				       mod->btf_data, mod->btf_data_size, false,
 				       mod->btf_base_data, mod->btf_base_data_size);
 		if (IS_ERR(btf)) {
 			kfree(btf_mod);
@@ -8652,37 +8705,12 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 
 		purge_cand_cache(NULL);
 		mutex_lock(&btf_module_mutex);
-		btf_mod->module = module;
 		btf_mod->btf = btf;
 		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;
-		}
-
+		/* not fatal, the module BTF is usable without the sysfs file */
+		btf_module_sysfs_add(btf_mod, btf->name, btf->data, btf->data_size);
 		break;
 	case MODULE_STATE_LIVE:
 		mutex_lock(&btf_module_mutex);
@@ -8709,12 +8737,7 @@ 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);
-			purge_cand_cache(btf_mod->btf);
-			btf_put(btf_mod->btf);
-			kfree(btf_mod->sysfs_attr);
-			kfree(btf_mod);
+			btf_module_free(btf_mod);
 			break;
 		}
 		mutex_unlock(&btf_module_mutex);
-- 
2.47.3


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

* [PATCH bpf-next 2/6] bpf: split the kfunc, dtor kfunc and struct_ops registration bodies
  2026-09-23  5:39 [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Jay Wang
  2026-09-23  5:39 ` [PATCH bpf-next 1/6] bpf: pass the vmlinux BTF to btf_parse_module() and let it adopt the data Jay Wang
@ 2026-09-23  5:39 ` Jay Wang
  2026-09-23  6:16   ` bot+bpf-ci
  2026-09-23  5:39 ` [PATCH bpf-next 3/6] bpf: fetch the vmlinux BTF where kernel types enter a program Jay Wang
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 11+ messages in thread
From: Jay Wang @ 2026-09-23  5:39 UTC (permalink / raw)
  To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Kumar Kartikeya Dwivedi
  Cc: Alan Maguire, Martin KaFai Lau, Yonghong Song, Nathan Chancellor,
	Nicolas Schier, linux-kbuild, Luis Chamberlain, Petr Pavlu,
	linux-modules, Arnd Bergmann, linux-kernel,
	Hazem Mohamed Abuelfotoh, Bjoern Doebel, jay.wang.upstream

Split __register_btf_kfunc_id_set(), register_btf_id_dtor_kfuncs() and
__register_bpf_struct_ops() into the part that looks up the BTF for the
owner and the part that adds the registration to a given BTF:
btf_kfunc_id_set_add(), btf_dtor_kfuncs_add() and btf_struct_ops_add().

In is_valid_value_type(), look up bpf_struct_ops_common_value in the btf
the function was given rather than in the btf_vmlinux global.  The id is
a vmlinux id and a module BTF resolves it through its base, so the result
is the same; the function already uses the passed btf for every other
lookup.

No functional change.  With CONFIG_DEBUG_INFO_BTF=m, registrations made
from initcalls before the vmlinux BTF is available are queued and applied
later by the BTF parsing code, which needs the add-to-this-btf half on
its own; the struct_ops ones are applied before the parsed vmlinux BTF is
published, i.e. while btf_vmlinux is still NULL.

Signed-off-by: Jay Wang <wanjay@amazon.com>
---
 kernel/bpf/bpf_struct_ops.c |  3 +-
 kernel/bpf/btf.c            | 91 ++++++++++++++++++++++---------------
 2 files changed, 57 insertions(+), 37 deletions(-)

diff --git a/kernel/bpf/bpf_struct_ops.c b/kernel/bpf/bpf_struct_ops.c
index 1178acd72296..bf3004908d15 100644
--- a/kernel/bpf/bpf_struct_ops.c
+++ b/kernel/bpf/bpf_struct_ops.c
@@ -103,7 +103,8 @@ static bool is_valid_value_type(struct btf *btf, s32 value_id,
 	}
 	member = btf_type_member(vt);
 	mt = btf_type_by_id(btf, member->type);
-	common_value_type = btf_type_by_id(btf_vmlinux,
+	/* a vmlinux id resolves through the base BTF of a module BTF too */
+	common_value_type = btf_type_by_id(btf,
 					   st_ops_ids[IDX_ST_OPS_COMMON_VALUE_ID]);
 	if (mt != common_value_type) {
 		pr_warn("The first member of %s should be bpf_struct_ops_common_value\n",
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index a6634237dc89..c3c1421208b4 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -9313,11 +9313,26 @@ u32 *btf_kfunc_is_modify_return(const struct btf *btf, u32 kfunc_btf_id,
 	return btf_kfunc_id_set_contains(btf, BTF_KFUNC_HOOK_FMODRET, kfunc_btf_id);
 }
 
+static int btf_kfunc_id_set_add(struct btf *btf, enum btf_kfunc_hook hook,
+				const struct btf_kfunc_id_set *kset)
+{
+	int ret, i;
+
+	for (i = 0; i < kset->set->cnt; i++) {
+		ret = btf_check_kfunc_protos(btf, btf_relocate_id(btf, kset->set->pairs[i].id),
+					     kset->set->pairs[i].flags);
+		if (ret)
+			return ret;
+	}
+
+	return btf_populate_kfunc_set(btf, hook, kset);
+}
+
 static int __register_btf_kfunc_id_set(enum btf_kfunc_hook hook,
 				       const struct btf_kfunc_id_set *kset)
 {
 	struct btf *btf;
-	int ret, i;
+	int ret;
 
 	btf = btf_get_module_btf(kset->owner);
 	if (!btf)
@@ -9325,16 +9340,7 @@ static int __register_btf_kfunc_id_set(enum btf_kfunc_hook hook,
 	if (IS_ERR(btf))
 		return PTR_ERR(btf);
 
-	for (i = 0; i < kset->set->cnt; i++) {
-		ret = btf_check_kfunc_protos(btf, btf_relocate_id(btf, kset->set->pairs[i].id),
-					     kset->set->pairs[i].flags);
-		if (ret)
-			goto err_out;
-	}
-
-	ret = btf_populate_kfunc_set(btf, hook, kset);
-
-err_out:
+	ret = btf_kfunc_id_set_add(btf, hook, kset);
 	btf_put(btf);
 	return ret;
 }
@@ -9426,21 +9432,13 @@ static int btf_check_dtor_kfuncs(struct btf *btf, const struct btf_id_dtor_kfunc
 	return 0;
 }
 
-/* This function must be invoked only from initcalls/module init functions */
-int register_btf_id_dtor_kfuncs(const struct btf_id_dtor_kfunc *dtors, u32 add_cnt,
-				struct module *owner)
+static int btf_dtor_kfuncs_add(struct btf *btf, const struct btf_id_dtor_kfunc *dtors,
+			       u32 add_cnt)
 {
 	struct btf_id_dtor_kfunc_tab *tab;
-	struct btf *btf;
 	u32 tab_cnt, i;
 	int ret;
 
-	btf = btf_get_module_btf(owner);
-	if (!btf)
-		return check_btf_kconfigs(owner, "dtor kfuncs");
-	if (IS_ERR(btf))
-		return PTR_ERR(btf);
-
 	if (add_cnt >= BTF_DTOR_KFUNC_MAX_CNT) {
 		pr_err("cannot register more than %d kfunc destructors\n", BTF_DTOR_KFUNC_MAX_CNT);
 		ret = -E2BIG;
@@ -9497,6 +9495,23 @@ int register_btf_id_dtor_kfuncs(const struct btf_id_dtor_kfunc *dtors, u32 add_c
 end:
 	if (ret)
 		btf_free_dtor_kfunc_tab(btf);
+	return ret;
+}
+
+/* This function must be invoked only from initcalls/module init functions */
+int register_btf_id_dtor_kfuncs(const struct btf_id_dtor_kfunc *dtors, u32 add_cnt,
+				struct module *owner)
+{
+	struct btf *btf;
+	int ret;
+
+	btf = btf_get_module_btf(owner);
+	if (!btf)
+		return check_btf_kconfigs(owner, "dtor kfuncs");
+	if (IS_ERR(btf))
+		return PTR_ERR(btf);
+
+	ret = btf_dtor_kfuncs_add(btf, dtors, add_cnt);
 	btf_put(btf);
 	return ret;
 }
@@ -10135,32 +10150,36 @@ bpf_struct_ops_find(struct btf *btf, u32 type_id)
 	return NULL;
 }
 
-int __register_bpf_struct_ops(struct bpf_struct_ops *st_ops)
+static int btf_struct_ops_add(struct btf *btf, struct bpf_struct_ops *st_ops)
 {
 	struct bpf_verifier_log *log;
-	struct btf *btf;
-	int err = 0;
-
-	btf = btf_get_module_btf(st_ops->owner);
-	if (!btf)
-		return check_btf_kconfigs(st_ops->owner, "struct_ops");
-	if (IS_ERR(btf))
-		return PTR_ERR(btf);
+	int err;
 
 	log = kzalloc_obj(*log, GFP_KERNEL | __GFP_NOWARN);
-	if (!log) {
-		err = -ENOMEM;
-		goto errout;
-	}
+	if (!log)
+		return -ENOMEM;
 
 	log->level = BPF_LOG_KERNEL;
 
 	err = btf_add_struct_ops(btf, st_ops, log);
 
-errout:
 	kfree(log);
-	btf_put(btf);
+	return err;
+}
 
+int __register_bpf_struct_ops(struct bpf_struct_ops *st_ops)
+{
+	struct btf *btf;
+	int err;
+
+	btf = btf_get_module_btf(st_ops->owner);
+	if (!btf)
+		return check_btf_kconfigs(st_ops->owner, "struct_ops");
+	if (IS_ERR(btf))
+		return PTR_ERR(btf);
+
+	err = btf_struct_ops_add(btf, st_ops);
+	btf_put(btf);
 	return err;
 }
 EXPORT_SYMBOL_GPL(__register_bpf_struct_ops);
-- 
2.47.3


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

* [PATCH bpf-next 3/6] bpf: fetch the vmlinux BTF where kernel types enter a program
  2026-09-23  5:39 [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Jay Wang
  2026-09-23  5:39 ` [PATCH bpf-next 1/6] bpf: pass the vmlinux BTF to btf_parse_module() and let it adopt the data Jay Wang
  2026-09-23  5:39 ` [PATCH bpf-next 2/6] bpf: split the kfunc, dtor kfunc and struct_ops registration bodies Jay Wang
@ 2026-09-23  5:39 ` Jay Wang
  2026-09-23  6:28   ` bot+bpf-ci
  2026-09-23  5:39 ` [PATCH bpf-next 4/6] bpf: take the vmlinux BTF from the btf_vmlinux module Jay Wang
                   ` (3 subsequent siblings)
  6 siblings, 1 reply; 11+ messages in thread
From: Jay Wang @ 2026-09-23  5:39 UTC (permalink / raw)
  To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Kumar Kartikeya Dwivedi
  Cc: Alan Maguire, Martin KaFai Lau, Yonghong Song, Nathan Chancellor,
	Nicolas Schier, linux-kbuild, Luis Chamberlain, Petr Pavlu,
	linux-modules, Arnd Bergmann, linux-kernel,
	Hazem Mohamed Abuelfotoh, Bjoern Doebel, jay.wang.upstream

bpf_check() fetches the vmlinux BTF up front for every program, whether
the program uses kernel types or not.  With the upcoming
CONFIG_DEBUG_INFO_BTF=m that fetch loads a module and parses 5 MiB of
BTF, and since systemd loads socket filters at boot, it would happen on
every system, whether anything uses BTF or not.

Stop fetching up front and fetch at the points where kernel types enter
the verifier state instead:

 - bpf_add_kfunc_call(), for the first kfunc call of a program;
 - check_pseudo_btf_id(), for ldimm64 of a kernel variable;
 - check_ptr_to_map_access(), for accessing a map pointer's fields;
 - check_helper_call(), when the helper's prototype takes or returns a
   PTR_TO_BTF_ID (helper_uses_vmlinux_btf()).

Together with the existing fetch in bpf_prog_load() for attach_btf and
the struct_ops map creation, every PTR_TO_BTF_ID register a program can
hold originates from one of these sites.  A program that uses none of
them, such as a socket filter, no longer touches the vmlinux BTF.

bpf_snprintf_btf() and bpf_seq_printf_btf() run in program context and
cannot afford a fetch that may sleep.  Add bpf_peek_btf_vmlinux(), which
returns the parsed vmlinux BTF or NULL without parsing anything, and use
it there; the helpers fail with -EINVAL if the BTF is not parsed, as they
do on a kernel without BTF.

With CONFIG_DEBUG_INFO_BTF=y the vmlinux BTF is parsed at boot by the
first kfunc registration, so nothing changes.  Without BTF, a helper that
takes or returns a kernel pointer is now rejected with -ENOTSUPP at the
call rather than with -EINVAL for its zero return type id.

Signed-off-by: Jay Wang <wanjay@amazon.com>
---
 include/linux/bpf.h      |  1 +
 kernel/bpf/verifier.c    | 54 +++++++++++++++++++++++++++++++++++-----
 kernel/trace/bpf_trace.c |  3 ++-
 3 files changed, 51 insertions(+), 7 deletions(-)

diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index e7c5e203eddd..a3c4caad5dfc 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -3165,6 +3165,7 @@ static inline s32 bpf_call_args_imm(s16 idx)
 #endif
 
 struct btf *bpf_get_btf_vmlinux(void);
+struct btf *bpf_peek_btf_vmlinux(void);
 
 /* Map specifics */
 struct xdp_frame;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a7c9e2d8965d..2425ea74b61d 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -2873,7 +2873,8 @@ int bpf_add_kfunc_call(struct bpf_verifier_env *env, u32 func_id, u16 offset)
 	tab = prog_aux->kfunc_tab;
 	btf_tab = prog_aux->kfunc_btf_tab;
 	if (!tab) {
-		if (!btf_vmlinux) {
+		/* with CONFIG_DEBUG_INFO_BTF=m this is where the vmlinux BTF gets loaded */
+		if (IS_ERR_OR_NULL(bpf_get_btf_vmlinux())) {
 			verbose(env, "calling kernel function is not supported without CONFIG_DEBUG_INFO_BTF\n");
 			return -ENOTSUPP;
 		}
@@ -6257,7 +6258,8 @@ static int check_ptr_to_map_access(struct bpf_verifier_env *env,
 	u32 btf_id;
 	int ret;
 
-	if (!btf_vmlinux) {
+	/* with CONFIG_DEBUG_INFO_BTF=m this is where the vmlinux BTF gets loaded */
+	if (IS_ERR_OR_NULL(bpf_get_btf_vmlinux())) {
 		verbose(env, "map_ptr access not supported without CONFIG_DEBUG_INFO_BTF\n");
 		return -ENOTSUPP;
 	}
@@ -11568,6 +11570,20 @@ static int release_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
 	return err;
 }
 
+/* Does calling @fn bring kernel BTF types into the program state? */
+static bool helper_uses_vmlinux_btf(const struct bpf_func_proto *fn)
+{
+	int i;
+
+	if (base_type(fn->ret_type) == RET_PTR_TO_BTF_ID)
+		return true;
+	for (i = 0; i < MAX_BPF_FUNC_ARGS; i++) {
+		if (base_type(fn->arg_type[i]) == ARG_PTR_TO_BTF_ID)
+			return true;
+	}
+	return false;
+}
+
 static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
 			     int *insn_idx_p)
 {
@@ -11637,6 +11653,16 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
 		return err;
 	}
 
+	/*
+	 * Helpers that take or return kernel BTF pointers need the vmlinux
+	 * BTF; with CONFIG_DEBUG_INFO_BTF=m this is where it gets loaded.
+	 */
+	if (helper_uses_vmlinux_btf(fn) && IS_ERR_OR_NULL(bpf_get_btf_vmlinux())) {
+		verbose(env, "helper %s#%d is not supported without vmlinux BTF\n",
+			func_id_name(func_id), func_id);
+		return -ENOTSUPP;
+	}
+
 	if (fn->might_sleep && !in_sleepable_context(env)) {
 		verbose(env, "sleepable helper %s#%d in %s\n", func_id_name(func_id), func_id,
 			non_sleepable_context_description(env));
@@ -19235,12 +19261,13 @@ static int check_pseudo_btf_id(struct bpf_verifier_env *env,
 			return -EINVAL;
 		}
 	} else {
-		if (!btf_vmlinux) {
+		/* with CONFIG_DEBUG_INFO_BTF=m this is where the vmlinux BTF gets loaded */
+		btf = bpf_get_btf_vmlinux();
+		if (IS_ERR_OR_NULL(btf)) {
 			verbose(env, "kernel is missing BTF, make sure CONFIG_DEBUG_INFO_BTF=y is specified in Kconfig.\n");
 			return -EINVAL;
 		}
-		btf_get(btf_vmlinux);
-		btf = btf_vmlinux;
+		btf_get(btf);
 	}
 
 	err = __check_pseudo_btf_id(env, insn, aux, btf);
@@ -21153,6 +21180,17 @@ struct btf *bpf_get_btf_vmlinux(void)
 	return btf;
 }
 
+/*
+ * The vmlinux BTF if it has been parsed already, else NULL.  Unlike
+ * bpf_get_btf_vmlinux() this never loads or parses anything, so it is safe
+ * to call from a running BPF program.
+ */
+struct btf *bpf_peek_btf_vmlinux(void)
+{
+	/* Pairs with the smp_store_release() in bpf_get_btf_vmlinux() */
+	return smp_load_acquire(&btf_vmlinux);
+}
+
 /*
  * The add_fd_from_fd_array() is executed only if fd_array_cnt is non-zero. In
  * this case expect that every file descriptor in the array is either a map or
@@ -21724,7 +21762,11 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
 	if (ret)
 		goto err_prep;
 
-	bpf_get_btf_vmlinux();
+	/*
+	 * The vmlinux BTF is not fetched up front: with CONFIG_DEBUG_INFO_BTF=m
+	 * it is loaded on demand, at the points where kernel types enter the
+	 * program (attach_btf, kfuncs, ksyms, map pointers, BTF-typed helpers).
+	 */
 
 	/* Serialize verification of unprivileged programs. */
 	if (!is_priv)
diff --git a/kernel/trace/bpf_trace.c b/kernel/trace/bpf_trace.c
index 195f78db9bda..c022b2877f0b 100644
--- a/kernel/trace/bpf_trace.c
+++ b/kernel/trace/bpf_trace.c
@@ -1015,7 +1015,8 @@ static int bpf_btf_printf_prepare(struct btf_ptr *ptr, u32 btf_ptr_size,
 	if (btf_ptr_size != sizeof(struct btf_ptr))
 		return -EINVAL;
 
-	*btf = bpf_get_btf_vmlinux();
+	/* Called from a running program: only use the BTF if it is parsed. */
+	*btf = bpf_peek_btf_vmlinux();
 
 	if (IS_ERR_OR_NULL(*btf))
 		return IS_ERR(*btf) ? PTR_ERR(*btf) : -EINVAL;
-- 
2.47.3


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

* [PATCH bpf-next 4/6] bpf: take the vmlinux BTF from the btf_vmlinux module
  2026-09-23  5:39 [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Jay Wang
                   ` (2 preceding siblings ...)
  2026-09-23  5:39 ` [PATCH bpf-next 3/6] bpf: fetch the vmlinux BTF where kernel types enter a program Jay Wang
@ 2026-09-23  5:39 ` Jay Wang
  2026-09-23  5:39 ` [PATCH bpf-next 5/6] bpf: defer registrations until the vmlinux BTF is available Jay Wang
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Jay Wang @ 2026-09-23  5:39 UTC (permalink / raw)
  To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Kumar Kartikeya Dwivedi
  Cc: Alan Maguire, Martin KaFai Lau, Yonghong Song, Nathan Chancellor,
	Nicolas Schier, linux-kbuild, Luis Chamberlain, Petr Pavlu,
	linux-modules, Arnd Bergmann, linux-kernel,
	Hazem Mohamed Abuelfotoh, Bjoern Doebel, jay.wang.upstream

Add the runtime side of delivering the vmlinux BTF as a module: with
CONFIG_DEBUG_INFO_BTF=m the BTF is carried by a module named btf_vmlinux
and installed by the BTF module notifier when it loads, and whoever needs
the BTF first loads the module.  Nothing in this patch is reachable yet:
CONFIG_DEBUG_INFO_BTF is still a bool and every new path is under
IS_MODULE(CONFIG_DEBUG_INFO_BTF); the kbuild side and the Kconfig change
follow.

With CONFIG_DEBUG_INFO_BTF=y the vmlinux BTF, 5.4 MiB on x86-64 with a
distribution config, is part of the kernel image and resident from boot
whether anything uses it or not.  Most systems never do.  Carrying it in
a module that is loaded on first use makes the memory a cost of using
BTF rather than of having a kernel that supports it.

btf_vmlinux_data() hands out the raw vmlinux BTF: from __start_BTF with
=y, or with =m from a vmalloc_user() copy that the notifier makes when
the btf_vmlinux module loads.  If the copy is not there and the caller
asked to load, it calls request_module("btf_vmlinux"); the notifier
installs the copy before init_module() returns, so the data is either
present afterwards or the module is not available (yet).  The result is
not cached, a later call retries.  btf_parse_vmlinux() and
bpf_get_btf_vmlinux() use it; the latter loads outside btf_vmlinux_lock
so that the notifier is never blocked by the caller, and returns NULL
like a kernel without BTF when the module cannot be loaded.

The verifier trusts the BTF as the description of this kernel's types, so
the notifier only accepts a payload whose size and SHA-256 match the
values linked into the kernel as .BTF.meta (struct btf_vmlinux_meta,
filled in by scripts/gen-btf.sh in a later patch).  A carrier from
another build is refused with -EINVAL even if vermagic lets it load, and
a second carrier is ignored.  The copy is never freed: as with =y, the
BTF stays for the lifetime of the kernel, and the carrier has no exit.

The module notifier, btf_parse_module() and the btf_data fields in struct
module are compiled for CONFIG_DEBUG_INFO_BTF_MODULES or =m; with =m and
no module BTF, the notifier only recognizes the carrier.

/sys/kernel/btf/vmlinux exists from boot with its final size, which is
known from .BTF.meta before the BTF is loaded; the first read() or mmap()
loads it.  mmap() uses remap_vmalloc_range() on the copy.  This keeps
stat() working before the load, which is what the btf_sysfs selftest
does.

BPF_BTF_GET_NEXT_ID loads the BTF too: kernel BTFs get their ids when
the vmlinux BTF is parsed, and whoever enumerates BTF ids wants them.

Signed-off-by: Jay Wang <wanjay@amazon.com>
---
 include/linux/btf.h    |   2 +
 include/linux/module.h |   2 +-
 kernel/bpf/btf.c       | 168 +++++++++++++++++++++++++++++++++++++++--
 kernel/bpf/syscall.c   |   6 ++
 kernel/bpf/sysfs_btf.c |  80 +++++++++++++++++++-
 kernel/bpf/verifier.c  |  43 +++++++----
 kernel/module/main.c   |   4 +-
 7 files changed, 277 insertions(+), 28 deletions(-)

diff --git a/include/linux/btf.h b/include/linux/btf.h
index ddd0f4f32d24..0bf10811fe53 100644
--- a/include/linux/btf.h
+++ b/include/linux/btf.h
@@ -581,6 +581,8 @@ __u32 *btf_field_iter_next(struct btf_field_iter *it);
 const char *btf_name_by_offset(const struct btf *btf, u32 offset);
 const char *btf_str_by_offset(const struct btf *btf, u32 offset);
 struct btf *btf_parse_vmlinux(void);
+void *btf_vmlinux_data(u32 *size, bool load);
+u32 btf_vmlinux_size(void);
 struct btf *bpf_prog_get_target_btf(const struct bpf_prog *prog);
 u32 *btf_kfunc_flags(const struct btf *btf, u32 kfunc_btf_id, const struct bpf_prog *prog);
 int btf_kfunc_check_flag(const struct btf *btf, u32 kfunc_btf_id, u32 flag);
diff --git a/include/linux/module.h b/include/linux/module.h
index 96cc98568eea..82734996a862 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -497,7 +497,7 @@ struct module {
 	unsigned int num_bpf_raw_events;
 	struct bpf_raw_event_map *bpf_raw_events;
 #endif
-#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES) || IS_MODULE(CONFIG_DEBUG_INFO_BTF)
 	unsigned int btf_data_size;
 	unsigned int btf_base_data_size;
 	void *btf_data;
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index c3c1421208b4..50eb7a95fd82 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -29,6 +29,7 @@
 #include <linux/string.h>
 #include <linux/sysfs.h>
 #include <linux/overflow.h>
+#include <crypto/sha2.h>
 #include <linux/bitops.h>
 
 #include <net/netfilter/nf_bpf_link.h>
@@ -6092,10 +6093,86 @@ static struct btf *btf_parse(const union bpf_attr *attr, bpfptr_t uattr,
 	return ERR_PTR(err);
 }
 
+#if IS_BUILTIN(CONFIG_DEBUG_INFO_BTF)
 extern char __start_BTF[];
 extern char __stop_BTF[];
+#endif
 extern struct btf *btf_vmlinux;
 
+#if IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+/*
+ * With CONFIG_DEBUG_INFO_BTF=m the vmlinux BTF is not part of the kernel
+ * image.  The btf_vmlinux module carries it in its .BTF section; when the
+ * module loads, btf_module_notify() copies the section here.  The copy is
+ * made with vmalloc_user() so that /sys/kernel/btf/vmlinux can be mmap()ed
+ * as with the built-in BTF.  Set once, never cleared: like the built-in
+ * BTF, once present it stays for the lifetime of the kernel.
+ *
+ * The size and SHA-256 of the BTF are linked into the kernel as .BTF.meta
+ * by scripts/gen-btf.sh: the size makes /sys/kernel/btf/vmlinux report its
+ * size before the BTF is loaded, the hash makes sure only the BTF this
+ * kernel was built with is accepted.
+ */
+struct btf_vmlinux_meta {
+	u32 size;
+	u8 sha256[SHA256_DIGEST_SIZE];
+} __packed;
+
+extern const struct btf_vmlinux_meta __start_BTF_meta[];
+#define btf_vmlinux_meta (__start_BTF_meta[0])
+
+static void *btf_vmlinux_raw;
+#endif
+
+/**
+ * btf_vmlinux_data - get the raw vmlinux BTF
+ * @size: where to store the size of the BTF
+ * @load: with CONFIG_DEBUG_INFO_BTF=m, load the btf_vmlinux module if the
+ *	  BTF is not present yet; may sleep
+ *
+ * Return: the raw BTF, or NULL if it is not available.
+ */
+void *btf_vmlinux_data(u32 *size, bool load)
+{
+#if IS_BUILTIN(CONFIG_DEBUG_INFO_BTF)
+	*size = __stop_BTF - __start_BTF;
+	return __start_BTF;
+#elif IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+	/* Pairs with the smp_store_release() in btf_vmlinux_module_coming() */
+	void *data = smp_load_acquire(&btf_vmlinux_raw);
+
+	if (!data && load) {
+		/*
+		 * The module notifier installs the BTF before init_module()
+		 * returns, so it is either there after this or the module is
+		 * not available (yet).  Not cached: a later call retries,
+		 * e.g. once the module becomes reachable on the root fs.
+		 */
+		request_module("btf_vmlinux");
+		/* Same pairing as above */
+		data = smp_load_acquire(&btf_vmlinux_raw);
+	}
+	*size = btf_vmlinux_meta.size;
+	return data;
+#else
+	return NULL;
+#endif
+}
+
+/**
+ * btf_vmlinux_size - size of the vmlinux BTF, known even before it is loaded
+ */
+u32 btf_vmlinux_size(void)
+{
+#if IS_BUILTIN(CONFIG_DEBUG_INFO_BTF)
+	return __stop_BTF - __start_BTF;
+#elif IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+	return btf_vmlinux_meta.size;
+#else
+	return 0;
+#endif
+}
+
 #define BPF_MAP_TYPE(_id, _ops)
 #define BPF_LINK_TYPE(_id, _name)
 static union {
@@ -6479,15 +6556,22 @@ struct btf *btf_parse_vmlinux(void)
 	struct btf_verifier_env *env = NULL;
 	struct bpf_verifier_log *log;
 	struct btf *btf;
+	void *data;
+	u32 size;
 	int err;
 
+	/* The caller made sure the BTF is present, see bpf_get_btf_vmlinux() */
+	data = btf_vmlinux_data(&size, false);
+	if (!data)
+		return ERR_PTR(-ENOENT);
+
 	env = kzalloc_obj(*env, GFP_KERNEL | __GFP_NOWARN);
 	if (!env)
 		return ERR_PTR(-ENOMEM);
 
 	log = &env->log;
 	log->level = BPF_LOG_KERNEL;
-	btf = btf_parse_base(env, "vmlinux", __start_BTF, __stop_BTF - __start_BTF);
+	btf = btf_parse_base(env, "vmlinux", data, size);
 	if (IS_ERR(btf))
 		goto err_out;
 
@@ -6514,7 +6598,7 @@ __u32 btf_relocate_id(const struct btf *btf, __u32 id)
 	return btf->base_id_map[id];
 }
 
-#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES) || IS_MODULE(CONFIG_DEBUG_INFO_BTF)
 
 /*
  * Parse split module BTF against @vmlinux_btf.  @data is the module's .BTF
@@ -6620,7 +6704,7 @@ static struct btf *btf_parse_module(const char *module_name, struct btf *vmlinux
 	return ERR_PTR(err);
 }
 
-#endif /* CONFIG_DEBUG_INFO_BTF_MODULES */
+#endif /* CONFIG_DEBUG_INFO_BTF_MODULES || CONFIG_DEBUG_INFO_BTF=m */
 
 struct btf *bpf_prog_get_target_btf(const struct bpf_prog *prog)
 {
@@ -8604,7 +8688,16 @@ enum {
 	BTF_MODULE_F_LIVE = (1 << 0),
 };
 
-#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+/*
+ * The module notifier registers module BTF (CONFIG_DEBUG_INFO_BTF_MODULES)
+ * and picks up the vmlinux BTF from the btf_vmlinux module
+ * (CONFIG_DEBUG_INFO_BTF=m).
+ */
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES) || IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+#define BTF_MODULE_NOTIFIER 1
+#endif
+
+#ifdef BTF_MODULE_NOTIFIER
 struct btf_module {
 	struct list_head list;
 	struct module *module;
@@ -8660,6 +8753,52 @@ static void btf_module_free(struct btf_module *btf_mod)
 	kfree(btf_mod);
 }
 
+#if IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+/*
+ * The btf_vmlinux module carries the vmlinux BTF in its .BTF section
+ * (scripts/gen-btf.sh).  Keep a copy; the module is only the carrier and
+ * has no BTF of its own.
+ */
+static int btf_vmlinux_module_coming(struct module *mod)
+{
+	u8 sha256sum[SHA256_DIGEST_SIZE];
+	void *data;
+
+	if (btf_vmlinux_raw)
+		return 0;
+
+	/*
+	 * The verifier trusts the BTF as the description of this kernel's
+	 * types, so a BTF from a different build must not get in even if
+	 * the module otherwise loads (same release string, same vermagic).
+	 */
+	if (mod->btf_data_size != btf_vmlinux_meta.size) {
+		pr_err("module [%s]: BTF size %u does not match this kernel (%u)\n",
+		       mod->name, mod->btf_data_size, btf_vmlinux_meta.size);
+		return -EINVAL;
+	}
+	sha256(mod->btf_data, mod->btf_data_size, sha256sum);
+	if (memcmp(sha256sum, btf_vmlinux_meta.sha256, sizeof(sha256sum))) {
+		pr_err("module [%s]: BTF does not match this kernel\n", mod->name);
+		return -EINVAL;
+	}
+
+	data = vmalloc_user(mod->btf_data_size);
+	if (!data)
+		return -ENOMEM;
+	memcpy(data, mod->btf_data, mod->btf_data_size);
+
+	/* Pairs with the smp_load_acquire() in btf_vmlinux_data() */
+	smp_store_release(&btf_vmlinux_raw, data);
+	return 0;
+}
+#else
+static int btf_vmlinux_module_coming(struct module *mod)
+{
+	return 0;
+}
+#endif
+
 static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			     void *module)
 {
@@ -8668,9 +8807,17 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 	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;
+
+	if (IS_MODULE(CONFIG_DEBUG_INFO_BTF) && !strcmp(mod->name, "btf_vmlinux")) {
+		if (op == MODULE_STATE_COMING)
+			err = btf_vmlinux_module_coming(mod);
+		goto out;
+	}
+
+	if (!IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES) || mod->btf_data_size == 0)
 		goto out;
 
 	switch (op) {
@@ -8758,7 +8905,7 @@ static int __init btf_module_init(void)
 }
 
 fs_initcall(btf_module_init);
-#endif /* CONFIG_DEBUG_INFO_BTF_MODULES */
+#endif /* BTF_MODULE_NOTIFIER */
 
 struct module *btf_try_get_module(const struct btf *btf)
 {
@@ -9715,6 +9862,11 @@ static void purge_cand_cache(struct btf *btf)
 	__purge_cand_cache(btf, module_cand_cache, MODULE_CAND_CACHE_SIZE);
 	mutex_unlock(&cand_cache_mutex);
 }
+#elif defined(BTF_MODULE_NOTIFIER)
+/* CONFIG_DEBUG_INFO_BTF=m without module BTF: nothing is ever cached */
+static void purge_cand_cache(struct btf *btf)
+{
+}
 #endif
 
 static struct bpf_cand_cache *
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index 74496fd716d3..9e9eaa798813 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -6406,6 +6406,12 @@ static int __sys_bpf(enum bpf_cmd cmd, bpfptr_t uattr, unsigned int size,
 					  &map_idr, &map_idr_lock);
 		break;
 	case BPF_BTF_GET_NEXT_ID:
+		/*
+		 * With CONFIG_DEBUG_INFO_BTF=m the kernel BTFs get ids when the
+		 * vmlinux BTF is loaded; whoever enumerates them wants them.
+		 */
+		if (IS_MODULE(CONFIG_DEBUG_INFO_BTF))
+			bpf_get_btf_vmlinux();
 		err = bpf_obj_get_next_id(&attr, uattr.user,
 					  &btf_idr, &btf_idr_lock);
 		break;
diff --git a/kernel/bpf/sysfs_btf.c b/kernel/bpf/sysfs_btf.c
index 9cbe15ce3540..03f47734987f 100644
--- a/kernel/bpf/sysfs_btf.c
+++ b/kernel/bpf/sysfs_btf.c
@@ -9,8 +9,13 @@
 #include <linux/sysfs.h>
 #include <linux/mm.h>
 #include <linux/io.h>
+#include <linux/bpf.h>
 #include <linux/btf.h>
+#include <linux/vmalloc.h>
 
+struct kobject *btf_kobj;
+
+#if IS_BUILTIN(CONFIG_DEBUG_INFO_BTF)
 /* See scripts/link-vmlinux.sh, gen_btf() func for details */
 extern char __start_BTF[];
 extern char __stop_BTF[];
@@ -49,12 +54,81 @@ static struct bin_attribute bin_attr_btf_vmlinux __ro_after_init = {
 	.mmap = btf_sysfs_vmlinux_mmap,
 };
 
-struct kobject *btf_kobj;
-
-static int __init btf_vmlinux_init(void)
+static void __init btf_sysfs_vmlinux_init(void)
 {
 	bin_attr_btf_vmlinux.private = __start_BTF;
 	bin_attr_btf_vmlinux.size = __stop_BTF - __start_BTF;
+}
+
+#else /* CONFIG_DEBUG_INFO_BTF=m */
+
+/*
+ * The BTF is carried by the btf_vmlinux module and only loaded when
+ * something needs it.  Its size is known from the start, so the file has
+ * its final size from boot; the first read() or mmap() loads the BTF.
+ */
+static ssize_t btf_sysfs_vmlinux_read(struct file *filp, struct kobject *kobj,
+				      const struct bin_attribute *attr,
+				      char *buf, loff_t off, size_t count)
+{
+	void *data;
+	u32 size;
+
+	/* Loads the module, parses the BTF and registers module BTFs. */
+	if (IS_ERR_OR_NULL(bpf_get_btf_vmlinux()))
+		return -ENODEV;
+	data = btf_vmlinux_data(&size, false);
+	if (!data)
+		return -ENODEV;
+
+	/* sysfs clamps @off and @count to attr->size, which is @size */
+	memcpy(buf, data + off, count);
+	return count;
+}
+
+static int btf_sysfs_vmlinux_mmap(struct file *filp, struct kobject *kobj,
+				  const struct bin_attribute *attr,
+				  struct vm_area_struct *vma)
+{
+	size_t vm_size = vma->vm_end - vma->vm_start;
+	void *data;
+	u32 size;
+
+	if (IS_ERR_OR_NULL(bpf_get_btf_vmlinux()))
+		return -ENODEV;
+	data = btf_vmlinux_data(&size, false);
+	if (!data)
+		return -ENODEV;
+
+	if (vma->vm_pgoff)
+		return -EINVAL;
+
+	if (vma->vm_flags & (VM_WRITE | VM_EXEC | VM_MAYSHARE))
+		return -EACCES;
+
+	if (vm_size > PAGE_ALIGN(size))
+		return -EINVAL;
+
+	vm_flags_mod(vma, VM_DONTDUMP, VM_MAYEXEC | VM_MAYWRITE);
+	/* the copy was made with vmalloc_user() for this purpose */
+	return remap_vmalloc_range(vma, data, 0);
+}
+
+static struct bin_attribute bin_attr_btf_vmlinux __ro_after_init = {
+	.attr = { .name = "vmlinux", .mode = 0444, },
+	.read = btf_sysfs_vmlinux_read,
+	.mmap = btf_sysfs_vmlinux_mmap,
+};
+
+static void __init btf_sysfs_vmlinux_init(void)
+{
+	bin_attr_btf_vmlinux.size = btf_vmlinux_size();
+}
+#endif
+
+static int __init btf_vmlinux_init(void)
+{
+	btf_sysfs_vmlinux_init();
 
 	if (bin_attr_btf_vmlinux.size == 0)
 		return 0;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 2425ea74b61d..a7b73bc146a8 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -21157,26 +21157,41 @@ int bpf_check_attach_btf_id_multi(struct btf *btf, struct bpf_prog *prog, u32 bt
 	return 0;
 }
 
+/*
+ * Returns the parsed vmlinux BTF, NULL if the kernel has none, or an ERR_PTR
+ * if it is malformed.  With CONFIG_DEBUG_INFO_BTF=m the BTF lives in the
+ * btf_vmlinux module; the first caller loads it and parses it.  May sleep.
+ */
 struct btf *bpf_get_btf_vmlinux(void)
 {
 	/* Pairs with the smp_store_release() on the parse path below. */
 	struct btf *btf = smp_load_acquire(&btf_vmlinux);
+	u32 size;
 
-	if (!btf && IS_ENABLED(CONFIG_DEBUG_INFO_BTF)) {
-		mutex_lock(&btf_vmlinux_lock);
-		btf = btf_vmlinux;
-		if (!btf) {
-			btf = btf_parse_vmlinux();
-			/*
-			 * Order the parsed BTF contents and the globals the
-			 * parse populated (e.g. bpf_ctx_convert.t) before
-			 * the pointer publication. Pairs with the acquire
-			 * on the lockless fast path above.
-			 */
-			smp_store_release(&btf_vmlinux, btf);
-		}
-		mutex_unlock(&btf_vmlinux_lock);
+	if (btf || !IS_ENABLED(CONFIG_DEBUG_INFO_BTF))
+		return btf;
+
+	/*
+	 * Loading the module may take a while and its notifier must not be
+	 * blocked by us, so do it outside btf_vmlinux_lock.  Not available:
+	 * behave like a kernel without BTF, and retry next time.
+	 */
+	if (!btf_vmlinux_data(&size, true))
+		return NULL;
+
+	mutex_lock(&btf_vmlinux_lock);
+	btf = btf_vmlinux;
+	if (!btf) {
+		btf = btf_parse_vmlinux();
+		/*
+		 * Order the parsed BTF contents and the globals the
+		 * parse populated (e.g. bpf_ctx_convert.t) before
+		 * the pointer publication. Pairs with the acquire
+		 * on the lockless fast path above.
+		 */
+		smp_store_release(&btf_vmlinux, btf);
 	}
+	mutex_unlock(&btf_vmlinux_lock);
 	return btf;
 }
 
diff --git a/kernel/module/main.c b/kernel/module/main.c
index d0e1e0bd2ad0..694c4bc7e679 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2718,7 +2718,7 @@ static int find_module_sections(struct module *mod, struct load_info *info)
 					   sizeof(*mod->bpf_raw_events),
 					   &mod->num_bpf_raw_events);
 #endif
-#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES) || IS_MODULE(CONFIG_DEBUG_INFO_BTF)
 	mod->btf_data = any_section_objs(info, ".BTF", 1, &mod->btf_data_size);
 	mod->btf_base_data = any_section_objs(info, ".BTF.base", 1,
 					      &mod->btf_base_data_size);
@@ -3172,7 +3172,7 @@ static noinline int do_init_module(struct module *mod)
 		mod->mem[type].size = 0;
 	}
 
-#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES) || IS_MODULE(CONFIG_DEBUG_INFO_BTF)
 	/* .BTF is not SHF_ALLOC and will get removed, so sanitize pointers */
 	mod->btf_data = NULL;
 	mod->btf_base_data = NULL;
-- 
2.47.3


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

* [PATCH bpf-next 5/6] bpf: defer registrations until the vmlinux BTF is available
  2026-09-23  5:39 [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Jay Wang
                   ` (3 preceding siblings ...)
  2026-09-23  5:39 ` [PATCH bpf-next 4/6] bpf: take the vmlinux BTF from the btf_vmlinux module Jay Wang
@ 2026-09-23  5:39 ` Jay Wang
  2026-09-23  6:41   ` bot+bpf-ci
  2026-09-23  5:39 ` [PATCH bpf-next 6/6] kbuild, bpf: allow building the vmlinux BTF as a module Jay Wang
  2026-09-23  8:27 ` [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Alan Maguire
  6 siblings, 1 reply; 11+ messages in thread
From: Jay Wang @ 2026-09-23  5:39 UTC (permalink / raw)
  To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Kumar Kartikeya Dwivedi
  Cc: Alan Maguire, Martin KaFai Lau, Yonghong Song, Nathan Chancellor,
	Nicolas Schier, linux-kbuild, Luis Chamberlain, Petr Pavlu,
	linux-modules, Arnd Bergmann, linux-kernel,
	Hazem Mohamed Abuelfotoh, Bjoern Doebel, jay.wang.upstream

With CONFIG_DEBUG_INFO_BTF=m the vmlinux BTF is loaded on first use.  For
that to save anything, nothing may pull it in at boot.  The verifier no
longer does since the previous patches; two things still do:

 - register_btf_kfunc_id_set(), register_btf_id_dtor_kfuncs() and
   register_bpf_struct_ops() for vmlinux run from initcalls and need the
   parsed BTF.  Queue them instead (btf_defer_reg()) and apply them in
   btf_parse_vmlinux(), before the BTF is published, so that no program
   can see a vmlinux BTF without its kfuncs and struct_ops.  Applying a
   struct_ops runs its ->init(), which registers the kfunc sets of its
   hook; those land back on the queue, so it is drained in a loop until a
   pass adds nothing, and only then are new registrations applied
   directly.  The dtor arrays are copied: every caller in the tree builds
   them on the stack of its initcall.

 - Module BTF is split BTF against the vmlinux BTF and used to be parsed
   in the module notifier.  The notifier cannot load btf_vmlinux (that
   would nest a module load in a module load), so a module loaded before
   the vmlinux BTF keeps a copy of its .BTF and .BTF.base, is exposed in
   /sys/kernel/btf right away (the raw bytes need no parsing) and gets a
   list entry with btf == NULL.  Its kfunc, dtor kfunc and struct_ops
   registrations wait on that entry.  When the vmlinux BTF arrives,
   btf_parse_deferred_modules() parses the kept copies, gives them ids
   and applies the waiting registrations; the copy is the one
   btf_parse_module() makes anyway, so the sysfs file keeps pointing at
   valid data.  Registrations are applied after dropping btf_module_mutex
   because they walk the module list (btf_check_kfunc_name()).

A module whose BTF turns out to mismatch at that point is already
running and keeps running without BTF, with a warning.  With =y such a
module would have been refused at load time unless
CONFIG_MODULE_ALLOW_BTF_MISMATCH; that check only applies to modules
loaded after the vmlinux BTF.

Walkers of the module BTF list skip entries whose BTF is not parsed yet.

With =y the BTF is present from boot, so the queues never fill and the
notifier takes the existing path.  Still nothing is reachable until the
Kconfig symbol becomes a tristate.

Signed-off-by: Jay Wang <wanjay@amazon.com>
---
 include/linux/btf.h   |   5 +
 kernel/bpf/btf.c      | 426 +++++++++++++++++++++++++++++++++++++++++-
 kernel/bpf/verifier.c |   9 +-
 3 files changed, 433 insertions(+), 7 deletions(-)

diff --git a/include/linux/btf.h b/include/linux/btf.h
index 0bf10811fe53..3b99d6386dec 100644
--- a/include/linux/btf.h
+++ b/include/linux/btf.h
@@ -583,6 +583,11 @@ const char *btf_str_by_offset(const struct btf *btf, u32 offset);
 struct btf *btf_parse_vmlinux(void);
 void *btf_vmlinux_data(u32 *size, bool load);
 u32 btf_vmlinux_size(void);
+#if IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+void btf_parse_deferred_modules(void);
+#else
+static inline void btf_parse_deferred_modules(void) {}
+#endif
 struct btf *bpf_prog_get_target_btf(const struct bpf_prog *prog);
 u32 *btf_kfunc_flags(const struct btf *btf, u32 kfunc_btf_id, const struct bpf_prog *prog);
 int btf_kfunc_check_flag(const struct btf *btf, u32 kfunc_btf_id, u32 flag);
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 50eb7a95fd82..cbba20a908e9 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -6551,6 +6551,8 @@ static struct btf *btf_parse_base(struct btf_verifier_env *env, const char *name
 	return ERR_PTR(err);
 }
 
+static void btf_apply_deferred_vmlinux_regs(struct btf *btf);
+
 struct btf *btf_parse_vmlinux(void)
 {
 	struct btf_verifier_env *env = NULL;
@@ -6581,7 +6583,15 @@ struct btf *btf_parse_vmlinux(void)
 	if (err) {
 		btf_free(btf);
 		btf = ERR_PTR(err);
+		goto err_out;
 	}
+
+	/*
+	 * With CONFIG_DEBUG_INFO_BTF=m, kfunc, dtor kfunc and struct_ops
+	 * registrations for vmlinux made before the BTF was available were
+	 * queued; apply them now, before the BTF becomes visible to anyone.
+	 */
+	btf_apply_deferred_vmlinux_regs(btf);
 err_out:
 	btf_verifier_env_free(env);
 	return btf;
@@ -8697,13 +8707,62 @@ enum {
 #define BTF_MODULE_NOTIFIER 1
 #endif
 
+/*
+ * CONFIG_DEBUG_INFO_BTF=m: a kfunc, dtor kfunc or struct_ops registration
+ * made while the BTF it applies to is not available yet.  Kept until the BTF
+ * arrives, see btf_defer_reg() and btf_apply_deferred_regs().
+ */
+enum btf_deferred_reg_kind {
+	BTF_DEFERRED_KFUNC_SET,
+	BTF_DEFERRED_DTOR_KFUNCS,
+	BTF_DEFERRED_STRUCT_OPS,
+};
+
+struct btf_deferred_reg {
+	struct list_head list;
+	enum btf_deferred_reg_kind kind;
+	/* set while the registration is queued for a module BTF */
+	struct btf *btf;
+	struct module *module;
+	union {
+		struct {
+			enum btf_kfunc_hook hook;
+			const struct btf_kfunc_id_set *kset;
+		} kfunc;
+		struct {
+			const struct btf_id_dtor_kfunc *dtors;
+			u32 cnt;
+		} dtor;
+		struct bpf_struct_ops *st_ops;
+	};
+};
+
 #ifdef BTF_MODULE_NOTIFIER
+static void btf_free_deferred_regs(struct list_head *regs);
+#if IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+static void btf_free_deferred_reg(struct btf_deferred_reg *reg);
+static void btf_apply_deferred_regs(struct list_head *regs);
+#endif
+
 struct btf_module {
 	struct list_head list;
 	struct module *module;
 	struct btf *btf;
 	struct bin_attribute *sysfs_attr;
 	int flags;
+	/*
+	 * CONFIG_DEBUG_INFO_BTF=m: a module loaded before the vmlinux BTF is
+	 * available cannot have its BTF parsed yet.  Its .BTF and .BTF.base
+	 * sections are copied here and parsed once the vmlinux BTF arrives
+	 * (btf_parse_deferred_modules()); @btf is NULL until then.
+	 * Registrations of the module's kfuncs, dtor kfuncs and struct_ops
+	 * wait in @deferred_regs.
+	 */
+	void *data;
+	void *base_data;
+	u32 data_size;
+	u32 base_data_size;
+	struct list_head deferred_regs;
 };
 
 static LIST_HEAD(btf_modules);
@@ -8747,8 +8806,14 @@ static void btf_module_free(struct btf_module *btf_mod)
 {
 	if (btf_mod->sysfs_attr)
 		sysfs_remove_bin_file(btf_kobj, btf_mod->sysfs_attr);
-	purge_cand_cache(btf_mod->btf);
-	btf_put(btf_mod->btf);
+	if (btf_mod->btf) {
+		purge_cand_cache(btf_mod->btf);
+		btf_put(btf_mod->btf);
+	} else {
+		kvfree(btf_mod->data);
+		kvfree(btf_mod->base_data);
+	}
+	btf_free_deferred_regs(&btf_mod->deferred_regs);
 	kfree(btf_mod->sysfs_attr);
 	kfree(btf_mod);
 }
@@ -8792,11 +8857,55 @@ static int btf_vmlinux_module_coming(struct module *mod)
 	smp_store_release(&btf_vmlinux_raw, data);
 	return 0;
 }
+
+/*
+ * The vmlinux BTF is not available yet and must not be loaded from the
+ * module notifier (that would nest a module load into a module load).  Keep
+ * the module's BTF for btf_parse_deferred_modules().  The .BTF data can be
+ * exposed in sysfs right away, it needs no parsing.
+ */
+static int btf_module_defer(struct btf_module *btf_mod, struct module *mod)
+{
+	int err;
+
+	btf_mod->data = kvmemdup(mod->btf_data, mod->btf_data_size,
+				 GFP_KERNEL | __GFP_NOWARN);
+	if (!btf_mod->data)
+		return -ENOMEM;
+	btf_mod->data_size = mod->btf_data_size;
+
+	if (mod->btf_base_data) {
+		btf_mod->base_data = kvmemdup(mod->btf_base_data,
+					      mod->btf_base_data_size,
+					      GFP_KERNEL | __GFP_NOWARN);
+		if (!btf_mod->base_data) {
+			kvfree(btf_mod->data);
+			return -ENOMEM;
+		}
+		btf_mod->base_data_size = mod->btf_base_data_size;
+	}
+
+	err = btf_module_sysfs_add(btf_mod, mod->name, btf_mod->data,
+				   btf_mod->data_size);
+	if (err) {
+		kvfree(btf_mod->data);
+		kvfree(btf_mod->base_data);
+		return err;
+	}
+
+	list_add(&btf_mod->list, &btf_modules);
+	return 0;
+}
 #else
 static int btf_vmlinux_module_coming(struct module *mod)
 {
 	return 0;
 }
+
+static int btf_module_defer(struct btf_module *btf_mod, struct module *mod)
+{
+	return 0;
+}
 #endif
 
 static int btf_module_notify(struct notifier_block *nb, unsigned long op,
@@ -8828,6 +8937,24 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			goto out;
 		}
 		btf_mod->module = module;
+		INIT_LIST_HEAD(&btf_mod->deferred_regs);
+
+		if (IS_MODULE(CONFIG_DEBUG_INFO_BTF)) {
+			mutex_lock(&btf_module_mutex);
+			/* Pairs with the publication in bpf_get_btf_vmlinux() */
+			if (!smp_load_acquire(&btf_vmlinux)) {
+				err = btf_module_defer(btf_mod, mod);
+				mutex_unlock(&btf_module_mutex);
+				if (err) {
+					pr_warn("failed to keep module [%s] BTF: %d\n",
+						mod->name, err);
+					kfree(btf_mod);
+					err = 0;
+				}
+				goto out;
+			}
+			mutex_unlock(&btf_module_mutex);
+		}
 
 		btf = btf_parse_module(mod->name, bpf_get_btf_vmlinux(),
 				       mod->btf_data, mod->btf_data_size, false,
@@ -8882,7 +9009,8 @@ static int btf_module_notify(struct notifier_block *nb, unsigned long op,
 			 * btf_try_get_module() on such BTFs will fail. This may
 			 * be called again on btf_put(), but it's ok to do so.
 			 */
-			btf_free_id(btf_mod->btf);
+			if (btf_mod->btf)
+				btf_free_id(btf_mod->btf);
 			list_del(&btf_mod->list);
 			btf_module_free(btf_mod);
 			break;
@@ -8905,6 +9033,87 @@ static int __init btf_module_init(void)
 }
 
 fs_initcall(btf_module_init);
+
+#if IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+/*
+ * CONFIG_DEBUG_INFO_BTF=m: the vmlinux BTF has just become available.  Parse
+ * the BTF of the modules that were loaded before it, and apply the
+ * registrations that waited for them.  Called from bpf_get_btf_vmlinux()
+ * once btf_vmlinux is published, with no locks held.
+ */
+void btf_parse_deferred_modules(void)
+{
+	/* Pairs with the publication in bpf_get_btf_vmlinux() */
+	struct btf *vmlinux_btf = smp_load_acquire(&btf_vmlinux);
+	struct btf_deferred_reg *reg, *rtmp;
+	struct btf_module *btf_mod, *tmp;
+	bool parsed = false;
+	LIST_HEAD(regs);
+	struct btf *btf;
+	int err;
+
+	if (IS_ERR_OR_NULL(vmlinux_btf))
+		return;
+
+	mutex_lock(&btf_module_mutex);
+	list_for_each_entry_safe(btf_mod, tmp, &btf_modules, list) {
+		if (btf_mod->btf)
+			continue;
+
+		btf = btf_parse_module(btf_mod->module->name, vmlinux_btf,
+				       btf_mod->data, btf_mod->data_size, true,
+				       btf_mod->base_data, btf_mod->base_data_size);
+		err = PTR_ERR_OR_ZERO(btf);
+		if (!err) {
+			err = btf_alloc_id(btf);
+			if (err) {
+				/* btf owns the data now, btf_free() drops it */
+				btf_mod->data = NULL;
+				btf_free(btf);
+			}
+		}
+		if (err) {
+			/*
+			 * The module is loaded and stays.  Unlike at load time
+			 * there is no way to reject it, so drop its BTF.
+			 */
+			pr_warn("failed to validate module [%s] BTF: %d\n",
+				btf_mod->module->name, err);
+			list_del(&btf_mod->list);
+			btf_module_free(btf_mod);
+			continue;
+		}
+
+		/* btf->data is btf_mod->data now, the sysfs file keeps working */
+		btf_mod->data = NULL;
+		kvfree(btf_mod->base_data);
+		btf_mod->base_data = NULL;
+		btf_mod->btf = btf;
+		parsed = true;
+
+		/*
+		 * Registrations are applied after dropping the mutex (they
+		 * walk btf_modules); pin what they need until then.
+		 */
+		list_for_each_entry_safe(reg, rtmp, &btf_mod->deferred_regs, list) {
+			list_del(&reg->list);
+			if (!try_module_get(btf_mod->module)) {
+				btf_free_deferred_reg(reg);
+				continue;
+			}
+			btf_get(btf);
+			reg->btf = btf;
+			reg->module = btf_mod->module;
+			list_add_tail(&reg->list, &regs);
+		}
+	}
+	mutex_unlock(&btf_module_mutex);
+
+	if (parsed)
+		purge_cand_cache(NULL);
+	btf_apply_deferred_regs(&regs);
+}
+#endif /* IS_MODULE(CONFIG_DEBUG_INFO_BTF) */
 #endif /* BTF_MODULE_NOTIFIER */
 
 struct module *btf_try_get_module(const struct btf *btf)
@@ -8957,8 +9166,11 @@ struct btf *btf_get_module_btf(const struct module *module)
 		if (btf_mod->module != module)
 			continue;
 
-		btf_get(btf_mod->btf);
-		btf = btf_mod->btf;
+		/* NULL while waiting for the vmlinux BTF (CONFIG_DEBUG_INFO_BTF=m) */
+		if (btf_mod->btf) {
+			btf_get(btf_mod->btf);
+			btf = btf_mod->btf;
+		}
 		break;
 	}
 	mutex_unlock(&btf_module_mutex);
@@ -9135,7 +9347,8 @@ static int btf_check_kfunc_name(struct btf *btf, const char *func_name, u32 kind
 #ifdef CONFIG_DEBUG_INFO_BTF_MODULES
 	guard(mutex)(&btf_module_mutex);
 	list_for_each_entry_safe(btf_mod, tmp, &btf_modules, list) {
-		if (btf_mod->btf == btf)
+		/* skip ourselves and, with CONFIG_DEBUG_INFO_BTF=m, unparsed BTF */
+		if (btf_mod->btf == btf || !btf_mod->btf)
 			continue;
 		id = btf_find_by_name_kind(btf_mod->btf, func_name, kind);
 		if (id >= 0) {
@@ -9475,12 +9688,22 @@ static int btf_kfunc_id_set_add(struct btf *btf, enum btf_kfunc_hook hook,
 	return btf_populate_kfunc_set(btf, hook, kset);
 }
 
+static int btf_defer_reg(struct module *owner, const struct btf_deferred_reg *tmpl);
+
 static int __register_btf_kfunc_id_set(enum btf_kfunc_hook hook,
 				       const struct btf_kfunc_id_set *kset)
 {
+	struct btf_deferred_reg tmpl = {
+		.kind = BTF_DEFERRED_KFUNC_SET,
+		.kfunc = { .hook = hook, .kset = kset },
+	};
 	struct btf *btf;
 	int ret;
 
+	ret = btf_defer_reg(kset->owner, &tmpl);
+	if (ret)
+		return ret > 0 ? 0 : ret;
+
 	btf = btf_get_module_btf(kset->owner);
 	if (!btf)
 		return check_btf_kconfigs(kset->owner, "kfunc");
@@ -9649,9 +9872,17 @@ static int btf_dtor_kfuncs_add(struct btf *btf, const struct btf_id_dtor_kfunc *
 int register_btf_id_dtor_kfuncs(const struct btf_id_dtor_kfunc *dtors, u32 add_cnt,
 				struct module *owner)
 {
+	struct btf_deferred_reg tmpl = {
+		.kind = BTF_DEFERRED_DTOR_KFUNCS,
+		.dtor = { .dtors = dtors, .cnt = add_cnt },
+	};
 	struct btf *btf;
 	int ret;
 
+	ret = btf_defer_reg(owner, &tmpl);
+	if (ret)
+		return ret > 0 ? 0 : ret;
+
 	btf = btf_get_module_btf(owner);
 	if (!btf)
 		return check_btf_kconfigs(owner, "dtor kfuncs");
@@ -10321,9 +10552,17 @@ static int btf_struct_ops_add(struct btf *btf, struct bpf_struct_ops *st_ops)
 
 int __register_bpf_struct_ops(struct bpf_struct_ops *st_ops)
 {
+	struct btf_deferred_reg tmpl = {
+		.kind = BTF_DEFERRED_STRUCT_OPS,
+		.st_ops = st_ops,
+	};
 	struct btf *btf;
 	int err;
 
+	err = btf_defer_reg(st_ops->owner, &tmpl);
+	if (err)
+		return err > 0 ? 0 : err;
+
 	btf = btf_get_module_btf(st_ops->owner);
 	if (!btf)
 		return check_btf_kconfigs(st_ops->owner, "struct_ops");
@@ -10335,8 +10574,183 @@ int __register_bpf_struct_ops(struct bpf_struct_ops *st_ops)
 	return err;
 }
 EXPORT_SYMBOL_GPL(__register_bpf_struct_ops);
+#else
+static int btf_struct_ops_add(struct btf *btf, struct bpf_struct_ops *st_ops)
+{
+	return -EOPNOTSUPP;
+}
+#endif
+
+/*
+ * CONFIG_DEBUG_INFO_BTF=m: registrations made before the BTF they apply to
+ * is available.  Registrations for vmlinux wait in btf_vmlinux_deferred_regs
+ * until btf_parse_vmlinux() applies them; registrations for a module wait in
+ * its struct btf_module until btf_parse_deferred_modules() does.  Both lists
+ * are protected by btf_module_mutex.
+ */
+#ifdef BTF_MODULE_NOTIFIER
+static LIST_HEAD(btf_vmlinux_deferred_regs);
+/* Set when the vmlinux BTF is parsed; new registrations apply directly */
+static bool btf_vmlinux_regs_closed;
+
+/*
+ * Queue @tmpl if the BTF for @owner is not available yet.  Returns 1 if the
+ * registration was queued and is to be considered done, 0 if the caller has
+ * to apply it, or -ENOMEM.
+ */
+static int btf_defer_reg(struct module *owner, const struct btf_deferred_reg *tmpl)
+{
+	struct list_head *head = NULL;
+	struct btf_deferred_reg *reg;
+	struct btf_module *btf_mod;
+
+	if (!IS_MODULE(CONFIG_DEBUG_INFO_BTF))
+		return 0;
+
+	guard(mutex)(&btf_module_mutex);
+	if (!owner) {
+		if (!btf_vmlinux_regs_closed)
+			head = &btf_vmlinux_deferred_regs;
+	} else {
+		list_for_each_entry(btf_mod, &btf_modules, list) {
+			if (btf_mod->module != owner)
+				continue;
+			if (!btf_mod->btf)
+				head = &btf_mod->deferred_regs;
+			break;
+		}
+	}
+	if (!head)
+		return 0;
+
+	reg = kmemdup(tmpl, sizeof(*reg), GFP_KERNEL);
+	if (!reg)
+		return -ENOMEM;
+	/*
+	 * kfunc id sets and struct_ops are static data of their owner, but
+	 * the dtor arrays are commonly built on the stack of the initcall.
+	 */
+	if (reg->kind == BTF_DEFERRED_DTOR_KFUNCS) {
+		reg->dtor.dtors = kmemdup_array(tmpl->dtor.dtors, tmpl->dtor.cnt,
+						sizeof(*tmpl->dtor.dtors), GFP_KERNEL);
+		if (!reg->dtor.dtors) {
+			kfree(reg);
+			return -ENOMEM;
+		}
+	}
+	list_add_tail(&reg->list, head);
+	return 1;
+}
+
+static void btf_free_deferred_reg(struct btf_deferred_reg *reg)
+{
+	if (reg->kind == BTF_DEFERRED_DTOR_KFUNCS)
+		kfree(reg->dtor.dtors);
+	kfree(reg);
+}
+
+static const char *btf_deferred_reg_name(const struct btf_deferred_reg *reg)
+{
+	switch (reg->kind) {
+	case BTF_DEFERRED_KFUNC_SET:	return "kfunc set";
+	case BTF_DEFERRED_DTOR_KFUNCS:	return "dtor kfuncs";
+	case BTF_DEFERRED_STRUCT_OPS:	return "struct_ops";
+	}
+	return "?";
+}
+
+static int btf_apply_deferred_reg(struct btf *btf, const struct btf_deferred_reg *reg)
+{
+	switch (reg->kind) {
+	case BTF_DEFERRED_KFUNC_SET:
+		return btf_kfunc_id_set_add(btf, reg->kfunc.hook, reg->kfunc.kset);
+	case BTF_DEFERRED_DTOR_KFUNCS:
+		return btf_dtor_kfuncs_add(btf, reg->dtor.dtors, reg->dtor.cnt);
+	case BTF_DEFERRED_STRUCT_OPS:
+		return btf_struct_ops_add(btf, reg->st_ops);
+	}
+	return -EINVAL;
+}
+
+#if IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+/* Apply and free the registrations in @regs; each is pinned to its BTF and module. */
+static void btf_apply_deferred_regs(struct list_head *regs)
+{
+	struct btf_deferred_reg *reg, *tmp;
+	int err;
+
+	list_for_each_entry_safe(reg, tmp, regs, list) {
+		err = btf_apply_deferred_reg(reg->btf, reg);
+		if (err)
+			pr_warn("failed to register deferred %s for module [%s] BTF: %d\n",
+				btf_deferred_reg_name(reg), reg->btf->name, err);
+		btf_put(reg->btf);
+		module_put(reg->module);
+		list_del(&reg->list);
+		btf_free_deferred_reg(reg);
+	}
+}
 #endif
 
+static void btf_free_deferred_regs(struct list_head *regs)
+{
+	struct btf_deferred_reg *reg, *tmp;
+
+	list_for_each_entry_safe(reg, tmp, regs, list) {
+		list_del(&reg->list);
+		btf_free_deferred_reg(reg);
+	}
+}
+
+/*
+ * The vmlinux BTF has just been parsed; apply the registrations that waited
+ * for it.  Runs under btf_vmlinux_lock, before @btf is published, so nothing
+ * can observe a vmlinux BTF without its kfuncs and struct_ops.
+ *
+ * Applying a registration can queue further ones: a struct_ops ->init()
+ * registers the kfuncs of its hook.  Those must not go through
+ * bpf_get_btf_vmlinux() (we hold its lock), so the queue stays open until
+ * a pass applies nothing new, and only then are registrations applied directly.
+ */
+static void btf_apply_deferred_vmlinux_regs(struct btf *btf)
+{
+	struct btf_deferred_reg *reg, *tmp;
+	LIST_HEAD(regs);
+	int err;
+
+	if (!IS_MODULE(CONFIG_DEBUG_INFO_BTF))
+		return;
+
+	mutex_lock(&btf_module_mutex);
+	while (!list_empty(&btf_vmlinux_deferred_regs)) {
+		list_splice_init(&btf_vmlinux_deferred_regs, &regs);
+		mutex_unlock(&btf_module_mutex);
+
+		list_for_each_entry_safe(reg, tmp, &regs, list) {
+			err = btf_apply_deferred_reg(btf, reg);
+			if (err)
+				pr_warn("failed to register deferred %s for vmlinux BTF: %d\n",
+					btf_deferred_reg_name(reg), err);
+			list_del(&reg->list);
+			btf_free_deferred_reg(reg);
+		}
+
+		mutex_lock(&btf_module_mutex);
+	}
+	btf_vmlinux_regs_closed = true;
+	mutex_unlock(&btf_module_mutex);
+}
+#else
+static int btf_defer_reg(struct module *owner, const struct btf_deferred_reg *tmpl)
+{
+	return 0;
+}
+
+static void btf_apply_deferred_vmlinux_regs(struct btf *btf)
+{
+}
+#endif /* BTF_MODULE_NOTIFIER */
+
 bool btf_param_match_suffix(const struct btf *btf,
 			    const struct btf_param *arg,
 			    const char *suffix)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a7b73bc146a8..b6f094d5306a 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -21160,12 +21160,14 @@ int bpf_check_attach_btf_id_multi(struct btf *btf, struct bpf_prog *prog, u32 bt
 /*
  * Returns the parsed vmlinux BTF, NULL if the kernel has none, or an ERR_PTR
  * if it is malformed.  With CONFIG_DEBUG_INFO_BTF=m the BTF lives in the
- * btf_vmlinux module; the first caller loads it and parses it.  May sleep.
+ * btf_vmlinux module; the first caller loads it, parses it and then registers
+ * the BTF of the modules that were loaded before it.  May sleep.
  */
 struct btf *bpf_get_btf_vmlinux(void)
 {
 	/* Pairs with the smp_store_release() on the parse path below. */
 	struct btf *btf = smp_load_acquire(&btf_vmlinux);
+	bool parsed = false;
 	u32 size;
 
 	if (btf || !IS_ENABLED(CONFIG_DEBUG_INFO_BTF))
@@ -21190,8 +21192,13 @@ struct btf *bpf_get_btf_vmlinux(void)
 		 * on the lockless fast path above.
 		 */
 		smp_store_release(&btf_vmlinux, btf);
+		parsed = true;
 	}
 	mutex_unlock(&btf_vmlinux_lock);
+
+	if (parsed && IS_MODULE(CONFIG_DEBUG_INFO_BTF))
+		btf_parse_deferred_modules();
+
 	return btf;
 }
 
-- 
2.47.3


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

* [PATCH bpf-next 6/6] kbuild, bpf: allow building the vmlinux BTF as a module
  2026-09-23  5:39 [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Jay Wang
                   ` (4 preceding siblings ...)
  2026-09-23  5:39 ` [PATCH bpf-next 5/6] bpf: defer registrations until the vmlinux BTF is available Jay Wang
@ 2026-09-23  5:39 ` Jay Wang
  2026-09-23  8:27 ` [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Alan Maguire
  6 siblings, 0 replies; 11+ messages in thread
From: Jay Wang @ 2026-09-23  5:39 UTC (permalink / raw)
  To: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Kumar Kartikeya Dwivedi
  Cc: Alan Maguire, Martin KaFai Lau, Yonghong Song, Nathan Chancellor,
	Nicolas Schier, linux-kbuild, Luis Chamberlain, Petr Pavlu,
	linux-modules, Arnd Bergmann, linux-kernel,
	Hazem Mohamed Abuelfotoh, Bjoern Doebel, jay.wang.upstream

Make CONFIG_DEBUG_INFO_BTF a tristate.  With =m the vmlinux BTF is not
part of the kernel image: it is carried by a new module, btf_vmlinux, and
loaded the first time something needs it.  Nothing that works with =y
stops working; the 5.4 MiB of read-only data (distribution config) is
simply not there on systems where nothing uses it.

The only way to save that memory today is CONFIG_DEBUG_INFO_BTF=n, which
a distribution cannot ship: one binary goes to every user, and off takes
BTF away from the users of CO-RE, fentry/fexit, kfuncs, struct_ops,
sched_ext or bpf-lsm.  Whether BTF is used is a property of the
workload, not of the build, so let the first user decide.

The BTF is generated as before, but with =m the .BTF section is linked
into vmlinux as a non-loadable section (like .comment), so the vmlinux
ELF still carries it for module BTF generation and tooling while the
image does not.  .BTF_ids stays loadable, the verifier needs it once the
BTF is loaded.  The object that carries .BTF also carries .BTF.meta, the
size and SHA-256 of the BTF (struct btf_vmlinux_meta, checked by the
module notifier); the first link, which the BTF is generated from, gets a
zeroed .BTF.meta of the same size.  kernel/bpf/btf_vmlinux.c is an empty
carrier module; scripts/gen-btf.sh gives it the vmlinux .BTF as its own
.BTF section instead of generating split BTF for it, so modules depend
on vmlinux with =m as they do with CONFIG_DEBUG_INFO_BTF_MODULES.

Makefiles that compiled kfunc objects with obj-$(CONFIG_DEBUG_INFO_BTF)
now treat m as y, and the #ifdef CONFIG_DEBUG_INFO_BTF sites that must
also apply with =m (the .BTF_ids tables, type tags, tracepoint and
syscall BTF ids) use IS_ENABLED(): the generated BTF and its id tables
are the same for =y and =m, only the delivery of the blob differs.

CONFIG_BPF_PRELOAD is not selectable with =m: its iterator programs
attach through the vmlinux BTF, so every bpffs mount (systemd does one
at boot) would load it and defeat the point.

The module has no exit: once loaded the BTF stays, as with =y.  The
runtime side -- loading the module on first use, checking it against
.BTF.meta, deferring kfunc and struct_ops registrations and module BTF
until it arrives -- is in the preceding patches; this one makes it
selectable.

Tested with 1 GiB of memory, same tree, =y vs =m, both with
CONFIG_DEBUG_INFO_BTF_MODULES=y:

 - MemTotal is ~5.4 MB higher with =m while the BTF is unused: the size
   of the .BTF section.
 - stat() of /sys/kernel/btf/vmlinux reports the BTF size before it is
   loaded, as the btf_sysfs selftest expects.
 - With BTF in use, MemFree is the same within run-to-run noise.
 - Modules loaded before the trigger (ext4, nf_conntrack and its kfuncs,
   xfrm_interface) appear in /sys/kernel/btf immediately and get BTF ids
   once the BTF is loaded; a socket filter loads without loading the
   module; a kprobe program calling bpf_get_current_task_btf(), opening
   /sys/kernel/btf/vmlinux or BPF_BTF_GET_NEXT_ID each load it.
 - A carrier module with one byte of its .BTF changed is refused with
   "BTF does not match this kernel" and leaves no state behind.
 - After the load: fstat/read/mmap of /sys/kernel/btf/vmlinux, a
   struct_ops map for tcp_congestion_ops, a syscall program calling the
   bpf_task_from_pid()/bpf_task_release() kfuncs, and modules loaded
   afterwards (nf_nat) all work as with =y.
 - =m without DEBUG_INFO_BTF_MODULES, and =y, build and pass the same
   tests.

Signed-off-by: Jay Wang <wanjay@amazon.com>
---
 Documentation/bpf/btf.rst         | 35 ++++++++++++
 Makefile                          |  8 ++-
 include/asm-generic/vmlinux.lds.h | 30 +++++++++-
 include/linux/btf_ids.h           |  2 +-
 include/linux/compiler_types.h    |  2 +-
 include/trace/trace_events.h      |  2 +-
 kernel/bpf/Makefile               |  6 +-
 kernel/bpf/btf_vmlinux.c          | 23 ++++++++
 kernel/bpf/preload/Kconfig        |  4 ++
 kernel/trace/trace_syscalls.c     |  6 +-
 lib/Kconfig.debug                 | 13 ++++-
 net/netfilter/Makefile            |  6 +-
 net/xfrm/Makefile                 |  4 +-
 scripts/Makefile.modfinal         | 14 +++--
 scripts/gen-btf.sh                | 93 +++++++++++++++++++++++++++++--
 scripts/link-vmlinux.sh           | 25 +++++++--
 16 files changed, 244 insertions(+), 29 deletions(-)
 create mode 100644 kernel/bpf/btf_vmlinux.c

diff --git a/Documentation/bpf/btf.rst b/Documentation/bpf/btf.rst
index 004aa1058d85..a835231187ef 100644
--- a/Documentation/bpf/btf.rst
+++ b/Documentation/bpf/btf.rst
@@ -1197,6 +1197,41 @@ format.::
             .long   58
             .long   8206                    # Line 8 Col 14
 
+6.1 Kernel BTF
+--------------
+
+With CONFIG_DEBUG_INFO_BTF=y the BTF of the kernel is generated at link time
+from its DWARF and placed in the .BTF section of vmlinux, which is read-only
+data of the kernel image. It is available as /sys/kernel/btf/vmlinux and, if
+CONFIG_DEBUG_INFO_BTF_MODULES is set, module BTF is generated as split BTF
+against it and available as /sys/kernel/btf/<module>.
+
+With CONFIG_DEBUG_INFO_BTF=m the same BTF is generated, but it is not part of
+the kernel image (the vmlinux ELF file still carries it in a non-loadable .BTF
+section for tooling and module BTF generation). It is delivered by the
+btf_vmlinux module, which the kernel loads on demand the first time the BTF is
+needed: when /sys/kernel/btf/vmlinux is read or mmap()ed, when kernel BTF
+objects are enumerated (BPF_BTF_GET_NEXT_ID), or when a BPF program needs
+kernel type information (an attach_btf_id, a kfunc call, a ksym, a map pointer
+or a helper that takes or returns a kernel BTF pointer). Until then no memory
+is used for it, and afterwards nothing differs from =y. In particular:
+
+  * /sys/kernel/btf/vmlinux exists from boot with its final size.
+  * Modules loaded before the vmlinux BTF are exposed in /sys/kernel/btf right
+    away, their BTF is parsed and gets a BTF id once the vmlinux BTF is
+    loaded, together with their kfunc and struct_ops registrations.
+  * kfunc, dtor kfunc and struct_ops registrations of the kernel itself are
+    applied before the BTF becomes visible.
+  * The kernel only accepts the BTF it was built with: the size and SHA-256 of
+    the BTF are linked into the kernel and checked against the module.
+  * Once loaded the BTF stays; the module cannot be unloaded.
+
+If the module is not available (not installed, or the root file system is not
+mounted yet), the kernel behaves as one built without BTF and retries next
+time. CONFIG_BPF_PRELOAD is not available with =m: its iterators attach through
+the vmlinux BTF, so mounting bpffs would load it. bpf_snprintf_btf() and bpf_seq_printf_btf() only use the BTF if it has
+already been parsed, as they run in program context.
+
 7. Testing
 ==========
 
diff --git a/Makefile b/Makefile
index 66654fa71655..0a37decd9d01 100644
--- a/Makefile
+++ b/Makefile
@@ -1208,7 +1208,8 @@ endif
 # include additional Makefiles when needed
 include-y			:= scripts/Makefile.warn
 include-$(CONFIG_DEBUG_INFO)	+= scripts/Makefile.debug
-include-$(CONFIG_DEBUG_INFO_BTF)+= scripts/Makefile.btf
+# CONFIG_DEBUG_INFO_BTF is a tristate; BTF is generated for both y and m
+include-$(subst m,y,$(CONFIG_DEBUG_INFO_BTF)) += scripts/Makefile.btf
 include-$(CONFIG_KASAN)		+= scripts/Makefile.kasan
 include-$(CONFIG_KCSAN)		+= scripts/Makefile.kcsan
 include-$(CONFIG_KMSAN)		+= scripts/Makefile.kmsan
@@ -1744,8 +1745,9 @@ endif
 #
 
 # *.ko are usually independent of vmlinux, but CONFIG_DEBUG_INFO_BTF_MODULES
-# is an exception.
-ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+# is an exception, and so is the btf_vmlinux module with CONFIG_DEBUG_INFO_BTF=m,
+# which carries the vmlinux BTF.
+ifneq ($(CONFIG_DEBUG_INFO_BTF_MODULES)$(filter m,$(CONFIG_DEBUG_INFO_BTF)),)
 KBUILD_BUILTIN := y
 modules: vmlinux
 endif
diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
index b2988aa12f66..9e2f4861fef2 100644
--- a/include/asm-generic/vmlinux.lds.h
+++ b/include/asm-generic/vmlinux.lds.h
@@ -674,8 +674,17 @@
 
 /*
  * .BTF
+ *
+ * With CONFIG_DEBUG_INFO_BTF=y the vmlinux BTF is loaded as read-only data and
+ * bounded by __start_BTF/__stop_BTF.  With CONFIG_DEBUG_INFO_BTF=m it is still
+ * emitted into the vmlinux ELF file so that module BTF generation and tooling
+ * can read it, but as a non-loadable section (see BTF_NOLOAD in ELF_DETAILS):
+ * the btf_vmlinux module carries a copy and provides it on demand at runtime.
+ * What is loaded instead is .BTF.meta, the size and hash of that BTF (see
+ * scripts/gen-btf.sh), empty in the first link that the BTF is generated
+ * from.  .BTF_ids is needed by the kernel in both cases.
  */
-#ifdef CONFIG_DEBUG_INFO_BTF
+#if IS_BUILTIN(CONFIG_DEBUG_INFO_BTF)
 #define BTF								\
 	. = ALIGN(PAGE_SIZE);						\
 	.BTF : AT(ADDR(.BTF) - LOAD_OFFSET) {				\
@@ -685,10 +694,28 @@
 	.BTF_ids : AT(ADDR(.BTF_ids) - LOAD_OFFSET) {			\
 		*(.BTF_ids)						\
 	}
+#elif IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+#define BTF								\
+	. = ALIGN(8);							\
+	.BTF.meta : AT(ADDR(.BTF.meta) - LOAD_OFFSET) {			\
+		BOUNDED_SECTION_BY(.BTF.meta, _BTF_meta)		\
+	}								\
+	. = ALIGN(PAGE_SIZE);						\
+	.BTF_ids : AT(ADDR(.BTF_ids) - LOAD_OFFSET) {			\
+		*(.BTF_ids)						\
+	}
 #else
 #define BTF
 #endif
 
+#if IS_MODULE(CONFIG_DEBUG_INFO_BTF)
+/* quoted: BTF is a macro, an unquoted .BTF here would expand it */
+#define BTF_NOLOAD							\
+		".BTF" 0 : { *(".BTF") }
+#else
+#define BTF_NOLOAD
+#endif
+
 /*
  * Init task
  */
@@ -849,6 +876,7 @@
 /* Required sections not related to debugging. */
 #define ELF_DETAILS							\
 		.comment 0 : { *(.comment) }				\
+		BTF_NOLOAD						\
 		.symtab 0 : { *(.symtab) }				\
 		.strtab 0 : { *(.strtab) }				\
 		.shstrtab 0 : { *(.shstrtab) }				\
diff --git a/include/linux/btf_ids.h b/include/linux/btf_ids.h
index 8b5a9ee92513..c665afff100e 100644
--- a/include/linux/btf_ids.h
+++ b/include/linux/btf_ids.h
@@ -22,7 +22,7 @@ struct btf_id_set8 {
 	} pairs[];
 };
 
-#ifdef CONFIG_DEBUG_INFO_BTF
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF)
 
 #include <linux/compiler.h> /* for __PASTE */
 #include <linux/compiler_attributes.h> /* for __maybe_unused */
diff --git a/include/linux/compiler_types.h b/include/linux/compiler_types.h
index c5921f139007..a90a99849cee 100644
--- a/include/linux/compiler_types.h
+++ b/include/linux/compiler_types.h
@@ -34,7 +34,7 @@
  * Skipped when running bindgen due to a libclang issue;
  * see https://github.com/rust-lang/rust-bindgen/issues/2244.
  */
-#if defined(CONFIG_DEBUG_INFO_BTF) && defined(CONFIG_PAHOLE_HAS_BTF_TAG) && \
+#if IS_ENABLED(CONFIG_DEBUG_INFO_BTF) && defined(CONFIG_PAHOLE_HAS_BTF_TAG) && \
 	__has_attribute(btf_type_tag) && !defined(__BINDGEN__)
 # define BTF_TYPE_TAG(value) __attribute__((btf_type_tag(#value)))
 #else
diff --git a/include/trace/trace_events.h b/include/trace/trace_events.h
index 93011f800d0f..2a0098929771 100644
--- a/include/trace/trace_events.h
+++ b/include/trace/trace_events.h
@@ -398,7 +398,7 @@ static inline notrace int trace_event_get_offsets_##call(		\
 #define _TRACE_PERF_INIT(call)
 #endif /* CONFIG_PERF_EVENTS */
 
-#if defined(CONFIG_BPF_EVENTS) && defined(CONFIG_DEBUG_INFO_BTF)
+#if defined(CONFIG_BPF_EVENTS) && IS_ENABLED(CONFIG_DEBUG_INFO_BTF)
 /*
  * Per-template BTF id list, populated at link time by resolve_btfids:
  *   [0] FUNC   __bpf_trace_<call>     (the BPF dispatcher)
diff --git a/kernel/bpf/Makefile b/kernel/bpf/Makefile
index 9a92c348bbda..8ab46f496fa4 100644
--- a/kernel/bpf/Makefile
+++ b/kernel/bpf/Makefile
@@ -41,7 +41,11 @@ ifeq ($(CONFIG_INET),y)
 obj-$(CONFIG_BPF_SYSCALL) += reuseport_array.o
 endif
 ifeq ($(CONFIG_SYSFS),y)
-obj-$(CONFIG_DEBUG_INFO_BTF) += sysfs_btf.o
+obj-$(subst m,y,$(CONFIG_DEBUG_INFO_BTF)) += sysfs_btf.o
+endif
+# With CONFIG_DEBUG_INFO_BTF=m the vmlinux BTF is carried by this module
+ifeq ($(CONFIG_DEBUG_INFO_BTF),m)
+obj-m += btf_vmlinux.o
 endif
 ifeq ($(CONFIG_BPF_JIT),y)
 obj-$(CONFIG_BPF_SYSCALL) += bpf_struct_ops.o
diff --git a/kernel/bpf/btf_vmlinux.c b/kernel/bpf/btf_vmlinux.c
new file mode 100644
index 000000000000..8d89b4bb3c43
--- /dev/null
+++ b/kernel/bpf/btf_vmlinux.c
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Carrier module for the vmlinux BTF when CONFIG_DEBUG_INFO_BTF=m.
+ *
+ * This module has no code of its own.  Its .BTF section is a copy of the
+ * vmlinux BTF (see scripts/gen-btf.sh), which the BTF module notifier in
+ * kernel/bpf/btf.c recognizes by module name and installs as the vmlinux BTF.
+ * The kernel loads it on demand, the first time the vmlinux BTF is needed.
+ *
+ * There is deliberately no module_exit(): once the BTF is in use it cannot
+ * be taken away again, exactly as with CONFIG_DEBUG_INFO_BTF=y.
+ */
+#include <linux/init.h>
+#include <linux/module.h>
+
+static int __init btf_vmlinux_init(void)
+{
+	return 0;
+}
+module_init(btf_vmlinux_init);
+
+MODULE_DESCRIPTION("BTF type information for vmlinux");
+MODULE_LICENSE("GPL");
diff --git a/kernel/bpf/preload/Kconfig b/kernel/bpf/preload/Kconfig
index aef7b0bc96d6..b1600bdce7a0 100644
--- a/kernel/bpf/preload/Kconfig
+++ b/kernel/bpf/preload/Kconfig
@@ -6,6 +6,10 @@ menuconfig BPF_PRELOAD
 	# The dependency on !COMPILE_TEST prevents it from being enabled
 	# in allmodconfig or allyesconfig configurations
 	depends on !COMPILE_TEST
+	# The preloaded iterators attach through the vmlinux BTF, so with
+	# CONFIG_DEBUG_INFO_BTF=m every bpffs mount would load the BTF, which
+	# defeats the point of =m on any system that mounts bpffs at boot.
+	depends on DEBUG_INFO_BTF!=m
 	help
 	  This builds kernel module with several embedded BPF programs that are
 	  pinned into BPF FS mount point as human readable files that are
diff --git a/kernel/trace/trace_syscalls.c b/kernel/trace/trace_syscalls.c
index e35744049e3f..7a0d59c308c2 100644
--- a/kernel/trace/trace_syscalls.c
+++ b/kernel/trace/trace_syscalls.c
@@ -1304,7 +1304,7 @@ struct trace_event_functions exit_syscall_print_funcs = {
 	.trace		= print_syscall_exit,
 };
 
-#if defined(CONFIG_BPF_EVENTS) && defined(CONFIG_DEBUG_INFO_BTF)
+#if defined(CONFIG_BPF_EVENTS) && IS_ENABLED(CONFIG_DEBUG_INFO_BTF)
 /* BTF id lists for the shared sys_enter/sys_exit dispatcher tracepoints. */
 BTF_ID_LIST(syscall_enter_btf_ids)
 BTF_ID(func,   __bpf_trace_sys_enter)
@@ -1321,7 +1321,7 @@ struct trace_event_class __refdata event_class_syscall_enter = {
 	.fields_array	= syscall_enter_fields_array,
 	.get_fields	= syscall_get_enter_fields,
 	.raw_init	= init_syscall_trace,
-#if defined(CONFIG_BPF_EVENTS) && defined(CONFIG_DEBUG_INFO_BTF)
+#if defined(CONFIG_BPF_EVENTS) && IS_ENABLED(CONFIG_DEBUG_INFO_BTF)
 	.btf_ids	= syscall_enter_btf_ids,
 #endif
 };
@@ -1336,7 +1336,7 @@ struct trace_event_class __refdata event_class_syscall_exit = {
 	},
 	.fields		= LIST_HEAD_INIT(event_class_syscall_exit.fields),
 	.raw_init	= init_syscall_trace,
-#if defined(CONFIG_BPF_EVENTS) && defined(CONFIG_DEBUG_INFO_BTF)
+#if defined(CONFIG_BPF_EVENTS) && IS_ENABLED(CONFIG_DEBUG_INFO_BTF)
 	.btf_ids	= syscall_exit_btf_ids,
 #endif
 };
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 134b15a44625..5307caa39176 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -396,7 +396,7 @@ config DEBUG_INFO_SPLIT
 	  Incompatible with older versions of ccache.
 
 config DEBUG_INFO_BTF
-	bool "Generate BTF type information"
+	tristate "Generate BTF type information"
 	depends on !DEBUG_INFO_SPLIT && !DEBUG_INFO_REDUCED
 	depends on !GCC_PLUGIN_RANDSTRUCT || COMPILE_TEST
 	depends on BPF_SYSCALL
@@ -408,6 +408,17 @@ config DEBUG_INFO_BTF
 	  Turning this on requires pahole v1.22 or later, which will convert
 	  DWARF type info into equivalent deduplicated BTF type info.
 
+	  If built as a module (=m), the vmlinux BTF is not part of the
+	  kernel image.  It is carried by the btf_vmlinux module, which is
+	  loaded on demand the first time the BTF is needed: when a BPF
+	  program requires kernel type information, or when
+	  /sys/kernel/btf/vmlinux is opened.  Until then, no memory is
+	  spent on it.  The BTF is still emitted into the vmlinux ELF file
+	  (as a non-loadable section) so tooling and module BTF generation
+	  work as before.  Module BTF (DEBUG_INFO_BTF_MODULES) is registered
+	  when the vmlinux BTF becomes available.  Not compatible with
+	  BPF_PRELOAD, whose iterators would load the BTF at every bpffs mount.
+
 config PAHOLE_HAS_BTF_TAG
 	def_bool PAHOLE_VERSION >= 123
 	depends on CC_IS_CLANG
diff --git a/net/netfilter/Makefile b/net/netfilter/Makefile
index 6bf74d488a29..a2c7f00794d2 100644
--- a/net/netfilter/Makefile
+++ b/net/netfilter/Makefile
@@ -18,7 +18,7 @@ nf_conntrack-$(CONFIG_NF_CT_PROTO_GRE) += nf_conntrack_proto_gre.o
 ifeq ($(CONFIG_NF_CONNTRACK),m)
 nf_conntrack-$(CONFIG_DEBUG_INFO_BTF_MODULES) += nf_conntrack_bpf.o
 else ifeq ($(CONFIG_NF_CONNTRACK),y)
-nf_conntrack-$(CONFIG_DEBUG_INFO_BTF) += nf_conntrack_bpf.o
+nf_conntrack-$(subst m,y,$(CONFIG_DEBUG_INFO_BTF)) += nf_conntrack_bpf.o
 endif
 
 obj-$(CONFIG_NETFILTER) = netfilter.o
@@ -65,7 +65,7 @@ nf_nat-$(CONFIG_NF_NAT_OVS) += nf_nat_ovs.o
 ifeq ($(CONFIG_NF_NAT),m)
 nf_nat-$(CONFIG_DEBUG_INFO_BTF_MODULES) += nf_nat_bpf.o
 else ifeq ($(CONFIG_NF_NAT),y)
-nf_nat-$(CONFIG_DEBUG_INFO_BTF) += nf_nat_bpf.o
+nf_nat-$(subst m,y,$(CONFIG_DEBUG_INFO_BTF)) += nf_nat_bpf.o
 endif
 
 # NAT helpers
@@ -147,7 +147,7 @@ nf_flow_table-$(CONFIG_NF_FLOW_TABLE_PROCFS) += nf_flow_table_procfs.o
 ifeq ($(CONFIG_NF_FLOW_TABLE),m)
 nf_flow_table-$(CONFIG_DEBUG_INFO_BTF_MODULES) += nf_flow_table_bpf.o
 else ifeq ($(CONFIG_NF_FLOW_TABLE),y)
-nf_flow_table-$(CONFIG_DEBUG_INFO_BTF) += nf_flow_table_bpf.o
+nf_flow_table-$(subst m,y,$(CONFIG_DEBUG_INFO_BTF)) += nf_flow_table_bpf.o
 endif
 
 obj-$(CONFIG_NF_FLOW_TABLE_INET) += nf_flow_table_inet.o
diff --git a/net/xfrm/Makefile b/net/xfrm/Makefile
index 5a1787587cb3..b7f6e5046a0e 100644
--- a/net/xfrm/Makefile
+++ b/net/xfrm/Makefile
@@ -8,7 +8,7 @@ xfrm_interface-$(CONFIG_XFRM_INTERFACE) += xfrm_interface_core.o
 ifeq ($(CONFIG_XFRM_INTERFACE),m)
 xfrm_interface-$(CONFIG_DEBUG_INFO_BTF_MODULES) += xfrm_interface_bpf.o
 else ifeq ($(CONFIG_XFRM_INTERFACE),y)
-xfrm_interface-$(CONFIG_DEBUG_INFO_BTF) += xfrm_interface_bpf.o
+xfrm_interface-$(subst m,y,$(CONFIG_DEBUG_INFO_BTF)) += xfrm_interface_bpf.o
 endif
 
 obj-$(CONFIG_XFRM) := xfrm_policy.o xfrm_state.o xfrm_hash.o \
@@ -23,4 +23,4 @@ obj-$(CONFIG_XFRM_IPCOMP) += xfrm_ipcomp.o
 obj-$(CONFIG_XFRM_INTERFACE) += xfrm_interface.o
 obj-$(CONFIG_XFRM_IPTFS) += xfrm_iptfs.o
 obj-$(CONFIG_XFRM_ESPINTCP) += espintcp.o
-obj-$(CONFIG_DEBUG_INFO_BTF) += xfrm_state_bpf.o
+obj-$(subst m,y,$(CONFIG_DEBUG_INFO_BTF)) += xfrm_state_bpf.o
diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal
index 01a37ec872b9..ad182f84b5fc 100644
--- a/scripts/Makefile.modfinal
+++ b/scripts/Makefile.modfinal
@@ -46,12 +46,18 @@ quiet_cmd_btf_ko = BTF [M] $@
 		$(CONFIG_SHELL) $(srctree)/scripts/gen-btf.sh --btf_base $(objtree)/vmlinux $@; \
 	fi;
 
-# Re-generate module BTFs if either module's .ko or vmlinux changed
-%.ko: %.o %.mod.o .module-common.o $(objtree)/scripts/module.lds $(and $(CONFIG_DEBUG_INFO_BTF_MODULES),$(KBUILD_BUILTIN),$(objtree)/vmlinux) FORCE
-	+$(call if_changed,ld_ko_o)
+# Modules that get a .BTF section: all of them with CONFIG_DEBUG_INFO_BTF_MODULES,
+# otherwise only the vmlinux BTF carrier module with CONFIG_DEBUG_INFO_BTF=m.
 ifdef CONFIG_DEBUG_INFO_BTF_MODULES
-	+$(if $(newer-prereqs),$(call cmd,btf_ko))
+btf-modules := $(modules:%.o=%.ko)
+else ifeq ($(CONFIG_DEBUG_INFO_BTF),m)
+btf-modules := $(filter %/btf_vmlinux.ko,$(modules:%.o=%.ko))
 endif
+
+# Re-generate module BTFs if either module's .ko or vmlinux changed
+%.ko: %.o %.mod.o .module-common.o $(objtree)/scripts/module.lds $(and $(btf-modules),$(KBUILD_BUILTIN),$(objtree)/vmlinux) FORCE
+	+$(call if_changed,ld_ko_o)
+	+$(if $(and $(filter $@,$(btf-modules)),$(newer-prereqs)),$(call cmd,btf_ko))
 	+$(call cmd,check_tracepoint)
 
 targets += $(modules:%.o=%.ko) $(modules:%.o=%.mod.o) .module-common.o
diff --git a/scripts/gen-btf.sh b/scripts/gen-btf.sh
index 8ca96eb10a69..7fa3189a3ded 100755
--- a/scripts/gen-btf.sh
+++ b/scripts/gen-btf.sh
@@ -22,16 +22,26 @@
 #   - ${1}.btf.o ready for linking into vmlinux
 #   - ${1}.BTF_ids with .BTF_ids data blob
 # This output is consumed by scripts/link-vmlinux.sh
+#
+# With CONFIG_DEBUG_INFO_BTF=m the .BTF section in ${1}.btf.o is not
+# allocatable, so the vmlinux ELF file carries the BTF but the kernel image
+# does not.  ${1}.btf.o then also carries .BTF.meta, the size and SHA-256 of
+# the BTF for the kernel (struct btf_vmlinux_meta); "--placeholder ${1}"
+# produces a ${1}.btf.o with a zeroed .BTF.meta and no .BTF for the first
+# vmlinux link, which the BTF is generated from.  The btf_vmlinux module gets
+# no BTF of its own; its .BTF section is a copy of the vmlinux BTF, extracted
+# from --btf_base.
 
 set -e
 
 usage()
 {
-	echo "Usage: $0 [--btf_base <file>] <target ELF file>"
+	echo "Usage: $0 [--btf_base <file>] [--placeholder] <target ELF file>"
 	exit 1
 }
 
 BTF_BASE=""
+PLACEHOLDER=""
 
 while [ $# -gt 0 ]; do
 	case "$1" in
@@ -39,6 +49,10 @@ while [ $# -gt 0 ]; do
 		BTF_BASE="$2"
 		shift 2
 		;;
+	--placeholder)
+		PLACEHOLDER=1
+		shift
+		;;
 	-*)
 		echo "Unknown option: $1" >&2
 		usage
@@ -60,6 +74,10 @@ is_enabled() {
 	grep -q "^$1=y" ${objtree}/include/config/auto.conf
 }
 
+is_module() {
+	grep -q "^$1=m" ${objtree}/include/config/auto.conf
+}
+
 case "${KBUILD_VERBOSE}" in
 *1*)
 	set -x
@@ -79,6 +97,30 @@ gen_btf_data()
 		--btf ${btf1} "${ELF_FILE}"
 }
 
+# Write one byte with value $1 (0..255)
+put_byte()
+{
+	printf "\\$(printf '%03o' "$1")"
+}
+
+# CONFIG_DEBUG_INFO_BTF=m: write struct btf_vmlinux_meta { u32 size; u8
+# sha256[32]; } for the BTF in $1 to $2, in the target's byte order.
+gen_btf_meta()
+{
+	size=$(${CONFIG_SHELL} "${srctree}/scripts/file-size.sh" "$1")
+	sha256=$(sha256sum < "$1" | cut -d' ' -f1)
+	{
+		if is_enabled CONFIG_CPU_BIG_ENDIAN; then
+			for shift in 24 16 8 0; do put_byte $(( (size >> shift) & 255 )); done
+		else
+			for shift in 0 8 16 24; do put_byte $(( (size >> shift) & 255 )); done
+		fi
+		for byte in $(echo "${sha256}" | sed 's/../& /g'); do
+			put_byte $(( 0x${byte} ))
+		done
+	} > "$2"
+}
+
 gen_btf_o()
 {
 	btf_data=${ELF_FILE}.btf.o
@@ -88,9 +130,23 @@ gen_btf_o()
 	# 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}
+	if is_module CONFIG_DEBUG_INFO_BTF; then
+		# CONFIG_DEBUG_INFO_BTF=m: .BTF stays non-allocatable, kept in the
+		# vmlinux ELF file for tooling but not loaded; the btf_vmlinux
+		# module provides it at runtime.  What is loaded is .BTF.meta,
+		# its size and hash, so that /sys/kernel/btf/vmlinux has the right
+		# size from boot and only the matching BTF is accepted.
+		gen_btf_meta ${ELF_FILE}.BTF ${ELF_FILE}.BTF.meta
+		${OBJCOPY} --add-section .BTF=${ELF_FILE}.BTF \
+			--set-section-flags .BTF=readonly \
+			--add-section .BTF.meta=${ELF_FILE}.BTF.meta \
+			--set-section-flags .BTF.meta=alloc,readonly ${btf_data}
+		${OBJCOPY} --only-section=.BTF --only-section=.BTF.meta --strip-all ${btf_data}
+	else
+		${OBJCOPY} --add-section .BTF=${ELF_FILE}.BTF \
+			--set-section-flags .BTF=alloc,readonly ${btf_data}
+		${OBJCOPY} --only-section=.BTF --strip-all ${btf_data}
+	fi
 
 	# 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.
@@ -121,6 +177,7 @@ cleanup()
 {
 	rm -f "${ELF_FILE}.BTF.1"
 	rm -f "${ELF_FILE}.BTF"
+	rm -f "${ELF_FILE}.BTF.meta"
 	if [ "${BTFGEN_MODE}" = "module" ]; then
 		rm -f "${ELF_FILE}.BTF.base"
 		rm -f "${ELF_FILE}.BTF_ids"
@@ -133,6 +190,34 @@ if [ -n "${BTF_BASE}" ]; then
 	BTFGEN_MODE="module"
 fi
 
+if [ -n "${PLACEHOLDER}" ]; then
+	btf_data=${ELF_FILE}.btf.o
+	echo "" | ${CC} ${CLANG_FLAGS} ${KBUILD_CPPFLAGS} ${KBUILD_CFLAGS} -fno-lto -c -x c -o ${btf_data} -
+	head -c 36 /dev/zero > ${ELF_FILE}.BTF.meta
+	${OBJCOPY} --add-section .BTF.meta=${ELF_FILE}.BTF.meta \
+		--set-section-flags .BTF.meta=alloc,readonly ${btf_data}
+	${OBJCOPY} --only-section=.BTF.meta --strip-all ${btf_data}
+	exit 0
+fi
+
+# CONFIG_DEBUG_INFO_BTF=m: the btf_vmlinux module carries the vmlinux BTF
+# itself.  Its own types are of no interest, so instead of generating split
+# BTF for it, copy the (non-loadable) .BTF section of vmlinux into the module.
+# The kernel recognizes the module by name and treats its .BTF as base BTF.
+case "${BTFGEN_MODE}:${ELF_FILE}" in
+module:*/btf_vmlinux.ko)
+	if is_module CONFIG_DEBUG_INFO_BTF; then
+		# -O binary only emits allocatable sections; make .BTF one for
+		# the extraction.  ${BTF_BASE} itself is not modified.
+		${OBJCOPY} -O binary --only-section=.BTF			\
+			--set-section-flags .BTF=alloc,load,readonly	\
+			"${BTF_BASE}" "${ELF_FILE}.BTF"
+		${OBJCOPY} --add-section .BTF="${ELF_FILE}.BTF" "${ELF_FILE}"
+		exit 0
+	fi
+	;;
+esac
+
 gen_btf_data
 
 case "${BTFGEN_MODE}" in
diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
index ab0b8125c8cb..a9a066267ef6 100755
--- a/scripts/link-vmlinux.sh
+++ b/scripts/link-vmlinux.sh
@@ -37,6 +37,15 @@ is_enabled() {
 	grep -q "^$1=y" include/config/auto.conf
 }
 
+is_module() {
+	grep -q "^$1=m" include/config/auto.conf
+}
+
+# =y or =m
+is_set() {
+	grep -q "^$1=[ym]" include/config/auto.conf
+}
+
 # Nice output in kbuild format
 # Will be suppressed by "make -s"
 info()
@@ -211,17 +220,25 @@ if is_enabled CONFIG_KALLSYMS; then
 	kallsyms .tmp_vmlinux0.syms .tmp_vmlinux0.kallsyms
 fi
 
-if is_enabled CONFIG_KALLSYMS || is_enabled CONFIG_DEBUG_INFO_BTF; then
+if is_module CONFIG_DEBUG_INFO_BTF; then
+	# The kernel refers to the size and hash of its BTF, which only the
+	# BTF generated from the first link can provide; link a placeholder
+	# of the same layout until then.
+	${CONFIG_SHELL} ${srctree}/scripts/gen-btf.sh --placeholder .tmp_vmlinux0
+	btf_vmlinux_bin_o=.tmp_vmlinux0.btf.o
+fi
+
+if is_enabled CONFIG_KALLSYMS || is_set CONFIG_DEBUG_INFO_BTF; then
 
 	# The kallsyms linking does not need debug symbols, but the BTF does.
-	if ! is_enabled CONFIG_DEBUG_INFO_BTF; then
+	if ! is_set CONFIG_DEBUG_INFO_BTF; then
 		strip_debug=1
 	fi
 
 	vmlinux_link .tmp_vmlinux1
 fi
 
-if is_enabled CONFIG_DEBUG_INFO_BTF; then
+if is_set CONFIG_DEBUG_INFO_BTF; then
 	info BTF .tmp_vmlinux1
 	if ! ${CONFIG_SHELL} ${srctree}/scripts/gen-btf.sh .tmp_vmlinux1; then
 		echo >&2 "Failed to generate BTF for vmlinux"
@@ -287,7 +304,7 @@ fi
 
 vmlinux_link "${VMLINUX}"
 
-if is_enabled CONFIG_DEBUG_INFO_BTF; then
+if is_set CONFIG_DEBUG_INFO_BTF; then
 	info BTFIDS ${VMLINUX}
 	${RESOLVE_BTFIDS} --patch_btfids ${btfids_vmlinux} ${VMLINUX}
 fi
-- 
2.47.3


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

* Re: [PATCH bpf-next 2/6] bpf: split the kfunc, dtor kfunc and struct_ops registration bodies
  2026-09-23  5:39 ` [PATCH bpf-next 2/6] bpf: split the kfunc, dtor kfunc and struct_ops registration bodies Jay Wang
@ 2026-09-23  6:16   ` bot+bpf-ci
  0 siblings, 0 replies; 11+ messages in thread
From: bot+bpf-ci @ 2026-09-23  6:16 UTC (permalink / raw)
  To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
  Cc: alan.maguire, martin.lau, yonghong.song, nathan, nsc,
	linux-kbuild, mcgrof, petr.pavlu, linux-modules, arnd,
	linux-kernel, abuehaze, doebel, jay.wang.upstream, ast, andrii,
	daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai

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

> diff --git a/kernel/bpf/bpf_struct_ops.c b/kernel/bpf/bpf_struct_ops.c
> index 1178acd72296..bf3004908d15 100644
> --- a/kernel/bpf/bpf_struct_ops.c
> +++ b/kernel/bpf/bpf_struct_ops.c

[ ... ]

> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> index a6634237dc89..c3c1421208b4 100644
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c

[ ... ]

> @@ -10135,32 +10150,36 @@ bpf_struct_ops_find(struct btf *btf, u32 type_id)
>  	return NULL;
>  }
>
> -int __register_bpf_struct_ops(struct bpf_struct_ops *st_ops)
> +static int btf_struct_ops_add(struct btf *btf, struct bpf_struct_ops *st_ops)
>  {
>  	struct bpf_verifier_log *log;
> -	struct btf *btf;
> -	int err = 0;
> -
> -	btf = btf_get_module_btf(st_ops->owner);
> -	if (!btf)
> -		return check_btf_kconfigs(st_ops->owner, "struct_ops");
> -	if (IS_ERR(btf))
> -		return PTR_ERR(btf);
> +	int err;
>
>  	log = kzalloc_obj(*log, GFP_KERNEL | __GFP_NOWARN);
> -	if (!log) {
> -		err = -ENOMEM;
> -		goto errout;
> -	}
> +	if (!log)
> +		return -ENOMEM;
>
>  	log->level = BPF_LOG_KERNEL;
>
>  	err = btf_add_struct_ops(btf, st_ops, log);

This isn't a bug, but btf_struct_ops_add() sits next to the existing
btf_add_struct_ops() that it calls - would a name that does not mirror
the callee, for example btf_register_struct_ops() or
btf_struct_ops_add_locked(), read more clearly here?

The two names differ only in word order, and a reader scanning the file
or grepping has to check which of the two is the wrapper.

>
> -errout:
>  	kfree(log);
> -	btf_put(btf);
> +	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/35824427607

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

* Re: [PATCH bpf-next 3/6] bpf: fetch the vmlinux BTF where kernel types enter a program
  2026-09-23  5:39 ` [PATCH bpf-next 3/6] bpf: fetch the vmlinux BTF where kernel types enter a program Jay Wang
@ 2026-09-23  6:28   ` bot+bpf-ci
  0 siblings, 0 replies; 11+ messages in thread
From: bot+bpf-ci @ 2026-09-23  6:28 UTC (permalink / raw)
  To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
  Cc: alan.maguire, martin.lau, yonghong.song, nathan, nsc,
	linux-kbuild, mcgrof, petr.pavlu, linux-modules, arnd,
	linux-kernel, abuehaze, doebel, jay.wang.upstream, ast, andrii,
	daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai

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

> diff --git a/include/linux/bpf.h b/include/linux/bpf.h
> index e7c5e203eddd..a3c4caad5dfc 100644
> --- a/include/linux/bpf.h
> +++ b/include/linux/bpf.h
> @@ -3165,6 +3165,7 @@ static inline s32 bpf_call_args_imm(s16 idx)
>  #endif
>
>  struct btf *bpf_get_btf_vmlinux(void);
> +struct btf *bpf_peek_btf_vmlinux(void);
>
>  /* Map specifics */
>  struct xdp_frame;

[ ... ]

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index a7c9e2d8965d..2425ea74b61d 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c

[ ... ]

> @@ -21724,7 +21762,11 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
>  	if (ret)
>  		goto err_prep;
>
> -	bpf_get_btf_vmlinux();
> +	/*
> +	 * The vmlinux BTF is not fetched up front: with CONFIG_DEBUG_INFO_BTF=m
> +	 * it is loaded on demand, at the points where kernel types enter the
> +	 * program (attach_btf, kfuncs, ksyms, map pointers, BTF-typed helpers).
> +	 */
>
>  	/* Serialize verification of unprivileged programs. */
>  	if (!is_priv)

Does removing this unconditional bpf_get_btf_vmlinux() call break
program-context type resolution? The removed call was the sole caller of
btf_parse_vmlinux(), which is the only place that populates the file-scope
bpf_ctx_convert.t table needed by find_canonical_prog_ctx_type().

On a CONFIG_DEBUG_INFO_BTF=m kernel where the BTF hasn't been parsed yet,
consider this chain:

bpf_check() -> do_check_subprogs() -> do_check_common() ->
btf_prepare_func_args() -> btf_is_prog_ctx_type() ->
find_canonical_prog_ctx_type()

At this point, find_canonical_prog_ctx_type() returns NULL because
conv_struct = bpf_ctx_convert.t is still NULL. Then btf_is_prog_ctx_type()
logs "btf_vmlinux is malformed" and returns false.

A global subprog argument like 'struct __sk_buff *skb' or 'struct xdp_md
*ctx' then falls through to the 'generic user data pointer' branch and gets
typed ARG_PTR_TO_MEM instead of ARG_PTR_TO_CTX. At the call site
btf_check_func_arg_match() -> check_mem_reg() rejects the caller's
PTR_TO_CTX register with a confusing type error.

When libbpf emits the common 'arg:ctx' decl tag, btf_prepare_func_args()
-> btf_validate_prog_ctx_type() returns -EINVAL directly with "btf_vmlinux
is malformed".

The same NULL bpf_ctx_convert.t also breaks btf_ctx_access() ->
btf_translate_to_vmlinux() for BPF_PROG_TYPE_EXT programs loaded with
attach_prog_fd, so ctx member access in the replacement program fails.

The changelog lists the new fetch sites: kfuncs, ksyms, map_ptr access,
BTF-typed helpers, attach_btf. None of these cover the program-context
path, which needs the vmlinux BTF but never touches a PTR_TO_BTF_ID
register. The claim that "every PTR_TO_BTF_ID register a program can hold
originates from one of these sites" is true but insufficient as a coverage
argument.

The failure is non-deterministic: if anything else (a kfunc-using program,
mounting bpffs, BPF_BTF_GET_NEXT_ID) already forced the parse, the same
program loads fine. Programs that work on CONFIG_DEBUG_INFO_BTF=y
intermittently fail on =m.

Commits later in this series make this reachable: d80d57b4ec13 defers
kfunc registration until the BTF is available (removing the boot-time parse
from the first kfunc registration), and 2f5aab95d2b8 makes
CONFIG_DEBUG_INFO_BTF=m selectable. A search of the forward range
90fe51b6b36a..2f5aab95d2b8 for bpf_ctx_convert, find_canonical_prog_ctx_type,
btf_is_prog_ctx_type and btf_prepare_func_args finds only
comment/documentation text - no fix.

> @@ -21790,0 +21832,5 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
>  	if (IS_ERR(btf_vmlinux)) {
>  		/* Either gcc or pahole or kernel are broken. */
>  		verbose(env, "in-kernel BTF is malformed\n");
>  		ret = PTR_ERR(btf_vmlinux);
>  		goto skip_full_check;
>  	}

This check a few lines below the deleted fetch was the consumer of that
fetch: bpf_get_btf_vmlinux() stores an error pointer with
smp_store_release(&btf_vmlinux, btf) on parse failure, and bpf_check() then
converted it into the "in-kernel BTF is malformed" diagnostic.

With the fetch gone, this check reads a global that this program hasn't
caused to be populated. Whether a malformed-BTF kernel reports the clear
error or a confusing downstream one now depends on whether some earlier,
unrelated program already triggered the parse.

It also reads btf_vmlinux directly rather than through the new
bpf_peek_btf_vmlinux() accessor, so the load is not ordered against the
smp_store_release() that publishes it.

Could this check either move next to a real fetch or be dropped and let the
per-site IS_ERR_OR_NULL() handling report the failure?

> diff --git a/kernel/trace/bpf_trace.c b/kernel/trace/bpf_trace.c
> index 195f78db9bda..c022b2877f0b 100644
> --- a/kernel/trace/bpf_trace.c
> +++ b/kernel/trace/bpf_trace.c
> @@ -1015,7 +1015,8 @@ static int bpf_btf_printf_prepare(struct btf_ptr *ptr, u32 btf_ptr_size,
>  	if (btf_ptr_size != sizeof(struct btf_ptr))
>  		return -EINVAL;
>
> -	*btf = bpf_get_btf_vmlinux();
> +	/* Called from a running program: only use the BTF if it is parsed. */
> +	*btf = bpf_peek_btf_vmlinux();
>
>  	if (IS_ERR_OR_NULL(*btf))
>  		return IS_ERR(*btf) ? PTR_ERR(*btf) : -EINVAL;

Swapping bpf_get_btf_vmlinux() for bpf_peek_btf_vmlinux() is correct as a
sleeping-in-program-context fix, but does anything in this commit arrange
for the BTF to have been parsed by the time bpf_snprintf_btf() or
bpf_seq_printf_btf() run?

The compensating verifier-time fetch in check_helper_call() is gated on
helper_uses_vmlinux_btf(), which only matches base_type(ret_type) ==
RET_PTR_TO_BTF_ID or base_type(arg_type[i]) == ARG_PTR_TO_BTF_ID.

bpf_snprintf_btf_proto and bpf_seq_printf_btf_proto contain neither: their
arguments are ARG_PTR_TO_MEM / ARG_CONST_SIZE / ARG_ANYTHING and the return
is RET_INTEGER, because the BTF type id travels inside the struct btf_ptr
the program passes by value rather than in a register type.

So helper_uses_vmlinux_btf() returns false for exactly the two helpers
whose runtime fetch was just weakened.

On a CONFIG_DEBUG_INFO_BTF=m kernel where the BTF is present but not yet
parsed, bpf_peek_btf_vmlinux() returns NULL and the helper returns -EINVAL.
A working bpf_snprintf_btf() or bpf_seq_printf_btf() call silently starts
failing, depending on whether unrelated activity already triggered the
parse.

The changelog frames this as behaving "as they do on a kernel without BTF",
but the BTF is available, just unparsed, and the previous code would have
produced correct output.

Should the fix be at verify time - teaching helper_uses_vmlinux_btf(), or
check_bpf_snprintf_call(), or the *_btf protos, to force the fetch? The
forward range 90fe51b6b36a..2f5aab95d2b8 contains no code change for this.


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

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

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

* Re: [PATCH bpf-next 5/6] bpf: defer registrations until the vmlinux BTF is available
  2026-09-23  5:39 ` [PATCH bpf-next 5/6] bpf: defer registrations until the vmlinux BTF is available Jay Wang
@ 2026-09-23  6:41   ` bot+bpf-ci
  0 siblings, 0 replies; 11+ messages in thread
From: bot+bpf-ci @ 2026-09-23  6:41 UTC (permalink / raw)
  To: wanjay, bpf, ast, daniel, andrii, eddyz87, memxor
  Cc: alan.maguire, martin.lau, yonghong.song, nathan, nsc,
	linux-kbuild, mcgrof, petr.pavlu, linux-modules, arnd,
	linux-kernel, abuehaze, doebel, jay.wang.upstream, ast, andrii,
	daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai

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

> diff --git a/include/linux/btf.h b/include/linux/btf.h
> index 0bf10811fe53..3b99d6386dec 100644
> --- a/include/linux/btf.h
> +++ b/include/linux/btf.h

[ ... ]

> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> index 50eb7a95fd82..cbba20a908e9 100644
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c

[ ... ]

> @@ -8792,11 +8857,55 @@ static int btf_vmlinux_module_coming(struct module *mod)
>  	smp_store_release(&btf_vmlinux_raw, data);
>  	return 0;
>  }
> +
> +/*
> + * The vmlinux BTF is not available yet and must not be loaded from the
> + * module notifier (that would nest a module load into a module load).  Keep
> + * the module's BTF for btf_parse_deferred_modules().  The .BTF data can be
> + * exposed in sysfs right away, it needs no parsing.
> + */
> +static int btf_module_defer(struct btf_module *btf_mod, struct module *mod)
> +{
> +	int err;
> +
> +	btf_mod->data = kvmemdup(mod->btf_data, mod->btf_data_size,
> +				 GFP_KERNEL | __GFP_NOWARN);
> +	if (!btf_mod->data)
> +		return -ENOMEM;
> +	btf_mod->data_size = mod->btf_data_size;
> +
> +	if (mod->btf_base_data) {
> +		btf_mod->base_data = kvmemdup(mod->btf_base_data,
> +					      mod->btf_base_data_size,
> +					      GFP_KERNEL | __GFP_NOWARN);
> +		if (!btf_mod->base_data) {
> +			kvfree(btf_mod->data);
> +			return -ENOMEM;
> +		}
> +		btf_mod->base_data_size = mod->btf_base_data_size;
> +	}
> +
> +	err = btf_module_sysfs_add(btf_mod, mod->name, btf_mod->data,
> +				   btf_mod->data_size);
> +	if (err) {
> +		kvfree(btf_mod->data);
> +		kvfree(btf_mod->base_data);
> +		return err;
> +	}

The comment states the .BTF data "can be exposed in sysfs right away,
it needs no parsing". For modules with a .BTF.base section (split BTF),
is this accurate?

btf_parse_deferred_modules() later calls btf_relocate() which rewrites
btf_mod->data in place (btf_relocate_rewrite_type_id() and
btf_relocate_rewrite_strs() modify btf->data directly). The sysfs file
exposes unrelocated BTF until that runs.

Can userspace tools reading /sys/kernel/btf/<module> between module load
and vmlinux BTF arrival get type ids and string offsets relative to the
distilled .BTF.base (which is never exposed), making the data unusable?

Also, can reads that overlap the btf_relocate() in-place rewrite get a
mix of pre- and post-relocation bytes, since sysfs_bin_attr_simple_read()
reads attr->private/attr->size with no synchronization against
btf_parse_deferred_modules()?

Does treating btf_module_sysfs_add() failure as fatal make sense here?
A few lines below, the non-deferred path at kernel/bpf/btf.c:8984
deliberately ignores the same failure as non-fatal:

    /* not fatal, the module BTF is usable without the sysfs file */
    btf_module_sysfs_add(btf_mod, btf->name, btf->data, btf->data_size);

A transient -ENOMEM or unexpected -EEXIST in sysfs_create_bin_file()
costs the module its BTF entirely instead of just its sysfs file.

> +
> +	list_add(&btf_mod->list, &btf_modules);
> +	return 0;
> +}

[ ... ]

> @@ -8905,6 +9033,87 @@ static int __init btf_module_init(void)
>  }
>
>  fs_initcall(btf_module_init);
> +
> +#if IS_MODULE(CONFIG_DEBUG_INFO_BTF)
> +/*
> + * CONFIG_DEBUG_INFO_BTF=m: the vmlinux BTF has just become available.  Parse
> + * the BTF of the modules that were loaded before it, and apply the
> + * registrations that waited for them.  Called from bpf_get_btf_vmlinux()
> + * once btf_vmlinux is published, with no locks held.
> + */
> +void btf_parse_deferred_modules(void)
> +{
> +	/* Pairs with the publication in bpf_get_btf_vmlinux() */
> +	struct btf *vmlinux_btf = smp_load_acquire(&btf_vmlinux);
> +	struct btf_deferred_reg *reg, *rtmp;
> +	struct btf_module *btf_mod, *tmp;
> +	bool parsed = false;
> +	LIST_HEAD(regs);
> +	struct btf *btf;
> +	int err;
> +
> +	if (IS_ERR_OR_NULL(vmlinux_btf))
> +		return;
> +
> +	mutex_lock(&btf_module_mutex);
> +	list_for_each_entry_safe(btf_mod, tmp, &btf_modules, list) {
> +		if (btf_mod->btf)
> +			continue;
> +
> +		btf = btf_parse_module(btf_mod->module->name, vmlinux_btf,
> +				       btf_mod->data, btf_mod->data_size, true,
> +				       btf_mod->base_data, btf_mod->base_data_size);
> +		err = PTR_ERR_OR_ZERO(btf);
> +		if (!err) {
> +			err = btf_alloc_id(btf);
> +			if (err) {
> +				/* btf owns the data now, btf_free() drops it */
> +				btf_mod->data = NULL;
> +				btf_free(btf);
> +			}
> +		}

[ ... ]

> +		/*
> +		 * Registrations are applied after dropping the mutex (they
> +		 * walk btf_modules); pin what they need until then.
> +		 */
> +		list_for_each_entry_safe(reg, rtmp, &btf_mod->deferred_regs, list) {
> +			list_del(&reg->list);
> +			if (!try_module_get(btf_mod->module)) {
> +				btf_free_deferred_reg(reg);
> +				continue;
> +			}
> +			btf_get(btf);
> +			reg->btf = btf;
> +			reg->module = btf_mod->module;
> +			list_add_tail(&reg->list, &regs);
> +		}
> +	}
> +	mutex_unlock(&btf_module_mutex);

Can try_module_get() on a MODULE_STATE_COMING module guarantee the module
image outlives the registration?

The deferred registrations are created from the module's own init routine
(register_btf_kfunc_id_set() / register_btf_id_dtor_kfuncs() /
__register_bpf_struct_ops() call btf_defer_reg()), so they are queued
while the module is still MODULE_STATE_COMING.

btf_try_get_module() at kernel/bpf/btf.c:9128 deliberately refuses to pin
COMING modules:

    /* We must only consider module whose __init routine has
     * finished, hence we must check for BTF_MODULE_F_LIVE flag,
     * which is set from the notifier callback for
     * MODULE_STATE_LIVE.
     */
    if ((btf_mod->flags & BTF_MODULE_F_LIVE) && try_module_get(btf_mod->module))

tools/testing/selftests/bpf/prog_tests/bpf_mod_race.c exercises this
check because a refcount on a COMING module is not a lifetime guarantee.

If that module's init then fails, kernel/module/main.c:3208 does:

  fail:
    mod->state = MODULE_STATE_GOING;
    synchronize_rcu();
    module_put(mod);
    blocking_notifier_call_chain(&module_notify_list, MODULE_STATE_GOING, mod);
    ...
    free_module(mod);

free_module() (kernel/module/main.c:1417) never waits for the module
refcount and goes straight to free_mod_mem(mod). The pin taken above
does not defer the free, so btf_apply_deferred_regs() then dereferences
reg->kfunc.kset / reg->st_ops in the freed module image and writes the
freed per-cpu refcount with module_put(reg->module).

[ ... ]

> @@ -10335,8 +10574,183 @@ int __register_bpf_struct_ops(struct bpf_struct_ops *st_ops)
>  	return err;
>  }
>  EXPORT_SYMBOL_GPL(__register_bpf_struct_ops);
> +#else
> +static int btf_struct_ops_add(struct btf *btf, struct bpf_struct_ops *st_ops)
> +{
> +	return -EOPNOTSUPP;
> +}
> +#endif

Is the new btf_struct_ops_add() stub reachable?

Preprocessor structure in kernel/bpf/btf.c:

  10421  #ifdef CONFIG_BPF_JIT
  10536    static int btf_struct_ops_add(...)        /* real */
  10553    int __register_bpf_struct_ops(...)
  10572      err = btf_struct_ops_add(btf, st_ops);
  10577  #else
  10578    static int btf_struct_ops_add(...)        /* NEW stub */
  10582  #endif
  10591  #ifdef BTF_MODULE_NOTIFIER
  10670    return btf_struct_ops_add(btf, reg->st_ops);
  10752  #endif /* BTF_MODULE_NOTIFIER */

The stub's only reference is inside btf_apply_deferred_reg(), compiled
only when BTF_MODULE_NOTIFIER is defined:

  kernel/bpf/btf.c:8706  #if IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES) || IS_MODULE(CONFIG_DEBUG_INFO_BTF)
  kernel/bpf/btf.c:8707  #define BTF_MODULE_NOTIFIER 1

With CONFIG_BPF_SYSCALL=y (btf.c is built), CONFIG_BPF_JIT=n and
CONFIG_DEBUG_INFO_BTF=n (or =y with CONFIG_MODULES=n), the stub is
defined and never referenced. CONFIG_BPF_JIT is a plain bool with no
default (kernel/bpf/Kconfig:46), so =n with BPF_SYSCALL=y is an ordinary
configuration.

This produces a -Wunused-function warning, which is a build failure with
CONFIG_WERROR=y.

> +
> +/*
> + * CONFIG_DEBUG_INFO_BTF=m: registrations made before the BTF they apply to
> + * is available.  Registrations for vmlinux wait in btf_vmlinux_deferred_regs
> + * until btf_parse_vmlinux() applies them; registrations for a module wait in
> + * its struct btf_module until btf_parse_deferred_modules() does.  Both lists
> + * are protected by btf_module_mutex.
> + */
> +#ifdef BTF_MODULE_NOTIFIER
> +static LIST_HEAD(btf_vmlinux_deferred_regs);
> +/* Set when the vmlinux BTF is parsed; new registrations apply directly */
> +static bool btf_vmlinux_regs_closed;

[ ... ]

> +/*
> + * The vmlinux BTF has just been parsed; apply the registrations that waited
> + * for it.  Runs under btf_vmlinux_lock, before @btf is published, so nothing
> + * can observe a vmlinux BTF without its kfuncs and struct_ops.
> + *
> + * Applying a registration can queue further ones: a struct_ops ->init()
> + * registers the kfuncs of its hook.  Those must not go through
> + * bpf_get_btf_vmlinux() (we hold its lock), so the queue stays open until
> + * a pass applies nothing new, and only then are registrations applied directly.
> + */
> +static void btf_apply_deferred_vmlinux_regs(struct btf *btf)
> +{
> +	struct btf_deferred_reg *reg, *tmp;
> +	LIST_HEAD(regs);
> +	int err;
> +
> +	if (!IS_MODULE(CONFIG_DEBUG_INFO_BTF))
> +		return;
> +
> +	mutex_lock(&btf_module_mutex);
> +	while (!list_empty(&btf_vmlinux_deferred_regs)) {
> +		list_splice_init(&btf_vmlinux_deferred_regs, &regs);
> +		mutex_unlock(&btf_module_mutex);

Can this introduce a circular lock dependency?

btf_apply_deferred_vmlinux_regs() is called from btf_parse_vmlinux()
(kernel/bpf/btf.c:6597), which runs with btf_vmlinux_lock held by
bpf_get_btf_vmlinux():

  kernel/bpf/verifier.c:21184  mutex_lock(&btf_vmlinux_lock);
  kernel/bpf/verifier.c:21187      btf = btf_parse_vmlinux();
  kernel/bpf/verifier.c:21197  mutex_unlock(&btf_vmlinux_lock);

The new mutex_lock(&btf_module_mutex) at kernel/bpf/btf.c:10724 creates:

  (A) btf_vmlinux_lock -> btf_module_mutex  [NEW]

Two pre-existing edges close the cycle:

  (B) cand_cache_mutex -> btf_vmlinux_lock
      bpf_core_apply() kernel/bpf/btf.c:10269  mutex_lock(&cand_cache_mutex);
      bpf_core_apply() kernel/bpf/btf.c:10270  cc = bpf_core_find_cands(...);
      bpf_core_find_cands() kernel/bpf/btf.c:10165  main_btf = bpf_get_btf_vmlinux();

  (C) btf_module_mutex -> cand_cache_mutex
      btf_module_notify() MODULE_STATE_GOING kernel/bpf/btf.c:9001
          mutex_lock(&btf_module_mutex);
      btf_module_notify() kernel/bpf/btf.c:9015  btf_module_free(btf_mod);
      btf_module_free() kernel/bpf/btf.c:8810  purge_cand_cache(btf_mod->btf);
      purge_cand_cache() kernel/bpf/btf.c:10092  mutex_lock(&cand_cache_mutex);

Both edges (B) and (A) are taken by the same thread:

  CPU 0: BPF_PROG_LOAD with CO-RE relocations    |  CPU 1: rmmod <module with BTF>
    bpf_check() -> bpf_check_btf_info()          |
      -> check_core_relo()                       |
        -> bpf_core_apply()                      |
           mutex_lock(&cand_cache_mutex)  [10269]|    mutex_lock(&btf_module_mutex) [9001]
           -> bpf_core_find_cands()              |
              -> bpf_get_btf_vmlinux()           |
                 mutex_lock(&btf_vmlinux_lock)   |
                                        [21184]  |
                 -> btf_parse_vmlinux()          |
                   -> btf_apply_deferred_vmlinux_regs()
                      mutex_lock(&btf_module_mutex) <-- BLOCKS [10724]
                                                   |
                                                   |    btf_module_free() -> purge_cand_cache()
                                                   |      mutex_lock(&cand_cache_mutex) [10092]
                                                   |      <-- BLOCKS

Both waits are plain uninterruptible mutex_lock(); there is no trylock,
timeout or bounded retry, so the two tasks hang permanently.

Required config: CONFIG_DEBUG_INFO_BTF=m (the IS_MODULE() guard at 10721)
together with CONFIG_DEBUG_INFO_BTF_MODULES=y, which supplies the
module-BTF entries that edge (C) frees. That combination is supported and
explicitly exercised: DEBUG_INFO_BTF_MODULES is 'default y' and 'depends
on DEBUG_INFO_BTF && MODULES' (lib/Kconfig.debug:428), and the final
commit 2f5aab95d2b8 states it was 'Tested with ... =y vs =m, both with
CONFIG_DEBUG_INFO_BTF_MODULES=y'.

[ ... ]

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index a7b73bc146a8..b6f094d5306a 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -21160,12 +21160,14 @@ int bpf_check_attach_btf_id_multi(struct btf *btf, struct bpf_prog *prog, u32 bt
>  /*
>   * Returns the parsed vmlinux BTF, NULL if the kernel has none, or an ERR_PTR
>   * if it is malformed.  With CONFIG_DEBUG_INFO_BTF=m the BTF lives in the
> - * btf_vmlinux module; the first caller loads it and parses it.  May sleep.
> + * btf_vmlinux module; the first caller loads it, parses it and then registers
> + * the BTF of the modules that were loaded before it.  May sleep.
>   */
>  struct btf *bpf_get_btf_vmlinux(void)
>  {

[ ... ]


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

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

* Re: [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory
  2026-09-23  5:39 [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Jay Wang
                   ` (5 preceding siblings ...)
  2026-09-23  5:39 ` [PATCH bpf-next 6/6] kbuild, bpf: allow building the vmlinux BTF as a module Jay Wang
@ 2026-09-23  8:27 ` Alan Maguire
  6 siblings, 0 replies; 11+ messages in thread
From: Alan Maguire @ 2026-09-23  8:27 UTC (permalink / raw)
  To: Jay Wang, bpf, Alexei Starovoitov, Daniel Borkmann,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi
  Cc: Martin KaFai Lau, Yonghong Song, Nathan Chancellor,
	Nicolas Schier, linux-kbuild, Luis Chamberlain, Petr Pavlu,
	linux-modules, Arnd Bergmann, linux-kernel,
	Hazem Mohamed Abuelfotoh, Bjoern Doebel, jay.wang.upstream

On 23/09/2026 06:39, Jay Wang wrote:
> Based on and tested against bpf-next commit 91f8613d95ad ("bpf: Drop
> duplicate check_app_limited in tcp_bpf_push").
> 
> This series makes CONFIG_DEBUG_INFO_BTF a tristate, so that it can be
> set to =m.  With =m the vmlinux BTF is carried by a module, btf_vmlinux,
> that the kernel loads the first time anything needs the BTF.  On a
> system where nothing does, that saves ~5.4 MB of RAM with a distribution
> config; on a system that uses BTF, nothing differs from =y.  =y itself
> is untouched.
> 
> Problem
> -------
> 
> The vmlinux BTF that CONFIG_DEBUG_INFO_BTF=y builds into the kernel
> image takes ~5.4 MB of memory, resident from boot whether anything uses
> it or not.  On small instances that is not negligible.
> 
> A distribution cannot simply turn it off for the users who do not need
> it: it ships one kernel build for all its users, and BTF is not debug
> info anymore.  CO-RE, fentry/fexit, kfuncs, struct_ops, sched_ext and
> bpf-lsm all depend on it, so =n takes those away from everyone who does
> use them. 
> 
> Hence this series adds CONFIG_DEBUG_INFO_BTF=m: the BTF becomes an
> on-demand module.
> Users who never use BTF get the memory back; for users who do, the first
> use loads it automatically and everything works as with =y.
> 
> Approach
> --------
> 
> Do not remove anything, defer it.  The BTF is generated exactly as
> before, but with =m it is not part of the kernel image: it is packed
> into a module, btf_vmlinux.ko, which the kernel loads itself the first
> time anything needs the BTF.  Once loaded, the BTF stays, and nothing
> differs from =y.
> 
> Making that work ran into five problems.  The first three are existing
> components that rely on the vmlinux BTF being present from boot, which
> a loadable module cannot provide; the last two follow from the BTF no
> longer being part of the image.
> 
> 1. Verifier.  Problem: bpf_check() fetched the vmlinux BTF for every
>    program, so the first socket filter at boot would have loaded the
>    module on every system.  Solution: fetch the BTF only where kernel
>    types enter a program (attach_btf, kfunc calls, ksyms, map pointer
>    access, helpers that take or return kernel pointers).  A program
>    using none of these never touches it.
> 
> 2. Initcall registrations.  Problem: kfunc, dtor kfunc and struct_ops
>    registrations run from initcalls and need the parsed BTF, which
>    would again pull it in at boot.  Solution: queue them and apply the
>    queue when the BTF is parsed, before it is published, so no program
>    can ever see a vmlinux BTF that lacks its kfuncs or struct_ops.
> 
> 3. Module BTF.  Problem: module BTF is split BTF against the vmlinux
>    BTF and was parsed at module load.  A module loaded before the
>    vmlinux BTF cannot be parsed yet, and the module notifier cannot
>    load btf_vmlinux (that would nest a module load inside a module
>    load).  Solution: keep the module's BTF aside, expose it in
>    /sys/kernel/btf right away, and parse and register it, together
>    with the module's own kfuncs and struct_ops, once the vmlinux BTF
>    arrives.  Module BTF thus works regardless of load order.
> 
> 4. Trust.  Problem: the verifier treats the BTF as the description of
>    the kernel's types, so a carrier from a different build must be
>    refused even if vermagic lets it load.  Solution: link the size and
>    SHA-256 of the BTF into the image and check them when the module
>    loads.  The linked-in size also lets /sys/kernel/btf/vmlinux report
>    its final size before the load, which tooling expects.
> 
> 5. Tooling.  Problem: pahole --btf_base for module BTF, bpftool and
>    external module builds read .BTF from the vmlinux ELF.  Solution:
>    keep .BTF in the ELF as a non-loadable section, so it is in the file
>    but not in the image.
> 
> CONFIG_BPF_PRELOAD is made unavailable with =m: its preloaded programs
> attach through the vmlinux BTF, so every bpffs mount, which systemd does
> at boot, would load it and defeat the point.
> 
> Relation to the inline BTF series
> ---------------------------------
> 
> Alan's inline BTF series [2] adds inline function information to BTF,
> which is even larger than the BTF itself, and its cover letter leaves
> delivering that information on demand, through a module and sysfs, to a
> follow-up.  When Alexei suggested taking the same route for the vmlinux
> BTF, Alan pointed out where the difficulty would lie [3].
>

There are patches handling the sysfs details in

https://lore.kernel.org/bpf/20260901165757.801449-1-alan.maguire@oracle.com/
 
v3 and later do the split, but the above series has sysfs handling in
it. I have a question about the approach here; my experience with using sysfs
with a dummy placeholder file which triggered on-demand module load when
accessed was it didn't work with sysfs interfaces because the file size needed
to be refreshed, and the reader would usually error out since it saw an
empty file and gave up reading. I wound up having to switch to using
kernfs to update size attributes on open such that the caller would see the
size change synchronously once the load completed. See patch 15 in the above
series for the details. 

It seems like you used a different approach here, and by storing the size
in the BTF metadata this problem was avoided. From the below it seems like
there are no first-caller issues from userspace (like if the first caller
does "bpftool btf dump file /sys/kernel/btf/vmlinux")?

Another issue; during boot, request_module can call back out to modprobe 
and depending on where you are in the boot process, the module may not be
available due to filesystem not mounted yet etc. Maybe this just means that
btf_vmlinux.ko needs to be in the initramfs image? If that's the case, I 
would suggest highlighting that in the CONFIG_DEBUG_INFO_BTF Kconfig description,
something like

"For CONFIG_DEBUG_INFO_BTF=m on systems where BTF use is likely during
system startup, ensure that btf_vmlinux.ko is one of the modules added
to initramfs to avoid BPF-related failures."

I get that it's not the intended consitituency for this feature, but best
to highlight the risk just in case.

Another concern to balance; embedded folks were interested in vmlinux
BTF as a module to limit on-disk footprint rather than (or likely as
well as) runtime memory; for them having a smaller vmlinux image - even 
at the cost of a module - was fine because the modules lived on a different
partition on such systems. If I'm following, the final vmlinux image that winds 
up on disk doesn't contain the BTF section, right? If so that's great
news for them.

And another thing I was wondering about - did you test with modules
containing a .BTF.base (built standalone via "make -C path2module")?
The code has handling to preserve the .BTF.base sections such that BTF
can be relocated and available after deferred vmlinux BTF load, but I just
wanted to check prior to testing at my end.

> However, that approach cannot be used directly for the vmlinux BTF,
> because of what depends on the data:
> 
> 1. Boot-time consumers.  Nothing needs inline information at boot, so
>    loading it late only concerns the sysfs file.  The core vmlinux BTF
>    is needed at boot by the verifier, by kfunc and struct_ops
>    registrations, and by every module's BTF.  Therefore we need to make
>    each of those wait for the BTF instead of expecting it at boot: the
>    verifier fetches it only when a program brings kernel types in, and
>    the registrations are queued and replayed once it is parsed.  That
>    is most of this series.
> 
> 2. Module BTF.  Module BTF is split against the vmlinux BTF, so a module
>    loaded before it has nothing to be parsed against, and the module
>    notifier cannot load btf_vmlinux itself.  Therefore we need to keep
>    the module's BTF aside, expose it in sysfs as raw bytes right away,
>    and parse and register it once the vmlinux BTF arrives.  Alan named
>    this as the hard part; it works here regardless of whether the module
>    loads before or after the vmlinux BTF.
>

What I meant by the hard part (aside from deferred handling which is hard
enough!) is that there is a conflict between the goal of saving memory and being
forced to allocate memory for module BTF for a deferred-load scheme like this.
While the series is specific about the amount of memory saved, I think 
it would be good to highlight this aspect clearly; we still need to
allocate space for module BTF regardless of whether CONFIG_DEBUG_INFO_BTF=y
or m ; the saving is for vmlinux BTF only. So

CONFIG_DEBUG_INFO_BTF=y results in upfront allocation of memory for kernel and module BTF
CONFIG_DEBUG_INFO_BTF=m results in upfront allocation of memory for module BTF only

It might be worth thinking about providing a means to control whether such
module allocations happen prior to vmlinux BTF loading for highly memory-constrained
systems. Anything delivering kfuncs etc should probably always allocate since it 
constitutes core BPF infrastructure.

> The btf_vmlinux module added here could carry the inline information as
> well when that follow-up comes, so both would share one mechanism.
> 
> Patches Structure
> -----------------
> 
> Patch 1 (refactor, no functional change): btf_parse_module() takes the
> vmlinux BTF as an argument and can adopt an existing copy of the module
> BTF data instead of duplicating it.  Needed so that module BTF kept
> aside at load time can be parsed later without moving the buffer the
> sysfs file points at.
> 
> Patch 2 (refactor, no functional change): splits the kfunc, dtor kfunc
> and struct_ops registration functions into "find the BTF for the owner"
> and "add the registration to this BTF", so the second half can be
> replayed on a queued registration.
> 
> Patch 3 (verifier): stops fetching the vmlinux BTF up front in
> bpf_check() and fetches it where kernel types enter a program instead.
> Adds bpf_peek_btf_vmlinux() for the helpers that run in program
> context and cannot load anything.  With =y the BTF is parsed at boot
> anyway, so this is invisible there.
> 
> Patch 4 (carrier): the runtime side of taking the vmlinux BTF from the
> btf_vmlinux module: request it on first use, copy it out of the module
> in the BTF module notifier after checking size and SHA-256 against
> .BTF.meta, and serve /sys/kernel/btf/vmlinux from the copy with its
> size known from boot.  All under IS_MODULE(CONFIG_DEBUG_INFO_BTF), so
> unreachable until patch 6.
> 
> Patch 5 (deferrals): queues kfunc, dtor kfunc and struct_ops
> registrations made before their BTF is available and applies them when
> it is; keeps the BTF of modules loaded before the vmlinux BTF and parses
> and registers it when the vmlinux BTF arrives.  Also unreachable until
> patch 6.
> 
> Patch 6 (kbuild and Kconfig): makes CONFIG_DEBUG_INFO_BTF a tristate;
> with =m links .BTF into vmlinux as a non-loadable section, emits
> .BTF.meta, builds btf_vmlinux.ko with the vmlinux .BTF as its payload,
> makes the Makefile and #ifdef sites that must hold for both =y and =m
> do so, excludes CONFIG_BPF_PRELOAD, and documents the option.
> 
> Patches 1-3 are independently useful cleanups; 4 and 5 are dead code
> until 6 flips the switch, which keeps each bisect step building and
> behaving as before.
> 
> Testing
> -------
> 
> Tested with 1 GiB of memory, same tree, =y against =m, both with
> CONFIG_DEBUG_INFO_BTF_MODULES=y.  The on-demand behaviour is easy to
> see by hand on an =m kernel:
> 
>   # lsmod | grep btf_vmlinux
>       -> nothing: the BTF is not loaded at boot.
>   # ls -la /sys/kernel/btf/vmlinux
>       -> the file exists with its final size (from .BTF.meta), although
>          the BTF behind it is not loaded yet.
>   # modprobe ext4 nf_conntrack
>   # ls /sys/kernel/btf/
>       -> ext4, nf_conntrack, ... appear immediately, although their
>          BTF is only kept aside, not parsed: there is no vmlinux BTF to
>          parse it against yet.
>   # lsmod | grep btf_vmlinux
>       -> still nothing: loading modules does not load the vmlinux BTF.
>   # cat /sys/kernel/btf/ext4 > /dev/null
>   # lsmod | grep btf_vmlinux
>       -> still nothing: a module's BTF file is served from the raw copy,
>          reading it does not need the vmlinux BTF.
>   # grep VmallocUsed /proc/meminfo
>       -> baseline.
>   # cat /sys/kernel/btf/vmlinux > /dev/null
>   # lsmod | grep btf_vmlinux
>       -> btf_vmlinux ... [permanent]: the first use loaded it, and it
>          cannot be unloaded.
>   # grep VmallocUsed /proc/meminfo
>       -> up by ~5.5 MB: the BTF copy, allocated only now.
>   # bpftool btf list
>       -> vmlinux and every loaded module now have BTF ids; the modules
>          loaded before were parsed and registered on the way.
> 
> Any BPF program that uses kernel types (kprobe with
> bpf_get_current_task_btf(), a kfunc call, fentry, ...) triggers the
> same load; a plain socket filter does not.
> 
> Results:
> 
>  - MemTotal is ~5.4 MB higher with =m while the BTF is unused, which is
>    the size of the .BTF section.  Once the BTF is in use, MemFree is the
>    same within run-to-run noise.
>  - Modules loaded before the trigger (ext4, nf_conntrack, which
>    registers kfuncs from its init, xfrm_interface) get BTF ids once the
>    BTF is loaded; a struct_ops map for tcp_congestion_ops and a syscall
>    program calling bpf_task_from_pid() work; nf_nat loaded afterwards
>    takes the usual path.
>  - stat() of /sys/kernel/btf/vmlinux reports the final size before the
>    load; fstat/read/mmap agree afterwards.
>  - A carrier with one byte of .BTF changed is refused with -EINVAL.
>  - lockdep and kmemleak kernels are clean in all trigger orders.
>  - =y and =n build and behave as before; =m without module BTF works.
> 
> [1] https://lore.kernel.org/all/20260917000201.25581-2-wanjay@amazon.com/
> [2] https://lore.kernel.org/bpf/20260916074118.1007116-1-alan.maguire@oracle.com/
> [3] https://lore.kernel.org/all/33592fca-88a8-44aa-8d94-40e1e604554e@oracle.com/
> 
> Jay Wang (6):
>   bpf: pass the vmlinux BTF to btf_parse_module() and let it adopt the
>     data
>   bpf: split the kfunc, dtor kfunc and struct_ops registration bodies
>   bpf: fetch the vmlinux BTF where kernel types enter a program
>   bpf: take the vmlinux BTF from the btf_vmlinux module
>   bpf: defer registrations until the vmlinux BTF is available
>   kbuild, bpf: allow building the vmlinux BTF as a module
> 
>  Documentation/bpf/btf.rst         |  35 ++
>  Makefile                          |   8 +-
>  include/asm-generic/vmlinux.lds.h |  30 +-
>  include/linux/bpf.h               |   1 +
>  include/linux/btf.h               |   7 +
>  include/linux/btf_ids.h           |   2 +-
>  include/linux/compiler_types.h    |   2 +-
>  include/linux/module.h            |   2 +-
>  include/trace/trace_events.h      |   2 +-
>  kernel/bpf/Makefile               |   6 +-
>  kernel/bpf/bpf_struct_ops.c       |   3 +-
>  kernel/bpf/btf.c                  | 782 ++++++++++++++++++++++++++----
>  kernel/bpf/btf_vmlinux.c          |  23 +
>  kernel/bpf/preload/Kconfig        |   4 +
>  kernel/bpf/syscall.c              |   6 +
>  kernel/bpf/sysfs_btf.c            |  80 ++-
>  kernel/bpf/verifier.c             | 104 +++-
>  kernel/module/main.c              |   4 +-
>  kernel/trace/bpf_trace.c          |   3 +-
>  kernel/trace/trace_syscalls.c     |   6 +-
>  lib/Kconfig.debug                 |  13 +-
>  net/netfilter/Makefile            |   6 +-
>  net/xfrm/Makefile                 |   4 +-
>  scripts/Makefile.modfinal         |  14 +-
>  scripts/gen-btf.sh                |  93 +++-
>  scripts/link-vmlinux.sh           |  25 +-
>  26 files changed, 1121 insertions(+), 144 deletions(-)
>  create mode 100644 kernel/bpf/btf_vmlinux.c
> 
> 
> base-commit: 91f8613d95ad8cd99d8baf094806d1ef98bc6380
> -- 
> 2.47.3
> 


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

end of thread, other threads:[~2026-09-23  8:27 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-23  5:39 [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Jay Wang
2026-09-23  5:39 ` [PATCH bpf-next 1/6] bpf: pass the vmlinux BTF to btf_parse_module() and let it adopt the data Jay Wang
2026-09-23  5:39 ` [PATCH bpf-next 2/6] bpf: split the kfunc, dtor kfunc and struct_ops registration bodies Jay Wang
2026-09-23  6:16   ` bot+bpf-ci
2026-09-23  5:39 ` [PATCH bpf-next 3/6] bpf: fetch the vmlinux BTF where kernel types enter a program Jay Wang
2026-09-23  6:28   ` bot+bpf-ci
2026-09-23  5:39 ` [PATCH bpf-next 4/6] bpf: take the vmlinux BTF from the btf_vmlinux module Jay Wang
2026-09-23  5:39 ` [PATCH bpf-next 5/6] bpf: defer registrations until the vmlinux BTF is available Jay Wang
2026-09-23  6:41   ` bot+bpf-ci
2026-09-23  5:39 ` [PATCH bpf-next 6/6] kbuild, bpf: allow building the vmlinux BTF as a module Jay Wang
2026-09-23  8:27 ` [PATCH bpf-next 0/6] bpf: make the vmlinux BTF an on-demand loadable module (CONFIG_DEBUG_INFO_BTF=m) to save ~5.4 MB memory Alan Maguire

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

all inboxes | Powered by JetHome®