diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 9af5c0b08b8b..731a4a09f865 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -689,6 +689,7 @@ struct bpf_object {
 	bool loaded;
 	bool has_subcalls;
 	bool has_rodata;
+	bool has_dynload_progs;
 
 	struct bpf_gen *gen_loader;
 
@@ -7551,13 +7552,15 @@ static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog
 	 * custom log_buf is specified; if the program load fails, then we'll
 	 * bump log_level to 1 and use either custom log_buf or we'll allocate
 	 * our own and retry the load to get details on what failed
+	 * A shared buffer cannot be used for dynamically loaded programs as they
+	 * can be loaded concurrently.
 	 */
 	if (log_level) {
 		if (prog->log_buf) {
 			log_buf = prog->log_buf;
 			log_buf_size = prog->log_size;
 			own_log_buf = false;
-		} else if (obj->log_buf) {
+		} else if (obj->log_buf && prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC) {
 			log_buf = obj->log_buf;
 			log_buf_size = obj->log_size;
 			own_log_buf = false;
@@ -7911,6 +7914,7 @@ bpf_object__load_progs(struct bpf_object *obj, int log_level)
 			pr_debug("prog '%s': skipped auto-loading\n", prog->name);
 			continue;
 		}
+
 		prog->log_level |= log_level;
 
 		if (obj->gen_loader)
@@ -8588,8 +8592,11 @@ 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);
 	}
 
-	/* clean up fd_array */
-	zfree(&obj->fd_array);
+	/* The fd array is needed for dynamically loaded programs,
+	 * so defer freeing it in that case to the end of the object lifetime.
+	 */
+	if (!obj->has_dynload_progs || !obj->fd_array_cnt)
+		zfree(&obj->fd_array);
 
 	/* clean up module BTFs */
 	for (i = 0; i < obj->btf_module_cnt; i++) {
@@ -8597,11 +8604,17 @@ static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const ch
 		btf__free(obj->btf_modules[i].btf);
 		free(obj->btf_modules[i].name);
 	}
-	free(obj->btf_modules);
+	obj->btf_module_cnt = 0;
+	zfree(&obj->btf_modules);
 
-	/* clean up vmlinux BTF */
-	btf__free(obj->btf_vmlinux);
-	obj->btf_vmlinux = NULL;
+	/* The btf_vmlinux data is needed for dynamically loaded programs,
+	 * so defer freeing it in that case to the end of the object lifetime.
+	 */
+	if (!obj->has_dynload_progs) {
+		/* clean up vmlinux BTF */
+		btf__free(obj->btf_vmlinux);
+		obj->btf_vmlinux = NULL;
+	}
 
 	obj->loaded = true; /* doesn't matter if successfully or not */
 
@@ -9103,6 +9116,8 @@ void bpf_object__close(struct bpf_object *obj)
 
 	zfree(&obj->arena_data);
 
+	zfree(&obj->fd_array);
+
 	free(obj);
 }
 
@@ -9230,8 +9245,16 @@ bool bpf_program__autoload(const struct bpf_program *prog)
 
 int bpf_program__set_autoload(struct bpf_program *prog, bool autoload)
 {
-	return bpf_program__set_load_type(prog,
-		autoload ? BPF_PROG_LOAD_TYPE_AUTO : BPF_PROG_LOAD_TYPE_DISABLED);
+	enum bpf_prog_load_type type = prog->load_type;
+
+	if (autoload)
+		type = BPF_PROG_LOAD_TYPE_AUTO;
+	else if (prog->load_type == BPF_PROG_LOAD_TYPE_AUTO)
+		type = BPF_PROG_LOAD_TYPE_DISABLED;
+	else
+		return 0; /* Otherwise, keep the current load type. */
+
+	return bpf_program__set_load_type(prog, type);
 }
 
 bool bpf_program__autoattach(const struct bpf_program *prog)
@@ -14086,12 +14109,67 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
 	free(s);
 }
 
+static int bpf_program__set_dynamicload(struct bpf_program *prog)
+{
+	struct bpf_object *obj;
+	const char *attach_name;
+
+	obj = prog->obj;
+	if (!obj)
+		return libbpf_err(-EINVAL);
+
+	/* Dynamically loaded programs are not supported for gen_loader.
+	 * This limitation exists because bpf_object_load_prog is not invoked
+	 * for dynamically loaded programs, making them invisible to gen_loader.
+	 * To ensure compatibility, bpf_program__set_dynamicload should not be
+	 * called when gen_loader is used to generate a BPF object loader.
+	 * The gen_loader implementation handles autoloaded programs and follows
+	 * its own model for loading BPF programs. To pass a BPF program to
+	 * gen_loader, set the program's load type to BPF_PROG_LOAD_TYPE_AUTO.
+	 */
+	if (obj->gen_loader)
+		return libbpf_err(-ENOTSUP);
+
+	if (prog_is_subprog(obj, prog))
+		return libbpf_err(-EINVAL);
+
+	attach_name = strchr(prog->sec_name, '/');
+	if (!attach_name || strchr(attach_name, ':')) {
+		/* Dynamic loading is not supported if module's BTF
+		 * data is required for a bpf program.
+		 * The module's BTF data is required in the folowing cases:
+		 * - If a BPF program is annotated with just SEC("fentry")
+		 * (or similar) without declaratively specifying
+		 * target, then it is expected that target will be
+		 * specified with bpf_program__set_attach_target() at
+		 * runtime before BPF object load step. The module's
+		 * BTF data will be required by libbpf_prepare_prog_load and
+		 * libbpf_find_attach_btf_id.
+		 * - The attach name is prepended with a module name.
+		 */
+		return libbpf_err(-EINVAL);
+	}
+
+	obj->has_dynload_progs = true;
+	prog->load_type = BPF_PROG_LOAD_TYPE_DYNAMIC;
+	prog->autoattach = false;
+
+	return 0;
+}
+
 int bpf_program__set_load_type(struct bpf_program *prog, enum bpf_prog_load_type type)
 {
 	if (prog->obj->loaded)
 		return libbpf_err(-EINVAL);
 
-	prog->load_type = type;
+	switch (type) {
+	case BPF_PROG_LOAD_TYPE_DYNAMIC:
+		return bpf_program__set_dynamicload(prog);
+	default:
+		prog->load_type = type;
+		break;
+	}
+
 	return 0;
 }
 
@@ -14099,3 +14177,49 @@ enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog)
 {
 	return prog->load_type;
 }
+
+/*
+ * This function must be called after bpf_object__load_progs.
+ * Dynamically-loaded program data is initialized on object load.
+ * Post-load initialization is not supported.
+ */
+int
+bpf_program__load_dynamically(struct bpf_program *prog, int extra_log_level)
+{
+	int err;
+	struct bpf_object *obj;
+
+	obj = prog->obj;
+	if (!obj || !obj->loaded)
+		return libbpf_err(-EINVAL);
+
+	if (prog_is_subprog(obj, prog) || prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC)
+		return libbpf_err(-EINVAL);
+
+	prog->log_level |= extra_log_level;
+
+	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 dynamically load: %d\n", prog->name, err);
+		prog->log_level &= ~extra_log_level;
+		return err;
+	}
+
+	prog->log_level &= ~extra_log_level;
+	return 0;
+}
+
+int bpf_program__unload_dynamically(struct bpf_program *prog)
+{
+	int err;
+
+	if (!prog || prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC)
+		return libbpf_err(-EINVAL);
+
+	/* Close the file descriptor but retain the program's data to
+	 * support reloading the program if it is required again.
+	 */
+	err = zclose(prog->fd);
+	return err ? libbpf_err(-errno) : 0;
+}
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index 21e3d1f51cb3..531f30491f0b 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -1923,16 +1923,19 @@ LIBBPF_API int libbpf_unregister_prog_handler(int handler_id);
  *
  * - BPF_PROG_LOAD_TYPE_DISABLED: the program is not loaded.
  * - BPF_PROG_LOAD_TYPE_AUTO: the program is autoloaded when the bpf_object is loaded.
+ * - BPF_PROG_LOAD_TYPE_DYNAMIC: the program is loaded and attached dynamically.
  */
 enum bpf_prog_load_type {
 	BPF_PROG_LOAD_TYPE_DISABLED = 0,
 	BPF_PROG_LOAD_TYPE_AUTO,
+	BPF_PROG_LOAD_TYPE_DYNAMIC,
 };
 
 LIBBPF_API int bpf_program__set_load_type(struct bpf_program *prog,
 					    enum bpf_prog_load_type loadtype);
 LIBBPF_API enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog);
-
+LIBBPF_API int bpf_program__load_dynamically(struct bpf_program *prog, int extra_log_level);
+LIBBPF_API int bpf_program__unload_dynamically(struct bpf_program *prog);
 
 #ifdef __cplusplus
 } /* extern "C" */
diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
index 08323e7930fd..4d84e4794685 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -438,4 +438,6 @@ LIBBPF_1.6.0 {
 		bpf_linker__new_fd;
 		bpf_program__load_type;
 		bpf_program__set_load_type;
+		bpf_program__load_dynamically;
+		bpf_program__unload_dynamically;
 } LIBBPF_1.5.0;
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..9cde7dd45608
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include <time.h>
+#include "test_dynamicload.skel.h"
+
+void test_dynamicload(void)
+{
+	int duration = 0, err;
+	struct bpf_link *link;
+	struct test_dynamicload *skel;
+
+	skel = test_dynamicload__open();
+	if (CHECK(!skel, "skel_open", "failed to open skeleton\n"))
+		goto cleanup;
+
+	/* don't load prog1 */
+	bpf_program__set_load_type(skel->progs.prog1, BPF_PROG_LOAD_TYPE_DISABLED);
+
+	/* prog2 is autoload */
+	bpf_program__set_load_type(skel->progs.prog2, BPF_PROG_LOAD_TYPE_AUTO);
+
+	/* prog3 is dynamically loaded */
+	bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DYNAMIC);
+
+	err = test_dynamicload__load(skel);
+	if (CHECK(err, "skel_load", "failed to load skeleton: %d\n", err))
+		goto cleanup;
+
+	err = test_dynamicload__attach(skel);
+	if (CHECK(err, "skel_attach", "skeleton attach failed: %d\n", err))
+		goto cleanup;
+
+	/* trigger the BPF programs */
+	usleep(1);
+
+	CHECK(skel->bss->prog1_called, "prog1", "called?!\n");
+	CHECK(!skel->bss->prog2_called, "prog2", "not called\n");
+	CHECK(skel->bss->prog3_called, "prog3", "called?!\n");
+
+	/* prog1 is disabled for load */
+	err = bpf_program__load_dynamically(skel->progs.prog1, 0);
+	if (CHECK(!err, "load_dynamically", "disabled program loaded?!\n"))
+		goto cleanup;
+
+	/* prog1 is disabled for load */
+	err = bpf_program__unload_dynamically(skel->progs.prog1);
+	if (CHECK(!err, "load_dynamically", "disabled program unloaded?!\n"))
+		goto cleanup;
+
+	/* prog2 is autoload */
+	err = bpf_program__load_dynamically(skel->progs.prog1, 0);
+	if (CHECK(!err, "load_dynamically", "autoload loaded dynamically?!\n"))
+		goto cleanup;
+
+	/* prog2 is autoload */
+	err = bpf_program__unload_dynamically(skel->progs.prog1);
+	if (CHECK(!err, "load_dynamically", "autoload unloaded dynamically?!\n"))
+		goto cleanup;
+
+	/* reset the call flags */
+	skel->bss->prog2_called = false;
+	skel->bss->prog3_called = false;
+
+	usleep(1);
+
+	CHECK(skel->bss->prog1_called, "prog1", "called?!\n");
+	CHECK(!skel->bss->prog2_called, "prog2", "not called\n");
+	CHECK(skel->bss->prog3_called, "prog3", "called?!\n");
+
+	/* load prog3 */
+	err = bpf_program__load_dynamically(skel->progs.prog3, 0);
+	if (CHECK(err, "load_dynamically", "dynamic loading failed: %d\n", err))
+		goto cleanup;
+
+	/* attach prog3 */
+	link = bpf_program__attach(skel->progs.prog3);
+	if (CHECK(libbpf_get_error(link), "attach", "attaching failed: %ld\n",
+		  libbpf_get_error(link)))
+		goto cleanup;
+
+	usleep(1);
+
+	CHECK(!skel->bss->prog3_called, "prog3", "not called\n");
+
+	/* detach prog3 as test_dynamicload__destroy doesn't detach dynamically loaded programs */
+	err = bpf_link__destroy(link);
+	if (CHECK(err, "link__destroy", "link destroy failed: %d\n", err))
+		goto cleanup;
+
+	/* reset the call flags after detach */
+	skel->bss->prog2_called = false;
+	skel->bss->prog3_called = false;
+
+	usleep(1);
+
+	CHECK(!skel->bss->prog2_called, "prog2", "not called\n");
+	CHECK(skel->bss->prog3_called, "prog3", "called?!\n");
+
+	/* unload prog3 */
+	err = bpf_program__unload_dynamically(skel->progs.prog3);
+	if (CHECK(err, "unload_dynamically", "unload dynamically failed: %d\n", err))
+		goto cleanup;
+
+	/* reload prog3 */
+	err = bpf_program__load_dynamically(skel->progs.prog3, 0);
+	if (CHECK(err, "load_dynamically", "dynamic reloading failed: %d\n", err))
+		goto cleanup;
+
+	/* reattach prog3 */
+	link = bpf_program__attach(skel->progs.prog3);
+	if (CHECK(libbpf_get_error(link), "attach", "reattaching failed: %d\n", err))
+		goto cleanup;
+
+	usleep(1);
+
+	CHECK(!skel->bss->prog3_called, "prog3", "not called\n");
+
+	/* detach prog3 as test_dynamicload__destroy doesn't detach dynamically loaded programs */
+	err = bpf_link__destroy(link);
+	if (CHECK(err, "link__destroy", "link destroy failed: %d\n", err))
+		goto cleanup;
+
+	/* verify regular unload for dynamically 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);
+
+	CHECK(!skel->bss->prog2_called, "prog2", "not called\n");
+	CHECK(skel->bss->prog3_called, "prog3", "called?!\n");
+
+	/* reloading prog3 must fail as it was unloaded as a regular program */
+	err = bpf_program__load_dynamically(skel->progs.prog3, 0);
+	if (CHECK(!err, "load_dynamically", "dynamic reloading succeeded?! %d\n", err))
+		goto cleanup;
+
+cleanup:
+	test_dynamicload__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/prog_tests/load_type.c b/tools/testing/selftests/bpf/prog_tests/load_type.c
index 7c8d55173b2b..8bd082b3bc9d 100644
--- a/tools/testing/selftests/bpf/prog_tests/load_type.c
+++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
@@ -7,6 +7,7 @@
 void test_load_type(void)
 {
 	int duration = 0, err;
+	struct bpf_link *link;
 	struct test_load_type *skel;
 
 	skel = test_load_type__open();
@@ -20,11 +21,47 @@ void test_load_type(void)
 	bpf_program__set_load_type(skel->progs.prog2, BPF_PROG_LOAD_TYPE_AUTO);
 	CHECK(!bpf_program__autoload(skel->progs.prog2), "prog2", "not autoload?!\n");
 
+	err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DYNAMIC);
+	if (CHECK(err, "set_load_type", "set_load_type(DYNAMIC) failed: %d\n", err))
+		goto cleanup;
+	CHECK(bpf_program__load_type(skel->progs.prog3) != BPF_PROG_LOAD_TYPE_DYNAMIC,
+		"prog3", "didn't set type?!\n");
+
+	/* bpf_program__set_autoload(program, false) doesn't have effect if the program
+	 * type is not BPF_PROG_LOAD_TYPE_AUTO
+	 */
+	err = bpf_program__set_autoload(skel->progs.prog3, false);
+	if (CHECK(err, "set_autoload", "set_autoload(false) failed: %d\n", err))
+		goto cleanup;
+
+	CHECK(bpf_program__load_type(skel->progs.prog3) != BPF_PROG_LOAD_TYPE_DYNAMIC,
+		"prog3", "changed type?!\n");
+
+	err = bpf_program__set_autoload(skel->progs.prog3, true);
+	if (CHECK(err, "set_autoload", "set_autoload(true) failed: %d\n", err))
+		goto cleanup;
+
+	CHECK(bpf_program__load_type(skel->progs.prog3) != BPF_PROG_LOAD_TYPE_AUTO,
+		"prog3", "didn't change type to auto?!\n");
+
+	/* change the type back to BPF_PROG_LOAD_TYPE_DYNAMIC */
+	err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DYNAMIC);
+	if (CHECK(err, "set_load_type", "changing from AUTO to DYNAMIC failed: %d\n", err))
+		goto cleanup;
+
+	CHECK(bpf_program__load_type(skel->progs.prog3) != BPF_PROG_LOAD_TYPE_DYNAMIC,
+		"prog3", "didn't change type from autoload to dynamic?!\n");
+
 	err = test_load_type__load(skel);
 	if (CHECK(err, "skel_load", "failed to load skeleton: %d\n", err))
 		goto cleanup;
 
 	CHECK(!bpf_program__autoattach(skel->progs.prog2), "prog2", "not autoattach?!\n");
+	CHECK(bpf_program__autoattach(skel->progs.prog3), "prog3", "autoattach?!\n");
+
+	/* loaded program type cannot be changed */
+	err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DISABLED);
+	CHECK(!err, "prog3", "changed type after load?!\n");
 
 	err = test_load_type__attach(skel);
 	if (CHECK(err, "skel_attach", "skeleton attach failed: %d\n", err))
@@ -34,6 +71,30 @@ void test_load_type(void)
 
 	CHECK(skel->bss->prog1_called, "prog1", "called?!\n");
 	CHECK(!skel->bss->prog2_called, "prog2", "not called\n");
+	CHECK(skel->bss->prog3_called, "prog3", "called?!\n");
+
+	err = bpf_program__load_dynamically(skel->progs.prog3, 0);
+	if (CHECK(err, "load_dynamically", "load dynamically failed: %d\n", err))
+		goto cleanup;
+
+	err = bpf_program__load_dynamically(skel->progs.prog3, 0);
+	if (CHECK(err, "load_dynamically", "load dynamically failed: %d\n", err))
+		goto cleanup;
+
+	/* attach prog3 */
+	link = bpf_program__attach(skel->progs.prog3);
+	if (CHECK(libbpf_get_error(link), "attach", "attaching failed: %ld\n",
+		  libbpf_get_error(link)))
+		goto cleanup;
+
+	usleep(1);
+
+	CHECK(!skel->bss->prog3_called, "prog3", "not called?!\n");
+
+	/* detach prog3 as test_load_type__destroy doesn't detach dynamically loaded programs */
+	err = bpf_link__destroy(link);
+	if (CHECK(err, "link__destroy", "link destroy failed: %d\n", err))
+		goto cleanup;
 
 cleanup:
 	test_load_type__destroy(skel);
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..3d9b81691d7a
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_dynamicload.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_load_type.c b/tools/testing/selftests/bpf/progs/test_load_type.c
index a0d39757c5b9..3d9b81691d7a 100644
--- a/tools/testing/selftests/bpf/progs/test_load_type.c
+++ b/tools/testing/selftests/bpf/progs/test_load_type.c
@@ -5,6 +5,7 @@
 
 bool prog1_called = false;
 bool prog2_called = false;
+bool prog3_called = false;
 
 SEC("raw_tp/sys_enter")
 int prog1(const void *ctx)
@@ -20,4 +21,11 @@ int prog2(const void *ctx)
 	return 0;
 }
 
+SEC("raw_tp/sys_enter")
+int prog3(const void *ctx)
+{
+	prog3_called = true;
+	return 0;
+}
+
 char _license[] SEC("license") = "GPL";
