Re: [RFC PATCH bpf-next v2 1/3] libbpf: Auto-upgrade uprobes to multi-uprobes when supported
From: Jiri Olsa
Date: Mon Mar 30 2026 - 11:00:48 EST
On Mon, Mar 30, 2026 at 04:30:17PM +0530, Varun R Mallya wrote:
> 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.
> + */
nice, I missed that uprobe.multi does not support versioned symbols,
I guess we should fix that
> + if (((strncmp(sec_name, "uprobe", 6) == 0 &&
str_has_pfx ?
> + (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),
should we add sleepable counterparts?
> 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),
just an idea for discussion.. I wonder if it'd be better to add new uprobe
section that will upgrade itself to uprobe.multi if it's present, instead
of changing the existing (expected) type
but I guess we want existing uprobe programs to benefit from that and
there's not really a reason anyone would want perf based uprobe when
uprobe_multi is supported
ok I talked myself out of it ;-)
> 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;
could we do the same as for ref_ctr_off case and drop the offsets array?
multi_opts.offsets = &func_offset;
> + 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;
I think it's better just to set multi_opts.cnt = 1 once outside those if conditions
> + }
> +
> + 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;
is there any functional change above? looks like just opts.retprobe
is replaced with is_retprobe ?
jirka