Add dynamicload test covering the manual load/attach/detach/reload cycle, declarative MANUAL via SEC("!...") and its imperative override, bpf_object__prepare() alone being sufficient for manual load, and a deferred load of a module BTF attach target. Also add a signed_loader test verifying bpf_object__gen_loader() rejects objects with a MANUAL program. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Andrey Grodzovsky --- .../selftests/bpf/prog_tests/dynamicload.c | 350 ++++++++++++++++++ .../selftests/bpf/prog_tests/signed_loader.c | 28 ++ .../selftests/bpf/progs/test_dynamicload.c | 54 +++ 3 files changed, 432 insertions(+) create mode 100644 tools/testing/selftests/bpf/prog_tests/dynamicload.c create mode 100644 tools/testing/selftests/bpf/progs/test_dynamicload.c diff --git a/tools/testing/selftests/bpf/prog_tests/dynamicload.c b/tools/testing/selftests/bpf/prog_tests/dynamicload.c new file mode 100644 index 000000000000..979bdc14f4ff --- /dev/null +++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include +#include "test_dynamicload.skel.h" + +#define READ_SZ 456 + +/* + * prog4 is marked SEC("!...") in the source instead of being set + * imperatively; verify that an explicit bpf_program__set_load_strategy() call + * before load overrides the declarative default. + */ +static void dynamicload_verify_override(void) +{ + struct test_dynamicload *skel; + int err; + + skel = test_dynamicload__open(); + if (!ASSERT_OK_PTR(skel, "skel_open")) + return; + + err = bpf_program__set_load_strategy(skel->progs.prog4, BPF_PROG_LOAD_STRATEGY_DISABLED); + if (!ASSERT_OK(err, "set_load_strategy_disabled")) + goto cleanup; + + if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog4), + BPF_PROG_LOAD_STRATEGY_DISABLED, "prog4_load_strategy_overridden")) + goto cleanup; + + /* 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.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED); + + err = test_dynamicload__load(skel); + if (!ASSERT_OK(err, "skel_load")) + goto cleanup; + + /* prog4 was never loaded, so it cannot be loaded manually either */ + err = bpf_program__load_manually(skel->progs.prog4); + ASSERT_ERR(err, "load_manually_after_override"); + +cleanup: + test_dynamicload__destroy(skel); +} + +/* + * prog4 is MANUAL via its SEC("!...") marker; verify that + * bpf_object__prepare() alone -- without ever calling bpf_object__load() -- + * is sufficient for bpf_program__load_manually() to succeed, since BTF + * loading, map creation, and relocation of MANUAL programs are all + * completed by prepare() already. + */ +static void dynamicload_verify_prepare_only(void) +{ + struct test_dynamicload *skel; + struct bpf_link *link; + int err; + + skel = test_dynamicload__open(); + if (!ASSERT_OK_PTR(skel, "skel_open")) + return; + + err = bpf_object__prepare(skel->obj); + if (!ASSERT_OK(err, "bpf_object__prepare")) + goto cleanup; + + err = bpf_program__load_manually(skel->progs.prog4); + if (!ASSERT_OK(err, "load_manually_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); + if (!ASSERT_OK(err, "link_destroy_after_prepare")) + goto cleanup; + + err = bpf_program__unload_manually(skel->progs.prog4); + ASSERT_OK(err, "unload_manually_after_prepare"); + +cleanup: + test_dynamicload__destroy(skel); +} + +/* + * prog5 is disabled at parse time; resolve its attach target against + * module BTF via bpf_program__set_attach_target() before switching it + * to MANUAL and deferring its load past the bulk bpf_object__load(). + * Regression test for the module BTF fd/array lifetime bug: without + * deferring the module BTF fd/array close for MANUAL programs, the fd + * cached in prog->attach_btf_obj_fd is closed by the bulk load's + * cleanup before this deferred load runs, causing a deterministic + * -EINVAL. + */ +static void dynamicload_verify_module_btf(void) +{ + struct test_dynamicload *skel; + struct bpf_link *link; + int err; + + if (!env.has_testmod) { + test__skip(); + return; + } + + skel = test_dynamicload__open(); + if (!ASSERT_OK_PTR(skel, "skel_open")) + return; + + err = bpf_program__set_attach_target(skel->progs.prog5, 0, + "bpf_testmod:bpf_testmod_test_read"); + if (!ASSERT_OK(err, "set_attach_target")) + goto cleanup; + + err = bpf_program__set_load_strategy(skel->progs.prog5, BPF_PROG_LOAD_STRATEGY_MANUAL); + if (!ASSERT_OK(err, "set_load_strategy_manual")) + goto cleanup; + + /* keep the other autoload programs out of the way of this load */ + bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED); + bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_DISABLED); + bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED); + + /* + * bulk load: prog5 itself is skipped (MANUAL), but this is where + * module BTF gets torn down if not correctly deferred + */ + err = test_dynamicload__load(skel); + if (!ASSERT_OK(err, "skel_load")) + goto cleanup; + + /* + * deferred load must still succeed: the module BTF fd cached above + * by set_attach_target() must still be a valid, open fd here + */ + err = bpf_program__load_manually(skel->progs.prog5); + if (!ASSERT_OK(err, "load_manually_module_btf")) + goto cleanup; + + link = bpf_program__attach(skel->progs.prog5); + if (!ASSERT_OK_PTR(link, "attach")) + goto cleanup; + + ASSERT_OK(trigger_module_test_read(READ_SZ), "trigger_read"); + ASSERT_EQ(skel->bss->prog5_sz, READ_SZ, "prog5_sz"); + + bpf_link__destroy(link); + +cleanup: + test_dynamicload__destroy(skel); +} + +void test_dynamicload(void) +{ + struct bpf_link *link; + struct test_dynamicload *skel; + int err; + + skel = test_dynamicload__open(); + if (!ASSERT_OK_PTR(skel, "skel_open")) + return; + + /* + * the SEC("!...") prefix alone, with no imperative call, must set + * prog4's load strategy before it is ever touched below + */ + if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog4), + BPF_PROG_LOAD_STRATEGY_MANUAL, "prog4_prefix_load_strategy")) + goto cleanup; + if (!ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog4), "prog4_autoattach")) + goto cleanup; + + /* don't load prog1 */ + bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED); + + /* prog2 is autoload */ + bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_AUTO); + + /* prog3 is manually loaded */ + bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL); + + err = test_dynamicload__load(skel); + if (!ASSERT_OK(err, "skel_load")) + goto cleanup; + + err = test_dynamicload__attach(skel); + if (!ASSERT_OK(err, "skel_attach")) + goto cleanup; + + /* trigger the BPF programs */ + usleep(1); + + ASSERT_FALSE(skel->bss->prog1_called, "prog1_called"); + ASSERT_TRUE(skel->bss->prog2_called, "prog2_called"); + ASSERT_FALSE(skel->bss->prog3_called, "prog3_called"); + ASSERT_FALSE(skel->bss->prog4_called, "prog4_called"); + + /* prog1 is disabled for load */ + err = bpf_program__load_manually(skel->progs.prog1); + if (!ASSERT_ERR(err, "load_manually_disabled")) + goto cleanup; + + /* prog1 is disabled for load */ + err = bpf_program__unload_manually(skel->progs.prog1); + if (!ASSERT_ERR(err, "unload_manually_disabled")) + goto cleanup; + + /* prog2 is autoload */ + err = bpf_program__load_manually(skel->progs.prog2); + if (!ASSERT_ERR(err, "load_manually_autoload")) + goto cleanup; + + /* prog2 is autoload */ + err = bpf_program__unload_manually(skel->progs.prog2); + if (!ASSERT_ERR(err, "unload_manually_autoload")) + goto cleanup; + + /* reset the call flags */ + skel->bss->prog2_called = false; + skel->bss->prog3_called = false; + + usleep(1); + + ASSERT_FALSE(skel->bss->prog1_called, "prog1_called"); + ASSERT_TRUE(skel->bss->prog2_called, "prog2_called"); + ASSERT_FALSE(skel->bss->prog3_called, "prog3_called"); + + /* load prog3 */ + err = bpf_program__load_manually(skel->progs.prog3); + if (!ASSERT_OK(err, "load_manually")) + goto cleanup; + + /* attach prog3 */ + link = bpf_program__attach(skel->progs.prog3); + if (!ASSERT_OK_PTR(link, "attach")) + goto cleanup; + + usleep(1); + + if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called")) + goto cleanup; + + /* detach prog3 as test_dynamicload__destroy doesn't detach manually loaded programs */ + err = bpf_link__destroy(link); + if (!ASSERT_OK(err, "link_destroy")) + goto cleanup; + + /* reset the call flags after detach */ + skel->bss->prog2_called = false; + skel->bss->prog3_called = false; + + usleep(1); + + ASSERT_TRUE(skel->bss->prog2_called, "prog2_called"); + ASSERT_FALSE(skel->bss->prog3_called, "prog3_called"); + + /* unload prog3 */ + err = bpf_program__unload_manually(skel->progs.prog3); + if (!ASSERT_OK(err, "unload_manually")) + goto cleanup; + + /* reload prog3 */ + err = bpf_program__load_manually(skel->progs.prog3); + if (!ASSERT_OK(err, "load_manually_reload")) + goto cleanup; + + /* reattach prog3 */ + link = bpf_program__attach(skel->progs.prog3); + if (!ASSERT_OK_PTR(link, "reattach")) + goto cleanup; + + usleep(1); + + if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_reattach")) + goto cleanup; + + /* detach prog3 as test_dynamicload__destroy doesn't detach manually loaded programs */ + err = bpf_link__destroy(link); + if (!ASSERT_OK(err, "link_destroy_reattach")) + goto cleanup; + + /* + * verify regular unload for manually loaded program, + * unload prog3 as a regular program + */ + bpf_program__unload(skel->progs.prog3); + + /* reset the call flags after unload */ + skel->bss->prog2_called = false; + skel->bss->prog3_called = false; + + usleep(1); + + ASSERT_TRUE(skel->bss->prog2_called, "prog2_called"); + ASSERT_FALSE(skel->bss->prog3_called, "prog3_called"); + + /* reloading prog3 must fail as it was unloaded as a regular program */ + err = bpf_program__load_manually(skel->progs.prog3); + ASSERT_ERR(err, "load_manually_after_regular_unload"); + + /* + * run prog4 (declaratively marked) through the same manual + * load/attach/trigger/detach/unload cycle as prog3 + */ + err = bpf_program__load_manually(skel->progs.prog4); + if (!ASSERT_OK(err, "prog4_load_manually")) + 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); + if (!ASSERT_OK(err, "prog4_link_destroy")) + goto cleanup; + + err = bpf_program__unload_manually(skel->progs.prog4); + ASSERT_OK(err, "prog4_unload_manually"); + + test_dynamicload__destroy(skel); + + /* separate scenario: imperative override of the declarative marker */ + dynamicload_verify_override(); + + /* separate scenario: prepare()-only is sufficient for manual load */ + dynamicload_verify_prepare_only(); + + /* separate scenario: deferred manual load with a module BTF attach target */ + dynamicload_verify_module_btf(); + return; + +cleanup: + test_dynamicload__destroy(skel); +} + diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c index a0f93756e717..0648816e3d50 100644 --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c @@ -2168,6 +2168,33 @@ static void signed_module_kfunc_rejected(void) run_setup("cleanup", dir); } +/* + * a program marked MANUAL is invisible to gen_loader's program count (it is + * skipped by bpf_object_load_progs()), so bpf_object__gen_loader() must + * reject the whole object up front instead of silently generating a + * skeleton whose prog_fd slots no longer line up with the object's programs + */ +static void manual_prog_rejected(void) +{ + LIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true); + struct test_signed_loader *skel; + int err; + + skel = test_signed_loader__open(); + if (!ASSERT_OK_PTR(skel, "skel_open")) + return; + + err = bpf_program__set_load_strategy(skel->progs.probe, BPF_PROG_LOAD_STRATEGY_MANUAL); + if (!ASSERT_OK(err, "set_load_strategy_manual")) + goto cleanup; + + err = bpf_object__gen_loader(skel->obj, &gopts); + ASSERT_ERR(err, "gen_loader_rejected"); + +cleanup: + test_signed_loader__destroy(skel); +} + enum subtest_boot { BOOT_ANY, BOOT_SEALED, @@ -2211,6 +2238,7 @@ static const struct { { "signed_map_by_fd_rejected", signed_map_by_fd_rejected, BOOT_SEALED }, { "signed_sparse_fd_array_rejected", signed_sparse_fd_array_rejected, BOOT_SEALED }, { "bpf_keyring_provisioned", bpf_keyring_provisioned, BOOT_UNSEALED }, + { "manual_prog_rejected", manual_prog_rejected, BOOT_ANY }, }; void test_signed_loader(void) diff --git a/tools/testing/selftests/bpf/progs/test_dynamicload.c b/tools/testing/selftests/bpf/progs/test_dynamicload.c new file mode 100644 index 000000000000..156c90afe73e --- /dev/null +++ b/tools/testing/selftests/bpf/progs/test_dynamicload.c @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include "vmlinux.h" +#include +#include + +bool prog1_called = false; +bool prog2_called = false; +bool prog3_called = false; +bool prog4_called = false; +__u32 prog5_sz = 0; + +SEC("raw_tp/sys_enter") +int prog1(const void *ctx) +{ + prog1_called = true; + return 0; +} + +SEC("raw_tp/sys_enter") +int prog2(const void *ctx) +{ + prog2_called = true; + return 0; +} + +SEC("raw_tp/sys_enter") +int prog3(const void *ctx) +{ + prog3_called = true; + return 0; +} + +SEC("!raw_tp/sys_enter") +int prog4(const void *ctx) +{ + prog4_called = true; + return 0; +} + +/* + * disabled at parse time; its attach target is resolved against module + * BTF via bpf_program__set_attach_target() before it is switched to + * MANUAL and loaded + */ +SEC("?fentry") +int BPF_PROG(prog5, struct file *file, struct kobject *kobj, + struct bin_attribute *bin_attr, char *buf, loff_t off, size_t len) +{ + prog5_sz = len; + return 0; +} + +char _license[] SEC("license") = "GPL"; -- 2.34.1