| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/10/24 13:04 | flow | patch-triage |
0mArgs:null Results: {
"EnableConfigs": null,
"FocusSymbols": [],
"KMSANReasoning": "",
"NeedsKMSAN": false,
"Reasoning": "The changes are entirely confined to user-space code within tools/ (libbpf and BPF selftests). They do not modify the compiled kernel image (vmlinux) or any kernel modules, making them structurally outside the scope of syzkaller kernel fuzzing.",
"WorthFuzzing": false
} |
| 1/1 | 2026/10/24 13:04 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit 09d012e79d9da66381c0f42ca493a0d623bb2001\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Sat Oct 24 13:04:09 2026 +0000\n\n syz-cluster: applied patch under review\n\ndiff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c\nindex 8fcb8cf39cc0c..ad260cc132036 100644\n--- a/tools/lib/bpf/libbpf.c\n+++ b/tools/lib/bpf/libbpf.c\n@@ -494,7 +494,7 @@ struct bpf_program {\n \tstruct bpf_object *obj;\n \n \tint fd;\n-\tbool autoload;\n+\tenum bpf_prog_load_strategy load_strategy;\n \tbool autoattach;\n \tbool sym_global;\n \t/* the program or a function that it calls has callx */\n@@ -758,6 +758,7 @@ struct bpf_object {\n \n \tbool has_subcalls;\n \tbool has_rodata;\n+\tbool has_manual_progs;\n \n \tstruct bpf_gen *gen_loader;\n \n@@ -870,7 +871,7 @@ static Elf_Data *elf_sec_data(const struct bpf_object *obj, Elf_Scn *scn);\n static Elf64_Sym *elf_sym_by_idx(const struct bpf_object *obj, size_t idx);\n static Elf64_Rel *elf_rel_by_idx(Elf_Data *data, size_t idx);\n \n-void bpf_program__unload(struct bpf_program *prog)\n+static void bpf_program_unload_full(struct bpf_program *prog)\n {\n \tif (!prog)\n \t\treturn;\n@@ -882,12 +883,30 @@ void bpf_program__unload(struct bpf_program *prog)\n \tzfree(\u0026prog-\u003esubprogs);\n }\n \n+void bpf_program__unload(struct bpf_program *prog)\n+{\n+\tif (!prog)\n+\t\treturn;\n+\n+\t/*\n+\t * MANUAL programs retain their data here so bpf_program__load()\n+\t * can reload them later; object teardown paths call\n+\t * bpf_program_unload_full() instead to always release it fully.\n+\t */\n+\tif (prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {\n+\t\tzclose(prog-\u003efd);\n+\t\treturn;\n+\t}\n+\n+\tbpf_program_unload_full(prog);\n+}\n+\n static void bpf_program__exit(struct bpf_program *prog)\n {\n \tif (!prog)\n \t\treturn;\n \n-\tbpf_program__unload(prog);\n+\tbpf_program_unload_full(prog);\n \tzfree(\u0026prog-\u003ename);\n \tzfree(\u0026prog-\u003esec_name);\n \tzfree(\u0026prog-\u003einsns);\n@@ -955,13 +974,21 @@ bpf_object__init_prog(struct bpf_object *obj, struct bpf_program *prog,\n \t/* libbpf's convention for SEC(\"?abc...\") is that it's just like\n \t * SEC(\"abc...\") but the corresponding bpf_program starts out with\n \t * autoload set to false.\n+\t *\n+\t * Similarly, SEC(\"!abc...\") marks the program for manual loading:\n+\t * it is skipped by the bulk auto-load pass and must be explicitly\n+\t * loaded via bpf_program__load().\n \t */\n \tif (sec_name[0] == '?') {\n-\t\tprog-\u003eautoload = false;\n+\t\tprog-\u003eload_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;\n \t\t/* from now on forget there was ? in section name */\n \t\tsec_name++;\n+\t} else if (sec_name[0] == '!') {\n+\t\tprog-\u003eload_strategy = BPF_PROG_LOAD_STRATEGY_MANUAL;\n+\t\t/* from now on forget there was ! in section name */\n+\t\tsec_name++;\n \t} else {\n-\t\tprog-\u003eautoload = true;\n+\t\tprog-\u003eload_strategy = BPF_PROG_LOAD_STRATEGY_AUTO;\n \t}\n \n \tprog-\u003eautoattach = true;\n@@ -1228,6 +1255,14 @@ static int bpf_object_adjust_struct_ops_autoload(struct bpf_object *obj)\n \t\tif (prog-\u003etype != BPF_PROG_TYPE_STRUCT_OPS)\n \t\t\tcontinue;\n \n+\t\t/*\n+\t\t * If the user set the load strategy to MANUAL, don't guess it\n+\t\t * from map autocreate: a MANUAL program is loaded explicitly\n+\t\t * with bpf_program__load(), which rejects any other strategy.\n+\t\t */\n+\t\tif (prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)\n+\t\t\tcontinue;\n+\n \t\tfor (j = 0; j \u003c obj-\u003enr_maps; ++j) {\n \t\t\tconst struct btf_type *type;\n \n@@ -1248,7 +1283,8 @@ static int bpf_object_adjust_struct_ops_autoload(struct bpf_object *obj)\n \t\t\t}\n \t\t}\n \t\tif (use_cnt)\n-\t\t\tprog-\u003eautoload = should_load;\n+\t\t\tprog-\u003eload_strategy = should_load ? BPF_PROG_LOAD_STRATEGY_AUTO\n+\t\t\t\t: BPF_PROG_LOAD_STRATEGY_DISABLED;\n \t}\n \n \treturn 0;\n@@ -1339,7 +1375,7 @@ static int bpf_map__init_kern_struct_ops(struct bpf_map *map)\n \t\t\t\t * then bpf_object_adjust_struct_ops_autoload() will update its\n \t\t\t\t * autoload accordingly.\n \t\t\t\t */\n-\t\t\t\tst_ops-\u003eprogs[i]-\u003eautoload = false;\n+\t\t\t\tst_ops-\u003eprogs[i]-\u003eload_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;\n \t\t\t\tst_ops-\u003eprogs[i] = NULL;\n \t\t\t}\n \n@@ -1377,7 +1413,7 @@ static int bpf_map__init_kern_struct_ops(struct bpf_map *map)\n \t\t\t * if user replaced it with another program or NULL\n \t\t\t */\n \t\t\tif (st_ops-\u003eprogs[i] \u0026\u0026 st_ops-\u003eprogs[i] != prog)\n-\t\t\t\tst_ops-\u003eprogs[i]-\u003eautoload = false;\n+\t\t\t\tst_ops-\u003eprogs[i]-\u003eload_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;\n \n \t\t\t/* Update the value from the shadow type */\n \t\t\tst_ops-\u003eprogs[i] = prog;\n@@ -3812,7 +3848,7 @@ static bool obj_needs_vmlinux_btf(const struct bpf_object *obj)\n \t}\n \n \tbpf_object__for_each_program(prog, obj) {\n-\t\tif (!prog-\u003eautoload)\n+\t\tif (prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)\n \t\t\tcontinue;\n \t\tif (prog_needs_vmlinux_btf(prog))\n \t\t\treturn true;\n@@ -5873,6 +5909,20 @@ static int init_prog_array_slots(struct bpf_object *obj, struct bpf_map *map)\n \t\t\tcontinue;\n \n \t\ttarg_prog = map-\u003einit_slots[i];\n+\n+\t\t/*\n+\t\t * Be careful about manual progs, reject if they weren't loaded before\n+\t\t * this step to not insert invalid FDs into the map, otherwise we will\n+\t\t * get a much less clear rejection from the kernel on the element update\n+\t\t * below.\n+\t\t */\n+\t\tif (targ_prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL \u0026\u0026\n+\t\t targ_prog-\u003efd \u003c 0) {\n+\t\t\tpr_warn(\"map '%s': slot [%u] prog '%s' is MANUAL but not loaded\\n\",\n+\t\t\t\tmap-\u003ename, i, targ_prog-\u003ename);\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n \t\tfd = bpf_program__fd(targ_prog);\n \n \t\terr = bpf_map_update_elem(map-\u003efd, \u0026i, \u0026fd, 0);\n@@ -6501,7 +6551,7 @@ bpf_object__relocate_core(struct bpf_object *obj, const char *targ_btf_path)\n \t\t\t/* no need to apply CO-RE relocation if the program is\n \t\t\t * not going to be loaded\n \t\t\t */\n-\t\t\tif (!prog-\u003eautoload)\n+\t\t\tif (prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)\n \t\t\t\tcontinue;\n \n \t\t\t/* adjust insn_idx from section frame of reference to the local\n@@ -8172,7 +8222,7 @@ static int bpf_object__relocate(struct bpf_object *obj, const char *targ_btf_pat\n \t\t */\n \t\tif (prog_is_subprog(obj, prog))\n \t\t\tcontinue;\n-\t\tif (!prog-\u003eautoload)\n+\t\tif (prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)\n \t\t\tcontinue;\n \n \t\terr = bpf_object__relocate_calls(obj, prog);\n@@ -8208,7 +8258,7 @@ static int bpf_object__relocate(struct bpf_object *obj, const char *targ_btf_pat\n \t\tprog = \u0026obj-\u003eprograms[i];\n \t\tif (prog_is_subprog(obj, prog))\n \t\t\tcontinue;\n-\t\tif (!prog-\u003eautoload)\n+\t\tif (prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)\n \t\t\tcontinue;\n \n \t\t/* Process data relos for main programs */\n@@ -9300,8 +9350,8 @@ bpf_object__load_progs(struct bpf_object *obj, int log_level)\n \t\tprog = \u0026obj-\u003eprograms[i];\n \t\tif (prog_is_subprog(obj, prog))\n \t\t\tcontinue;\n-\t\tif (!prog-\u003eautoload) {\n-\t\t\tpr_debug(\"prog '%s': skipped loading\\n\", prog-\u003ename);\n+\t\tif (prog-\u003eload_strategy != BPF_PROG_LOAD_STRATEGY_AUTO) {\n+\t\t\tpr_debug(\"prog '%s': skipped auto-loading\\n\", prog-\u003ename);\n \t\t\tcontinue;\n \t\t}\n \t\tprog-\u003elog_level |= log_level;\n@@ -9329,6 +9379,8 @@ static int bpf_object_prepare_progs(struct bpf_object *obj)\n \n \tfor (i = 0; i \u003c obj-\u003enr_programs; i++) {\n \t\tprog = \u0026obj-\u003eprograms[i];\n+\t\tif (prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)\n+\t\t\tobj-\u003ehas_manual_progs = true;\n \t\terr = bpf_object__sanitize_prog(obj, prog);\n \t\tif (err)\n \t\t\treturn err;\n@@ -9355,6 +9407,18 @@ static int bpf_object_init_progs(struct bpf_object *obj, const struct bpf_object\n \t\tprog-\u003etype = prog-\u003esec_def-\u003eprog_type;\n \t\tprog-\u003eexpected_attach_type = prog-\u003esec_def-\u003eexpected_attach_type;\n \n+\t\t/*\n+\t\t * struct_ops programs declared SEC(\"!...\") are rejected here\n+\t\t * at declarative parse time for manual loading since those programs cannot\n+\t\t * load/unload independently of each other per a given struct ops.\n+\t\t */\n+\t\tif (prog-\u003etype == BPF_PROG_TYPE_STRUCT_OPS \u0026\u0026\n+\t\t prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {\n+\t\t\tpr_warn(\"prog '%s': struct_ops programs do not support manual loading\\n\",\n+\t\t\t\tprog-\u003ename);\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n \t\t/* sec_def can have custom callback which should be called\n \t\t * after bpf_program is initialized to adjust its properties\n \t\t */\n@@ -9555,7 +9619,7 @@ static int bpf_object_unload(struct bpf_object *obj)\n \t}\n \n \tfor (i = 0; i \u003c obj-\u003enr_programs; i++)\n-\t\tbpf_program__unload(\u0026obj-\u003eprograms[i]);\n+\t\tbpf_program_unload_full(\u0026obj-\u003eprograms[i]);\n \n \treturn 0;\n }\n@@ -9946,7 +10010,7 @@ static int bpf_object__resolve_externs(struct bpf_object *obj,\n \treturn 0;\n }\n \n-static void bpf_map_prepare_vdata(const struct bpf_map *map)\n+static int bpf_map_prepare_vdata(const struct bpf_map *map)\n {\n \tconst struct btf_type *type;\n \tstruct bpf_struct_ops *st_ops;\n@@ -9962,16 +10026,32 @@ static void bpf_map_prepare_vdata(const struct bpf_map *map)\n \t\tif (!prog)\n \t\t\tcontinue;\n \n+\t\t/*\n+\t\t * Manual struct_ops programs don't get along well with manual\n+\t\t * loading: all the progs belonging to the same struct_ops must\n+\t\t * load and unload together, which is the opposite of the manual\n+\t\t * load concept.\n+\t\t * Still, we allow manual progs here as long as they were loaded\n+\t\t * before this step because this way they do work.\n+\t\t */\n+\t\tif (prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL \u0026\u0026 prog-\u003efd \u003c 0) {\n+\t\t\tpr_warn(\"map '%s': struct_ops member prog '%s' is MANUAL but not loaded\\n\",\n+\t\t\t\tmap-\u003ename, prog-\u003ename);\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n \t\tprog_fd = bpf_program__fd(prog);\n \t\tkern_data = st_ops-\u003ekern_vdata + st_ops-\u003ekern_func_off[i];\n \t\t*(unsigned long *)kern_data = prog_fd;\n \t}\n+\n+\treturn 0;\n }\n \n static int bpf_object_prepare_struct_ops(struct bpf_object *obj)\n {\n \tstruct bpf_map *map;\n-\tint i;\n+\tint i, err;\n \n \tfor (i = 0; i \u003c obj-\u003enr_maps; i++) {\n \t\tmap = \u0026obj-\u003emaps[i];\n@@ -9982,7 +10062,9 @@ static int bpf_object_prepare_struct_ops(struct bpf_object *obj)\n \t\tif (!map-\u003eautocreate)\n \t\t\tcontinue;\n \n-\t\tbpf_map_prepare_vdata(map);\n+\t\terr = bpf_map_prepare_vdata(map);\n+\t\tif (err)\n+\t\t\treturn err;\n \t}\n \n \treturn 0;\n@@ -9998,10 +10080,19 @@ static void bpf_object_unpin(struct bpf_object *obj)\n \t\t\tbpf_map__unpin(\u0026obj-\u003emaps[i], NULL);\n }\n \n-static void bpf_object_cleanup_btf(struct bpf_object *obj)\n+static void bpf_object_cleanup_btf(struct bpf_object *obj, bool force)\n {\n \tint i;\n \n+\t/*\n+\t * Module BTF fds and btf_vmlinux are still needed by\n+\t * programs that have not been manually loaded yet, so defer freeing\n+\t * them to the end of the object lifetime, unless the caller forces\n+\t * immediate cleanup.\n+\t */\n+\tif (!force \u0026\u0026 obj-\u003ehas_manual_progs)\n+\t\treturn;\n+\n \t/* clean up module BTFs */\n \tfor (i = 0; i \u003c obj-\u003ebtf_module_cnt; i++) {\n \t\tclose(obj-\u003ebtf_modules[i].fd);\n@@ -10018,15 +10109,42 @@ static void bpf_object_cleanup_btf(struct bpf_object *obj)\n \tobj-\u003ebtf_vmlinux = NULL;\n }\n \n-static void bpf_object_post_load_cleanup(struct bpf_object *obj)\n+static void bpf_object_post_load_cleanup(struct bpf_object *obj, bool force)\n {\n-\t/* clean up fd_array, and the slots handed out with it */\n-\tzfree(\u0026obj-\u003efd_array);\n-\tobj-\u003efd_array_cap = 0;\n-\tobj-\u003efd_array_used = 0;\n+\t/*\n+\t * The fd array, and the slots handed out with it, are needed for\n+\t * manually loaded programs, so defer freeing them in that case\n+\t * to the end of the object lifetime.\n+\t */\n+\tif (force || !obj-\u003ehas_manual_progs) {\n+\t\tzfree(\u0026obj-\u003efd_array);\n+\t\tobj-\u003efd_array_cap = 0;\n+\t\tobj-\u003efd_array_used = 0;\n+\t}\n \n \t/* clean up BTF */\n-\tbpf_object_cleanup_btf(obj);\n+\tbpf_object_cleanup_btf(obj, force);\n+}\n+\n+static int bpf_object_check_gen_loader(struct bpf_object *obj)\n+{\n+\tstruct bpf_program *prog;\n+\n+\tif (!obj-\u003egen_loader)\n+\t\treturn 0;\n+\n+\t/*\n+\t * gen_loader only records programs loaded by bpf_object__load_progs(),\n+\t * which skips MANUAL ones.\n+\t */\n+\tbpf_object__for_each_program(prog, obj) {\n+\t\tif (prog-\u003eload_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)\n+\t\t\tcontinue;\n+\t\tpr_warn(\"prog '%s': gen_loader does not support manually-loaded programs\\n\",\n+\t\t\tprog-\u003ename);\n+\t\treturn -EOPNOTSUPP;\n+\t}\n+\treturn 0;\n }\n \n static int bpf_object_prepare(struct bpf_object *obj, const char *target_btf_path)\n@@ -10038,7 +10156,8 @@ static int bpf_object_prepare(struct bpf_object *obj, const char *target_btf_pat\n \t\treturn -EINVAL;\n \t}\n \n-\terr = bpf_object_prepare_token(obj);\n+\terr = bpf_object_check_gen_loader(obj);\n+\terr = err ? : bpf_object_prepare_token(obj);\n \terr = err ? : bpf_object__probe_loading(obj);\n \terr = err ? : bpf_object__load_vmlinux_btf(obj, false);\n \terr = err ? : bpf_object__resolve_externs(obj, obj-\u003ekconfig);\n@@ -10093,7 +10212,8 @@ static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const ch\n \t\t\t prog_has_callx(\u0026obj-\u003eprograms[i]))\n \t\t\t\ttext_has_callx = true;\n \t\tfor (i = 0; i \u003c obj-\u003enr_programs; i++)\n-\t\t\tif (obj-\u003eprograms[i].autoload \u0026\u0026 !prog_is_subprog(obj, \u0026obj-\u003eprograms[i]) \u0026\u0026\n+\t\t\tif (obj-\u003eprograms[i].load_strategy != BPF_PROG_LOAD_STRATEGY_DISABLED \u0026\u0026\n+\t\t\t !prog_is_subprog(obj, \u0026obj-\u003eprograms[i]) \u0026\u0026\n \t\t\t (text_has_callx || prog_has_callx(\u0026obj-\u003eprograms[i])))\n \t\t\t\tnr_progs++;\n \t\tbpf_gen__init(obj-\u003egen_loader, obj-\u003elog_level | extra_log_level,\n@@ -10120,7 +10240,7 @@ static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const ch\n \t\t\terr = bpf_gen__finish(obj-\u003egen_loader, obj-\u003enr_programs, obj-\u003enr_maps);\n \t}\n \n-\tbpf_object_post_load_cleanup(obj);\n+\tbpf_object_post_load_cleanup(obj, false);\n \tobj-\u003estate = OBJ_LOADED; /* doesn't matter if successfully or not */\n \n \tif (err) {\n@@ -10588,7 +10708,7 @@ void bpf_object__close(struct bpf_object *obj)\n \t * bpf_object__load(), we need to clean up stuff that is normally\n \t * cleaned up at the end of loading step\n \t */\n-\tbpf_object_post_load_cleanup(obj);\n+\tbpf_object_post_load_cleanup(obj, true);\n \n \tusdt_manager_free(obj-\u003eusdt_man);\n \tobj-\u003eusdt_man = NULL;\n@@ -10597,7 +10717,6 @@ void bpf_object__close(struct bpf_object *obj)\n \tbpf_object__elf_finish(obj);\n \tbpf_object_unload(obj);\n \tbtf__free(obj-\u003ebtf);\n-\tbtf__free(obj-\u003ebtf_vmlinux);\n \tbtf_ext__free(obj-\u003ebtf_ext);\n \n \tfor (i = 0; i \u003c obj-\u003enr_maps; i++)\n@@ -10698,6 +10817,9 @@ int bpf_object__gen_loader(struct bpf_object *obj, struct gen_loader_opts *opts)\n \t\treturn libbpf_err(-EFAULT);\n \tif (!OPTS_VALID(opts, gen_loader_opts))\n \t\treturn libbpf_err(-EINVAL);\n+\tif (obj-\u003estate \u003e= OBJ_PREPARED)\n+\t\treturn libbpf_err(-EINVAL);\n+\n \tgen = calloc(1, sizeof(*gen));\n \tif (!gen)\n \t\treturn libbpf_err(-ENOMEM);\n@@ -10774,16 +10896,13 @@ const char *bpf_program__section_name(const struct bpf_program *prog)\n \n bool bpf_program__autoload(const struct bpf_program *prog)\n {\n-\treturn prog-\u003eautoload;\n+\treturn prog-\u003eload_strategy == BPF_PROG_LOAD_STRATEGY_AUTO;\n }\n \n int bpf_program__set_autoload(struct bpf_program *prog, bool autoload)\n {\n-\tif (prog-\u003eobj-\u003estate \u003e= OBJ_LOADED)\n-\t\treturn libbpf_err(-EINVAL);\n-\n-\tprog-\u003eautoload = autoload;\n-\treturn 0;\n+\treturn bpf_program__set_load_strategy(prog,\n+\t\tautoload ? BPF_PROG_LOAD_STRATEGY_AUTO : BPF_PROG_LOAD_STRATEGY_DISABLED);\n }\n \n bool bpf_program__autoattach(const struct bpf_program *prog)\n@@ -10923,7 +11042,13 @@ __u32 bpf_program__log_level(const struct bpf_program *prog)\n \n int bpf_program__set_log_level(struct bpf_program *prog, __u32 log_level)\n {\n-\tif (prog-\u003eobj-\u003estate \u003e= OBJ_LOADED)\n+\t/*\n+\t * Here and below, OBJ_LOADED is not a relevant restriction for\n+\t * manual programs as those can load after the bpf object is\n+\t * already loaded.\n+\t */\n+\tif (prog-\u003eobj-\u003estate \u003e= OBJ_LOADED \u0026\u0026\n+\t prog-\u003eload_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)\n \t\treturn libbpf_err(-EBUSY);\n \n \tprog-\u003elog_level = log_level;\n@@ -10942,7 +11067,8 @@ int bpf_program__set_log_buf(struct bpf_program *prog, char *log_buf, size_t log\n \t\treturn libbpf_err(-EINVAL);\n \tif (log_size \u003e UINT_MAX)\n \t\treturn libbpf_err(-EINVAL);\n-\tif (prog-\u003eobj-\u003estate \u003e= OBJ_LOADED)\n+\tif (prog-\u003eobj-\u003estate \u003e= OBJ_LOADED \u0026\u0026\n+\t prog-\u003eload_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)\n \t\treturn libbpf_err(-EBUSY);\n \n \tprog-\u003elog_buf = log_buf;\n@@ -13728,7 +13854,7 @@ static int collect_func_ids_by_glob(const struct bpf_program *prog, const char *\n \terr = collect_btf_func_ids_by_glob(btf, pattern, ids);\n \n cleanup:\n-\tbpf_object_cleanup_btf(obj);\n+\tbpf_object_cleanup_btf(obj, false);\n \treturn err;\n }\n \n@@ -16172,7 +16298,7 @@ int bpf_object__attach_skeleton(struct bpf_object_skeleton *s)\n \t\tstruct bpf_program *prog = *prog_skel-\u003eprog;\n \t\tstruct bpf_link **link = prog_skel-\u003elink;\n \n-\t\tif (!prog-\u003eautoload || !prog-\u003eautoattach)\n+\t\tif (prog-\u003eload_strategy != BPF_PROG_LOAD_STRATEGY_AUTO || !prog-\u003eautoattach)\n \t\t\tcontinue;\n \n \t\t/* auto-attaching not supported for this program */\n@@ -16282,3 +16408,62 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)\n \tfree(s-\u003eprogs);\n \tfree(s);\n }\n+\n+int bpf_program__set_load_strategy(struct bpf_program *prog, enum bpf_prog_load_strategy strategy)\n+{\n+\tstruct bpf_object *obj = prog-\u003eobj;\n+\n+\tif (obj-\u003estate \u003e= OBJ_PREPARED)\n+\t\treturn libbpf_err(-EINVAL);\n+\n+\tswitch (strategy) {\n+\tcase BPF_PROG_LOAD_STRATEGY_DISABLED:\n+\tcase BPF_PROG_LOAD_STRATEGY_AUTO:\n+\t\tprog-\u003eload_strategy = strategy;\n+\t\tbreak;\n+\tcase BPF_PROG_LOAD_STRATEGY_MANUAL:\n+\t\tif (prog_is_subprog(obj, prog))\n+\t\t\treturn libbpf_err(-EINVAL);\n+\n+\t\tprog-\u003eload_strategy = BPF_PROG_LOAD_STRATEGY_MANUAL;\n+\t\tbreak;\n+\tdefault:\n+\t\treturn libbpf_err(-EINVAL);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog)\n+{\n+\treturn prog-\u003eload_strategy;\n+}\n+\n+int\n+bpf_program__load(struct bpf_program *prog)\n+{\n+\tint err;\n+\tstruct bpf_object *obj = prog-\u003eobj;\n+\n+\t/*\n+\t * This function cannot be called before OBJ_PREPARED -\n+\t * Program's data is initialized on object prepare.\n+\t */\n+\tif (obj-\u003estate \u003c OBJ_PREPARED)\n+\t\treturn libbpf_err(-EINVAL);\n+\n+\tif (prog_is_subprog(obj, prog) || prog-\u003eload_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)\n+\t\treturn libbpf_err(-EINVAL);\n+\n+\tif (prog-\u003efd \u003e= 0)\n+\t\treturn libbpf_err(-EBUSY);\n+\n+\terr = bpf_object_load_prog(obj, prog, prog-\u003einsns, prog-\u003einsns_cnt,\n+\t\t\t\t obj-\u003elicense, obj-\u003ekern_version, \u0026prog-\u003efd);\n+\tif (err) {\n+\t\tpr_warn(\"prog '%s': failed to load: %s\\n\", prog-\u003ename, errstr(err));\n+\t\treturn libbpf_err(err);\n+\t}\n+\n+\treturn 0;\n+}\ndiff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h\nindex 03f794f4adb03..dd5380bc355e5 100644\n--- a/tools/lib/bpf/libbpf.h\n+++ b/tools/lib/bpf/libbpf.h\n@@ -473,6 +473,17 @@ LIBBPF_API int bpf_program__pin(struct bpf_program *prog, const char *path);\n * @return 0, on success; negative error code, otherwise\n */\n LIBBPF_API int bpf_program__unpin(struct bpf_program *prog, const char *path);\n+\n+/**\n+ * @brief **bpf_program__unload()** unloads a BPF program, closing its fd.\n+ *\n+ * If the program's load strategy is BPF_PROG_LOAD_STRATEGY_MANUAL, only the\n+ * fd is closed and the program's data is retained so it can be reloaded\n+ * later via bpf_program__load(). For any other load strategy, the program's\n+ * data is also freed and it cannot be reloaded.\n+ *\n+ * @param prog BPF program to unload\n+ */\n LIBBPF_API void bpf_program__unload(struct bpf_program *prog);\n \n struct bpf_link;\n@@ -1055,8 +1066,39 @@ LIBBPF_API int bpf_program__clear_flags(struct bpf_program *prog, __u32 flags);\n * interactions.\n */\n LIBBPF_API __u32 bpf_program__log_level(const struct bpf_program *prog);\n+\n+/**\n+ * @brief **bpf_program__set_log_level()** sets the verifier log level used\n+ * when the BPF program is loaded.\n+ *\n+ * Fails with -EBUSY once the enclosing bpf_object is loaded, unless the\n+ * program's load strategy is BPF_PROG_LOAD_STRATEGY_MANUAL, as such a program\n+ * can still be loaded afterwards with bpf_program__load().\n+ *\n+ * @param prog BPF program to set the log level for\n+ * @param log_level verifier log level, 0 requests no logging on the first\n+ * load attempt\n+ * @return 0, on success; negative error code, otherwise\n+ */\n LIBBPF_API int bpf_program__set_log_level(struct bpf_program *prog, __u32 log_level);\n LIBBPF_API const char *bpf_program__log_buf(const struct bpf_program *prog, size_t *log_size);\n+\n+/**\n+ * @brief **bpf_program__set_log_buf()** sets the buffer the verifier log of\n+ * the BPF program is written to when it is loaded.\n+ *\n+ * Fails with -EBUSY once the enclosing bpf_object is loaded, unless the\n+ * program's load strategy is BPF_PROG_LOAD_STRATEGY_MANUAL, as such a program\n+ * can still be loaded afterwards with bpf_program__load().\n+ *\n+ * @param prog BPF program to set the log buffer for\n+ * @param log_buf buffer for the verifier log, or NULL to use the object's\n+ * kernel_log_buf\n+ * @param log_size size of @log_buf in bytes; must be 0 if @log_buf is NULL and\n+ * non-zero otherwise\n+ * @return 0, on success; -EINVAL if @log_buf and @log_size are inconsistent or\n+ * @log_size exceeds UINT_MAX; -EBUSY as described above\n+ */\n LIBBPF_API int bpf_program__set_log_buf(struct bpf_program *prog, char *log_buf, size_t log_size);\n \n LIBBPF_API struct bpf_func_info *bpf_program__func_info(const struct bpf_program *prog);\n@@ -2117,6 +2159,53 @@ LIBBPF_API int libbpf_unregister_prog_handler(int handler_id);\n */\n LIBBPF_API int bpf_program__clone(struct bpf_program *prog, const struct bpf_prog_load_opts *opts);\n \n+/**\n+ * The program load strategy:\n+ *\n+ * - BPF_PROG_LOAD_STRATEGY_DISABLED: the program is not loaded.\n+ * - BPF_PROG_LOAD_STRATEGY_AUTO: the program is autoloaded when the bpf_object is loaded.\n+ * - BPF_PROG_LOAD_STRATEGY_MANUAL: the program is loaded and attached manually.\n+ * It is skipped by the skeleton's auto-attach, whatever its autoattach\n+ * setting is.\n+ */\n+enum bpf_prog_load_strategy {\n+\tBPF_PROG_LOAD_STRATEGY_DISABLED = 0,\n+\tBPF_PROG_LOAD_STRATEGY_AUTO,\n+\tBPF_PROG_LOAD_STRATEGY_MANUAL,\n+};\n+\n+/**\n+ * @brief **bpf_program__set_load_strategy()** sets the load strategy of a\n+ * BPF program, controlling whether and when it gets loaded into the kernel.\n+ *\n+ * Can only be called before the enclosing bpf_object is prepared.\n+ *\n+ * @param prog BPF program to update\n+ * @param strategy new load strategy for the program\n+ * @return 0 on success; negative error code if the object was already prepared\n+ */\n+LIBBPF_API int bpf_program__set_load_strategy(struct bpf_program *prog,\n+\t\t\t\t\t enum bpf_prog_load_strategy strategy);\n+\n+/**\n+ * @brief **bpf_program__load_strategy()** returns the current load strategy\n+ * of a BPF program.\n+ *\n+ * @param prog BPF program to query\n+ * @return current load strategy of the program\n+ */\n+LIBBPF_API enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog);\n+\n+/**\n+ * @brief **bpf_program__load()** loads a MANUAL BPF program. The enclosing\n+ * bpf_object must already be prepared.\n+ *\n+ * @param prog BPF program to load; non MANUAL programs, already loaded\n+ * programs and subprograms are rejected.\n+ * @return 0 on success; negative error code otherwise\n+ */\n+LIBBPF_API int bpf_program__load(struct bpf_program *prog);\n+\n #ifdef __cplusplus\n } /* extern \"C\" */\n #endif\ndiff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map\nindex eeba4072a5dac..a45666f546fae 100644\n--- a/tools/lib/bpf/libbpf.map\n+++ b/tools/lib/bpf/libbpf.map\n@@ -475,4 +475,8 @@ LIBBPF_1.8.0 {\n } LIBBPF_1.7.0;\n \n LIBBPF_1.9.0 {\n+\tglobal:\n+\t\tbpf_program__load;\n+\t\tbpf_program__load_strategy;\n+\t\tbpf_program__set_load_strategy;\n } LIBBPF_1.8.0;\ndiff --git a/tools/testing/selftests/bpf/prog_tests/load_type.c b/tools/testing/selftests/bpf/prog_tests/load_type.c\nnew file mode 100644\nindex 0000000000000..7e344c0b04450\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/prog_tests/load_type.c\n@@ -0,0 +1,154 @@\n+// SPDX-License-Identifier: GPL-2.0\n+\n+#include \u003ctest_progs.h\u003e\n+#include \u003ctime.h\u003e\n+#include \"test_load_type.skel.h\"\n+\n+void test_load_type(void)\n+{\n+\tstruct bpf_link *link = NULL;\n+\tstruct test_load_type *skel;\n+\tint err;\n+\n+\tskel = test_load_type__open();\n+\tif (!ASSERT_OK_PTR(skel, \"skel_open\"))\n+\t\treturn;\n+\n+\t/* don't load prog1 */\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_disabled_prog1\"))\n+\t\tgoto cleanup;\n+\n+\t/* load and attach prog2 */\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog2, BPF_PROG_LOAD_STRATEGY_AUTO);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_auto_prog2\"))\n+\t\tgoto cleanup;\n+\tif (!ASSERT_TRUE(bpf_program__autoload(skel-\u003eprogs.prog2), \"prog2_autoload\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_manual\"))\n+\t\tgoto cleanup;\n+\tif (!ASSERT_EQ(bpf_program__load_strategy(skel-\u003eprogs.prog3), BPF_PROG_LOAD_STRATEGY_MANUAL,\n+\t\t \"prog3_load_strategy\"))\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * bpf_program__set_autoload() is a thin forwarder to\n+\t * set_load_strategy(), restricted to AUTO/DISABLED to preserve its\n+\t * original bool on/off meaning;\n+\t */\n+\terr = bpf_program__set_autoload(skel-\u003eprogs.prog3, false);\n+\tif (!ASSERT_OK(err, \"set_autoload_false\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_EQ(bpf_program__load_strategy(skel-\u003eprogs.prog3),\n+\t\t BPF_PROG_LOAD_STRATEGY_DISABLED, \"prog3_load_strategy_after_false\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_program__set_autoload(skel-\u003eprogs.prog3, true);\n+\tif (!ASSERT_OK(err, \"set_autoload_true\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_EQ(bpf_program__load_strategy(skel-\u003eprogs.prog3), BPF_PROG_LOAD_STRATEGY_AUTO,\n+\t\t \"prog3_load_strategy_after_true\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_manual_enum\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_EQ(bpf_program__load_strategy(skel-\u003eprogs.prog3), BPF_PROG_LOAD_STRATEGY_MANUAL,\n+\t\t \"prog3_load_strategy_after_manual_enum\"))\n+\t\tgoto cleanup;\n+\n+\t/* changing the load strategy leaves autoattach alone */\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog3, BPF_PROG_LOAD_STRATEGY_AUTO);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_auto\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_EQ(bpf_program__load_strategy(skel-\u003eprogs.prog3), BPF_PROG_LOAD_STRATEGY_AUTO,\n+\t\t \"prog3_load_strategy_auto\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_TRUE(bpf_program__autoattach(skel-\u003eprogs.prog3), \"prog3_autoattach_default\"))\n+\t\tgoto cleanup;\n+\n+\tbpf_program__set_autoattach(skel-\u003eprogs.prog3, false);\n+\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_manual_again\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog3, BPF_PROG_LOAD_STRATEGY_AUTO);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_auto_again\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_FALSE(bpf_program__autoattach(skel-\u003eprogs.prog3), \"prog3_autoattach_kept\"))\n+\t\tgoto cleanup;\n+\n+\t/* restore autoattach to true for the rest of the test */\n+\tbpf_program__set_autoattach(skel-\u003eprogs.prog3, true);\n+\n+\t/* an out-of-range load strategy is rejected */\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog3, (enum bpf_prog_load_strategy)999);\n+\tif (!ASSERT_EQ(err, -EINVAL, \"set_load_strategy_invalid\"))\n+\t\tgoto cleanup;\n+\n+\t/* change the strategy back to BPF_PROG_LOAD_STRATEGY_MANUAL for the rest of the test */\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_manual_final\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_EQ(bpf_program__load_strategy(skel-\u003eprogs.prog3), BPF_PROG_LOAD_STRATEGY_MANUAL,\n+\t\t \"prog3_load_strategy_final\"))\n+\t\tgoto cleanup;\n+\n+\t/* Test post loaded behaviors */\n+\terr = test_load_type__load(skel);\n+\tif (!ASSERT_OK(err, \"skel_load\"))\n+\t\tgoto cleanup;\n+\n+\t/* Post load we cannot update a program's strategy */\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);\n+\tASSERT_EQ(err, -EINVAL, \"set_load_strategy_after_load\");\n+\n+\terr = test_load_type__attach(skel);\n+\tif (!ASSERT_OK(err, \"skel_attach\"))\n+\t\tgoto cleanup;\n+\n+\tusleep(1);\n+\n+\t/* Post load only AUTOLOAD program is expected to run */\n+\tASSERT_FALSE(skel-\u003ebss-\u003eprog1_called, \"prog1_called\");\n+\tASSERT_TRUE(skel-\u003ebss-\u003eprog2_called, \"prog2_called\");\n+\tASSERT_FALSE(skel-\u003ebss-\u003eprog3_called, \"prog3_called\");\n+\n+\terr = bpf_program__load(skel-\u003eprogs.prog3);\n+\tif (!ASSERT_OK(err, \"load_manually\"))\n+\t\tgoto cleanup;\n+\n+\t/* attach prog3 */\n+\tlink = bpf_program__attach(skel-\u003eprogs.prog3);\n+\tif (!ASSERT_OK_PTR(link, \"attach\"))\n+\t\tgoto cleanup;\n+\n+\tusleep(1);\n+\n+\t/* Post explicit load and attach, MANUAL program is also expected to run */\n+\tif (!ASSERT_TRUE(skel-\u003ebss-\u003eprog3_called, \"prog3_called_again\"))\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * detach prog3 explicitly: its link is kept in a local variable, not in\n+\t * skel-\u003elinks, so test_load_type__destroy() has nothing to detach\n+\t */\n+\terr = bpf_link__destroy(link);\n+\tASSERT_OK(err, \"link_destroy\");\n+\tlink = NULL;\n+\n+cleanup:\n+\tif (link)\n+\t\tbpf_link__destroy(link);\n+\ttest_load_type__destroy(skel);\n+}\ndiff --git a/tools/testing/selftests/bpf/prog_tests/manual_load.c b/tools/testing/selftests/bpf/prog_tests/manual_load.c\nnew file mode 100644\nindex 0000000000000..78a7b2904a768\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/prog_tests/manual_load.c\n@@ -0,0 +1,393 @@\n+// SPDX-License-Identifier: GPL-2.0\n+\n+#include \u003ctest_progs.h\u003e\n+#include \u003ctime.h\u003e\n+#include \"kfunc_call_test.skel.h\"\n+#include \"test_manual_load.skel.h\"\n+\n+#define READ_SZ 256\n+\n+/*\n+ * prog4 is marked SEC(\"!...\") in the source instead of being set\n+ * imperatively; verify that an explicit bpf_program__set_load_strategy() call\n+ * before load overrides the declarative default.\n+ */\n+static void manual_load_verify_override(void)\n+{\n+\tstruct test_manual_load *skel;\n+\tint err;\n+\n+\tskel = test_manual_load__open();\n+\tif (!ASSERT_OK_PTR(skel, \"skel_open\"))\n+\t\treturn;\n+\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog4, BPF_PROG_LOAD_STRATEGY_DISABLED);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_disabled\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_EQ(bpf_program__load_strategy(skel-\u003eprogs.prog4),\n+\t\t BPF_PROG_LOAD_STRATEGY_DISABLED, \"prog4_load_strategy_overridden\"))\n+\t\tgoto cleanup;\n+\n+\terr = test_manual_load__load(skel);\n+\tif (!ASSERT_OK(err, \"skel_load\"))\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * prog4 was overridden to DISABLED, so its load_strategy != MANUAL\n+\t * and load() must reject it\n+\t */\n+\tif (!ASSERT_EQ(bpf_program__fd(skel-\u003eprogs.prog4), -ENOENT, \"prog4_not_loaded\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_program__load(skel-\u003eprogs.prog4);\n+\tASSERT_EQ(err, -EINVAL, \"load_after_override\");\n+\n+cleanup:\n+\ttest_manual_load__destroy(skel);\n+}\n+\n+/*\n+ * prog4 is MANUAL via its SEC(\"!...\") marker; verify that\n+ * bpf_object__prepare() alone -- without ever calling bpf_object__load() --\n+ * is sufficient for bpf_program__load() to succeed, since BTF\n+ * loading, map creation, and relocation of MANUAL programs are all\n+ * completed by prepare() already.\n+ */\n+static void manual_load_verify_prepare_only(void)\n+{\n+\tstruct test_manual_load *skel;\n+\tstruct bpf_link *link = NULL;\n+\tint err;\n+\n+\tskel = test_manual_load__open();\n+\tif (!ASSERT_OK_PTR(skel, \"skel_open\"))\n+\t\treturn;\n+\n+\tskel-\u003ebss-\u003etest_pid = getpid();\n+\n+\terr = bpf_object__prepare(skel-\u003eobj);\n+\tif (!ASSERT_OK(err, \"bpf_object__prepare\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_program__load(skel-\u003eprogs.prog4);\n+\tif (!ASSERT_OK(err, \"load_after_prepare\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_GE(bpf_program__fd(skel-\u003eprogs.prog4), 0, \"prog4_fd_after_prepare\"))\n+\t\tgoto cleanup;\n+\n+\tlink = bpf_program__attach(skel-\u003eprogs.prog4);\n+\tif (!ASSERT_OK_PTR(link, \"attach_after_prepare\"))\n+\t\tgoto cleanup;\n+\n+\tusleep(1);\n+\n+\tif (!ASSERT_TRUE(skel-\u003ebss-\u003eprog4_called, \"prog4_called_after_prepare\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_link__destroy(link);\n+\tlink = NULL;\n+\tif (!ASSERT_OK(err, \"link_destroy_after_prepare\"))\n+\t\tgoto cleanup;\n+\n+\tbpf_program__unload(skel-\u003eprogs.prog4);\n+\tASSERT_LT(bpf_program__fd(skel-\u003eprogs.prog4), 0, \"prog4_fd_closed_after_unload\");\n+\n+cleanup:\n+\tif (link)\n+\t\tbpf_link__destroy(link);\n+\ttest_manual_load__destroy(skel);\n+}\n+\n+/*\n+ * prog5 is disabled at parse time; resolve its attach target against\n+ * module BTF via bpf_program__set_attach_target() before switching it\n+ * to MANUAL and deferring its load past the bulk bpf_object__load().\n+ * Regression test for the module BTF fd/array lifetime.\n+ */\n+static void manual_load_verify_module_btf(void)\n+{\n+\tstruct test_manual_load *skel;\n+\tstruct bpf_link *link;\n+\tint err;\n+\n+\tif (!env.has_testmod) {\n+\t\ttest__skip();\n+\t\treturn;\n+\t}\n+\n+\tskel = test_manual_load__open();\n+\tif (!ASSERT_OK_PTR(skel, \"skel_open\"))\n+\t\treturn;\n+\n+\tskel-\u003ebss-\u003etest_pid = getpid();\n+\n+\terr = bpf_program__set_attach_target(skel-\u003eprogs.prog5, 0,\n+\t\t\t\t\t \"bpf_testmod:bpf_testmod_test_read\");\n+\tif (!ASSERT_OK(err, \"set_attach_target\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.prog5, BPF_PROG_LOAD_STRATEGY_MANUAL);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_manual\"))\n+\t\tgoto cleanup;\n+\n+\t/* keep the other autoload programs out of the way of this load */\n+\tbpf_program__set_load_strategy(skel-\u003eprogs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);\n+\tbpf_program__set_load_strategy(skel-\u003eprogs.prog2, BPF_PROG_LOAD_STRATEGY_DISABLED);\n+\tbpf_program__set_load_strategy(skel-\u003eprogs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);\n+\n+\t/*\n+\t * bulk load: prog5 itself is skipped (MANUAL), but this is where\n+\t * module BTF gets torn down if not correctly deferred\n+\t */\n+\terr = test_manual_load__load(skel);\n+\tif (!ASSERT_OK(err, \"skel_load\"))\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * deferred load must still succeed: the module BTF fd cached above\n+\t * by set_attach_target() must still be a valid, open fd here\n+\t */\n+\terr = bpf_program__load(skel-\u003eprogs.prog5);\n+\tif (!ASSERT_OK(err, \"load_module_btf\"))\n+\t\tgoto cleanup;\n+\n+\tlink = bpf_program__attach(skel-\u003eprogs.prog5);\n+\tif (!ASSERT_OK_PTR(link, \"attach\"))\n+\t\tgoto cleanup;\n+\n+\tASSERT_OK(trigger_module_test_read(READ_SZ), \"trigger_read\");\n+\tASSERT_EQ(skel-\u003ebss-\u003eprog5_sz, READ_SZ, \"prog5_sz\");\n+\n+\tbpf_link__destroy(link);\n+\n+cleanup:\n+\ttest_manual_load__destroy(skel);\n+}\n+\n+static void manual_load_verify_main_cycle(void)\n+{\n+\tstruct bpf_link *link = NULL;\n+\tstruct test_manual_load *skel;\n+\tint err;\n+\n+\tskel = test_manual_load__open();\n+\tif (!ASSERT_OK_PTR(skel, \"skel_open\"))\n+\t\treturn;\n+\n+\tskel-\u003ebss-\u003etest_pid = getpid();\n+\n+\t/*\n+\t * the SEC(\"!...\") prefix alone, with no imperative call, must set\n+\t * prog4's load strategy before it is ever touched below\n+\t */\n+\tif (!ASSERT_EQ(bpf_program__load_strategy(skel-\u003eprogs.prog4),\n+\t\t BPF_PROG_LOAD_STRATEGY_MANUAL, \"prog4_prefix_load_strategy\"))\n+\t\tgoto cleanup;\n+\n+\t/* don't load prog1 */\n+\tbpf_program__set_load_strategy(skel-\u003eprogs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);\n+\n+\t/* prog2 is autoload */\n+\tbpf_program__set_load_strategy(skel-\u003eprogs.prog2, BPF_PROG_LOAD_STRATEGY_AUTO);\n+\n+\t/* prog3 is manually loaded */\n+\tbpf_program__set_load_strategy(skel-\u003eprogs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);\n+\n+\terr = test_manual_load__load(skel);\n+\tif (!ASSERT_OK(err, \"skel_load\"))\n+\t\tgoto cleanup;\n+\n+\terr = test_manual_load__attach(skel);\n+\tif (!ASSERT_OK(err, \"skel_attach\"))\n+\t\tgoto cleanup;\n+\n+\t/* trigger the BPF programs */\n+\tusleep(1);\n+\n+\tASSERT_FALSE(skel-\u003ebss-\u003eprog1_called, \"prog1_called\");\n+\tASSERT_TRUE(skel-\u003ebss-\u003eprog2_called, \"prog2_called\");\n+\tASSERT_FALSE(skel-\u003ebss-\u003eprog3_called, \"prog3_called\");\n+\tASSERT_FALSE(skel-\u003ebss-\u003eprog4_called, \"prog4_called\");\n+\n+\t/* prog1 is disabled for load */\n+\tif (!ASSERT_EQ(bpf_program__fd(skel-\u003eprogs.prog1), -ENOENT, \"prog1_not_loaded\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_program__load(skel-\u003eprogs.prog1);\n+\tif (!ASSERT_EQ(err, -EINVAL, \"load_disabled\"))\n+\t\tgoto cleanup;\n+\n+\t/* prog2 is autoload */\n+\terr = bpf_program__load(skel-\u003eprogs.prog2);\n+\tif (!ASSERT_EQ(err, -EINVAL, \"load_autoload\"))\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * bpf_program__unload() no longer rejects based on load strategy:\n+\t * calling it on prog2 (AUTO, currently loaded and attached) performs\n+\t * a full, irreversible unload instead of returning an error\n+\t */\n+\tbpf_program__unload(skel-\u003eprogs.prog2);\n+\tASSERT_LT(bpf_program__fd(skel-\u003eprogs.prog2), 0, \"prog2_fd_closed_after_unload\");\n+\n+\t/* load prog3 */\n+\terr = bpf_program__load(skel-\u003eprogs.prog3);\n+\tif (!ASSERT_OK(err, \"load\"))\n+\t\tgoto cleanup;\n+\n+\t/* attach prog3 */\n+\tlink = bpf_program__attach(skel-\u003eprogs.prog3);\n+\tif (!ASSERT_OK_PTR(link, \"attach\"))\n+\t\tgoto cleanup;\n+\n+\tusleep(1);\n+\n+\tif (!ASSERT_TRUE(skel-\u003ebss-\u003eprog3_called, \"prog3_called\"))\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * detach prog3 explicitly: its link is kept in a local variable, not in\n+\t * skel-\u003elinks, so test_manual_load__destroy() has nothing to detach\n+\t */\n+\terr = bpf_link__destroy(link);\n+\tlink = NULL;\n+\tif (!ASSERT_OK(err, \"link_destroy\"))\n+\t\tgoto cleanup;\n+\n+\t/* reset the call flag after detach */\n+\tskel-\u003ebss-\u003eprog3_called = false;\n+\n+\tusleep(1);\n+\n+\tASSERT_FALSE(skel-\u003ebss-\u003eprog3_called, \"prog3_called\");\n+\n+\t/* unload prog3; MANUAL strategy means its data is retained for reload */\n+\tbpf_program__unload(skel-\u003eprogs.prog3);\n+\n+\t/* reload prog3 */\n+\terr = bpf_program__load(skel-\u003eprogs.prog3);\n+\tif (!ASSERT_OK(err, \"load_reload\"))\n+\t\tgoto cleanup;\n+\n+\t/* reattach prog3 */\n+\tlink = bpf_program__attach(skel-\u003eprogs.prog3);\n+\tif (!ASSERT_OK_PTR(link, \"reattach\"))\n+\t\tgoto cleanup;\n+\n+\tusleep(1);\n+\n+\tif (!ASSERT_TRUE(skel-\u003ebss-\u003eprog3_called, \"prog3_called_reattach\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_link__destroy(link);\n+\tlink = NULL;\n+\tif (!ASSERT_OK(err, \"link_destroy_reattach\"))\n+\t\tgoto cleanup;\n+\n+\t/* reset the call flag after detach */\n+\tskel-\u003ebss-\u003eprog3_called = false;\n+\n+\tusleep(1);\n+\n+\tASSERT_FALSE(skel-\u003ebss-\u003eprog3_called, \"prog3_called\");\n+\n+\t/*\n+\t * run prog4 (declaratively marked) through the same manual\n+\t * load/attach/trigger/detach/unload cycle as prog3\n+\t */\n+\terr = bpf_program__load(skel-\u003eprogs.prog4);\n+\tif (!ASSERT_OK(err, \"prog4_load\"))\n+\t\tgoto cleanup;\n+\n+\tlink = bpf_program__attach(skel-\u003eprogs.prog4);\n+\tif (!ASSERT_OK_PTR(link, \"prog4_attach\"))\n+\t\tgoto cleanup;\n+\n+\tusleep(1);\n+\n+\tif (!ASSERT_TRUE(skel-\u003ebss-\u003eprog4_called, \"prog4_called\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_link__destroy(link);\n+\tlink = NULL;\n+\tif (!ASSERT_OK(err, \"prog4_link_destroy\"))\n+\t\tgoto cleanup;\n+\n+\tbpf_program__unload(skel-\u003eprogs.prog4);\n+\n+\ttest_manual_load__destroy(skel);\n+\treturn;\n+\n+cleanup:\n+\tif (link)\n+\t\tbpf_link__destroy(link);\n+\ttest_manual_load__destroy(skel);\n+}\n+\n+/*\n+ * A MANUAL program that calls a kfunc of a module: the module BTF fd sits in\n+ * a slot of the fd_array of the object, which has to survive the bulk\n+ * bpf_object__load() for the deferred bpf_program__load() to work.\n+ */\n+static void manual_load_verify_module_kfunc(void)\n+{\n+\tstruct bpf_program *prog, *p;\n+\tstruct kfunc_call_test *skel;\n+\tint err;\n+\n+\tif (!env.has_testmod) {\n+\t\ttest__skip();\n+\t\treturn;\n+\t}\n+\n+\tskel = kfunc_call_test__open();\n+\tif (!ASSERT_OK_PTR(skel, \"skel_open\"))\n+\t\treturn;\n+\n+\tprog = skel-\u003eprogs.kfunc_call_test1;\n+\terr = bpf_program__set_load_strategy(prog, BPF_PROG_LOAD_STRATEGY_MANUAL);\n+\tif (!ASSERT_OK(err, \"set_load_strategy\"))\n+\t\tgoto out;\n+\n+\t/* keep the other programs out of the way of this load */\n+\tbpf_object__for_each_program(p, skel-\u003eobj) {\n+\t\tif (p != prog)\n+\t\t\tbpf_program__set_load_strategy(p, BPF_PROG_LOAD_STRATEGY_DISABLED);\n+\t}\n+\n+\t/* the bulk load skips the program, but this is where the fd_array could go */\n+\terr = kfunc_call_test__load(skel);\n+\tif (!ASSERT_OK(err, \"skel_load\"))\n+\t\tgoto out;\n+\tif (!ASSERT_LT(bpf_program__fd(prog), 0, \"not_loaded\"))\n+\t\tgoto out;\n+\n+\terr = bpf_program__load(prog);\n+\tif (ASSERT_OK(err, \"load\")) {\n+\t\tASSERT_GE(bpf_program__fd(prog), 0, \"prog_fd\");\n+\t\tbpf_program__unload(prog);\n+\t\tASSERT_LT(bpf_program__fd(prog), 0, \"unloaded\");\n+\t}\n+out:\n+\tkfunc_call_test__destroy(skel);\n+}\n+\n+void test_manual_load(void)\n+{\n+\tif (test__start_subtest(\"main_cycle\"))\n+\t\tmanual_load_verify_main_cycle();\n+\n+\tif (test__start_subtest(\"verify_override\"))\n+\t\tmanual_load_verify_override();\n+\n+\tif (test__start_subtest(\"verify_prepare_only\"))\n+\t\tmanual_load_verify_prepare_only();\n+\n+\tif (test__start_subtest(\"verify_module_btf\"))\n+\t\tmanual_load_verify_module_btf();\n+\n+\tif (test__start_subtest(\"verify_module_kfunc\"))\n+\t\tmanual_load_verify_module_kfunc();\n+}\ndiff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c\nindex e43dba8fed8e4..8005cba5bf05c 100644\n--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c\n+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c\n@@ -2213,6 +2213,37 @@ static void signed_module_kfunc_rejected(void)\n \trun_setup(\"cleanup\", dir);\n }\n \n+/*\n+ * gen_loader only records programs loaded by bpf_object__load_progs(), which\n+ * skips MANUAL ones, so an object with a MANUAL program must fail to load\n+ * instead of generating a skeleton whose prog_fd slots no longer line up\n+ * with the object's programs\n+ */\n+static void manual_prog_rejected(void)\n+{\n+\tLIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true);\n+\tstruct test_signed_loader *skel;\n+\tint err;\n+\n+\tskel = test_signed_loader__open();\n+\tif (!ASSERT_OK_PTR(skel, \"skel_open\"))\n+\t\treturn;\n+\n+\terr = bpf_program__set_load_strategy(skel-\u003eprogs.probe, BPF_PROG_LOAD_STRATEGY_MANUAL);\n+\tif (!ASSERT_OK(err, \"set_load_strategy_manual\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_object__gen_loader(skel-\u003eobj, \u0026gopts);\n+\tif (!ASSERT_OK(err, \"gen_loader\"))\n+\t\tgoto cleanup;\n+\n+\terr = bpf_object__load(skel-\u003eobj);\n+\tASSERT_EQ(err, -EOPNOTSUPP, \"load_rejected\");\n+\n+cleanup:\n+\ttest_signed_loader__destroy(skel);\n+}\n+\n enum subtest_boot {\n \tBOOT_ANY,\n \tBOOT_SEALED,\n@@ -2257,6 +2288,7 @@ static const struct {\n \t{ \"signed_map_by_fd_rejected\", signed_map_by_fd_rejected, BOOT_SEALED },\n \t{ \"signed_sparse_fd_array_rejected\", signed_sparse_fd_array_rejected, BOOT_SEALED },\n \t{ \"bpf_keyring_provisioned\", bpf_keyring_provisioned, BOOT_UNSEALED },\n+\t{ \"manual_prog_rejected\", manual_prog_rejected, BOOT_ANY },\n };\n \n void test_signed_loader(void)\ndiff --git a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c\nindex e7580b9f0fc2a..a8bd10bab5ddc 100644\n--- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c\n+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c\n@@ -192,9 +192,9 @@ static void arena_autobind(void)\n }\n \n /*\n- * The same binding through bpf_object__prepare() and bpf_program__clone(), the\n- * way veristat loads a program: the arena takes slot 0 of a continuous\n- * fd_array, next to the module BTF of the kfunc the trigger calls.\n+ * The same binding through bpf_object__prepare() and bpf_program__clone(),\n+ * the arena takes slot 0 of a continuous fd_array, next to the module BTF\n+ * of the kfunc the trigger calls.\n */\n static void arena_autobind_clone(void)\n {\n@@ -225,6 +225,46 @@ static void arena_autobind_clone(void)\n \tstruct_ops_arena_autobind__destroy(skel);\n }\n \n+/*\n+ * The same binding through BPF_PROG_LOAD_STRATEGY_MANUAL: every program is\n+ * loaded with bpf_program__load() after bpf_object__prepare() and unloaded\n+ * again, the way veristat loads programs.\n+ */\n+static void arena_autobind_manual(void)\n+{\n+\tstruct struct_ops_arena_autobind *skel;\n+\tstruct bpf_map_info info = {};\n+\t__u32 len = sizeof(info);\n+\tstruct bpf_program *prog;\n+\tint err;\n+\n+\tskel = struct_ops_arena_autobind__open();\n+\tif (!ASSERT_OK_PTR(skel, \"struct_ops_arena_autobind__open\"))\n+\t\treturn;\n+\n+\tbpf_object__for_each_program(prog, skel-\u003eobj) {\n+\t\terr = bpf_program__set_load_strategy(prog, BPF_PROG_LOAD_STRATEGY_MANUAL);\n+\t\tif (!ASSERT_OK(err, \"set_load_strategy\"))\n+\t\t\tgoto out;\n+\t}\n+\terr = bpf_object__prepare(skel-\u003eobj);\n+\tif (!ASSERT_OK(err, \"prepare\"))\n+\t\tgoto out;\n+\terr = bpf_map_get_info_by_fd(bpf_map__fd(skel-\u003emaps.arena), \u0026info, \u0026len);\n+\tif (!ASSERT_OK(err, \"arena_info\"))\n+\t\tgoto out;\n+\tbpf_object__for_each_program(prog, skel-\u003eobj) {\n+\t\terr = bpf_program__load(prog);\n+\t\tif (!ASSERT_OK(err, bpf_program__name(prog)))\n+\t\t\tcontinue;\n+\t\tassert_prog_map(bpf_program__fd(prog), info.id, bpf_program__name(prog));\n+\t\tbpf_program__unload(prog);\n+\t\tASSERT_LT(bpf_program__fd(prog), 0, \"unloaded\");\n+\t}\n+out:\n+\tstruct_ops_arena_autobind__destroy(skel);\n+}\n+\n /*\n * An arena exclusive to a program is bound to that program all the same: the\n * kernel checks an exclusive map against the digest of the final program, so\n@@ -315,6 +355,8 @@ void serial_test_struct_ops_arena(void)\n \t\tarena_autobind();\n \tif (test__start_subtest(\"arena_autobind_clone\"))\n \t\tarena_autobind_clone();\n+\tif (test__start_subtest(\"arena_autobind_manual\"))\n+\t\tarena_autobind_manual();\n \tif (test__start_subtest(\"arena_autobind_excl\"))\n \t\tarena_autobind_excl();\n #else\ndiff --git a/tools/testing/selftests/bpf/progs/test_load_type.c b/tools/testing/selftests/bpf/progs/test_load_type.c\nnew file mode 100644\nindex 0000000000000..3d9b81691d7ad\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/test_load_type.c\n@@ -0,0 +1,31 @@\n+// SPDX-License-Identifier: GPL-2.0\n+\n+#include \"vmlinux.h\"\n+#include \u003cbpf/bpf_helpers.h\u003e\n+\n+bool prog1_called = false;\n+bool prog2_called = false;\n+bool prog3_called = false;\n+\n+SEC(\"raw_tp/sys_enter\")\n+int prog1(const void *ctx)\n+{\n+\tprog1_called = true;\n+\treturn 0;\n+}\n+\n+SEC(\"raw_tp/sys_enter\")\n+int prog2(const void *ctx)\n+{\n+\tprog2_called = true;\n+\treturn 0;\n+}\n+\n+SEC(\"raw_tp/sys_enter\")\n+int prog3(const void *ctx)\n+{\n+\tprog3_called = true;\n+\treturn 0;\n+}\n+\n+char _license[] SEC(\"license\") = \"GPL\";\ndiff --git a/tools/testing/selftests/bpf/progs/test_manual_load.c b/tools/testing/selftests/bpf/progs/test_manual_load.c\nnew file mode 100644\nindex 0000000000000..d91d5267a0edb\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/test_manual_load.c\n@@ -0,0 +1,70 @@\n+// SPDX-License-Identifier: GPL-2.0\n+\n+#include \"vmlinux.h\"\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+\n+bool prog1_called = false;\n+bool prog2_called = false;\n+bool prog3_called = false;\n+bool prog4_called = false;\n+__u32 prog5_sz = 0;\n+__u32 test_pid = 0;\n+\n+SEC(\"raw_tp/sys_enter\")\n+int prog1(const void *ctx)\n+{\n+\tif ((bpf_get_current_pid_tgid() \u003e\u003e 32) != test_pid)\n+\t\treturn 0;\n+\n+\tprog1_called = true;\n+\treturn 0;\n+}\n+\n+SEC(\"raw_tp/sys_enter\")\n+int prog2(const void *ctx)\n+{\n+\tif ((bpf_get_current_pid_tgid() \u003e\u003e 32) != test_pid)\n+\t\treturn 0;\n+\n+\tprog2_called = true;\n+\treturn 0;\n+}\n+\n+SEC(\"raw_tp/sys_enter\")\n+int prog3(const void *ctx)\n+{\n+\tif ((bpf_get_current_pid_tgid() \u003e\u003e 32) != test_pid)\n+\t\treturn 0;\n+\n+\tprog3_called = true;\n+\treturn 0;\n+}\n+\n+SEC(\"!raw_tp/sys_enter\")\n+int prog4(const void *ctx)\n+{\n+\tif ((bpf_get_current_pid_tgid() \u003e\u003e 32) != test_pid)\n+\t\treturn 0;\n+\n+\tprog4_called = true;\n+\treturn 0;\n+}\n+\n+/*\n+ * disabled at parse time; its attach target is resolved against module\n+ * BTF via bpf_program__set_attach_target() before it is switched to\n+ * MANUAL and loaded\n+ */\n+SEC(\"?fentry\")\n+int BPF_PROG(prog5, struct file *file, struct kobject *kobj,\n+\t const struct bin_attribute *bin_attr, char *buf, loff_t off, size_t len)\n+{\n+\tif ((bpf_get_current_pid_tgid() \u003e\u003e 32) != test_pid)\n+\t\treturn 0;\n+\n+\tprog5_sz = len;\n+\treturn 0;\n+}\n+\n+char _license[] SEC(\"license\") = \"GPL\";\ndiff --git a/tools/testing/selftests/bpf/veristat.c b/tools/testing/selftests/bpf/veristat.c\nindex 9cfc9b4b41c12..0cd19b59037f6 100644\n--- a/tools/testing/selftests/bpf/veristat.c\n+++ b/tools/testing/selftests/bpf/veristat.c\n@@ -1678,7 +1678,6 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf\n \tconst char *base_filename = basename(strdupa(filename));\n \tconst char *prog_name = bpf_program__name(prog);\n \tlong mem_peak_a, mem_peak_b, mem_peak = -1;\n-\tLIBBPF_OPTS(bpf_prog_load_opts, opts);\n \tchar *buf;\n \tint buf_sz, log_level;\n \tstruct verif_stats *stats;\n@@ -1726,18 +1725,15 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf\n \tif (env.force_reg_invariants)\n \t\tbpf_program__add_flags(prog, BPF_F_TEST_REG_INVARIANTS);\n \n-\topts.log_buf = buf;\n-\topts.log_size = buf_sz;\n-\topts.log_level = log_level;\n+\tbpf_program__set_log_buf(prog, buf, buf_sz);\n+\tbpf_program__set_log_level(prog, log_level);\n \n \tcgroup_err = reset_stat_cgroup();\n \tmem_peak_a = cgroup_memory_peak();\n-\tfd = bpf_program__clone(prog, \u0026opts);\n-\tif (fd \u003c 0) {\n-\t\terr = fd;\n-\t\tif (env.verbose)\n-\t\t\tfprintf(stderr, \"Failed to load program %s %d\\n\", prog_name, err);\n-\t}\n+\terr = bpf_program__load(prog);\n+\tfd = err == 0 ? bpf_program__fd(prog) : -1;\n+\tif (err \u0026\u0026 env.verbose)\n+\t\tfprintf(stderr, \"Failed to load program %s %d\\n\", prog_name, err);\n \tmem_peak_b = cgroup_memory_peak();\n \tif (!cgroup_err \u0026\u0026 mem_peak_a \u003e= 0 \u0026\u0026 mem_peak_b \u003e= 0)\n \t\tmem_peak = mem_peak_b - mem_peak_a;\n@@ -1774,7 +1770,7 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf\n \tif (verif_log_buf != buf)\n \t\tfree(buf);\n \tif (fd \u003e 0)\n-\t\tclose(fd);\n+\t\tbpf_program__unload(prog);\n \treturn 0;\n }\n \n@@ -2297,7 +2293,7 @@ static int process_obj(const char *filename)\n \tenv.files_processed++;\n \n \tbpf_object__for_each_program(prog, obj) {\n-\t\tbpf_program__set_autoload(prog, true);\n+\t\tbpf_program__set_load_strategy(prog, BPF_PROG_LOAD_STRATEGY_MANUAL);\n \t\tprog_cnt++;\n \t}\n \n"
} |
| 2/1 | 2026/10/24 13:04 | agent | patch-evaluator |
0mModel:core Args: null Results: {
"EnableConfigs": null,
"FocusSymbols": [],
"Reasoning": "The changes are entirely confined to user-space code within tools/ (libbpf and BPF selftests). They do not modify the compiled kernel image (vmlinux) or any kernel modules, making them structurally outside the scope of syzkaller kernel fuzzing.",
"WorthFuzzing": false
}Instruction: You are an expert Linux kernel maintainer and security engineer.
Your job is to review a provided patch series and evaluate whether it warrants fuzzing with syzkaller.
IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on internal assumptions. You must actively use your code access
tools to inspect the actual source code, callers, and surrounding context.
================================================================================
1. CORE TRIAGE PHILOSOPHY
================================================================================
The goal of patch fuzzing is to discover crashes, regressions, exposed latent bugs,
and newly triggered assertions introduced by the patch series.
- REACHABILITY IS THE PRIMARY GATE:
Fuzzing can only discover bugs in code that can actually execute in standard virtualized
environments (GCE or QEMU, utilizing software-emulated devices like USB gadgets, netdev, tun/tap).
If the modified code is structurally unreachable (see Section 2), it MUST NOT be fuzzed,
regardless of whether it adds assertions or complex logic.
- DO NOT BLINDLY TRUST "NO FUNCTIONAL CHANGE" (NFCI) OR "REFACTORING" CLAIMS:
Patch authors routinely label changes as "cleanups", "refactorings", or state
"No functional change intended". Do NOT take these claims at face value.
Code refactorings that rearrange logic, introduce helper functions, or alter state management
in core subsystems frequently introduce subtle semantic shifts or uncover latent kernel bugs.
If reachable executable code is modified or refactored, it MUST be fuzzed.
- NEW OR MODIFIED ASSERTIONS IN REACHABLE CODE MUST BE FUZZED:
When a patch introduces or modifies runtime checks or assertions (e.g., WARN_ON*, VM_WARN_ON*,
BUG_ON*, lockdep_assert*) in reachable code paths, it enforces new or stricter invariants.
Even if the author believes the invariant always holds, fuzzing is essential to verify whether
an unusual sequence of operations can violate it.
================================================================================
2. WHEN TO RETURN WorthFuzzing=false (NEGATIVE CRITERIA)
================================================================================
Return WorthFuzzing=false ONLY IF all modified code falls strictly into one or more of these categories:
- Non-kernel and non-executable changes:
* Modifications to Documentation/, comments, or spelling fixes.
* User-space directories, self-tests, samples, or scripts (e.g., tools/, samples/, scripts/, usr/)
that do not affect the compiled kernel image (vmlinux) or kernel modules.
* Purely decorative logging (e.g., message strings in pr_err, printk, dev_info) or tracepoints
that do not alter control flow or data structures.
* Build system or Kconfig changes that do not alter compiled C logic.
- Structurally unreachable hardware:
* Vendor-specific PCIe switches, SmartNICs, or GPU drivers (e.g., mlxsw, pds_core, qed,
ionic, amdgpu) requiring physical ASIC/PCIe cards not emulated in standard QEMU.
- Unreachable execution paths:
* Driver teardown callbacks (.remove, .shutdown, pci_unregister_driver) executed only during
physical PCI hot-unplug or manual sysfs driver unbinding.
* Code paths exclusive to architectures other than the target architecture.
================================================================================
3. WHEN TO RETURN WorthFuzzing=true (POSITIVE CRITERIA)
================================================================================
Return WorthFuzzing=true whenever the patch touches reachable executable code, including:
- Core Subsystems:
* Any logic modifications in memory management (mm/), synchronization/locking (kernel/locking/),
BPF, scheduler, core networking, VFS, or syscall handling.
- Refactorings and Code Cleanups:
* Any restructuring of reachable data structures, helper abstractions, or algorithm flows.
- Runtime Assertions and Defensive Checks:
* Any introduction or alteration of assertions (WARN_ON*, VM_WARN_ON*, BUG_ON*, etc.) in reachable paths.
- Reachable Drivers and Protocols:
* Drivers accessible via virtual buses (virtio, USB gadget, loopback, netlink, binder, sockets, etc.).
================================================================================
4. EXTRACTING FocusSymbols (PREVENTING DILUTION)
================================================================================
When WorthFuzzing=true, you must extract specific kernel functions into FocusSymbols to guide the fuzzer:
- AVOID UBIQUITOUS LIFECYCLE HOT-PATHS:
Do NOT list generic, ubiquitous functions called by almost every program in the corpus
(including, but not limited to: general memory allocators and deallocators, page fault
and trap handlers, or core synchronization primitives; this is not an exhaustive list).
Listing ubiquitous functions causes the fuzzer to classify thousands of unrelated tests as "focused",
which severely dilutes fuzzing effort away from the actual changes.
- TARGET SPECIFIC FEATURE LOGIC AND ENTRYPOINTS:
List functions that specifically implement the logic being added or altered, or direct API entrypoints
for the subsystem feature under review.
- HANDLING STATIC INLINE FUNCTIONS IN HEADERS (.h):
Compiler-inlined static functions (such as static inlines in mm/*.h or include/linux/*.h) lack
distinct symbol addresses in vmlinux and cannot be targeted directly by symbol coverage filters.
If the changes are primarily in static inline helpers, identify non-static, feature-specific caller
functions in .c files that exercise them (avoiding ubiquitous lifecycle wrappers).
================================================================================
5. IDENTIFYING EnableConfigs
================================================================================
Identify any specific CONFIG_ options required to properly compile and reach the modified code:
- Inspect Kconfig files and #ifdef guards; do not make assumptions.
- Check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed.
- Strip any 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt: Target architecture: amd64
For your convenience, here is the diff of the changes:
commit 09d012e79d9da66381c0f42ca493a0d623bb2001
Author: syz-cluster <triage@syzkaller.com>
Date: Sat Oct 24 13:04:09 2026 +0000
syz-cluster: applied patch under review
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 8fcb8cf39cc0c..ad260cc132036 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;
/* the program or a function that it calls has callx */
@@ -758,6 +758,7 @@ struct bpf_object {
bool has_subcalls;
bool has_rodata;
+ bool has_manual_progs;
struct bpf_gen *gen_loader;
@@ -870,7 +871,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;
@@ -882,12 +883,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);
@@ -955,13 +974,21 @@ 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 via bpf_program__load().
*/
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 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->autoload = true;
+ prog->load_strategy = BPF_PROG_LOAD_STRATEGY_AUTO;
}
prog->autoattach = true;
@@ -1228,6 +1255,14 @@ static int bpf_object_adjust_struct_ops_autoload(struct bpf_object *obj)
if (prog->type != BPF_PROG_TYPE_STRUCT_OPS)
continue;
+ /*
+ * If the user set the load strategy to MANUAL, don't guess it
+ * from map autocreate: a MANUAL program is loaded explicitly
+ * with bpf_program__load(), which rejects any other strategy.
+ */
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
+ continue;
+
for (j = 0; j < obj->nr_maps; ++j) {
const struct btf_type *type;
@@ -1248,7 +1283,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;
@@ -1339,7 +1375,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;
}
@@ -1377,7 +1413,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;
@@ -3812,7 +3848,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;
@@ -5873,6 +5909,20 @@ static int init_prog_array_slots(struct bpf_object *obj, struct bpf_map *map)
continue;
targ_prog = map->init_slots[i];
+
+ /*
+ * Be careful about manual progs, reject if they weren't loaded before
+ * this step to not insert invalid FDs into the map, otherwise we will
+ * get a much less clear rejection from the kernel on the element update
+ * below.
+ */
+ if (targ_prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL &&
+ targ_prog->fd < 0) {
+ pr_warn("map '%s': slot [%u] prog '%s' is MANUAL but not loaded\n",
+ map->name, i, targ_prog->name);
+ return -EINVAL;
+ }
+
fd = bpf_program__fd(targ_prog);
err = bpf_map_update_elem(map->fd, &i, &fd, 0);
@@ -6501,7 +6551,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
@@ -8172,7 +8222,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);
@@ -8208,7 +8258,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 */
@@ -9300,8 +9350,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;
@@ -9329,6 +9379,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;
@@ -9355,6 +9407,18 @@ 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 declared SEC("!...") are rejected here
+ * at declarative parse time for manual loading since those programs cannot
+ * load/unload independently of each other per a given struct ops.
+ */
+ 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
*/
@@ -9555,7 +9619,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;
}
@@ -9946,7 +10010,7 @@ static int bpf_object__resolve_externs(struct bpf_object *obj,
return 0;
}
-static void bpf_map_prepare_vdata(const struct bpf_map *map)
+static int bpf_map_prepare_vdata(const struct bpf_map *map)
{
const struct btf_type *type;
struct bpf_struct_ops *st_ops;
@@ -9962,16 +10026,32 @@ static void bpf_map_prepare_vdata(const struct bpf_map *map)
if (!prog)
continue;
+ /*
+ * Manual struct_ops programs don't get along well with manual
+ * loading: all the progs belonging to the same struct_ops must
+ * load and unload together, which is the opposite of the manual
+ * load concept.
+ * Still, we allow manual progs here as long as they were loaded
+ * before this step because this way they do work.
+ */
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL && prog->fd < 0) {
+ pr_warn("map '%s': struct_ops member prog '%s' is MANUAL but not loaded\n",
+ map->name, prog->name);
+ return -EINVAL;
+ }
+
prog_fd = bpf_program__fd(prog);
kern_data = st_ops->kern_vdata + st_ops->kern_func_off[i];
*(unsigned long *)kern_data = prog_fd;
}
+
+ return 0;
}
static int bpf_object_prepare_struct_ops(struct bpf_object *obj)
{
struct bpf_map *map;
- int i;
+ int i, err;
for (i = 0; i < obj->nr_maps; i++) {
map = &obj->maps[i];
@@ -9982,7 +10062,9 @@ static int bpf_object_prepare_struct_ops(struct bpf_object *obj)
if (!map->autocreate)
continue;
- bpf_map_prepare_vdata(map);
+ err = bpf_map_prepare_vdata(map);
+ if (err)
+ return err;
}
return 0;
@@ -9998,10 +10080,19 @@ 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;
+ /*
+ * Module BTF fds and btf_vmlinux are still needed by
+ * programs that have not been manually loaded yet, so defer freeing
+ * them to the end of the object lifetime, unless the caller forces
+ * immediate cleanup.
+ */
+ if (!force && obj->has_manual_progs)
+ return;
+
/* clean up module BTFs */
for (i = 0; i < obj->btf_module_cnt; i++) {
close(obj->btf_modules[i].fd);
@@ -10018,15 +10109,42 @@ static void bpf_object_cleanup_btf(struct bpf_object *obj)
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, and the slots handed out with it */
- zfree(&obj->fd_array);
- obj->fd_array_cap = 0;
- obj->fd_array_used = 0;
+ /*
+ * The fd array, and the slots handed out with it, are needed for
+ * manually loaded programs, so defer freeing them in that case
+ * to the end of the object lifetime.
+ */
+ if (force || !obj->has_manual_progs) {
+ zfree(&obj->fd_array);
+ obj->fd_array_cap = 0;
+ obj->fd_array_used = 0;
+ }
/* clean up BTF */
- bpf_object_cleanup_btf(obj);
+ bpf_object_cleanup_btf(obj, force);
+}
+
+static int bpf_object_check_gen_loader(struct bpf_object *obj)
+{
+ struct bpf_program *prog;
+
+ if (!obj->gen_loader)
+ return 0;
+
+ /*
+ * gen_loader only records programs loaded by bpf_object__load_progs(),
+ * which skips MANUAL ones.
+ */
+ bpf_object__for_each_program(prog, obj) {
+ if (prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
+ continue;
+ pr_warn("prog '%s': gen_loader does not support manually-loaded programs\n",
+ prog->name);
+ return -EOPNOTSUPP;
+ }
+ return 0;
}
static int bpf_object_prepare(struct bpf_object *obj, const char *target_btf_path)
@@ -10038,7 +10156,8 @@ static int bpf_object_prepare(struct bpf_object *obj, const char *target_btf_pat
return -EINVAL;
}
- err = bpf_object_prepare_token(obj);
+ err = bpf_object_check_gen_loader(obj);
+ err = err ? : bpf_object_prepare_token(obj);
err = err ? : bpf_object__probe_loading(obj);
err = err ? : bpf_object__load_vmlinux_btf(obj, false);
err = err ? : bpf_object__resolve_externs(obj, obj->kconfig);
@@ -10093,7 +10212,8 @@ static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const ch
prog_has_callx(&obj->programs[i]))
text_has_callx = true;
for (i = 0; i < obj->nr_programs; i++)
- if (obj->programs[i].autoload && !prog_is_subprog(obj, &obj->programs[i]) &&
+ if (obj->programs[i].load_strategy != BPF_PROG_LOAD_STRATEGY_DISABLED &&
+ !prog_is_subprog(obj, &obj->programs[i]) &&
(text_has_callx || prog_has_callx(&obj->programs[i])))
nr_progs++;
bpf_gen__init(obj->gen_loader, obj->log_level | extra_log_level,
@@ -10120,7 +10240,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) {
@@ -10588,7 +10708,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;
@@ -10597,7 +10717,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++)
@@ -10698,6 +10817,9 @@ int bpf_object__gen_loader(struct bpf_object *obj, struct gen_loader_opts *opts)
return libbpf_err(-EFAULT);
if (!OPTS_VALID(opts, gen_loader_opts))
return libbpf_err(-EINVAL);
+ if (obj->state >= OBJ_PREPARED)
+ return libbpf_err(-EINVAL);
+
gen = calloc(1, sizeof(*gen));
if (!gen)
return libbpf_err(-ENOMEM);
@@ -10774,16 +10896,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)
@@ -10923,7 +11042,13 @@ __u32 bpf_program__log_level(const struct bpf_program *prog)
int bpf_program__set_log_level(struct bpf_program *prog, __u32 log_level)
{
- if (prog->obj->state >= OBJ_LOADED)
+ /*
+ * Here and below, OBJ_LOADED is not a relevant restriction for
+ * manual programs as those can load after the bpf object is
+ * already loaded.
+ */
+ if (prog->obj->state >= OBJ_LOADED &&
+ prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
return libbpf_err(-EBUSY);
prog->log_level = log_level;
@@ -10942,7 +11067,8 @@ int bpf_program__set_log_buf(struct bpf_program *prog, char *log_buf, size_t log
return libbpf_err(-EINVAL);
if (log_size > UINT_MAX)
return libbpf_err(-EINVAL);
- if (prog->obj->state >= OBJ_LOADED)
+ if (prog->obj->state >= OBJ_LOADED &&
+ prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
return libbpf_err(-EBUSY);
prog->log_buf = log_buf;
@@ -13728,7 +13854,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;
}
@@ -16172,7 +16298,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 */
@@ -16282,3 +16408,62 @@ 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)
+{
+ struct bpf_object *obj = prog->obj;
+
+ if (obj->state >= OBJ_PREPARED)
+ return libbpf_err(-EINVAL);
+
+ switch (strategy) {
+ case BPF_PROG_LOAD_STRATEGY_DISABLED:
+ case BPF_PROG_LOAD_STRATEGY_AUTO:
+ prog->load_strategy = strategy;
+ break;
+ case BPF_PROG_LOAD_STRATEGY_MANUAL:
+ if (prog_is_subprog(obj, prog))
+ return libbpf_err(-EINVAL);
+
+ prog->load_strategy = BPF_PROG_LOAD_STRATEGY_MANUAL;
+ break;
+ default:
+ return libbpf_err(-EINVAL);
+ }
+
+ return 0;
+}
+
+enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog)
+{
+ return prog->load_strategy;
+}
+
+int
+bpf_program__load(struct bpf_program *prog)
+{
+ int err;
+ struct bpf_object *obj = prog->obj;
+
+ /*
+ * This function cannot be called before OBJ_PREPARED -
+ * Program's data is initialized on object prepare.
+ */
+ 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 03f794f4adb03..dd5380bc355e5 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -473,6 +473,17 @@ 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;
@@ -1055,8 +1066,39 @@ LIBBPF_API int bpf_program__clear_flags(struct bpf_program *prog, __u32 flags);
* interactions.
*/
LIBBPF_API __u32 bpf_program__log_level(const struct bpf_program *prog);
+
+/**
+ * @brief **bpf_program__set_log_level()** sets the verifier log level used
+ * when the BPF program is loaded.
+ *
+ * Fails with -EBUSY once the enclosing bpf_object is loaded, unless the
+ * program's load strategy is BPF_PROG_LOAD_STRATEGY_MANUAL, as such a program
+ * can still be loaded afterwards with bpf_program__load().
+ *
+ * @param prog BPF program to set the log level for
+ * @param log_level verifier log level, 0 requests no logging on the first
+ * load attempt
+ * @return 0, on success; negative error code, otherwise
+ */
LIBBPF_API int bpf_program__set_log_level(struct bpf_program *prog, __u32 log_level);
LIBBPF_API const char *bpf_program__log_buf(const struct bpf_program *prog, size_t *log_size);
+
+/**
+ * @brief **bpf_program__set_log_buf()** sets the buffer the verifier log of
+ * the BPF program is written to when it is loaded.
+ *
+ * Fails with -EBUSY once the enclosing bpf_object is loaded, unless the
+ * program's load strategy is BPF_PROG_LOAD_STRATEGY_MANUAL, as such a program
+ * can still be loaded afterwards with bpf_program__load().
+ *
+ * @param prog BPF program to set the log buffer for
+ * @param log_buf buffer for the verifier log, or NULL to use the object's
+ * kernel_log_buf
+ * @param log_size size of @log_buf in bytes; must be 0 if @log_buf is NULL and
+ * non-zero otherwise
+ * @return 0, on success; -EINVAL if @log_buf and @log_size are inconsistent or
+ * @log_size exceeds UINT_MAX; -EBUSY as described above
+ */
LIBBPF_API int bpf_program__set_log_buf(struct bpf_program *prog, char *log_buf, size_t log_size);
LIBBPF_API struct bpf_func_info *bpf_program__func_info(const struct bpf_program *prog);
@@ -2117,6 +2159,53 @@ 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.
+ * - BPF_PROG_LOAD_STRATEGY_MANUAL: the program is loaded and attached manually.
+ * It is skipped by the skeleton's auto-attach, whatever its autoattach
+ * setting is.
+ */
+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 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 prepared
+ */
+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);
+
+/**
+ * @brief **bpf_program__load()** loads a MANUAL BPF program. The enclosing
+ * bpf_object must already be prepared.
+ *
+ * @param prog BPF program to load; non MANUAL programs, already loaded
+ * programs and subprograms are rejected.
+ * @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 eeba4072a5dac..a45666f546fae 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -475,4 +475,8 @@ LIBBPF_1.8.0 {
} LIBBPF_1.7.0;
LIBBPF_1.9.0 {
+ global:
+ bpf_program__load;
+ bpf_program__load_strategy;
+ bpf_program__set_load_strategy;
} LIBBPF_1.8.0;
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..7e344c0b04450
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
@@ -0,0 +1,154 @@
+// 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 */
+ 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;
+
+ /* load and attach prog2 */
+ err = bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_AUTO);
+ if (!ASSERT_OK(err, "set_load_strategy_auto_prog2"))
+ goto cleanup;
+ 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;
+ */
+ 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;
+
+ /* changing the load strategy leaves autoattach alone */
+ 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_default"))
+ goto cleanup;
+
+ bpf_program__set_autoattach(skel->progs.prog3, false);
+
+ 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_AUTO);
+ if (!ASSERT_OK(err, "set_load_strategy_auto_again"))
+ goto cleanup;
+
+ if (!ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog3), "prog3_autoattach_kept"))
+ goto cleanup;
+
+ /* restore autoattach to true for the rest of the test */
+ bpf_program__set_autoattach(skel->progs.prog3, true);
+
+ /* 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_EQ(err, -EINVAL, "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;
+
+ /* Test post loaded behaviors */
+ err = test_load_type__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ /* Post load we cannot update a program's strategy */
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ ASSERT_EQ(err, -EINVAL, "set_load_strategy_after_load");
+
+ err = test_load_type__attach(skel);
+ if (!ASSERT_OK(err, "skel_attach"))
+ goto cleanup;
+
+ usleep(1);
+
+ /* Post load only AUTOLOAD program is expected to run */
+ 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);
+
+ /* Post explicit load and attach, MANUAL program is also expected to run */
+ if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_again"))
+ goto cleanup;
+
+ /*
+ * detach prog3 explicitly: its link is kept in a local variable, not in
+ * skel->links, so test_load_type__destroy() has nothing to detach
+ */
+ 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/prog_tests/manual_load.c b/tools/testing/selftests/bpf/prog_tests/manual_load.c
new file mode 100644
index 0000000000000..78a7b2904a768
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/manual_load.c
@@ -0,0 +1,393 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include <time.h>
+#include "kfunc_call_test.skel.h"
+#include "test_manual_load.skel.h"
+
+#define READ_SZ 256
+
+/*
+ * 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 manual_load_verify_override(void)
+{
+ struct test_manual_load *skel;
+ int err;
+
+ skel = test_manual_load__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;
+
+ err = test_manual_load__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
+ */
+ if (!ASSERT_EQ(bpf_program__fd(skel->progs.prog4), -ENOENT, "prog4_not_loaded"))
+ goto cleanup;
+
+ err = bpf_program__load(skel->progs.prog4);
+ ASSERT_EQ(err, -EINVAL, "load_after_override");
+
+cleanup:
+ test_manual_load__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 manual_load_verify_prepare_only(void)
+{
+ struct test_manual_load *skel;
+ struct bpf_link *link = NULL;
+ int err;
+
+ skel = test_manual_load__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ skel->bss->test_pid = getpid();
+
+ 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(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_manual_load__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.
+ */
+static void manual_load_verify_module_btf(void)
+{
+ struct test_manual_load *skel;
+ struct bpf_link *link;
+ int err;
+
+ if (!env.has_testmod) {
+ test__skip();
+ return;
+ }
+
+ skel = test_manual_load__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ skel->bss->test_pid = getpid();
+
+ 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_manual_load__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_manual_load__destroy(skel);
+}
+
+static void manual_load_verify_main_cycle(void)
+{
+ struct bpf_link *link = NULL;
+ struct test_manual_load *skel;
+ int err;
+
+ skel = test_manual_load__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ skel->bss->test_pid = getpid();
+
+ /*
+ * 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;
+
+ /* 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_manual_load__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ err = test_manual_load__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 */
+ if (!ASSERT_EQ(bpf_program__fd(skel->progs.prog1), -ENOENT, "prog1_not_loaded"))
+ goto cleanup;
+
+ err = bpf_program__load(skel->progs.prog1);
+ if (!ASSERT_EQ(err, -EINVAL, "load_disabled"))
+ goto cleanup;
+
+ /* prog2 is autoload */
+ err = bpf_program__load(skel->progs.prog2);
+ if (!ASSERT_EQ(err, -EINVAL, "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");
+
+ /* 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 explicitly: its link is kept in a local variable, not in
+ * skel->links, so test_manual_load__destroy() has nothing to detach
+ */
+ err = bpf_link__destroy(link);
+ link = NULL;
+ if (!ASSERT_OK(err, "link_destroy"))
+ goto cleanup;
+
+ /* reset the call flag after detach */
+ skel->bss->prog3_called = false;
+
+ usleep(1);
+
+ 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;
+
+ err = bpf_link__destroy(link);
+ link = NULL;
+ if (!ASSERT_OK(err, "link_destroy_reattach"))
+ goto cleanup;
+
+ /* reset the call flag after detach */
+ skel->bss->prog3_called = false;
+
+ usleep(1);
+
+ 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_manual_load__destroy(skel);
+ return;
+
+cleanup:
+ if (link)
+ bpf_link__destroy(link);
+ test_manual_load__destroy(skel);
+}
+
+/*
+ * A MANUAL program that calls a kfunc of a module: the module BTF fd sits in
+ * a slot of the fd_array of the object, which has to survive the bulk
+ * bpf_object__load() for the deferred bpf_program__load() to work.
+ */
+static void manual_load_verify_module_kfunc(void)
+{
+ struct bpf_program *prog, *p;
+ struct kfunc_call_test *skel;
+ int err;
+
+ if (!env.has_testmod) {
+ test__skip();
+ return;
+ }
+
+ skel = kfunc_call_test__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ prog = skel->progs.kfunc_call_test1;
+ err = bpf_program__set_load_strategy(prog, BPF_PROG_LOAD_STRATEGY_MANUAL);
+ if (!ASSERT_OK(err, "set_load_strategy"))
+ goto out;
+
+ /* keep the other programs out of the way of this load */
+ bpf_object__for_each_program(p, skel->obj) {
+ if (p != prog)
+ bpf_program__set_load_strategy(p, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ }
+
+ /* the bulk load skips the program, but this is where the fd_array could go */
+ err = kfunc_call_test__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto out;
+ if (!ASSERT_LT(bpf_program__fd(prog), 0, "not_loaded"))
+ goto out;
+
+ err = bpf_program__load(prog);
+ if (ASSERT_OK(err, "load")) {
+ ASSERT_GE(bpf_program__fd(prog), 0, "prog_fd");
+ bpf_program__unload(prog);
+ ASSERT_LT(bpf_program__fd(prog), 0, "unloaded");
+ }
+out:
+ kfunc_call_test__destroy(skel);
+}
+
+void test_manual_load(void)
+{
+ if (test__start_subtest("main_cycle"))
+ manual_load_verify_main_cycle();
+
+ if (test__start_subtest("verify_override"))
+ manual_load_verify_override();
+
+ if (test__start_subtest("verify_prepare_only"))
+ manual_load_verify_prepare_only();
+
+ if (test__start_subtest("verify_module_btf"))
+ manual_load_verify_module_btf();
+
+ if (test__start_subtest("verify_module_kfunc"))
+ manual_load_verify_module_kfunc();
+}
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index e43dba8fed8e4..8005cba5bf05c 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -2213,6 +2213,37 @@ static void signed_module_kfunc_rejected(void)
run_setup("cleanup", dir);
}
+/*
+ * gen_loader only records programs loaded by bpf_object__load_progs(), which
+ * skips MANUAL ones, so an object with a MANUAL program must fail to load
+ * instead of 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);
+ if (!ASSERT_OK(err, "gen_loader"))
+ goto cleanup;
+
+ err = bpf_object__load(skel->obj);
+ ASSERT_EQ(err, -EOPNOTSUPP, "load_rejected");
+
+cleanup:
+ test_signed_loader__destroy(skel);
+}
+
enum subtest_boot {
BOOT_ANY,
BOOT_SEALED,
@@ -2257,6 +2288,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/prog_tests/test_struct_ops_arena.c b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
index e7580b9f0fc2a..a8bd10bab5ddc 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
@@ -192,9 +192,9 @@ static void arena_autobind(void)
}
/*
- * The same binding through bpf_object__prepare() and bpf_program__clone(), the
- * way veristat loads a program: the arena takes slot 0 of a continuous
- * fd_array, next to the module BTF of the kfunc the trigger calls.
+ * The same binding through bpf_object__prepare() and bpf_program__clone(),
+ * the arena takes slot 0 of a continuous fd_array, next to the module BTF
+ * of the kfunc the trigger calls.
*/
static void arena_autobind_clone(void)
{
@@ -225,6 +225,46 @@ static void arena_autobind_clone(void)
struct_ops_arena_autobind__destroy(skel);
}
+/*
+ * The same binding through BPF_PROG_LOAD_STRATEGY_MANUAL: every program is
+ * loaded with bpf_program__load() after bpf_object__prepare() and unloaded
+ * again, the way veristat loads programs.
+ */
+static void arena_autobind_manual(void)
+{
+ struct struct_ops_arena_autobind *skel;
+ struct bpf_map_info info = {};
+ __u32 len = sizeof(info);
+ struct bpf_program *prog;
+ int err;
+
+ skel = struct_ops_arena_autobind__open();
+ if (!ASSERT_OK_PTR(skel, "struct_ops_arena_autobind__open"))
+ return;
+
+ bpf_object__for_each_program(prog, skel->obj) {
+ err = bpf_program__set_load_strategy(prog, BPF_PROG_LOAD_STRATEGY_MANUAL);
+ if (!ASSERT_OK(err, "set_load_strategy"))
+ goto out;
+ }
+ err = bpf_object__prepare(skel->obj);
+ if (!ASSERT_OK(err, "prepare"))
+ goto out;
+ err = bpf_map_get_info_by_fd(bpf_map__fd(skel->maps.arena), &info, &len);
+ if (!ASSERT_OK(err, "arena_info"))
+ goto out;
+ bpf_object__for_each_program(prog, skel->obj) {
+ err = bpf_program__load(prog);
+ if (!ASSERT_OK(err, bpf_program__name(prog)))
+ continue;
+ assert_prog_map(bpf_program__fd(prog), info.id, bpf_program__name(prog));
+ bpf_program__unload(prog);
+ ASSERT_LT(bpf_program__fd(prog), 0, "unloaded");
+ }
+out:
+ struct_ops_arena_autobind__destroy(skel);
+}
+
/*
* An arena exclusive to a program is bound to that program all the same: the
* kernel checks an exclusive map against the digest of the final program, so
@@ -315,6 +355,8 @@ void serial_test_struct_ops_arena(void)
arena_autobind();
if (test__start_subtest("arena_autobind_clone"))
arena_autobind_clone();
+ if (test__start_subtest("arena_autobind_manual"))
+ arena_autobind_manual();
if (test__start_subtest("arena_autobind_excl"))
arena_autobind_excl();
#else
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 0000000000000..3d9b81691d7ad
--- /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";
diff --git a/tools/testing/selftests/bpf/progs/test_manual_load.c b/tools/testing/selftests/bpf/progs/test_manual_load.c
new file mode 100644
index 0000000000000..d91d5267a0edb
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_manual_load.c
@@ -0,0 +1,70 @@
+// 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;
+__u32 test_pid = 0;
+
+SEC("raw_tp/sys_enter")
+int prog1(const void *ctx)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != test_pid)
+ return 0;
+
+ prog1_called = true;
+ return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog2(const void *ctx)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != test_pid)
+ return 0;
+
+ prog2_called = true;
+ return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog3(const void *ctx)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != test_pid)
+ return 0;
+
+ prog3_called = true;
+ return 0;
+}
+
+SEC("!raw_tp/sys_enter")
+int prog4(const void *ctx)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != test_pid)
+ return 0;
+
+ 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)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != test_pid)
+ return 0;
+
+ prog5_sz = len;
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/veristat.c b/tools/testing/selftests/bpf/veristat.c
index 9cfc9b4b41c12..0cd19b59037f6 100644
--- a/tools/testing/selftests/bpf/veristat.c
+++ b/tools/testing/selftests/bpf/veristat.c
@@ -1678,7 +1678,6 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf
const char *base_filename = basename(strdupa(filename));
const char *prog_name = bpf_program__name(prog);
long mem_peak_a, mem_peak_b, mem_peak = -1;
- LIBBPF_OPTS(bpf_prog_load_opts, opts);
char *buf;
int buf_sz, log_level;
struct verif_stats *stats;
@@ -1726,18 +1725,15 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf
if (env.force_reg_invariants)
bpf_program__add_flags(prog, BPF_F_TEST_REG_INVARIANTS);
- opts.log_buf = buf;
- opts.log_size = buf_sz;
- opts.log_level = log_level;
+ bpf_program__set_log_buf(prog, buf, buf_sz);
+ bpf_program__set_log_level(prog, log_level);
cgroup_err = reset_stat_cgroup();
mem_peak_a = cgroup_memory_peak();
- fd = bpf_program__clone(prog, &opts);
- if (fd < 0) {
- err = fd;
- if (env.verbose)
- fprintf(stderr, "Failed to load program %s %d\n", prog_name, err);
- }
+ err = bpf_program__load(prog);
+ fd = err == 0 ? bpf_program__fd(prog) : -1;
+ if (err && env.verbose)
+ fprintf(stderr, "Failed to load program %s %d\n", prog_name, err);
mem_peak_b = cgroup_memory_peak();
if (!cgroup_err && mem_peak_a >= 0 && mem_peak_b >= 0)
mem_peak = mem_peak_b - mem_peak_a;
@@ -1774,7 +1770,7 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf
if (verif_log_buf != buf)
free(buf);
if (fd > 0)
- close(fd);
+ bpf_program__unload(prog);
return 0;
}
@@ -2297,7 +2293,7 @@ static int process_obj(const char *filename)
env.files_processed++;
bpf_object__for_each_program(prog, obj) {
- bpf_program__set_autoload(prog, true);
+ bpf_program__set_load_strategy(prog, BPF_PROG_LOAD_STRATEGY_MANUAL);
prog_cnt++;
}
|
| 3/2 | 2026/10/24 13:04 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 22426 output: 87 thoughts: 1008 Thoughts: **Analysis of the Patch: WorthFuzzing=false** Okay, let's break this down. First, I see the diff touches a bunch of files, and they're all under the `tools/` directory. Specifically, `tools/lib/bpf/` and `tools/testing/selftests/bpf/`. My immediate thought is, "Hmm, are these strictly userspace components?" I need to consult the criteria. Right, Section 2 is what I need. It outlines the negative criteria for WorthFuzzing. It says to return `WorthFuzzing=false` if all the modified code falls into certain categories. The key one here is: "User-space directories, self-tests, samples, or scripts (e.g., tools/, samples/, scripts/, usr/) that do not affect the compiled kernel image (vmlinux) or kernel modules." Bingo! That's precisely what we're looking at. `tools/lib/bpf/` is libbpf, a userspace C library, no question. `tools/testing/selftests/` contains userspace self-tests. Neither of these impact the kernel image or modules. I *always* need to verify. Double-checking the file list, I confirm all files are within the `tools/` hierarchy. So, according to Section 2, because this patch *only* modifies userspace tools and selftests, namely, libbpf and BPF selftests, which are not part of the kernel runtime, vmlinux, or kernel modules, and the changes are therefore strictly outside of syzkaller's kernel fuzzing scope, I conclude `WorthFuzzing=false`. The reasoning is solid: userspace code. No need to go further. There's no relevant kernel surface here. Let's call `set-results`. |
| 4/2 | 2026/10/24 13:04 | tool | set-results |
0mArgs:{
"EnableConfigs": [],
"FocusSymbols": [],
"Reasoning": "The changes are entirely confined to user-space code within tools/ (libbpf and BPF selftests). They do not modify the compiled kernel image (vmlinux) or any kernel modules, making them structurally outside the scope of syzkaller kernel fuzzing.",
"WorthFuzzing": false
}Results: {
"EnableConfigs": null,
"FocusSymbols": [],
"Reasoning": "The changes are entirely confined to user-space code within tools/ (libbpf and BPF selftests). They do not modify the compiled kernel image (vmlinux) or any kernel modules, making them structurally outside the scope of syzkaller kernel fuzzing.",
"WorthFuzzing": false
} |
| 5/1 | 2026/10/24 13:04 | action | If |
0mArgs:{
"WorthFuzzing": false
}Results: null |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|