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

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

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

Signed-off-by: Slava Imameev <slava.imam...@crowdstrike.com>
---
 tools/lib/bpf/libbpf.c                        | 144 +++++++++++++++--
 tools/lib/bpf/libbpf.h                        |   5 +-
 tools/lib/bpf/libbpf.map                      |   2 +
 .../selftests/bpf/prog_tests/dynamicload.c    | 145 ++++++++++++++++++
 .../selftests/bpf/prog_tests/load_type.c      |  61 ++++++++
 .../selftests/bpf/progs/test_dynamicload.c    |  31 ++++
 .../selftests/bpf/progs/test_load_type.c      |   8 +
 7 files changed, 385 insertions(+), 11 deletions(-)
 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/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";
-- 
2.39.5 (Apple Git-154)


Reply via email to