[RFC PATCH bpf-next v2 1/3] libbpf: Auto-upgrade uprobes to multi-uprobes when supported

From: Varun R Mallya

Date: Mon Mar 30 2026 - 07:06:02 EST


This patch modifies libbpf to automatically "upgrade" standard
SEC("uprobe") and SEC("uretprobe") programs to use the multi-uprobe
infrastructure (BPF_TRACE_UPROBE_MULTI) at load time if the kernel
supports it, making them compatible with BPF tokens.

To maintain backward compatibility and handle rare cases where singular
uprobes are required, new SEC("uprobe.single") and SEC("uretprobe.single")
section types are introduced. These force libbpf to use the legacy
perf_event_open() attachment path.

tools/testing/selftests/bpf/progs/test_fill_link_info.c has been
modified to use SEC("uprobe.single") as it asserts the program type to be
`BPF_LINK_TYPE_PERF_EVENT` and checks properties related to uprobes that
use perf.

Signed-off-by: Varun R Mallya <varunrmallya@xxxxxxxxx>
---
tools/lib/bpf/libbpf.c | 53 +++++++++++++++++--
.../selftests/bpf/progs/test_fill_link_info.c | 2 +-
2 files changed, 51 insertions(+), 4 deletions(-)

diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 1eaa7527d4da..bd7b6f486430 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -8248,6 +8248,23 @@ static int bpf_object_prepare_progs(struct bpf_object *obj)

for (i = 0; i < obj->nr_programs; i++) {
prog = &obj->programs[i];
+
+ if (kernel_supports(obj, FEAT_UPROBE_MULTI_LINK)) {
+ const char *sec_name = prog->sec_name;
+ /* Here, we filter out for u[ret]probe or "u[ret]probe/"
+ * but we leave out anything with an '@'
+ * in it as uprobe_multi does not support versioned
+ * symbols yet, so we don't upgrade.
+ */
+ if (((strncmp(sec_name, "uprobe", 6) == 0 &&
+ (sec_name[6] == '/' || sec_name[6] == '\0')) ||
+ (strncmp(sec_name, "uretprobe", 9) == 0 &&
+ (sec_name[9] == '/' || sec_name[9] == '\0'))) &&
+ !strchr(sec_name, '@')) {
+ prog->expected_attach_type = BPF_TRACE_UPROBE_MULTI;
+ }
+ }
+
err = bpf_object__sanitize_prog(obj, prog);
if (err)
return err;
@@ -9909,9 +9926,11 @@ static const struct bpf_sec_def section_defs[] = {
SEC_DEF("kprobe+", KPROBE, 0, SEC_NONE, attach_kprobe),
SEC_DEF("uprobe+", KPROBE, 0, SEC_NONE, attach_uprobe),
SEC_DEF("uprobe.s+", KPROBE, 0, SEC_SLEEPABLE, attach_uprobe),
+ SEC_DEF("uprobe.single+", KPROBE, 0, SEC_NONE, attach_uprobe),
SEC_DEF("kretprobe+", KPROBE, 0, SEC_NONE, attach_kprobe),
SEC_DEF("uretprobe+", KPROBE, 0, SEC_NONE, attach_uprobe),
SEC_DEF("uretprobe.s+", KPROBE, 0, SEC_SLEEPABLE, attach_uprobe),
+ SEC_DEF("uretprobe.single+", KPROBE, 0, SEC_NONE, attach_uprobe),
SEC_DEF("kprobe.multi+", KPROBE, BPF_TRACE_KPROBE_MULTI, SEC_NONE, attach_kprobe_multi),
SEC_DEF("kretprobe.multi+", KPROBE, BPF_TRACE_KPROBE_MULTI, SEC_NONE, attach_kprobe_multi),
SEC_DEF("kprobe.session+", KPROBE, BPF_TRACE_KPROBE_SESSION, SEC_NONE, attach_kprobe_session),
@@ -12737,6 +12756,32 @@ bpf_program__attach_uprobe_opts(const struct bpf_program *prog, pid_t pid,
func_offset += sym_off;
}

+ /* This provides backwards compatibility to programs using uprobe, but
+ * have been auto-upgraded to multi uprobe.
+ */
+ if (prog->expected_attach_type == BPF_TRACE_UPROBE_MULTI) {
+ LIBBPF_OPTS(bpf_uprobe_multi_opts, multi_opts);
+ unsigned long offsets[1] = {func_offset};
+ __u64 bpf_cookie;
+
+ multi_opts.retprobe = OPTS_GET(opts, retprobe, false);
+ if (offsets[0] || func_name) {
+ multi_opts.offsets = offsets;
+ multi_opts.cnt = 1;
+ }
+ if (ref_ctr_off) {
+ multi_opts.ref_ctr_offsets = &ref_ctr_off;
+ multi_opts.cnt = 1;
+ }
+ bpf_cookie = OPTS_GET(opts, bpf_cookie, 0);
+ if (bpf_cookie) {
+ multi_opts.cookies = &bpf_cookie;
+ multi_opts.cnt = 1;
+ }
+
+ return bpf_program__attach_uprobe_multi(prog, pid, binary_path,
+ NULL, &multi_opts);
+ }
legacy = determine_uprobe_perf_type() < 0;
switch (attach_mode) {
case PROBE_ATTACH_MODE_LEGACY:
@@ -12830,6 +12875,7 @@ static int attach_uprobe(const struct bpf_program *prog, long cookie, struct bpf
char *probe_type = NULL, *binary_path = NULL, *func_name = NULL, *func_off;
int n, c, ret = -EINVAL;
long offset = 0;
+ bool is_retprobe;

*link = NULL;

@@ -12856,13 +12902,14 @@ static int attach_uprobe(const struct bpf_program *prog, long cookie, struct bpf
else
offset = 0;
}
- opts.retprobe = strcmp(probe_type, "uretprobe") == 0 ||
- strcmp(probe_type, "uretprobe.s") == 0;
- if (opts.retprobe && offset != 0) {
+ is_retprobe = strcmp(probe_type, "uretprobe") == 0 ||
+ strcmp(probe_type, "uretprobe.s") == 0;
+ if (is_retprobe && offset != 0) {
pr_warn("prog '%s': uretprobes do not support offset specification\n",
prog->name);
break;
}
+ opts.retprobe = is_retprobe;
opts.func_name = func_name;
*link = bpf_program__attach_uprobe_opts(prog, -1, binary_path, offset, &opts);
ret = libbpf_get_error(*link);
diff --git a/tools/testing/selftests/bpf/progs/test_fill_link_info.c b/tools/testing/selftests/bpf/progs/test_fill_link_info.c
index fac33a14f200..8e47a818462f 100644
--- a/tools/testing/selftests/bpf/progs/test_fill_link_info.c
+++ b/tools/testing/selftests/bpf/progs/test_fill_link_info.c
@@ -28,7 +28,7 @@ int BPF_PROG(kprobe_run)
return 0;
}

-SEC("uprobe")
+SEC("uprobe.single")
int BPF_PROG(uprobe_run)
{
return 0;
--
2.52.0