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@gmail.com>
---
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
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@gmail.com>
> ---
> 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
On Mon, Mar 30, 2026 at 04:52:27PM +0200, Jiri Olsa wrote:
> On Mon, Mar 30, 2026 at 04:30:17PM +0530, Varun R Mallya wrote:
> > + 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
Thanks! I intend to fix that after I am done with this patch.
> > + if (((strncmp(sec_name, "uprobe", 6) == 0 &&
>
> str_has_pfx ?
>
Implementing on v3. This looks much cleaner.
> > @@ -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 ;-)
Yeah, that does seem like it's redundant. I think integrating this into
uprobe and kprobe is the best we can do. I have tried my best to ensure
that it does not really break any current functionality though.
> > + 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;
>
An artifact from a previous version. Fixing this.
> > + 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
Fixed this as well. I think it solves a part of the AI review as well.
> > + }
> > +
> > + 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
Again, sorry about that. It was an artifact from a previous version.
Fixing it in v3.
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index 9ea41f40dc82..056bc50a0bbb 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -12783,6 +12802,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;
> + }
When func_offset is 0 and func_name is NULL, this condition is false
and multi_opts.offsets and multi_opts.cnt are never set. The subsequent
call to bpf_program__attach_uprobe_multi() then fails with -EINVAL
because both func_pattern and cnt are 0:
bpf_program__attach_uprobe_multi() {
...
if (!func_pattern && cnt == 0)
return libbpf_err_ptr(-EINVAL);
...
}
This can happen when callers use bpf_program__attach_uprobe() on an
auto-upgraded program with offset 0:
bpf_program__attach_uprobe() {
DECLARE_LIBBPF_OPTS(bpf_uprobe_opts, opts, .retprobe = retprobe);
return bpf_program__attach_uprobe_opts(..., func_offset, &opts);
}
Here func_name is never set in opts. The old perf_event path handled
offset 0 without issue. Should multi_opts.offsets and multi_opts.cnt
always be set unconditionally, since offset 0 is a valid uprobe offset?
> + 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);
> + }
[ ... ]
> @@ -12902,13 +12948,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;
Shouldn't this also match "uretprobe.single"? When auto-attaching
with SEC("uretprobe.single/binary:func"), sscanf sets probe_type to
"uretprobe.single", which doesn't match either "uretprobe" or
"uretprobe.s". As a result is_retprobe is false and opts.retprobe
stays false, so the program gets attached as a regular uprobe instead
of a return probe.
Something like strncmp(probe_type, "uretprobe", 9) == 0 would cover
all uretprobe variants ("uretprobe", "uretprobe.s", "uretprobe.single").
> + 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);
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/23741893093
© 2016 - 2026 Red Hat, Inc.