BPF List
 help / color / mirror / Atom feed
* [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading
@ 2026-09-21 22:39 Andrey Grodzovsky
  2026-09-21 22:39 ` [PATCH bpf-next v4 1/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
                   ` (7 more replies)
  0 siblings, 8 replies; 17+ messages in thread
From: Andrey Grodzovsky @ 2026-09-21 22:39 UTC (permalink / raw)
  To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source

This series was originally posted in January 2025 [1] as a two-patch RFC
introducing a per-program tri-state load strategy (disabled/auto/dynamic)
to let large BPF applications load a subset of their programs lazily,
after the initial bpf_object load. This v4 addresses all outstanding review
feedback from v3's two CI bots, folds bpf_program__load_manually()/
unload_manually() into a single load()/unload() naming pair, and adds
a new patch that properly versions bpf_program__set_autoattach()'s
ABI change instead of leaving it as a silent break for out-of-tree
consumers (see changelog below).


Motivation:

Security tools built on top of libbpf commonly ship as a single large
BPF object containing many programs, only a subset of which are needed
on any given system -- the rest are gated on optional features or on
kernel/runtime capabilities that vary across deployments. For this class
of application, per-program manual loading helps in two ways:

  - it shortens the initial load, since only the programs actually
    needed for the running configuration are loaded and attached up
    front instead of the whole object; and
  - it lets a single failing program be unloaded and possibly reloaded
    (or a feature toggled at runtime) without tearing down and reloading
    the entire bpf_object.

Both reduce the time window during which the tool is partially loaded or
inactive -- for a security tool specifically, that is also the time
window during which the system it protects is unprotected.

We have been running this with our internal fork of libbpf in production
for about a year. Depending on kernel and configuration support, around
250 of our programs are potentially loadable on any given system; with
manual loading, only around 90 of those are actually loaded by default,
growing to the full 250 only when every optional feature is enabled.
Cutting the default set of loaded programs from 250 down to 90 measurably
reduces load time (we observed ~60% reduction in our own measurements),
and since unload time scales with the number of loaded programs, it
correspondingly shortens unload time as well -- which matters, since
slow unload can delay system shutdown. We believe this functionality
would benefit other libbpf consumers with similarly large, modular BPF
applications.

Patch overview:

 1/7: replace bpf_program's boolean autoload field with an enum
      (bpf_prog_load_strategy: DISABLED/AUTO), so a third state can be
      added later without an ABI break

 2/7: add BPF_PROG_LOAD_STRATEGY_MANUAL and bpf_program__load()/
      bpf_program__unload(), letting a program be loaded and attached
      independently of the bulk bpf_object load/attach pass, and
      reloaded/reattached multiple times; unload() is now a single,
      MANUAL-aware verb instead of a separate load()/load_manually()
      and unload()/unload_manually() pair (see changelog)

 3/7: let a program mark itself MANUAL from its own section name via a
      SEC("!...") prefix, instead of requiring an imperative
      bpf_program__set_load_strategy() call, mirroring the existing
      SEC("?...") convention

 4/7: reject bpf_object__gen_loader() for an object with a MANUAL
      program, which would otherwise silently corrupt every later
      program's generated prog_fd slot

 5/7: version bpf_program__set_autoattach()'s void->int return-type and
      behavior change via ELF symbol versioning (COMPAT_VERSION()/
      DEFAULT_VERSION()), modeled on the xsk_umem__create and
      bpf_prog_load precedents, so binaries already linked against the
      old void-returning ABI keep that behavior instead of silently
      hitting the new MANUAL-rejection logic underneath them

 6/7: selftest covering every load-strategy transition
      (DISABLED/AUTO/MANUAL), bpf_program__set_autoload()'s bool
      compatibility, and autoattach restoration -- now including a case
      that discriminates the restore logic from a hard-coded `true`

 7/7: selftest covering the manual load/attach/detach/reload cycle, the
      declarative SEC("!...") marker, prepare()-only load sufficiency,
      a module BTF deferred load, and gen_loader rejection -- updated
      for the merged load()/unload() API and with all four scenarios
      now individually subtested

Changes since v3 [2]:
  - Log the raw error via errstr(err) instead of a bare %d in
    bpf_program__load()'s failure message (sashiko-bot)
  - Let MANUAL programs use the caller-supplied kernel log buffer
    instead of always falling back to libbpf's own realloc'd buffer
    (bot+bpf-ci)
  - Reject BPF_PROG_LOAD_STRATEGY_MANUAL for struct_ops programs:
    bpf_map_prepare_vdata() bakes every member's fd into kern_vdata
    during bpf_object__load(), before a MANUAL member could ever be
    loaded, and nothing re-bakes it afterwards (bot+bpf-ci)
  - Fix bpf_link leaks on every assertion-failure path in load_type.c
    and dynamicload.c (sashiko-bot, bot+bpf-ci)
  - Wrap all 4 dynamicload.c scenarios in test__start_subtest(), so
    bpf_testmod's absence only skips the one scenario that needs it
    instead of masking the other three as SKIP (bot+bpf-ci)
  - Drop the dead !obj->fd_array_cnt disjunct in
    bpf_object_post_load_cleanup()'s fd_array free guard (Andrii)
  - Rename bpf_program__load_manually() to bpf_program__load(), and
    fold bpf_program__unload_manually() into bpf_program__unload():
    split the existing full-free body into an internal
    bpf_program_unload_full() helper used unconditionally by
    object-teardown paths and make the public unload() skip the helper for
    manual programs (Andrii)
  - New: version bpf_program__set_autoattach()'s void->int ABI change
    via COMPAT_VERSION()/DEFAULT_VERSION() instead of leaving the
    symbol under its original LIBBPF_1.0.0 node -- an already-linked
    binary now keeps the old unconditional-set behavior at runtime
    instead of silently gaining the new MANUAL-rejection behavior
    underneath it after a libbpf upgrade (bot+bpf-ci)
  - Misc. cosmetic fixes pointed out by the bots (Sashiko + bpf-ci)

[1] https://lore.kernel.org/bpf/20250122215206.59859-1-slava.imameev@crowdstrike.com/
[2] https://lore.kernel.org/bpf/20260917203040.3150212-1-andrey.grodzovsky@crowdstrike.com/

Andrey Grodzovsky (4):
  libbpf: Support declarative manual load via SEC("!...") prefix
  libbpf: Reject gen_loader for objects with already-manual programs
  libbpf: Version bpf_program__set_autoattach() ABI change
  selftests/bpf: Cover BPF program manual loading

Slava Imameev (3):
  libbpf: BPF program load strategy enum
  libbpf: BPF programs manual loading and attaching
  selftests/bpf: Cover BPF program load strategy transitions

 tools/lib/bpf/libbpf.c                        | 278 ++++++++++---
 tools/lib/bpf/libbpf.h                        |  69 +++-
 tools/lib/bpf/libbpf.map                      |   5 +
 tools/lib/bpf/libbpf_common.h                 |   6 +
 .../selftests/bpf/prog_tests/dynamicload.c    | 365 ++++++++++++++++++
 .../selftests/bpf/prog_tests/load_type.c      | 186 +++++++++
 .../selftests/bpf/prog_tests/signed_loader.c  |  28 ++
 .../selftests/bpf/progs/test_dynamicload.c    |  54 +++
 .../selftests/bpf/progs/test_load_type.c      |  31 ++
 9 files changed, 976 insertions(+), 46 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/dynamicload.c
 create mode 100644 tools/testing/selftests/bpf/prog_tests/load_type.c
 create mode 100644 tools/testing/selftests/bpf/progs/test_dynamicload.c
 create mode 100644 tools/testing/selftests/bpf/progs/test_load_type.c

-- 
2.34.1


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

* [PATCH bpf-next v4 1/7] libbpf: BPF program load strategy enum
  2026-09-21 22:39 [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
@ 2026-09-21 22:39 ` Andrey Grodzovsky
  2026-09-21 23:21   ` bot+bpf-ci
  2026-09-21 22:39 ` [PATCH bpf-next v4 2/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
                   ` (6 subsequent siblings)
  7 siblings, 1 reply; 17+ messages in thread
From: Andrey Grodzovsky @ 2026-09-21 22:39 UTC (permalink / raw)
  To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source

From: Slava Imameev <slava.imameev@crowdstrike.com>

Replacing the boolean field with an enum simplifies the addition
of new load strategies. Currently, the bpf_program structure defines
the autoload behavior using a boolean field. This field is now
replaced with an enum, allowing new BPF program loading strategies
to be introduced by extending the enum value range.

Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
 tools/lib/bpf/libbpf.c   | 50 +++++++++++++++++++++++++---------------
 tools/lib/bpf/libbpf.h   | 33 ++++++++++++++++++++++++++
 tools/lib/bpf/libbpf.map |  2 ++
 3 files changed, 66 insertions(+), 19 deletions(-)

diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 2f53afc985cf..d88df7e4c86d 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -494,7 +494,7 @@ struct bpf_program {
 	struct bpf_object *obj;
 
 	int fd;
-	bool autoload;
+	enum bpf_prog_load_strategy load_strategy;
 	bool autoattach;
 	bool sym_global;
 	bool mark_btf_static;
@@ -872,11 +872,11 @@ bpf_object__init_prog(struct bpf_object *obj, struct bpf_program *prog,
 	 * autoload set to false.
 	 */
 	if (sec_name[0] == '?') {
-		prog->autoload = false;
+		prog->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
 		/* from now on forget there was ? in section name */
 		sec_name++;
 	} else {
-		prog->autoload = true;
+		prog->load_strategy = BPF_PROG_LOAD_STRATEGY_AUTO;
 	}
 
 	prog->autoattach = true;
@@ -1163,7 +1163,8 @@ static int bpf_object_adjust_struct_ops_autoload(struct bpf_object *obj)
 			}
 		}
 		if (use_cnt)
-			prog->autoload = should_load;
+			prog->load_strategy = should_load ? BPF_PROG_LOAD_STRATEGY_AUTO
+				: BPF_PROG_LOAD_STRATEGY_DISABLED;
 	}
 
 	return 0;
@@ -1254,7 +1255,7 @@ static int bpf_map__init_kern_struct_ops(struct bpf_map *map)
 				 * then bpf_object_adjust_struct_ops_autoload() will update its
 				 * autoload accordingly.
 				 */
-				st_ops->progs[i]->autoload = false;
+				st_ops->progs[i]->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
 				st_ops->progs[i] = NULL;
 			}
 
@@ -1292,7 +1293,7 @@ static int bpf_map__init_kern_struct_ops(struct bpf_map *map)
 			 * if user replaced it with another program or NULL
 			 */
 			if (st_ops->progs[i] && st_ops->progs[i] != prog)
-				st_ops->progs[i]->autoload = false;
+				st_ops->progs[i]->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
 
 			/* Update the value from the shadow type */
 			st_ops->progs[i] = prog;
@@ -3592,7 +3593,7 @@ static bool obj_needs_vmlinux_btf(const struct bpf_object *obj)
 	}
 
 	bpf_object__for_each_program(prog, obj) {
-		if (!prog->autoload)
+		if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)
 			continue;
 		if (prog_needs_vmlinux_btf(prog))
 			return true;
@@ -6186,7 +6187,7 @@ bpf_object__relocate_core(struct bpf_object *obj, const char *targ_btf_path)
 			/* no need to apply CO-RE relocation if the program is
 			 * not going to be loaded
 			 */
-			if (!prog->autoload)
+			if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)
 				continue;
 
 			/* adjust insn_idx from section frame of reference to the local
@@ -7530,7 +7531,7 @@ static int bpf_object__relocate(struct bpf_object *obj, const char *targ_btf_pat
 		 */
 		if (prog_is_subprog(obj, prog))
 			continue;
-		if (!prog->autoload)
+		if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)
 			continue;
 
 		err = bpf_object__relocate_calls(obj, prog);
@@ -7566,7 +7567,7 @@ static int bpf_object__relocate(struct bpf_object *obj, const char *targ_btf_pat
 		prog = &obj->programs[i];
 		if (prog_is_subprog(obj, prog))
 			continue;
-		if (!prog->autoload)
+		if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)
 			continue;
 
 		/* Process data relos for main programs */
@@ -8405,8 +8406,8 @@ bpf_object__load_progs(struct bpf_object *obj, int log_level)
 		prog = &obj->programs[i];
 		if (prog_is_subprog(obj, prog))
 			continue;
-		if (!prog->autoload) {
-			pr_debug("prog '%s': skipped loading\n", prog->name);
+		if (prog->load_strategy != BPF_PROG_LOAD_STRATEGY_AUTO) {
+			pr_debug("prog '%s': skipped auto-loading\n", prog->name);
 			continue;
 		}
 		prog->log_level |= log_level;
@@ -9807,16 +9808,13 @@ const char *bpf_program__section_name(const struct bpf_program *prog)
 
 bool bpf_program__autoload(const struct bpf_program *prog)
 {
-	return prog->autoload;
+	return prog->load_strategy == BPF_PROG_LOAD_STRATEGY_AUTO;
 }
 
 int bpf_program__set_autoload(struct bpf_program *prog, bool autoload)
 {
-	if (prog->obj->state >= OBJ_LOADED)
-		return libbpf_err(-EINVAL);
-
-	prog->autoload = autoload;
-	return 0;
+	return bpf_program__set_load_strategy(prog,
+		autoload ? BPF_PROG_LOAD_STRATEGY_AUTO : BPF_PROG_LOAD_STRATEGY_DISABLED);
 }
 
 bool bpf_program__autoattach(const struct bpf_program *prog)
@@ -15194,7 +15192,7 @@ int bpf_object__attach_skeleton(struct bpf_object_skeleton *s)
 		struct bpf_program *prog = *prog_skel->prog;
 		struct bpf_link **link = prog_skel->link;
 
-		if (!prog->autoload || !prog->autoattach)
+		if (prog->load_strategy != BPF_PROG_LOAD_STRATEGY_AUTO || !prog->autoattach)
 			continue;
 
 		/* auto-attaching not supported for this program */
@@ -15304,3 +15302,17 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
 	free(s->progs);
 	free(s);
 }
+
+int bpf_program__set_load_strategy(struct bpf_program *prog, enum bpf_prog_load_strategy strategy)
+{
+	if (prog->obj->state >= OBJ_LOADED)
+		return libbpf_err(-EINVAL);
+
+	prog->load_strategy = strategy;
+	return 0;
+}
+
+enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog)
+{
+	return prog->load_strategy;
+}
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index 026932f20962..a06259c4dacc 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -2103,6 +2103,39 @@ LIBBPF_API int libbpf_unregister_prog_handler(int handler_id);
  */
 LIBBPF_API int bpf_program__clone(struct bpf_program *prog, const struct bpf_prog_load_opts *opts);
 
+/**
+ * The program load strategy:
+ *
+ * - BPF_PROG_LOAD_STRATEGY_DISABLED: the program is not loaded.
+ * - BPF_PROG_LOAD_STRATEGY_AUTO: the program is autoloaded when the bpf_object is loaded.
+ */
+enum bpf_prog_load_strategy {
+	BPF_PROG_LOAD_STRATEGY_DISABLED = 0,
+	BPF_PROG_LOAD_STRATEGY_AUTO,
+};
+
+/**
+ * @brief **bpf_program__set_load_strategy()** sets the load strategy of a
+ * BPF program, controlling whether and when it gets loaded into the kernel.
+ *
+ * Can only be called before the enclosing bpf_object is loaded.
+ *
+ * @param prog BPF program to update
+ * @param strategy new load strategy for the program
+ * @return 0 on success; negative error code if the object was already loaded
+ */
+LIBBPF_API int bpf_program__set_load_strategy(struct bpf_program *prog,
+					      enum bpf_prog_load_strategy strategy);
+
+/**
+ * @brief **bpf_program__load_strategy()** returns the current load strategy
+ * of a BPF program.
+ *
+ * @param prog BPF program to query
+ * @return current load strategy of the program
+ */
+LIBBPF_API enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog);
+
 #ifdef __cplusplus
 } /* extern "C" */
 #endif
diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
index 7a84dd00ce95..03c3d2bd15bf 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -463,6 +463,8 @@ LIBBPF_1.8.0 {
 		bpf_program__attach_tracing_multi;
 		bpf_program__clear_flags;
 		bpf_program__clone;
+		bpf_program__load_strategy;
+		bpf_program__set_load_strategy;
 		btf__find_by_name_kind_own;
 		btf__new_empty_opts;
 } LIBBPF_1.7.0;
-- 
2.34.1


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

* [PATCH bpf-next v4 2/7] libbpf: BPF programs manual loading and attaching
  2026-09-21 22:39 [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
  2026-09-21 22:39 ` [PATCH bpf-next v4 1/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
@ 2026-09-21 22:39 ` Andrey Grodzovsky
  2026-09-21 23:03   ` sashiko-bot
  2026-09-21 22:39 ` [PATCH bpf-next v4 3/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
                   ` (5 subsequent siblings)
  7 siblings, 1 reply; 17+ messages in thread
From: Andrey Grodzovsky @ 2026-09-21 22:39 UTC (permalink / raw)
  To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source

From: Slava Imameev <slava.imameev@crowdstrike.com>

BPF programs designated as manually loaded can be loaded and
attached independently after the initial bpf_object loading and
attaching.

These programs can also be reloaded and reattached multiple times,
enabling more flexible management of a resident BPF program set.

A key motivation for this feature is to reduce load times for
utilities that include hundreds of BPF programs. When the selection
of a resident BPF program set cannot be determined at the time of
bpf_object loading and attaching, all BPF programs would otherwise
need to be marked as autoload, leading to unnecessary overhead.
This patch addresses that inefficiency.

A manual-strategy program is loaded via bpf_program__load_manually()
and unloaded via bpf_program__unload_manually(), gated on
bpf_object__prepare() having already run (BTF loaded, maps created,
relocations applied) rather than requiring a full bpf_object__load().
Manual programs are skipped by the object's own autoload pass.

Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
 tools/lib/bpf/libbpf.c   | 200 +++++++++++++++++++++++++++++++++------
 tools/lib/bpf/libbpf.h   |  32 ++++++-
 tools/lib/bpf/libbpf.map |   1 +
 3 files changed, 204 insertions(+), 29 deletions(-)

diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index d88df7e4c86d..61195d16ee0f 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -496,6 +496,7 @@ struct bpf_program {
 	int fd;
 	enum bpf_prog_load_strategy load_strategy;
 	bool autoattach;
+	bool saved_autoattach;
 	bool sym_global;
 	bool mark_btf_static;
 	enum bpf_prog_type type;
@@ -725,6 +726,7 @@ struct bpf_object {
 
 	bool has_subcalls;
 	bool has_rodata;
+	bool has_manual_progs;
 
 	struct bpf_gen *gen_loader;
 
@@ -795,7 +797,7 @@ static Elf_Data *elf_sec_data(const struct bpf_object *obj, Elf_Scn *scn);
 static Elf64_Sym *elf_sym_by_idx(const struct bpf_object *obj, size_t idx);
 static Elf64_Rel *elf_rel_by_idx(Elf_Data *data, size_t idx);
 
-void bpf_program__unload(struct bpf_program *prog)
+static void bpf_program_unload_full(struct bpf_program *prog)
 {
 	if (!prog)
 		return;
@@ -807,12 +809,30 @@ void bpf_program__unload(struct bpf_program *prog)
 	zfree(&prog->subprogs);
 }
 
+void bpf_program__unload(struct bpf_program *prog)
+{
+	if (!prog)
+		return;
+
+	/*
+	 * MANUAL programs retain their data here so bpf_program__load()
+	 * can reload them later; object teardown paths call
+	 * bpf_program_unload_full() instead to always release it fully.
+	 */
+	if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
+		zclose(prog->fd);
+		return;
+	}
+
+	bpf_program_unload_full(prog);
+}
+
 static void bpf_program__exit(struct bpf_program *prog)
 {
 	if (!prog)
 		return;
 
-	bpf_program__unload(prog);
+	bpf_program_unload_full(prog);
 	zfree(&prog->name);
 	zfree(&prog->sec_name);
 	zfree(&prog->insns);
@@ -8410,6 +8430,7 @@ bpf_object__load_progs(struct bpf_object *obj, int log_level)
 			pr_debug("prog '%s': skipped auto-loading\n", prog->name);
 			continue;
 		}
+
 		prog->log_level |= log_level;
 
 		if (obj->gen_loader)
@@ -8435,6 +8456,8 @@ static int bpf_object_prepare_progs(struct bpf_object *obj)
 
 	for (i = 0; i < obj->nr_programs; i++) {
 		prog = &obj->programs[i];
+		if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
+			obj->has_manual_progs = true;
 		err = bpf_object__sanitize_prog(obj, prog);
 		if (err)
 			return err;
@@ -8461,6 +8484,19 @@ static int bpf_object_init_progs(struct bpf_object *obj, const struct bpf_object
 		prog->type = prog->sec_def->prog_type;
 		prog->expected_attach_type = prog->sec_def->expected_attach_type;
 
+		/*
+		 * struct_ops programs are incompatible with manual loading
+		 * (see bpf_program__set_load_strategy()); reject SEC("!...")
+		 * here too, at declarative parse time, since that check only
+		 * runs on the imperative bpf_program__set_load_strategy() path.
+		 */
+		if (prog->type == BPF_PROG_TYPE_STRUCT_OPS &&
+		    prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
+			pr_warn("prog '%s': struct_ops programs do not support manual loading\n",
+				prog->name);
+			return -EINVAL;
+		}
+
 		/* sec_def can have custom callback which should be called
 		 * after bpf_program is initialized to adjust its properties
 		 */
@@ -8625,7 +8661,7 @@ static int bpf_object_unload(struct bpf_object *obj)
 	}
 
 	for (i = 0; i < obj->nr_programs; i++)
-		bpf_program__unload(&obj->programs[i]);
+		bpf_program_unload_full(&obj->programs[i]);
 
 	return 0;
 }
@@ -9068,33 +9104,50 @@ static void bpf_object_unpin(struct bpf_object *obj)
 			bpf_map__unpin(&obj->maps[i], NULL);
 }
 
-static void bpf_object_cleanup_btf(struct bpf_object *obj)
+static void bpf_object_cleanup_btf(struct bpf_object *obj, bool force)
 {
 	int i;
 
-	/* clean up module BTFs */
-	for (i = 0; i < obj->btf_module_cnt; i++) {
-		close(obj->btf_modules[i].fd);
-		btf__free(obj->btf_modules[i].btf);
-		free(obj->btf_modules[i].name);
+	/*
+	 * Module BTF fds may still be borrowed (via fd_array,
+	 * attach_btf_obj_fd, or baked into relocated instructions) by
+	 * programs that have not been manually loaded yet, so defer
+	 * closing them in that case to the end of the object lifetime,
+	 * unless the caller forces immediate cleanup.
+	 */
+	if (force || !obj->has_manual_progs) {
+		for (i = 0; i < obj->btf_module_cnt; i++) {
+			close(obj->btf_modules[i].fd);
+			btf__free(obj->btf_modules[i].btf);
+			free(obj->btf_modules[i].name);
+		}
+		obj->btf_module_cnt = 0;
+		obj->btf_module_cap = 0;
+		obj->btf_modules_loaded = false;
+		zfree(&obj->btf_modules);
 	}
-	obj->btf_module_cnt = 0;
-	obj->btf_module_cap = 0;
-	obj->btf_modules_loaded = false;
-	zfree(&obj->btf_modules);
 
-	/* clean up vmlinux BTF */
-	btf__free(obj->btf_vmlinux);
-	obj->btf_vmlinux = NULL;
+	/*
+	 * The btf_vmlinux data is needed for manually loaded programs,
+	 * so defer freeing it in that case to the end of the object lifetime.
+	 */
+	if (force || !obj->has_manual_progs) {
+		btf__free(obj->btf_vmlinux);
+		obj->btf_vmlinux = NULL;
+	}
 }
 
-static void bpf_object_post_load_cleanup(struct bpf_object *obj)
+static void bpf_object_post_load_cleanup(struct bpf_object *obj, bool force)
 {
-	/* clean up fd_array */
-	zfree(&obj->fd_array);
+	/*
+	 * The fd array is needed for manually loaded programs,
+	 * so defer freeing it in that case to the end of the object lifetime.
+	 */
+	if (force || !obj->has_manual_progs)
+		zfree(&obj->fd_array);
 
 	/* clean up BTF */
-	bpf_object_cleanup_btf(obj);
+	bpf_object_cleanup_btf(obj, force);
 }
 
 static int bpf_object_prepare(struct bpf_object *obj, const char *target_btf_path)
@@ -9169,7 +9222,7 @@ static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const ch
 			err = bpf_gen__finish(obj->gen_loader, obj->nr_programs, obj->nr_maps);
 	}
 
-	bpf_object_post_load_cleanup(obj);
+	bpf_object_post_load_cleanup(obj, false);
 	obj->state = OBJ_LOADED; /* doesn't matter if successfully or not */
 
 	if (err) {
@@ -9637,7 +9690,7 @@ void bpf_object__close(struct bpf_object *obj)
 	 * bpf_object__load(), we need to clean up stuff that is normally
 	 * cleaned up at the end of loading step
 	 */
-	bpf_object_post_load_cleanup(obj);
+	bpf_object_post_load_cleanup(obj, true);
 
 	usdt_manager_free(obj->usdt_man);
 	obj->usdt_man = NULL;
@@ -9646,7 +9699,6 @@ void bpf_object__close(struct bpf_object *obj)
 	bpf_object__elf_finish(obj);
 	bpf_object_unload(obj);
 	btf__free(obj->btf);
-	btf__free(obj->btf_vmlinux);
 	btf_ext__free(obj->btf_ext);
 
 	for (i = 0; i < obj->nr_maps; i++)
@@ -9822,9 +9874,13 @@ bool bpf_program__autoattach(const struct bpf_program *prog)
 	return prog->autoattach;
 }
 
-void bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach)
+int bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach)
 {
+	if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
+		return libbpf_err(-EINVAL);
+
 	prog->autoattach = autoattach;
+	return 0;
 }
 
 const struct bpf_insn *bpf_program__insns(const struct bpf_program *prog)
@@ -12748,7 +12804,7 @@ static int collect_func_ids_by_glob(const struct bpf_program *prog, const char *
 	err = collect_btf_func_ids_by_glob(btf, pattern, ids);
 
 cleanup:
-	bpf_object_cleanup_btf(obj);
+	bpf_object_cleanup_btf(obj, false);
 	return err;
 }
 
@@ -15305,10 +15361,69 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
 
 int bpf_program__set_load_strategy(struct bpf_program *prog, enum bpf_prog_load_strategy strategy)
 {
-	if (prog->obj->state >= OBJ_LOADED)
+	struct bpf_object *obj = prog->obj;
+
+	/*
+	 * has_manual_progs is snapshotted once in bpf_object_prepare_progs()
+	 * and never recomputed; once the object is prepared, no transition
+	 * into or out of MANUAL may change which programs are MANUAL,
+	 * regardless of direction. AUTO<->DISABLED transitions never touch
+	 * MANUAL and keep the looser, pre-existing OBJ_LOADED gate.
+	 */
+	if (strategy == BPF_PROG_LOAD_STRATEGY_MANUAL ||
+	    prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
+		if (obj->state >= OBJ_PREPARED)
+			return libbpf_err(-EINVAL);
+	} else if (obj->state >= OBJ_LOADED) {
+		return libbpf_err(-EINVAL);
+	}
+
+	if (strategy == prog->load_strategy)
+		return 0;
+
+	switch (strategy) {
+	case BPF_PROG_LOAD_STRATEGY_DISABLED:
+	case BPF_PROG_LOAD_STRATEGY_AUTO:
+		if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
+			prog->autoattach = prog->saved_autoattach;
+		prog->load_strategy = strategy;
+		break;
+	case BPF_PROG_LOAD_STRATEGY_MANUAL:
+		/*
+		 * Manually-loaded programs are not supported for gen_loader.
+		 * This is because bpf_object_load_prog is not called for
+		 * manually-loaded programs, so such programs are not visible
+		 * to gen_loader. For this reason, prevent calling
+		 * bpf_program__set_load_strategy(MANUAL) when gen_loader was
+		 * used to generate a BPF object loader.
+		 * A gen_loader implementation is being called for autoloaded
+		 * programs and defines its own model for loading BPF programs.
+		 * To pass a BPF program to gen_loader, set the program's load strategy
+		 * to LD_AUTOLOAD.
+		 */
+		if (obj->gen_loader)
+			return libbpf_err(-EOPNOTSUPP);
+
+		/*
+		 * struct_ops programs are incompatible with manual loading:
+		 * bpf_map_prepare_vdata() bakes each member's fd into kern_vdata
+		 * automatically during bpf_object__load(), before a MANUAL member
+		 * could ever be loaded, and nothing re-bakes it afterwards.
+		 */
+		if (prog->type == BPF_PROG_TYPE_STRUCT_OPS)
+			return libbpf_err(-EINVAL);
+
+		if (prog_is_subprog(obj, prog))
+			return libbpf_err(-EINVAL);
+
+		prog->saved_autoattach = prog->autoattach;
+		prog->load_strategy = BPF_PROG_LOAD_STRATEGY_MANUAL;
+		prog->autoattach = false;
+		break;
+	default:
 		return libbpf_err(-EINVAL);
+	}
 
-	prog->load_strategy = strategy;
 	return 0;
 }
 
@@ -15316,3 +15431,34 @@ enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program
 {
 	return prog->load_strategy;
 }
+
+/*
+ * This function must be called after bpf_object__prepare (or
+ * bpf_object__load, which calls bpf_object__prepare internally).
+ * Manually-loaded program data is initialized on object prepare.
+ * Post-prepare initialization is not supported.
+ */
+int
+bpf_program__load(struct bpf_program *prog)
+{
+	int err;
+	struct bpf_object *obj = prog->obj;
+
+	if (obj->state < OBJ_PREPARED)
+		return libbpf_err(-EINVAL);
+
+	if (prog_is_subprog(obj, prog) || prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
+		return libbpf_err(-EINVAL);
+
+	if (prog->fd >= 0)
+		return libbpf_err(-EBUSY);
+
+	err = bpf_object_load_prog(obj, prog, prog->insns, prog->insns_cnt,
+				   obj->license, obj->kern_version, &prog->fd);
+	if (err) {
+		pr_warn("prog '%s': failed to load: %s\n", prog->name, errstr(err));
+		return libbpf_err(err);
+	}
+
+	return 0;
+}
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index a06259c4dacc..5e44bcfa2ca1 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -378,7 +378,7 @@ LIBBPF_API const char *bpf_program__section_name(const struct bpf_program *prog)
 LIBBPF_API bool bpf_program__autoload(const struct bpf_program *prog);
 LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog, bool autoload);
 LIBBPF_API bool bpf_program__autoattach(const struct bpf_program *prog);
-LIBBPF_API void bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach);
+LIBBPF_API int bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach);
 
 struct bpf_insn;
 
@@ -459,6 +459,16 @@ LIBBPF_API int bpf_program__pin(struct bpf_program *prog, const char *path);
  * @return 0, on success; negative error code, otherwise
  */
 LIBBPF_API int bpf_program__unpin(struct bpf_program *prog, const char *path);
+/**
+ * @brief **bpf_program__unload()** unloads a BPF program, closing its fd.
+ *
+ * If the program's load strategy is BPF_PROG_LOAD_STRATEGY_MANUAL, only the
+ * fd is closed and the program's data is retained so it can be reloaded
+ * later via bpf_program__load(). For any other load strategy, the program's
+ * data is also freed and it cannot be reloaded.
+ *
+ * @param prog BPF program to unload
+ */
 LIBBPF_API void bpf_program__unload(struct bpf_program *prog);
 
 struct bpf_link;
@@ -2108,21 +2118,28 @@ LIBBPF_API int bpf_program__clone(struct bpf_program *prog, const struct bpf_pro
  *
  * - BPF_PROG_LOAD_STRATEGY_DISABLED: the program is not loaded.
  * - BPF_PROG_LOAD_STRATEGY_AUTO: the program is autoloaded when the bpf_object is loaded.
+ * - BPF_PROG_LOAD_STRATEGY_MANUAL: the program is loaded and attached manually.
  */
 enum bpf_prog_load_strategy {
 	BPF_PROG_LOAD_STRATEGY_DISABLED = 0,
 	BPF_PROG_LOAD_STRATEGY_AUTO,
+	BPF_PROG_LOAD_STRATEGY_MANUAL,
 };
 
 /**
  * @brief **bpf_program__set_load_strategy()** sets the load strategy of a
  * BPF program, controlling whether and when it gets loaded into the kernel.
  *
- * Can only be called before the enclosing bpf_object is loaded.
+ * Can only be called before the enclosing bpf_object is loaded, except when
+ * the program's current or new strategy is BPF_PROG_LOAD_STRATEGY_MANUAL, in
+ * which case it can only be called before the enclosing bpf_object is
+ * prepared.
  *
  * @param prog BPF program to update
  * @param strategy new load strategy for the program
  * @return 0 on success; negative error code if the object was already loaded
+ * (or, if the program's current or new strategy is
+ * BPF_PROG_LOAD_STRATEGY_MANUAL, already prepared)
  */
 LIBBPF_API int bpf_program__set_load_strategy(struct bpf_program *prog,
 					      enum bpf_prog_load_strategy strategy);
@@ -2136,6 +2153,17 @@ LIBBPF_API int bpf_program__set_load_strategy(struct bpf_program *prog,
  */
 LIBBPF_API enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog);
 
+/**
+ * @brief **bpf_program__load()** loads a BPF program whose load strategy is
+ * BPF_PROG_LOAD_STRATEGY_MANUAL. The enclosing bpf_object must already be
+ * prepared.
+ *
+ * @param prog BPF program to load; must not be a subprogram, must have load
+ * strategy BPF_PROG_LOAD_STRATEGY_MANUAL, and must not already be loaded
+ * @return 0 on success; negative error code otherwise
+ */
+LIBBPF_API int bpf_program__load(struct bpf_program *prog);
+
 #ifdef __cplusplus
 } /* extern "C" */
 #endif
diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
index 03c3d2bd15bf..8def5474885a 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -463,6 +463,7 @@ LIBBPF_1.8.0 {
 		bpf_program__attach_tracing_multi;
 		bpf_program__clear_flags;
 		bpf_program__clone;
+		bpf_program__load;
 		bpf_program__load_strategy;
 		bpf_program__set_load_strategy;
 		btf__find_by_name_kind_own;
-- 
2.34.1


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

* [PATCH bpf-next v4 3/7] libbpf: Support declarative manual load via SEC("!...") prefix
  2026-09-21 22:39 [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
  2026-09-21 22:39 ` [PATCH bpf-next v4 1/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
  2026-09-21 22:39 ` [PATCH bpf-next v4 2/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
@ 2026-09-21 22:39 ` Andrey Grodzovsky
  2026-09-21 23:12   ` sashiko-bot
  2026-09-21 23:21   ` bot+bpf-ci
  2026-09-21 22:39 ` [PATCH bpf-next v4 4/7] libbpf: Reject gen_loader for objects with already-manual programs Andrey Grodzovsky
                   ` (4 subsequent siblings)
  7 siblings, 2 replies; 17+ messages in thread
From: Andrey Grodzovsky @ 2026-09-21 22:39 UTC (permalink / raw)
  To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source

Add a SEC("!...") section-name prefix, letting a program declare
itself manually-loaded in its source instead of requiring an
imperative bpf_program__set_load_strategy() call, following the exact
convention already used for SEC("?...").

The prefix is recognized and stripped in bpf_object__init_prog(),
before find_sec_def() ever runs, directly setting load_strategy to
MANUAL and initializing autoattach/saved_autoattach - the same place
and same style '?' already uses, and before BTF or sec_def exist.

Assisted-by: Claude:claude-sonnet-5
Suggested-by: Andrii Nakryiko <andrii@kernel.org>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
 tools/lib/bpf/libbpf.c | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 61195d16ee0f..789d2df7eaae 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -890,16 +890,25 @@ bpf_object__init_prog(struct bpf_object *obj, struct bpf_program *prog,
 	/* libbpf's convention for SEC("?abc...") is that it's just like
 	 * SEC("abc...") but the corresponding bpf_program starts out with
 	 * autoload set to false.
+	 *
+	 * Similarly, SEC("!abc...") marks the program for manual loading:
+	 * it is skipped by the bulk auto-load pass and must be explicitly
+	 * loaded later via bpf_program__load_manually().
 	 */
 	if (sec_name[0] == '?') {
 		prog->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
 		/* from now on forget there was ? in section name */
 		sec_name++;
+	} else if (sec_name[0] == '!') {
+		prog->load_strategy = BPF_PROG_LOAD_STRATEGY_MANUAL;
+		/* from now on forget there was ! in section name */
+		sec_name++;
 	} else {
 		prog->load_strategy = BPF_PROG_LOAD_STRATEGY_AUTO;
 	}
 
-	prog->autoattach = true;
+	prog->saved_autoattach = true;
+	prog->autoattach = prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL;
 
 	/* inherit object's log_level */
 	prog->log_level = obj->log_level;
@@ -15399,7 +15408,7 @@ int bpf_program__set_load_strategy(struct bpf_program *prog, enum bpf_prog_load_
 		 * A gen_loader implementation is being called for autoloaded
 		 * programs and defines its own model for loading BPF programs.
 		 * To pass a BPF program to gen_loader, set the program's load strategy
-		 * to LD_AUTOLOAD.
+		 * to BPF_PROG_LOAD_STRATEGY_AUTO.
 		 */
 		if (obj->gen_loader)
 			return libbpf_err(-EOPNOTSUPP);
-- 
2.34.1


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

* [PATCH bpf-next v4 4/7] libbpf: Reject gen_loader for objects with already-manual programs
  2026-09-21 22:39 [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
                   ` (2 preceding siblings ...)
  2026-09-21 22:39 ` [PATCH bpf-next v4 3/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
@ 2026-09-21 22:39 ` Andrey Grodzovsky
  2026-09-21 22:39 ` [PATCH bpf-next v4 5/7] libbpf: Version bpf_program__set_autoattach() ABI change Andrey Grodzovsky
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 17+ messages in thread
From: Andrey Grodzovsky @ 2026-09-21 22:39 UTC (permalink / raw)
  To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source

bpf_program__set_load_strategy()'s MANUAL case rejects setting MANUAL
strategy while a gen_loader is already attached, but a program marked
MANUAL declaratively (SEC("!...")) gets that strategy during
bpf_object__open(), before bpf_object__gen_loader() can ever be
called, so the existing guard can never observe it.

Left unchecked, bpf_object_load_progs() skips such programs, so
gen->nr_progs undercounts relative to the object's real program
count. bpf_gen__finish() only rejects the opposite mismatch direction
(nr_progs < gen->nr_progs), so this passes silently, and every
generated skeleton program slot after the manual one ends up wired to
the wrong prog_fd.

Catch it at the one point guaranteed to run after any MANUAL marking
has already happened: reject in bpf_object__gen_loader() itself if any
program already has load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL.

Assisted-by: Claude:claude-sonnet-5
Suggested-by: Andrii Nakryiko <andrii@kernel.org>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
 tools/lib/bpf/libbpf.c | 34 ++++++++++++++++++++++++----------
 1 file changed, 24 insertions(+), 10 deletions(-)

diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 789d2df7eaae..6e7ec026d944 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -9788,11 +9788,31 @@ int bpf_object__set_kversion(struct bpf_object *obj, __u32 kern_version)
 int bpf_object__gen_loader(struct bpf_object *obj, struct gen_loader_opts *opts)
 {
 	struct bpf_gen *gen;
+	size_t i;
 
 	if (!opts)
 		return libbpf_err(-EFAULT);
 	if (!OPTS_VALID(opts, gen_loader_opts))
 		return libbpf_err(-EINVAL);
+
+	/*
+	 * Manually-loaded programs are not visible to gen_loader (see
+	 * bpf_program__set_load_strategy()'s MANUAL case), and marking a
+	 * program MANUAL happens during bpf_object__open(), before this
+	 * function can ever run, so that guard can never catch it here.
+	 * Reject any pre-existing MANUAL program now, since this is the
+	 * earliest point where both are known.
+	 */
+	for (i = 0; i < obj->nr_programs; i++) {
+		struct bpf_program *prog = &obj->programs[i];
+
+		if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
+			pr_warn("prog '%s': gen_loader does not support manually-loaded programs\n",
+				prog->name);
+			return libbpf_err(-EOPNOTSUPP);
+		}
+	}
+
 	gen = calloc(1, sizeof(*gen));
 	if (!gen)
 		return libbpf_err(-ENOMEM);
@@ -15399,16 +15419,10 @@ int bpf_program__set_load_strategy(struct bpf_program *prog, enum bpf_prog_load_
 		break;
 	case BPF_PROG_LOAD_STRATEGY_MANUAL:
 		/*
-		 * Manually-loaded programs are not supported for gen_loader.
-		 * This is because bpf_object_load_prog is not called for
-		 * manually-loaded programs, so such programs are not visible
-		 * to gen_loader. For this reason, prevent calling
-		 * bpf_program__set_load_strategy(MANUAL) when gen_loader was
-		 * used to generate a BPF object loader.
-		 * A gen_loader implementation is being called for autoloaded
-		 * programs and defines its own model for loading BPF programs.
-		 * To pass a BPF program to gen_loader, set the program's load strategy
-		 * to BPF_PROG_LOAD_STRATEGY_AUTO.
+		 * Manually-loaded programs are not visible to gen_loader,
+		 * since bpf_object__load_progs() skips them during the bulk
+		 * load pass; see bpf_object__gen_loader()'s own guard for
+		 * the full explanation.
 		 */
 		if (obj->gen_loader)
 			return libbpf_err(-EOPNOTSUPP);
-- 
2.34.1


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

* [PATCH bpf-next v4 5/7] libbpf: Version bpf_program__set_autoattach() ABI change
  2026-09-21 22:39 [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
                   ` (3 preceding siblings ...)
  2026-09-21 22:39 ` [PATCH bpf-next v4 4/7] libbpf: Reject gen_loader for objects with already-manual programs Andrey Grodzovsky
@ 2026-09-21 22:39 ` Andrey Grodzovsky
  2026-09-21 23:31   ` sashiko-bot
  2026-09-21 22:39 ` [PATCH bpf-next v4 6/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
                   ` (2 subsequent siblings)
  7 siblings, 1 reply; 17+ messages in thread
From: Andrey Grodzovsky @ 2026-09-21 22:39 UTC (permalink / raw)
  To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source

bpf_program__set_autoattach()'s return type changed from void to int to
let callers observe the new -EINVAL rejection for MANUAL-strategy
programs, but the symbol stayed under its original LIBBPF_1.0.0 node in
libbpf.map. Binaries already linked against the old void-returning ABI
recorded a request for exactly that symbol@version pair at link time;
without a version bump they would silently start hitting the new
rejection behavior underneath them after a libbpf upgrade.

Split the symbol via ELF symbol versioning:

  - bpf_program__set_autoattach_deprecated() keeps the original
    unconditional behavior, bound via COMPAT_VERSION() to the existing
    LIBBPF_1.0.0 node.
  - bpf_program__set_autoattach_v1_8_0() carries the new MANUAL-rejecting
    behavior, bound via DEFAULT_VERSION() to the new LIBBPF_1.8.0 node.

Old binaries keep resolving bpf_program__set_autoattach() to the old
behavior at runtime; anything linked against the current headers/map
gets the new int-returning, MANUAL-rejecting behavior. Also adds the
missing __LIBBPF_MARK_DEPRECATED_1_8 gate to libbpf_common.h (only the
1.0 gate existed) so LIBBPF_DEPRECATED_SINCE(1, 8, ...) can mark the
deprecated variant.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
 tools/lib/bpf/libbpf.c        | 9 ++++++++-
 tools/lib/bpf/libbpf.h        | 6 ++++++
 tools/lib/bpf/libbpf.map      | 2 ++
 tools/lib/bpf/libbpf_common.h | 6 ++++++
 4 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 6e7ec026d944..3e4eaeafea0e 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -9903,7 +9903,14 @@ bool bpf_program__autoattach(const struct bpf_program *prog)
 	return prog->autoattach;
 }
 
-int bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach)
+COMPAT_VERSION(bpf_program__set_autoattach_deprecated, bpf_program__set_autoattach, LIBBPF_1.0.0)
+void bpf_program__set_autoattach_deprecated(struct bpf_program *prog, bool autoattach)
+{
+	prog->autoattach = autoattach;
+}
+
+DEFAULT_VERSION(bpf_program__set_autoattach_v1_8_0, bpf_program__set_autoattach, LIBBPF_1.8.0)
+int bpf_program__set_autoattach_v1_8_0(struct bpf_program *prog, bool autoattach)
 {
 	if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
 		return libbpf_err(-EINVAL);
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index 5e44bcfa2ca1..8a04d81ccaeb 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -379,6 +379,12 @@ LIBBPF_API bool bpf_program__autoload(const struct bpf_program *prog);
 LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog, bool autoload);
 LIBBPF_API bool bpf_program__autoattach(const struct bpf_program *prog);
 LIBBPF_API int bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach);
+/* this "specialization" should go away once the deprecation window for
+ * bpf_program__set_autoattach_deprecated() closes
+ */
+LIBBPF_API int bpf_program__set_autoattach_v1_8_0(struct bpf_program *prog, bool autoattach);
+LIBBPF_DEPRECATED_SINCE(1, 8, "use int-returning bpf_program__set_autoattach() instead")
+LIBBPF_API void bpf_program__set_autoattach_deprecated(struct bpf_program *prog, bool autoattach);
 
 struct bpf_insn;
 
diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
index 8def5474885a..999e791d9887 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -465,6 +465,8 @@ LIBBPF_1.8.0 {
 		bpf_program__clone;
 		bpf_program__load;
 		bpf_program__load_strategy;
+		bpf_program__set_autoattach;
+		bpf_program__set_autoattach_deprecated;
 		bpf_program__set_load_strategy;
 		btf__find_by_name_kind_own;
 		btf__new_empty_opts;
diff --git a/tools/lib/bpf/libbpf_common.h b/tools/lib/bpf/libbpf_common.h
index 8fe248e14eb6..12ad4f5f558f 100644
--- a/tools/lib/bpf/libbpf_common.h
+++ b/tools/lib/bpf/libbpf_common.h
@@ -36,6 +36,12 @@
 #define __LIBBPF_MARK_DEPRECATED_1_0(X)
 #endif
 
+#if __LIBBPF_CURRENT_VERSION_GEQ(1, 8)
+#define __LIBBPF_MARK_DEPRECATED_1_8(X) X
+#else
+#define __LIBBPF_MARK_DEPRECATED_1_8(X)
+#endif
+
 /* This set of internal macros allows to do "function overloading" based on
  * number of arguments provided by used in backwards-compatible way during the
  * transition to libbpf 1.0
-- 
2.34.1


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

* [PATCH bpf-next v4 6/7] selftests/bpf: Cover BPF program load strategy transitions
  2026-09-21 22:39 [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
                   ` (4 preceding siblings ...)
  2026-09-21 22:39 ` [PATCH bpf-next v4 5/7] libbpf: Version bpf_program__set_autoattach() ABI change Andrey Grodzovsky
@ 2026-09-21 22:39 ` Andrey Grodzovsky
  2026-09-21 23:37   ` sashiko-bot
  2026-09-21 22:39 ` [PATCH bpf-next v4 7/7] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
  2026-09-22  1:37 ` [PATCH bpf-next v4 0/7] libbpf: " Alexei Starovoitov
  7 siblings, 1 reply; 17+ messages in thread
From: Andrey Grodzovsky @ 2026-09-21 22:39 UTC (permalink / raw)
  To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source

From: Slava Imameev <slava.imameev@crowdstrike.com>

Add load_type test covering load strategy transitions
(DISABLED/AUTO/MANUAL), set_autoload() bool/enum compatibility,
autoattach restore on leaving MANUAL, and the manual load/attach
cycle after the object is loaded.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
 .../selftests/bpf/prog_tests/load_type.c      | 186 ++++++++++++++++++
 .../selftests/bpf/progs/test_load_type.c      |  31 +++
 2 files changed, 217 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/load_type.c
 create mode 100644 tools/testing/selftests/bpf/progs/test_load_type.c

diff --git a/tools/testing/selftests/bpf/prog_tests/load_type.c b/tools/testing/selftests/bpf/prog_tests/load_type.c
new file mode 100644
index 000000000000..ab42c3e887fc
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
@@ -0,0 +1,186 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include <time.h>
+#include "test_load_type.skel.h"
+
+void test_load_type(void)
+{
+	struct bpf_link *link = NULL;
+	struct test_load_type *skel;
+	int err;
+
+	skel = test_load_type__open();
+	if (!ASSERT_OK_PTR(skel, "skel_open"))
+		return;
+
+	/* don't load prog1 */
+	bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);
+
+	/* load and attach prog2 */
+	bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_AUTO);
+	if (!ASSERT_TRUE(bpf_program__autoload(skel->progs.prog2), "prog2_autoload"))
+		goto cleanup;
+
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+	if (!ASSERT_OK(err, "set_load_strategy_manual"))
+		goto cleanup;
+	if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_MANUAL,
+		       "prog3_load_strategy"))
+		goto cleanup;
+
+	/*
+	 * bpf_program__set_autoload() is a thin forwarder to
+	 * set_load_strategy(), restricted to AUTO/DISABLED to preserve its
+	 * original bool on/off meaning; it does change the load strategy of
+	 * a program that isn't currently BPF_PROG_LOAD_STRATEGY_AUTO.
+	 */
+	err = bpf_program__set_autoload(skel->progs.prog3, false);
+	if (!ASSERT_OK(err, "set_autoload_false"))
+		goto cleanup;
+
+	if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3),
+		       BPF_PROG_LOAD_STRATEGY_DISABLED, "prog3_load_strategy_after_false"))
+		goto cleanup;
+
+	err = bpf_program__set_autoload(skel->progs.prog3, true);
+	if (!ASSERT_OK(err, "set_autoload_true"))
+		goto cleanup;
+
+	if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_AUTO,
+		       "prog3_load_strategy_after_true"))
+		goto cleanup;
+
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+	if (!ASSERT_OK(err, "set_load_strategy_manual_enum"))
+		goto cleanup;
+
+	if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_MANUAL,
+		       "prog3_load_strategy_after_manual_enum"))
+		goto cleanup;
+
+	/*
+	 * leaving MANUAL for AUTO must restore autoattach (regression test for
+	 * the autoattach residue bug: set_load_strategy(MANUAL) clears autoattach, and
+	 * nothing used to restore it on exit)
+	 */
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_AUTO);
+	if (!ASSERT_OK(err, "set_load_strategy_auto"))
+		goto cleanup;
+
+	if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_AUTO,
+		       "prog3_load_strategy_auto"))
+		goto cleanup;
+
+	if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog3), "prog3_autoattach_restored"))
+		goto cleanup;
+
+	/*
+	 * confirm the restore also holds across a MANUAL -> DISABLED -> AUTO
+	 * round-trip: AUTO and DISABLED share one guard keyed off the source
+	 * strategy being MANUAL, so autoattach is already restored at the
+	 * MANUAL -> DISABLED step, not by a separate DISABLED -> AUTO one
+	 */
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+	if (!ASSERT_OK(err, "set_load_strategy_manual_again"))
+		goto cleanup;
+
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+	if (!ASSERT_OK(err, "set_load_strategy_disabled"))
+		goto cleanup;
+
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_AUTO);
+	if (!ASSERT_OK(err, "set_load_strategy_auto_via_disabled"))
+		goto cleanup;
+
+	if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog3),
+			 "prog3_autoattach_restored_via_disabled"))
+		goto cleanup;
+
+	/*
+	 * discriminate the restore from a hard-coded `true`: force autoattach
+	 * to false before entering MANUAL, then confirm AUTO restores it back
+	 * to false rather than unconditionally re-enabling it
+	 */
+	err = bpf_program__set_autoattach(skel->progs.prog3, false);
+	if (!ASSERT_OK(err, "set_autoattach_false"))
+		goto cleanup;
+
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+	if (!ASSERT_OK(err, "set_load_strategy_manual_for_false_restore"))
+		goto cleanup;
+
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_AUTO);
+	if (!ASSERT_OK(err, "set_load_strategy_auto_for_false_restore"))
+		goto cleanup;
+
+	if (!ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog3),
+			  "prog3_autoattach_restored_false"))
+		goto cleanup;
+
+	/* restore autoattach to true for the rest of the test */
+	err = bpf_program__set_autoattach(skel->progs.prog3, true);
+	if (!ASSERT_OK(err, "set_autoattach_true_again"))
+		goto cleanup;
+
+	/* an out-of-range load strategy is rejected */
+	err = bpf_program__set_load_strategy(skel->progs.prog3, (enum bpf_prog_load_strategy)999);
+	if (!ASSERT_ERR(err, "set_load_strategy_invalid"))
+		goto cleanup;
+
+	/* change the strategy back to BPF_PROG_LOAD_STRATEGY_MANUAL for the rest of the test */
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+	if (!ASSERT_OK(err, "set_load_strategy_manual_final"))
+		goto cleanup;
+
+	if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_MANUAL,
+		       "prog3_load_strategy_final"))
+		goto cleanup;
+
+	err = test_load_type__load(skel);
+	if (!ASSERT_OK(err, "skel_load"))
+		goto cleanup;
+
+	if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog2), "prog2_autoattach"))
+		goto cleanup;
+	if (!ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog3), "prog3_autoattach"))
+		goto cleanup;
+
+	/* loaded program strategy cannot be changed */
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+	ASSERT_ERR(err, "set_load_strategy_after_load");
+
+	err = test_load_type__attach(skel);
+	if (!ASSERT_OK(err, "skel_attach"))
+		goto cleanup;
+
+	usleep(1);
+
+	ASSERT_FALSE(skel->bss->prog1_called, "prog1_called");
+	ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+	ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+	err = bpf_program__load(skel->progs.prog3);
+	if (!ASSERT_OK(err, "load_manually"))
+		goto cleanup;
+
+	/* attach prog3 */
+	link = bpf_program__attach(skel->progs.prog3);
+	if (!ASSERT_OK_PTR(link, "attach"))
+		goto cleanup;
+
+	usleep(1);
+
+	if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_again"))
+		goto cleanup;
+
+	/* detach prog3 as test_load_type__destroy doesn't detach manually loaded programs */
+	err = bpf_link__destroy(link);
+	ASSERT_OK(err, "link_destroy");
+	link = NULL;
+
+cleanup:
+	if (link)
+		bpf_link__destroy(link);
+	test_load_type__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/test_load_type.c b/tools/testing/selftests/bpf/progs/test_load_type.c
new file mode 100644
index 000000000000..3d9b81691d7a
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_load_type.c
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+
+bool prog1_called = false;
+bool prog2_called = false;
+bool prog3_called = false;
+
+SEC("raw_tp/sys_enter")
+int prog1(const void *ctx)
+{
+	prog1_called = true;
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog2(const void *ctx)
+{
+	prog2_called = true;
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog3(const void *ctx)
+{
+	prog3_called = true;
+	return 0;
+}
+
+char _license[] SEC("license") = "GPL";
-- 
2.34.1


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

* [PATCH bpf-next v4 7/7] selftests/bpf: Cover BPF program manual loading
  2026-09-21 22:39 [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
                   ` (5 preceding siblings ...)
  2026-09-21 22:39 ` [PATCH bpf-next v4 6/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
@ 2026-09-21 22:39 ` Andrey Grodzovsky
  2026-09-22  1:37 ` [PATCH bpf-next v4 0/7] libbpf: " Alexei Starovoitov
  7 siblings, 0 replies; 17+ messages in thread
From: Andrey Grodzovsky @ 2026-09-21 22:39 UTC (permalink / raw)
  To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source

Add dynamicload test covering the manual load/attach/detach/reload
cycle, declarative MANUAL via SEC("!...") and its imperative
override, bpf_object__prepare() alone being sufficient for manual
load, and a deferred load of a module BTF attach target. Also add
a signed_loader test verifying bpf_object__gen_loader() rejects
objects with a MANUAL program.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
 .../selftests/bpf/prog_tests/dynamicload.c    | 365 ++++++++++++++++++
 .../selftests/bpf/prog_tests/signed_loader.c  |  28 ++
 .../selftests/bpf/progs/test_dynamicload.c    |  54 +++
 3 files changed, 447 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/dynamicload.c
 create mode 100644 tools/testing/selftests/bpf/progs/test_dynamicload.c

diff --git a/tools/testing/selftests/bpf/prog_tests/dynamicload.c b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
new file mode 100644
index 000000000000..eaaa3e8bd54a
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
@@ -0,0 +1,365 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include <time.h>
+#include "test_dynamicload.skel.h"
+
+#define READ_SZ 456
+
+/*
+ * prog4 is marked SEC("!...") in the source instead of being set
+ * imperatively; verify that an explicit bpf_program__set_load_strategy() call
+ * before load overrides the declarative default.
+ */
+static void dynamicload_verify_override(void)
+{
+	struct test_dynamicload *skel;
+	int err;
+
+	skel = test_dynamicload__open();
+	if (!ASSERT_OK_PTR(skel, "skel_open"))
+		return;
+
+	err = bpf_program__set_load_strategy(skel->progs.prog4, BPF_PROG_LOAD_STRATEGY_DISABLED);
+	if (!ASSERT_OK(err, "set_load_strategy_disabled"))
+		goto cleanup;
+
+	if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog4),
+		       BPF_PROG_LOAD_STRATEGY_DISABLED, "prog4_load_strategy_overridden"))
+		goto cleanup;
+
+	/*
+	 * disable prog1/prog3 (also autoload by default) so this load only
+	 * has to succeed for prog2 and the disabled prog4; prog2 loading is
+	 * irrelevant to the assertion below and is left alone
+	 */
+	err = bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);
+	if (!ASSERT_OK(err, "set_load_strategy_disabled_prog1"))
+		goto cleanup;
+	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+	if (!ASSERT_OK(err, "set_load_strategy_disabled_prog3"))
+		goto cleanup;
+
+	err = test_dynamicload__load(skel);
+	if (!ASSERT_OK(err, "skel_load"))
+		goto cleanup;
+
+	/*
+	 * prog4 was overridden to DISABLED, so its load_strategy != MANUAL
+	 * and load() must reject it
+	 */
+	err = bpf_program__load(skel->progs.prog4);
+	ASSERT_ERR(err, "load_after_override");
+
+cleanup:
+	test_dynamicload__destroy(skel);
+}
+
+/*
+ * prog4 is MANUAL via its SEC("!...") marker; verify that
+ * bpf_object__prepare() alone -- without ever calling bpf_object__load() --
+ * is sufficient for bpf_program__load() to succeed, since BTF
+ * loading, map creation, and relocation of MANUAL programs are all
+ * completed by prepare() already.
+ */
+static void dynamicload_verify_prepare_only(void)
+{
+	struct test_dynamicload *skel;
+	struct bpf_link *link = NULL;
+	int err;
+
+	skel = test_dynamicload__open();
+	if (!ASSERT_OK_PTR(skel, "skel_open"))
+		return;
+
+	err = bpf_object__prepare(skel->obj);
+	if (!ASSERT_OK(err, "bpf_object__prepare"))
+		goto cleanup;
+
+	err = bpf_program__load(skel->progs.prog4);
+	if (!ASSERT_OK(err, "load_after_prepare"))
+		goto cleanup;
+
+	if (!ASSERT_GE(bpf_program__fd(skel->progs.prog4), 0, "prog4_fd_after_prepare"))
+		goto cleanup;
+
+	link = bpf_program__attach(skel->progs.prog4);
+	if (!ASSERT_OK_PTR(link, "attach_after_prepare"))
+		goto cleanup;
+
+	usleep(1);
+
+	if (!ASSERT_TRUE(skel->bss->prog4_called, "prog4_called_after_prepare"))
+		goto cleanup;
+
+	err = bpf_link__destroy(link);
+	link = NULL;
+	if (!ASSERT_OK(err, "link_destroy_after_prepare"))
+		goto cleanup;
+
+	/*
+	 * bpf_program__unload() is void now: for a MANUAL program it only
+	 * closes the fd and retains func_info/line_info/subprogs so the
+	 * program can be reloaded later.
+	 */
+	bpf_program__unload(skel->progs.prog4);
+	ASSERT_LT(bpf_program__fd(skel->progs.prog4), 0, "prog4_fd_closed_after_unload");
+
+cleanup:
+	if (link)
+		bpf_link__destroy(link);
+	test_dynamicload__destroy(skel);
+}
+
+/*
+ * prog5 is disabled at parse time; resolve its attach target against
+ * module BTF via bpf_program__set_attach_target() before switching it
+ * to MANUAL and deferring its load past the bulk bpf_object__load().
+ * Regression test for the module BTF fd/array lifetime bug: without
+ * deferring the module BTF fd/array close for MANUAL programs, the fd
+ * cached in prog->attach_btf_obj_fd is closed by the bulk load's
+ * cleanup before this deferred load runs, causing a deterministic
+ * -EINVAL.
+ */
+static void dynamicload_verify_module_btf(void)
+{
+	struct test_dynamicload *skel;
+	struct bpf_link *link;
+	int err;
+
+	if (!env.has_testmod) {
+		test__skip();
+		return;
+	}
+
+	skel = test_dynamicload__open();
+	if (!ASSERT_OK_PTR(skel, "skel_open"))
+		return;
+
+	err = bpf_program__set_attach_target(skel->progs.prog5, 0,
+					     "bpf_testmod:bpf_testmod_test_read");
+	if (!ASSERT_OK(err, "set_attach_target"))
+		goto cleanup;
+
+	err = bpf_program__set_load_strategy(skel->progs.prog5, BPF_PROG_LOAD_STRATEGY_MANUAL);
+	if (!ASSERT_OK(err, "set_load_strategy_manual"))
+		goto cleanup;
+
+	/* keep the other autoload programs out of the way of this load */
+	bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);
+	bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_DISABLED);
+	bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+
+	/*
+	 * bulk load: prog5 itself is skipped (MANUAL), but this is where
+	 * module BTF gets torn down if not correctly deferred
+	 */
+	err = test_dynamicload__load(skel);
+	if (!ASSERT_OK(err, "skel_load"))
+		goto cleanup;
+
+	/*
+	 * deferred load must still succeed: the module BTF fd cached above
+	 * by set_attach_target() must still be a valid, open fd here
+	 */
+	err = bpf_program__load(skel->progs.prog5);
+	if (!ASSERT_OK(err, "load_module_btf"))
+		goto cleanup;
+
+	link = bpf_program__attach(skel->progs.prog5);
+	if (!ASSERT_OK_PTR(link, "attach"))
+		goto cleanup;
+
+	ASSERT_OK(trigger_module_test_read(READ_SZ), "trigger_read");
+	ASSERT_EQ(skel->bss->prog5_sz, READ_SZ, "prog5_sz");
+
+	bpf_link__destroy(link);
+
+cleanup:
+	test_dynamicload__destroy(skel);
+}
+
+static void dynamicload_verify_main_cycle(void)
+{
+	struct bpf_link *link = NULL;
+	struct test_dynamicload *skel;
+	int err;
+
+	skel = test_dynamicload__open();
+	if (!ASSERT_OK_PTR(skel, "skel_open"))
+		return;
+
+	/*
+	 * the SEC("!...") prefix alone, with no imperative call, must set
+	 * prog4's load strategy before it is ever touched below
+	 */
+	if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog4),
+		       BPF_PROG_LOAD_STRATEGY_MANUAL, "prog4_prefix_load_strategy"))
+		goto cleanup;
+	if (!ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog4), "prog4_autoattach"))
+		goto cleanup;
+
+	/* don't load prog1 */
+	bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);
+
+	/* prog2 is autoload */
+	bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_AUTO);
+
+	/* prog3 is manually loaded */
+	bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+
+	err = test_dynamicload__load(skel);
+	if (!ASSERT_OK(err, "skel_load"))
+		goto cleanup;
+
+	err = test_dynamicload__attach(skel);
+	if (!ASSERT_OK(err, "skel_attach"))
+		goto cleanup;
+
+	/* trigger the BPF programs */
+	usleep(1);
+
+	ASSERT_FALSE(skel->bss->prog1_called, "prog1_called");
+	ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+	ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+	ASSERT_FALSE(skel->bss->prog4_called, "prog4_called");
+
+	/* prog1 is disabled for load */
+	err = bpf_program__load(skel->progs.prog1);
+	if (!ASSERT_ERR(err, "load_disabled"))
+		goto cleanup;
+
+	/* prog2 is autoload */
+	err = bpf_program__load(skel->progs.prog2);
+	if (!ASSERT_ERR(err, "load_autoload"))
+		goto cleanup;
+
+	/*
+	 * bpf_program__unload() no longer rejects based on load strategy:
+	 * calling it on prog2 (AUTO, currently loaded and attached) performs
+	 * a full, irreversible unload instead of returning an error
+	 */
+	bpf_program__unload(skel->progs.prog2);
+	ASSERT_LT(bpf_program__fd(skel->progs.prog2), 0, "prog2_fd_closed_after_unload");
+
+	/* reset the call flags */
+	skel->bss->prog2_called = false;
+	skel->bss->prog3_called = false;
+
+	usleep(1);
+
+	ASSERT_FALSE(skel->bss->prog1_called, "prog1_called");
+	ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+	ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+	/* load prog3 */
+	err = bpf_program__load(skel->progs.prog3);
+	if (!ASSERT_OK(err, "load"))
+		goto cleanup;
+
+	/* attach prog3 */
+	link = bpf_program__attach(skel->progs.prog3);
+	if (!ASSERT_OK_PTR(link, "attach"))
+		goto cleanup;
+
+	usleep(1);
+
+	if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called"))
+		goto cleanup;
+
+	/* detach prog3 as test_dynamicload__destroy doesn't detach manually loaded programs */
+	err = bpf_link__destroy(link);
+	link = NULL;
+	if (!ASSERT_OK(err, "link_destroy"))
+		goto cleanup;
+
+	/* reset the call flags after detach */
+	skel->bss->prog2_called = false;
+	skel->bss->prog3_called = false;
+
+	usleep(1);
+
+	ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+	ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+	/* unload prog3; MANUAL strategy means its data is retained for reload */
+	bpf_program__unload(skel->progs.prog3);
+
+	/* reload prog3 */
+	err = bpf_program__load(skel->progs.prog3);
+	if (!ASSERT_OK(err, "load_reload"))
+		goto cleanup;
+
+	/* reattach prog3 */
+	link = bpf_program__attach(skel->progs.prog3);
+	if (!ASSERT_OK_PTR(link, "reattach"))
+		goto cleanup;
+
+	usleep(1);
+
+	if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_reattach"))
+		goto cleanup;
+
+	/* detach prog3 as test_dynamicload__destroy doesn't detach manually loaded programs */
+	err = bpf_link__destroy(link);
+	link = NULL;
+	if (!ASSERT_OK(err, "link_destroy_reattach"))
+		goto cleanup;
+
+	/* reset the call flags after detach */
+	skel->bss->prog2_called = false;
+	skel->bss->prog3_called = false;
+
+	usleep(1);
+
+	ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+	ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+	/*
+	 * run prog4 (declaratively marked) through the same manual
+	 * load/attach/trigger/detach/unload cycle as prog3
+	 */
+	err = bpf_program__load(skel->progs.prog4);
+	if (!ASSERT_OK(err, "prog4_load"))
+		goto cleanup;
+
+	link = bpf_program__attach(skel->progs.prog4);
+	if (!ASSERT_OK_PTR(link, "prog4_attach"))
+		goto cleanup;
+
+	usleep(1);
+
+	if (!ASSERT_TRUE(skel->bss->prog4_called, "prog4_called"))
+		goto cleanup;
+
+	err = bpf_link__destroy(link);
+	link = NULL;
+	if (!ASSERT_OK(err, "prog4_link_destroy"))
+		goto cleanup;
+
+	bpf_program__unload(skel->progs.prog4);
+
+	test_dynamicload__destroy(skel);
+	return;
+
+cleanup:
+	if (link)
+		bpf_link__destroy(link);
+	test_dynamicload__destroy(skel);
+}
+
+void test_dynamicload(void)
+{
+	if (test__start_subtest("main_cycle"))
+		dynamicload_verify_main_cycle();
+
+	if (test__start_subtest("verify_override"))
+		dynamicload_verify_override();
+
+	if (test__start_subtest("verify_prepare_only"))
+		dynamicload_verify_prepare_only();
+
+	if (test__start_subtest("verify_module_btf"))
+		dynamicload_verify_module_btf();
+}
+
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index a0f93756e717..0648816e3d50 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -2168,6 +2168,33 @@ static void signed_module_kfunc_rejected(void)
 	run_setup("cleanup", dir);
 }
 
+/*
+ * a program marked MANUAL is invisible to gen_loader's program count (it is
+ * skipped by bpf_object_load_progs()), so bpf_object__gen_loader() must
+ * reject the whole object up front instead of silently generating a
+ * skeleton whose prog_fd slots no longer line up with the object's programs
+ */
+static void manual_prog_rejected(void)
+{
+	LIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true);
+	struct test_signed_loader *skel;
+	int err;
+
+	skel = test_signed_loader__open();
+	if (!ASSERT_OK_PTR(skel, "skel_open"))
+		return;
+
+	err = bpf_program__set_load_strategy(skel->progs.probe, BPF_PROG_LOAD_STRATEGY_MANUAL);
+	if (!ASSERT_OK(err, "set_load_strategy_manual"))
+		goto cleanup;
+
+	err = bpf_object__gen_loader(skel->obj, &gopts);
+	ASSERT_ERR(err, "gen_loader_rejected");
+
+cleanup:
+	test_signed_loader__destroy(skel);
+}
+
 enum subtest_boot {
 	BOOT_ANY,
 	BOOT_SEALED,
@@ -2211,6 +2238,7 @@ static const struct {
 	{ "signed_map_by_fd_rejected", signed_map_by_fd_rejected, BOOT_SEALED },
 	{ "signed_sparse_fd_array_rejected", signed_sparse_fd_array_rejected, BOOT_SEALED },
 	{ "bpf_keyring_provisioned", bpf_keyring_provisioned, BOOT_UNSEALED },
+	{ "manual_prog_rejected", manual_prog_rejected, BOOT_ANY },
 };
 
 void test_signed_loader(void)
diff --git a/tools/testing/selftests/bpf/progs/test_dynamicload.c b/tools/testing/selftests/bpf/progs/test_dynamicload.c
new file mode 100644
index 000000000000..6cdbfc21ca37
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_dynamicload.c
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+bool prog1_called = false;
+bool prog2_called = false;
+bool prog3_called = false;
+bool prog4_called = false;
+__u32 prog5_sz = 0;
+
+SEC("raw_tp/sys_enter")
+int prog1(const void *ctx)
+{
+	prog1_called = true;
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog2(const void *ctx)
+{
+	prog2_called = true;
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog3(const void *ctx)
+{
+	prog3_called = true;
+	return 0;
+}
+
+SEC("!raw_tp/sys_enter")
+int prog4(const void *ctx)
+{
+	prog4_called = true;
+	return 0;
+}
+
+/*
+ * disabled at parse time; its attach target is resolved against module
+ * BTF via bpf_program__set_attach_target() before it is switched to
+ * MANUAL and loaded
+ */
+SEC("?fentry")
+int BPF_PROG(prog5, struct file *file, struct kobject *kobj,
+	     const struct bin_attribute *bin_attr, char *buf, loff_t off, size_t len)
+{
+	prog5_sz = len;
+	return 0;
+}
+
+char _license[] SEC("license") = "GPL";
-- 
2.34.1


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

* Re: [PATCH bpf-next v4 2/7] libbpf: BPF programs manual loading and attaching
  2026-09-21 22:39 ` [PATCH bpf-next v4 2/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
@ 2026-09-21 23:03   ` sashiko-bot
  2026-09-22 23:51     ` Andrey Grodzovsky
  0 siblings, 1 reply; 17+ messages in thread
From: sashiko-bot @ 2026-09-21 23:03 UTC (permalink / raw)
  To: Andrey Grodzovsky; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] bpf: bpf_program__load() allows loading from failed object, causing Use-After-Close
--

commit 62d928e64d7108c312cff920bafa876c284cb8bd
Author: Slava Imameev <slava.imameev@crowdstrike.com>

libbpf: BPF programs manual loading and attaching

BPF programs designated as manually loaded can be loaded and attached
independently after the initial bpf_object loading and attaching. This
patch adds bpf_program__load() to support loading these manual-strategy
programs independently.

> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index a3085847cb97f..939f0d6378e38 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c

[ ... ]

> @@ -9233,7 +9286,7 @@ static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const ch
>  			err = bpf_gen__finish(obj->gen_loader, obj->nr_programs, obj->nr_maps);
>  	}
>  
> -	bpf_object_post_load_cleanup(obj);
> +	bpf_object_post_load_cleanup(obj, false);
>  	obj->state = OBJ_LOADED; /* doesn't matter if successfully or not */
>  
>  	if (err) {

[Severity: High]
Could this state assignment allow dangerous program loads if
bpf_object_load() fails?

When bpf_object_load() fails after bpf_object_prepare(), it falls through to
error handling which calls bpf_object_unload() and closes the map FDs.
However, the state is still unconditionally set to OBJ_LOADED.

[ ... ]

> @@ -15386,3 +15501,34 @@ enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program
>  {
>  	return prog->load_strategy;
>  }
> +
> +/*
> + * This function must be called after bpf_object__prepare (or
> + * bpf_object__load, which calls bpf_object__prepare internally).
> + * Manually-loaded program data is initialized on object prepare.
> + * Post-prepare initialization is not supported.
> + */
> +int
> +bpf_program__load(struct bpf_program *prog)
> +{
> +	int err;
> +	struct bpf_object *obj = prog->obj;
> +
> +	if (obj->state < OBJ_PREPARED)
> +		return libbpf_err(-EINVAL);

[Severity: High]
Is this state check sufficient to prevent a use-after-close if the underlying
object failed to load?

If an application ignores a prior bpf_object_load() error and calls
bpf_program__load(), this check will pass because the failed object is now in
the OBJ_LOADED state (which is >= OBJ_PREPARED).

Because the failure triggered bpf_object_unload(), the map FDs were closed,
allowing bpf_program__load() to proceed with stale map FDs.

If a map FD is reused, the kernel might attach the BPF program to an
unrelated map, causing data corruption or security boundary violations.
For programs with BTF, the kernel rejects the load with -EFAULT because
func_info is freed but func_info_cnt remains > 0. For programs without BTF,
the kernel proceeds using the stale FDs.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260921223937.3203093-1-andrey.grodzovsky@crowdstrike.com?part=2

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

* Re: [PATCH bpf-next v4 3/7] libbpf: Support declarative manual load via SEC("!...") prefix
  2026-09-21 22:39 ` [PATCH bpf-next v4 3/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
@ 2026-09-21 23:12   ` sashiko-bot
  2026-09-21 23:21   ` bot+bpf-ci
  1 sibling, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-09-21 23:12 UTC (permalink / raw)
  To: Andrey Grodzovsky; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] libbpf: BPF multi-line comment style violation in bpf_object__init_prog()
- [Low] libbpf: comment references non-existent bpf_program__load_manually() API
--

commit ad283ccfb2e02481009ae746b5802475a68fb97e
Author: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>

libbpf: Support declarative manual load via SEC("!...") prefix
    
Add a SEC("!...") section-name prefix, letting a program declare itself
manually-loaded in its source instead of requiring an imperative
bpf_program__set_load_strategy() call. The prefix is recognized and stripped in
bpf_object__init_prog(), directly setting the load_strategy to MANUAL.

> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index 939f0d6378e38..476172933a1fc 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
> @@ -892,16 +892,25 @@ bpf_object__init_prog(struct bpf_object *obj, struct bpf_program *prog,
>  	/* libbpf's convention for SEC("?abc...") is that it's just like
>  	 * SEC("abc...") but the corresponding bpf_program starts out with
>  	 * autoload set to false.

[Severity: Low]
This isn't a functional bug, but the BPF subsystem guidelines strictly require
all multi-line comments in tools/lib/bpf/ to place the opening /* on its own
line. Since this patch expands the comment block to document the new SEC("!...")
prefix, should the opening /* be moved to its own line to align with the
formatting standards?

> +	 *
> +	 * Similarly, SEC("!abc...") marks the program for manual loading:
> +	 * it is skipped by the bulk auto-load pass and must be explicitly
> +	 * loaded later via bpf_program__load_manually().

[Severity: Low]
Is this comment accurate? It references bpf_program__load_manually(), but there
is no such function in the libbpf API. Should this reference bpf_program__load()
instead?

>  	 */
>  	if (sec_name[0] == '?') {
>  		prog->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260921223937.3203093-1-andrey.grodzovsky@crowdstrike.com?part=3

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

* Re: [PATCH bpf-next v4 1/7] libbpf: BPF program load strategy enum
  2026-09-21 22:39 ` [PATCH bpf-next v4 1/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
@ 2026-09-21 23:21   ` bot+bpf-ci
  0 siblings, 0 replies; 17+ messages in thread
From: bot+bpf-ci @ 2026-09-21 23:21 UTC (permalink / raw)
  To: andrey.grodzovsky, bpf, andrii
  Cc: ast, martin.kelly, slava.imameev, linux-open-source, ast, andrii,
	daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai

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

> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index cd1ea1bb53cb..a3085847cb97 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c

[ ... ]

> @@ -15374,3 +15372,17 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
>  	free(s->progs);
>  	free(s);
>  }
> +
> +int bpf_program__set_load_strategy(struct bpf_program *prog, enum bpf_prog_load_strategy strategy)
> +{
> +	if (prog->obj->state >= OBJ_LOADED)
> +		return libbpf_err(-EINVAL);
> +
> +	prog->load_strategy = strategy;
> +	return 0;
> +}

Does this function need to validate that strategy is within the defined enum
range?

The setter stores any int value without checking whether it is one of the
two defined enumerators (0 or 1). When code elsewhere in libbpf tests the
stored value, it splits into two non-complementary forms:

  form A, 'is it disabled?':
    libbpf.c obj_needs_vmlinux_btf():        if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED) continue;
    libbpf.c bpf_object__relocate_core():    if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED) continue;
    libbpf.c bpf_object__relocate():         if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED) continue;
    libbpf.c bpf_object__relocate():         if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED) continue;

  form B, 'is it auto?':
    libbpf.c bpf_object__load_progs():       if (prog->load_strategy != BPF_PROG_LOAD_STRATEGY_AUTO) { ... continue; }
    libbpf.c bpf_object__attach_skeleton():  if (prog->load_strategy != BPF_PROG_LOAD_STRATEGY_AUTO || !prog->autoattach) continue;

With any value other than 0 or 1, a program is neither DISABLED nor AUTO,
so it falls on the 'will be loaded' side of every form-A test and on the
'skip' side of every form-B test. The program is fully CO-RE-relocated,
call-relocated and data-relocated, may force a vmlinux BTF load, and is
then silently never loaded and never auto-attached.

bpf_program__set_load_strategy() returns 0 (success) and
bpf_program__autoload() reports false. There is no diagnostic:
bpf_object__load() succeeds and bpf_program__fd() stays -1.

This is reachable beyond simple caller error, because of how the symbol
is versioned. bpf_program__set_load_strategy is exported under
LIBBPF_1.8.0. The stated purpose of the commit is to allow 'new BPF
program loading strategies to be introduced by extending the enum value
range', and the next patch in this series (7f73306ceddb) does exactly
that, adding BPF_PROG_LOAD_STRATEGY_MANUAL = 2 without re-versioning the
symbol.

An application compiled against the newer header and run against a
libbpf.so that predates MANUAL therefore resolves to this
implementation, which accepts value 2 and silently produces a program
that is never loaded, instead of returning -EINVAL/-EOPNOTSUPP as the
newer implementation does.

That follow-up commit rewrites this function with switch (strategy) {
case DISABLED: case AUTO: ... case MANUAL: ... default: return
libbpf_err(-EINVAL); } -- i.e. it adds precisely the range rejection
that is missing here. Adding the default: return libbpf_err(-EINVAL);
rejection in this commit, where the API is introduced, is what makes
the forward-compatibility contract work; libbpf already validates
public-API inputs this way elsewhere (OPTS_VALID(), if (!prog) return
libbpf_err(-EINVAL) in bpf_program__clone()).

Would it make sense to add a validation check here:

  switch (strategy) {
  case BPF_PROG_LOAD_STRATEGY_DISABLED:
  case BPF_PROG_LOAD_STRATEGY_AUTO:
      break;
  default:
      return libbpf_err(-EINVAL);
  }

so that out-of-range values are rejected at the API boundary?

> +
> +enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog)
> +{
> +	return prog->load_strategy;
> +}

[ ... ]

> diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
> index 7a84dd00ce95..03c3d2bd15bf 100644
> --- a/tools/lib/bpf/libbpf.map
> +++ b/tools/lib/bpf/libbpf.map
> @@ -463,6 +463,8 @@ LIBBPF_1.8.0 {
>  		bpf_program__attach_tracing_multi;
>  		bpf_program__clear_flags;
>  		bpf_program__clone;
> +		bpf_program__load_strategy;
> +		bpf_program__set_load_strategy;
>  		btf__find_by_name_kind_own;
>  		btf__new_empty_opts;
>  } LIBBPF_1.7.0;


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

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

* Re: [PATCH bpf-next v4 3/7] libbpf: Support declarative manual load via SEC("!...") prefix
  2026-09-21 22:39 ` [PATCH bpf-next v4 3/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
  2026-09-21 23:12   ` sashiko-bot
@ 2026-09-21 23:21   ` bot+bpf-ci
  1 sibling, 0 replies; 17+ messages in thread
From: bot+bpf-ci @ 2026-09-21 23:21 UTC (permalink / raw)
  To: andrey.grodzovsky, bpf, andrii
  Cc: ast, martin.kelly, slava.imameev, linux-open-source, ast, andrii,
	daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai

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

> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index 939f0d6378e3..476172933a1f 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
> @@ -892,16 +892,25 @@ bpf_object__init_prog(struct bpf_object *obj, struct bpf_program *prog,
>  	/* libbpf's convention for SEC("?abc...") is that it's just like
>  	 * SEC("abc...") but the corresponding bpf_program starts out with
>  	 * autoload set to false.
> +	 *
> +	 * Similarly, SEC("!abc...") marks the program for manual loading:
> +	 * it is skipped by the bulk auto-load pass and must be explicitly
> +	 * loaded later via bpf_program__load_manually().
> +	 */

Does bpf_program__load_manually() exist?  Looking at the tree at this
commit, git grep finds only this comment.  The real API added earlier in
the series (commit 7f73306ceddb) is bpf_program__load():

tools/lib/bpf/libbpf.h:2179  LIBBPF_API int bpf_program__load(struct bpf_program *prog);

The rest of the series uses the correct name - libbpf.h's doc for
bpf_program__unload() references bpf_program__load() for reloading a
MANUAL-strategy program, and the selftests added later in the series call
bpf_program__load(skel->progs.prog4) for the SEC("!...") program.  So the
only documentation of the new SEC("!...") prefix points at a symbol that
will fail to link.

[ ... ]


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

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

* Re: [PATCH bpf-next v4 5/7] libbpf: Version bpf_program__set_autoattach() ABI change
  2026-09-21 22:39 ` [PATCH bpf-next v4 5/7] libbpf: Version bpf_program__set_autoattach() ABI change Andrey Grodzovsky
@ 2026-09-21 23:31   ` sashiko-bot
  0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-09-21 23:31 UTC (permalink / raw)
  To: Andrey Grodzovsky; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] libbpf: internal versioned aliases improperly exposed in public ABI
--

commit f4a1074a0b47562f2c3d7f448c920fa57fb2ed38
Author: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
libbpf: Version bpf_program__set_autoattach() ABI change

This commit splits bpf_program__set_autoattach() via ELF symbol versioning. 
It introduces a deprecated variant bound to LIBBPF_1.0.0 and a new variant 
returning int bound to LIBBPF_1.8.0 to handle manual load strategy rejections.

> diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
> --- a/tools/lib/bpf/libbpf.h
> +++ b/tools/lib/bpf/libbpf.h
> @@ -393,6 +393,12 @@ LIBBPF_API bool bpf_program__autoload(const struct bpf_program *prog);
>  LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog, bool autoload);
>  LIBBPF_API bool bpf_program__autoattach(const struct bpf_program *prog);
>  LIBBPF_API int bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach);
> +/* this "specialization" should go away once the deprecation window for
> + * bpf_program__set_autoattach_deprecated() closes
> + */
> +LIBBPF_API int bpf_program__set_autoattach_v1_8_0(struct bpf_program *prog, bool autoattach);
> +LIBBPF_DEPRECATED_SINCE(1, 8, "use int-returning bpf_program__set_autoattach() instead")
> +LIBBPF_API void bpf_program__set_autoattach_deprecated(struct bpf_program *prog, bool autoattach);

[Severity: Medium]
Does exposing these internal versioned aliases in the public header 
defeat the purpose of transparent ELF versioning?

Building any application against this updated header might expose these 
internal symbols, and dynamic linking could permanently bind them to the 
polluted ABI. 

Additionally, because the deprecation macro is attached to the internal alias 
rather than the standard name, will users ever see the deprecation warning 
naturally when they call bpf_program__set_autoattach()?

[ ... ]

> diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
> --- a/tools/lib/bpf/libbpf.map
> +++ b/tools/lib/bpf/libbpf.map
> @@ -465,6 +465,8 @@ LIBBPF_1.8.0 {
>  		bpf_program__clone;
>  		bpf_program__load;
>  		bpf_program__load_strategy;
> +		bpf_program__set_autoattach;
> +		bpf_program__set_autoattach_deprecated;

[Severity: Medium]
Is it intended to export the internal compat implementation as a public 
symbol in the new LIBBPF_1.8.0 namespace?

This appears to permanently leak the internal compatibility symbol into the
public ABI.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260921223937.3203093-1-andrey.grodzovsky@crowdstrike.com?part=5

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

* Re: [PATCH bpf-next v4 6/7] selftests/bpf: Cover BPF program load strategy transitions
  2026-09-21 22:39 ` [PATCH bpf-next v4 6/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
@ 2026-09-21 23:37   ` sashiko-bot
  0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-09-21 23:37 UTC (permalink / raw)
  To: Andrey Grodzovsky; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] bpf: ignored bpf_program__set_load_strategy() return value in selftest
--

commit 9bfec64bd482deb3a43ba282aed9959f4435d987
Author: Slava Imameev <slava.imameev@crowdstrike.com>

selftests/bpf: Cover BPF program load strategy transitions

Adds a load_type test to cover load strategy transitions
(DISABLED/AUTO/MANUAL) and compatibility with boolean auto-attach states.
Also tests the manual load/attach cycle after loading an object.

> diff --git a/tools/testing/selftests/bpf/prog_tests/load_type.c b/tools/testing/selftests/bpf/prog_tests/load_type.c
> new file mode 100644
> index 0000000000000..ab42c3e887fce
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
> @@ -0,0 +1,186 @@
[ ... ]
> +void test_load_type(void)
> +{
> +	struct bpf_link *link = NULL;
> +	struct test_load_type *skel;
> +	int err;
> +
> +	skel = test_load_type__open();
> +	if (!ASSERT_OK_PTR(skel, "skel_open"))
> +		return;
> +
> +	/* don't load prog1 */
> +	bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);
> +
> +	/* load and attach prog2 */
> +	bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_AUTO);

[Severity: Medium]
Is it intentional that test_load_type() ignores the return value of
bpf_program__set_load_strategy() for prog1 and prog2?

Since this selftest is designed to verify load strategy transitions, failing
to check the return value might silently mask regressions. If the API
introduces a bug where transitioning to AUTO fails (e.g., returning -EINVAL),
the API call would return an error but the program's strategy would remain
its default (which is already AUTO for prog2).

Because prog2 remains AUTO, the subsequent test assertions expecting AUTO
behavior would still pass, completely missing the API failure it was designed
to catch. Should these calls use ASSERT_OK() similar to how prog3 is checked
below?

> +	if (!ASSERT_TRUE(bpf_program__autoload(skel->progs.prog2), "prog2_autoload"))
> +		goto cleanup;
> +
> +	err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
> +	if (!ASSERT_OK(err, "set_load_strategy_manual"))
> +		goto cleanup;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260921223937.3203093-1-andrey.grodzovsky@crowdstrike.com?part=6

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

* Re: [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading
  2026-09-21 22:39 [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
                   ` (6 preceding siblings ...)
  2026-09-21 22:39 ` [PATCH bpf-next v4 7/7] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
@ 2026-09-22  1:37 ` Alexei Starovoitov
  2026-09-22 14:34   ` Andrey Grodzovsky
  7 siblings, 1 reply; 17+ messages in thread
From: Alexei Starovoitov @ 2026-09-22  1:37 UTC (permalink / raw)
  To: Andrey Grodzovsky, bpf, andrii
  Cc: martin.kelly, slava.imameev, linux-open-source

On Mon, Sep 21, 2026 at 06:39 PM Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com> wrote:
> after the initial bpf_object load. This v4 addresses all outstanding review
> feedback from v3's two CI bots, folds bpf_program__load_manually()/
> unload_manually() into a single load()/unload() naming pair, and adds
> a new patch that properly versions bpf_program__set_autoattach()'s
> ABI change instead of leaving it as a silent break for out-of-tree
> consumers (see changelog below).

What happened to the veristat conversion?
In v2 Andrii asked to try this in veristat to see whether it can replace
bpf_program__clone():
https://lore.kernel.org/bpf/CAEf4Bzaf9AMBUWQeUj1Uor8F7A-tOPpVMa-CjRB0MNH4zRHEeQ@mail.gmail.com/
and you replied that it builds and passes test_progs -t veristat.
Pls include that patch in the respin.

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

* Re: [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading
  2026-09-22  1:37 ` [PATCH bpf-next v4 0/7] libbpf: " Alexei Starovoitov
@ 2026-09-22 14:34   ` Andrey Grodzovsky
  0 siblings, 0 replies; 17+ messages in thread
From: Andrey Grodzovsky @ 2026-09-22 14:34 UTC (permalink / raw)
  To: Alexei Starovoitov
  Cc: bpf, andrii, martin.kelly, slava.imameev, linux-open-source

On Mon, Sep 21, 2026 at 9:37 PM Alexei Starovoitov
<alexei.starovoitov@gmail.com> wrote:
>
> On Mon, Sep 21, 2026 at 06:39 PM Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com> wrote:
> > after the initial bpf_object load. This v4 addresses all outstanding review
> > feedback from v3's two CI bots, folds bpf_program__load_manually()/
> > unload_manually() into a single load()/unload() naming pair, and adds
> > a new patch that properly versions bpf_program__set_autoattach()'s
> > ABI change instead of leaving it as a silent break for out-of-tree
> > consumers (see changelog below).
>
> What happened to the veristat conversion?
> In v2 Andrii asked to try this in veristat to see whether it can replace
> bpf_program__clone():
> https://lore.kernel.org/bpf/CAEf4Bzaf9AMBUWQeUj1Uor8F7A-tOPpVMa-CjRB0MNH4zRHEeQ@mail.gmail.com/
> and you replied that it builds and passes test_progs -t veristat.
> Pls include that patch in the respin.

Hey, the reason I didn't include that patch is because  as I mentioned
to Andrii in the reply[1], I found out RODATA maps gets bound on every
load which clone()
deliberately skips to avoid mutating object state. Therefore, I was
awaiting follow-up input on whether this could be a deal breaker for
the substitution approach.

I will happily add this in the next iteration (V5), along with fixes
for other issues the BOTs raised during the review of V4.

Andery

[1] - https://lore.kernel.org/bpf/CAOu3gNhbz0m7=cPAAD-NWPix-TCL7B3iFFTHn1JzaaukAXP3oQ@mail.gmail.com/

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

* Re: [PATCH bpf-next v4 2/7] libbpf: BPF programs manual loading and attaching
  2026-09-21 23:03   ` sashiko-bot
@ 2026-09-22 23:51     ` Andrey Grodzovsky
  0 siblings, 0 replies; 17+ messages in thread
From: Andrey Grodzovsky @ 2026-09-22 23:51 UTC (permalink / raw)
  To: bpf; +Cc: Andrii Nakryiko, DL Linux Open Source Team

On Mon, Sep 21, 2026 at 7:03 PM <sashiko-bot@kernel.org> wrote:
>
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [High] bpf: bpf_program__load() allows loading from failed object, causing Use-After-Close
> --
>
> commit 62d928e64d7108c312cff920bafa876c284cb8bd
> Author: Slava Imameev <slava.imameev@crowdstrike.com>
>
> libbpf: BPF programs manual loading and attaching
>
> BPF programs designated as manually loaded can be loaded and attached
> independently after the initial bpf_object loading and attaching. This
> patch adds bpf_program__load() to support loading these manual-strategy
> programs independently.
>
> > diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> > index a3085847cb97f..939f0d6378e38 100644
> > --- a/tools/lib/bpf/libbpf.c
> > +++ b/tools/lib/bpf/libbpf.c
>
> [ ... ]
>
> > @@ -9233,7 +9286,7 @@ static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const ch
> >                       err = bpf_gen__finish(obj->gen_loader, obj->nr_programs, obj->nr_maps);
> >       }
> >
> > -     bpf_object_post_load_cleanup(obj);
> > +     bpf_object_post_load_cleanup(obj, false);
> >       obj->state = OBJ_LOADED; /* doesn't matter if successfully or not */
> >
> >       if (err) {
>
> [Severity: High]
> Could this state assignment allow dangerous program loads if
> bpf_object_load() fails?
>
> When bpf_object_load() fails after bpf_object_prepare(), it falls through to
> error handling which calls bpf_object_unload() and closes the map FDs.
> However, the state is still unconditionally set to OBJ_LOADED.
>
> [ ... ]
>
> > @@ -15386,3 +15501,34 @@ enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program
> >  {
> >       return prog->load_strategy;
> >  }
> > +
> > +/*
> > + * This function must be called after bpf_object__prepare (or
> > + * bpf_object__load, which calls bpf_object__prepare internally).
> > + * Manually-loaded program data is initialized on object prepare.
> > + * Post-prepare initialization is not supported.
> > + */
> > +int
> > +bpf_program__load(struct bpf_program *prog)
> > +{
> > +     int err;
> > +     struct bpf_object *obj = prog->obj;
> > +
> > +     if (obj->state < OBJ_PREPARED)
> > +             return libbpf_err(-EINVAL);
>
> [Severity: High]
> Is this state check sufficient to prevent a use-after-close if the underlying
> object failed to load?
>
> If an application ignores a prior bpf_object_load() error and calls
> bpf_program__load(), this check will pass because the failed object is now in
> the OBJ_LOADED state (which is >= OBJ_PREPARED).
>
> Because the failure triggered bpf_object_unload(), the map FDs were closed,
> allowing bpf_program__load() to proceed with stale map FDs.
>
> If a map FD is reused, the kernel might attach the BPF program to an
> unrelated map, causing data corruption or security boundary violations.
> For programs with BTF, the kernel rejects the load with -EFAULT because
> func_info is freed but func_info_cnt remains > 0. For programs without BTF,
> the kernel proceeds using the stale FDs.

Whle the issue in indeed real, I don't think we should conflate
solving it with this patchset because this is a pre-exsiting issue
IMHO and was introduced in [1] as defensive mechanism.
Same issue as described above I think can be triggerd if we call
bpf_program__clone after failed bpf_program__load.
I think the proper fix to this would be  adding a distinct OBJ_FAILED
> OBJ_LOADED state  and rejecting it in both bpf_program__clone() and
bpf_program__load() explcitily.

Andrey

[1] - https://lore.kernel.org/bpf/CAEf4BzZ1+wojkBW+LLdKa1bUE4Jt+FhfUq_4DwzW8YOq64S1tg@mail.gmail.com/
>
> --
> Sashiko AI review · https://urldefense.com/v3/__https://sashiko.dev/*/patchset/20260921223937.3203093-1-andrey.grodzovsky@crowdstrike.com?part=2__;Iw!!BmdzS3_lV9HdKG8!zpj66NXxmYFWO7pFsg0dgZQ0cWH_DAHXsaDnmRl26-hwiy42gvI00kcYonZ-0w_UhTKS66p4ChBVq6LURKHOj1bkZMHyS6ADaQ$
>

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

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

Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-21 22:39 [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-09-21 22:39 ` [PATCH bpf-next v4 1/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
2026-09-21 23:21   ` bot+bpf-ci
2026-09-21 22:39 ` [PATCH bpf-next v4 2/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
2026-09-21 23:03   ` sashiko-bot
2026-09-22 23:51     ` Andrey Grodzovsky
2026-09-21 22:39 ` [PATCH bpf-next v4 3/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
2026-09-21 23:12   ` sashiko-bot
2026-09-21 23:21   ` bot+bpf-ci
2026-09-21 22:39 ` [PATCH bpf-next v4 4/7] libbpf: Reject gen_loader for objects with already-manual programs Andrey Grodzovsky
2026-09-21 22:39 ` [PATCH bpf-next v4 5/7] libbpf: Version bpf_program__set_autoattach() ABI change Andrey Grodzovsky
2026-09-21 23:31   ` sashiko-bot
2026-09-21 22:39 ` [PATCH bpf-next v4 6/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
2026-09-21 23:37   ` sashiko-bot
2026-09-21 22:39 ` [PATCH bpf-next v4 7/7] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
2026-09-22  1:37 ` [PATCH bpf-next v4 0/7] libbpf: " Alexei Starovoitov
2026-09-22 14:34   ` Andrey Grodzovsky

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