This patch modifies libbpf to automatically upgrade standard
SEC("kprobe") and SEC("kretprobe") programs to use the multi-kprobe
infrastructure (BPF_TRACE_KPROBE_MULTI) at load time if the kernel
supports it, making them compatible with BPF tokens.
To maintain backward compatibility and handle cases where singular
kprobes are required, new SEC("kprobe.single") and SEC("kretprobe.single")
section types are introduced. These force libbpf to use the legacy
perf_event_open() attachment path.
The following explain the reasoning for changing selftests:
- test_fill_link_info.c kprobe→kprobe.single:
this test calls bpf_link_get_info_by_fd and asserts
BPF_LINK_TYPE_PERF_EVENT and it explicitly needs the perf_event
attachment path, which breaks after auto-upgrade to multi.
- test_attach_probe_manual.c kprobe→kprobe.single,
kretprobe→kretprobe.single:
this test exercises all four explicit attachment modes
(default, legacy, perf, link) and PROBE_ATTACH_MODE_LINK
creates a perf_event BPF link which the kernel rejects
for a prog loaded with expected_attach_type = BPF_TRACE_KPROBE_MULTI.
- missed_kprobe.c kprobe->kprobe.single:
The link is explicitly checked in the tests due to which it fails if
we do not specifically kprobe.single this.
- get_func_ip_test.c ?kprobe→?kprobe.single: the ? is stripped from
sec_name by libbpf at init time so the prog still gets auto-upgraded.
It is then manually attached with a non-zero body offset,
which kprobe_multi doesn't support.
Signed-off-by: Varun R Mallya <varunrmallya@gmail.com>
---
tools/lib/bpf/libbpf.c | 61 +++++++++++++++++--
.../selftests/bpf/progs/get_func_ip_test.c | 2 +-
.../selftests/bpf/progs/missed_kprobe.c | 4 +-
.../bpf/progs/test_attach_probe_manual.c | 4 +-
.../selftests/bpf/progs/test_fill_link_info.c | 2 +-
5 files changed, 61 insertions(+), 12 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index bd7b6f486430..9d0a36f8279a 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -8265,6 +8265,24 @@ static int bpf_object_prepare_progs(struct bpf_object *obj)
}
}
+ if (kernel_supports(obj, FEAT_KPROBE_MULTI_LINK)) {
+ const char *sec_name = prog->sec_name;
+ /* Here, we filter out for k[ret]probe or "k[ret]probe/"
+ * but we leave out anything with an '@'
+ * in it as kprobe_multi does not support versioned
+ * symbols, so we don't upgrade. Also for '+' as we do not
+ * support offsets.
+ */
+ if (((strncmp(sec_name, "kprobe", 6) == 0 &&
+ (sec_name[6] == '/' || sec_name[6] == '\0')) ||
+ (strncmp(sec_name, "kretprobe", 9) == 0 &&
+ (sec_name[9] == '/' || sec_name[9] == '\0'))) &&
+ !strchr(sec_name, '@') &&
+ !strchr(sec_name, '+') &&
+ !(prog->prog_flags & BPF_F_SLEEPABLE))
+ prog->expected_attach_type = BPF_TRACE_KPROBE_MULTI;
+ }
+
err = bpf_object__sanitize_prog(obj, prog);
if (err)
return err;
@@ -9924,10 +9942,12 @@ static const struct bpf_sec_def section_defs[] = {
SEC_DEF("sk_reuseport/migrate", SK_REUSEPORT, BPF_SK_REUSEPORT_SELECT_OR_MIGRATE, SEC_ATTACHABLE),
SEC_DEF("sk_reuseport", SK_REUSEPORT, BPF_SK_REUSEPORT_SELECT, SEC_ATTACHABLE),
SEC_DEF("kprobe+", KPROBE, 0, SEC_NONE, attach_kprobe),
+ SEC_DEF("kprobe.single+", 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("kretprobe.single+", 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),
@@ -11769,6 +11789,25 @@ bpf_program__attach_kprobe_opts(const struct bpf_program *prog,
offset = OPTS_GET(opts, offset, 0);
pe_opts.bpf_cookie = OPTS_GET(opts, bpf_cookie, 0);
+ /* This provides backwards compatibility to programs using kprobe, but
+ * have been auto-upgraded to multi kprobe.
+ */
+ if (prog->expected_attach_type == BPF_TRACE_KPROBE_MULTI &&
+ offset == 0 && attach_mode == PROBE_ATTACH_MODE_DEFAULT) {
+ LIBBPF_OPTS(bpf_kprobe_multi_opts, multi_opts);
+ const char *syms[1] = { func_name };
+ __u64 bpf_cookie;
+
+ multi_opts.retprobe = OPTS_GET(opts, retprobe, false);
+ multi_opts.syms = syms;
+ multi_opts.cnt = 1;
+ bpf_cookie = OPTS_GET(opts, bpf_cookie, 0);
+ if (bpf_cookie)
+ multi_opts.cookies = &bpf_cookie;
+
+ return bpf_program__attach_kprobe_multi_opts(prog, NULL, &multi_opts);
+ }
+
legacy = determine_kprobe_perf_type() < 0;
switch (attach_mode) {
case PROBE_ATTACH_MODE_LEGACY:
@@ -12223,14 +12262,24 @@ static int attach_kprobe(const struct bpf_program *prog, long cookie, struct bpf
*link = NULL;
/* no auto-attach for SEC("kprobe") and SEC("kretprobe") */
- if (strcmp(prog->sec_name, "kprobe") == 0 || strcmp(prog->sec_name, "kretprobe") == 0)
+ if (strcmp(prog->sec_name, "kprobe") == 0 ||
+ strcmp(prog->sec_name, "kretprobe") == 0 ||
+ strcmp(prog->sec_name, "kprobe.single") == 0 ||
+ strcmp(prog->sec_name, "kretprobe.single") == 0)
return 0;
- opts.retprobe = str_has_pfx(prog->sec_name, "kretprobe/");
- if (opts.retprobe)
- func_name = prog->sec_name + sizeof("kretprobe/") - 1;
- else
- func_name = prog->sec_name + sizeof("kprobe/") - 1;
+ if (str_has_pfx(prog->sec_name, "kretprobe/") ||
+ str_has_pfx(prog->sec_name, "kretprobe.single/")) {
+ opts.retprobe = true;
+ func_name = str_has_pfx(prog->sec_name, "kretprobe/")
+ ? prog->sec_name + sizeof("kretprobe/") - 1
+ : prog->sec_name + sizeof("kretprobe.single/") - 1;
+ } else {
+ opts.retprobe = false;
+ func_name = str_has_pfx(prog->sec_name, "kprobe.single/")
+ ? prog->sec_name + sizeof("kprobe.single/") - 1
+ : prog->sec_name + sizeof("kprobe/") - 1;
+ }
n = sscanf(func_name, "%m[a-zA-Z0-9_.]+%li", &func, &offset);
if (n < 1) {
diff --git a/tools/testing/selftests/bpf/progs/get_func_ip_test.c b/tools/testing/selftests/bpf/progs/get_func_ip_test.c
index 2011cacdeb18..a039760b8516 100644
--- a/tools/testing/selftests/bpf/progs/get_func_ip_test.c
+++ b/tools/testing/selftests/bpf/progs/get_func_ip_test.c
@@ -73,7 +73,7 @@ int BPF_PROG(test5, int a, int *b, int ret)
}
__u64 test6_result = 0;
-SEC("?kprobe")
+SEC("?kprobe.single")
int test6(struct pt_regs *ctx)
{
__u64 addr = bpf_get_func_ip(ctx);
diff --git a/tools/testing/selftests/bpf/progs/missed_kprobe.c b/tools/testing/selftests/bpf/progs/missed_kprobe.c
index 51a4fe64c917..5f405888ed0b 100644
--- a/tools/testing/selftests/bpf/progs/missed_kprobe.c
+++ b/tools/testing/selftests/bpf/progs/missed_kprobe.c
@@ -16,14 +16,14 @@ int BPF_PROG(trigger)
return 0;
}
-SEC("kprobe/bpf_fentry_test1")
+SEC("kprobe.single/bpf_fentry_test1")
int test1(struct pt_regs *ctx)
{
bpf_kfunc_common_test();
return 0;
}
-SEC("kprobe/bpf_kfunc_common_test")
+SEC("kprobe.single/bpf_kfunc_common_test")
int test2(struct pt_regs *ctx)
{
return 0;
diff --git a/tools/testing/selftests/bpf/progs/test_attach_probe_manual.c b/tools/testing/selftests/bpf/progs/test_attach_probe_manual.c
index 7f08bce94596..eb0fa3b39c5f 100644
--- a/tools/testing/selftests/bpf/progs/test_attach_probe_manual.c
+++ b/tools/testing/selftests/bpf/progs/test_attach_probe_manual.c
@@ -14,14 +14,14 @@ int uretprobe_res = 0;
int uprobe_byname_res = 0;
void *user_ptr = 0;
-SEC("kprobe")
+SEC("kprobe.single")
int handle_kprobe(struct pt_regs *ctx)
{
kprobe_res = 1;
return 0;
}
-SEC("kretprobe")
+SEC("kretprobe.single")
int handle_kretprobe(struct pt_regs *ctx)
{
kretprobe_res = 2;
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 8e47a818462f..bc6cfed71fb8 100644
--- a/tools/testing/selftests/bpf/progs/test_fill_link_info.c
+++ b/tools/testing/selftests/bpf/progs/test_fill_link_info.c
@@ -22,7 +22,7 @@ int unused(void)
CONFIG_PPC64 ? 0 : 1;
}
-SEC("kprobe")
+SEC("kprobe.single")
int BPF_PROG(kprobe_run)
{
return 0;
--
2.52.0
On Mon, Mar 30, 2026 at 04:30:19PM +0530, Varun R Mallya wrote:
> This patch modifies libbpf to automatically upgrade standard
> SEC("kprobe") and SEC("kretprobe") programs to use the multi-kprobe
> infrastructure (BPF_TRACE_KPROBE_MULTI) at load time if the kernel
> supports it, making them compatible with BPF tokens.
>
> To maintain backward compatibility and handle cases where singular
> kprobes are required, new SEC("kprobe.single") and SEC("kretprobe.single")
> section types are introduced. These force libbpf to use the legacy
> perf_event_open() attachment path.
>
> The following explain the reasoning for changing selftests:
> - test_fill_link_info.c kprobe→kprobe.single:
> this test calls bpf_link_get_info_by_fd and asserts
> BPF_LINK_TYPE_PERF_EVENT and it explicitly needs the perf_event
> attachment path, which breaks after auto-upgrade to multi.
>
> - test_attach_probe_manual.c kprobe→kprobe.single,
> kretprobe→kretprobe.single:
> this test exercises all four explicit attachment modes
> (default, legacy, perf, link) and PROBE_ATTACH_MODE_LINK
> creates a perf_event BPF link which the kernel rejects
> for a prog loaded with expected_attach_type = BPF_TRACE_KPROBE_MULTI.
>
> - missed_kprobe.c kprobe->kprobe.single:
> The link is explicitly checked in the tests due to which it fails if
> we do not specifically kprobe.single this.
>
> - get_func_ip_test.c ?kprobe→?kprobe.single: the ? is stripped from
> sec_name by libbpf at init time so the prog still gets auto-upgraded.
> It is then manually attached with a non-zero body offset,
> which kprobe_multi doesn't support.
>
> Signed-off-by: Varun R Mallya <varunrmallya@gmail.com>
> ---
> tools/lib/bpf/libbpf.c | 61 +++++++++++++++++--
> .../selftests/bpf/progs/get_func_ip_test.c | 2 +-
> .../selftests/bpf/progs/missed_kprobe.c | 4 +-
> .../bpf/progs/test_attach_probe_manual.c | 4 +-
> .../selftests/bpf/progs/test_fill_link_info.c | 2 +-
> 5 files changed, 61 insertions(+), 12 deletions(-)
>
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index bd7b6f486430..9d0a36f8279a 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
> @@ -8265,6 +8265,24 @@ static int bpf_object_prepare_progs(struct bpf_object *obj)
> }
> }
>
> + if (kernel_supports(obj, FEAT_KPROBE_MULTI_LINK)) {
> + const char *sec_name = prog->sec_name;
> + /* Here, we filter out for k[ret]probe or "k[ret]probe/"
> + * but we leave out anything with an '@'
> + * in it as kprobe_multi does not support versioned
> + * symbols, so we don't upgrade. Also for '+' as we do not
hum, kprobe versioned symbols?
> + * support offsets.
> + */
> + if (((strncmp(sec_name, "kprobe", 6) == 0 &&
str_has_pfx ?
> + (sec_name[6] == '/' || sec_name[6] == '\0')) ||
> + (strncmp(sec_name, "kretprobe", 9) == 0 &&
> + (sec_name[9] == '/' || sec_name[9] == '\0'))) &&
> + !strchr(sec_name, '@') &&
> + !strchr(sec_name, '+') &&
> + !(prog->prog_flags & BPF_F_SLEEPABLE))
is this check necessary?
> + prog->expected_attach_type = BPF_TRACE_KPROBE_MULTI;
> + }
> +
maybe add the upgrade logic into separate function, like
static int upgrade_program(struct bpf_program *prog)
> err = bpf_object__sanitize_prog(obj, prog);
> if (err)
> return err;
> @@ -9924,10 +9942,12 @@ static const struct bpf_sec_def section_defs[] = {
> SEC_DEF("sk_reuseport/migrate", SK_REUSEPORT, BPF_SK_REUSEPORT_SELECT_OR_MIGRATE, SEC_ATTACHABLE),
> SEC_DEF("sk_reuseport", SK_REUSEPORT, BPF_SK_REUSEPORT_SELECT, SEC_ATTACHABLE),
> SEC_DEF("kprobe+", KPROBE, 0, SEC_NONE, attach_kprobe),
> + SEC_DEF("kprobe.single+", 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("kretprobe.single+", 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),
> @@ -11769,6 +11789,25 @@ bpf_program__attach_kprobe_opts(const struct bpf_program *prog,
> offset = OPTS_GET(opts, offset, 0);
> pe_opts.bpf_cookie = OPTS_GET(opts, bpf_cookie, 0);
>
> + /* This provides backwards compatibility to programs using kprobe, but
> + * have been auto-upgraded to multi kprobe.
> + */
> + if (prog->expected_attach_type == BPF_TRACE_KPROBE_MULTI &&
> + offset == 0 && attach_mode == PROBE_ATTACH_MODE_DEFAULT) {
> + LIBBPF_OPTS(bpf_kprobe_multi_opts, multi_opts);
> + const char *syms[1] = { func_name };
> + __u64 bpf_cookie;
> +
> + multi_opts.retprobe = OPTS_GET(opts, retprobe, false);
> + multi_opts.syms = syms;
could we do directly:
multi_opts.syms = &func_name;
jirka
> + multi_opts.cnt = 1;
> + bpf_cookie = OPTS_GET(opts, bpf_cookie, 0);
> + if (bpf_cookie)
> + multi_opts.cookies = &bpf_cookie;
> +
> + return bpf_program__attach_kprobe_multi_opts(prog, NULL, &multi_opts);
> + }
> +
> legacy = determine_kprobe_perf_type() < 0;
> switch (attach_mode) {
> case PROBE_ATTACH_MODE_LEGACY:
> @@ -12223,14 +12262,24 @@ static int attach_kprobe(const struct bpf_program *prog, long cookie, struct bpf
> *link = NULL;
>
SNIP
On Mon, Mar 30, 2026 at 04:53:38PM +0200, Jiri Olsa wrote:
> On Mon, Mar 30, 2026 at 04:30:19PM +0530, Varun R Mallya wrote:
> > + const char *sec_name = prog->sec_name;
> > + /* Here, we filter out for k[ret]probe or "k[ret]probe/"
> > + * but we leave out anything with an '@'
> > + * in it as kprobe_multi does not support versioned
> > + * symbols, so we don't upgrade. Also for '+' as we do not
>
> hum, kprobe versioned symbols?
Thanks for catching that! Removed.
> > + (sec_name[9] == '/' || sec_name[9] == '\0'))) &&
> > + !strchr(sec_name, '@') &&
> > + !strchr( sec_name, '+') &&
> > + !(prog->prog_flags & BPF_F_SLEEPABLE))
>
> is this check necessary?
I had to add that check because selftests/bpf/prog_tests/attach_probe.c
was failing. Sleepable kprobes are not supposed to attach successfully,
but since this was upgraded to multi, it was doing so. So, I had to stop
all sleepable kprobes from being upgraded to maintain compatibility.
I'd like to know if kprobe_multi is even allowed to be sleepable. I
could not really find any selftests or patches for sleepable kprobe_multi
functionality anywhere, so I just want to check if this is not actually
intended behaviour and we are missing a check somewhere.
> > + prog->expected_attach_type = BPF_TRACE_KPROBE_MULTI;
> > + }
> > +
>
> maybe add the upgrade logic into separate function, like
>
> static int upgrade_program(struct bpf_program *prog)
I am thinking of making the return type void though.
I see that there is int everywhere else, but it does not make sense to me
to return an int here since I'm not doing any operation in here that
could return an error. Should I keep it as int or make it void ??
>
> > err = bpf_object__sanitize_prog(obj, prog);
> > if (err)
> > return err;
> > @@ -9924,10 +9942,12 @@ static const struct bpf_sec_def section_defs[] = {
> > SEC_DEF("sk_reuseport/migrate", SK_REUSEPORT, BPF_SK_REUSEPORT_SELECT_OR_MIGRATE, SEC_ATTACHABLE),
> > SEC_DEF("sk_reuseport", SK_REUSEPORT, BPF_SK_REUSEPORT_SELECT, SEC_ATTACHABLE),
> > SEC_DEF("kprobe+", KPROBE, 0, SEC_NONE, attach_kprobe),
> > + SEC_DEF("kprobe.single+", 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("kretprobe.single+", 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),
> > @@ -11769,6 +11789,25 @@ bpf_program__attach_kprobe_opts(const struct bpf_program *prog,
> > offset = OPTS_GET(opts, offset, 0);
> > pe_opts.bpf_cookie = OPTS_GET(opts, bpf_cookie, 0);
> >
> > + /* This provides backwards compatibility to programs using kprobe, but
> > + * have been auto-upgraded to multi kprobe.
> > + */
> > + if (prog->expected_attach_type == BPF_TRACE_KPROBE_MULTI &&
> > + offset == 0 && attach_mode == PROBE_ATTACH_MODE_DEFAULT) {
> > + LIBBPF_OPTS(bpf_kprobe_multi_opts, multi_opts);
> > + const char *syms[1] = { func_name };
> > + __u64 bpf_cookie;
> > +
> > + multi_opts.retprobe = OPTS_GET(opts, retprobe, false);
> > + multi_opts.syms = syms;
>
> could we do directly:
>
> multi_opts.syms = &func_name;
>
> jirka
Implemented.
On Wed, Apr 01, 2026 at 04:23:15PM +0530, Varun R Mallya wrote:
> I had to add that check because selftests/bpf/prog_tests/attach_probe.c
> was failing. Sleepable kprobes are not supposed to attach successfully,
> but since this was upgraded to multi, it was doing so. So, I had to stop
> all sleepable kprobes from being upgraded to maintain compatibility.
>
> I'd like to know if kprobe_multi is even allowed to be sleepable. I
> could not really find any selftests or patches for sleepable kprobe_multi
> functionality anywhere, so I just want to check if this is not actually
> intended behaviour and we are missing a check somewhere.
>
diff --git a/kernel/trace/bpf_trace.c b/kernel/trace/bpf_trace.c
index 0b040a417442..af7079aa0f36 100644
--- a/kernel/trace/bpf_trace.c
+++ b/kernel/trace/bpf_trace.c
@@ -2752,6 +2752,10 @@ int bpf_kprobe_multi_link_attach(const union bpf_attr *attr, struct bpf_prog *pr
if (!is_kprobe_multi(prog))
return -EINVAL;
+ /* kprobe_multi is not allowed to be sleepable. */
+ if (prog->sleepable)
+ return -EINVAL;
+
/* Writing to context is not allowed for kprobes. */
if (prog->aux->kprobe_write_ctx)
return -EINVAL;
Adding this removes the need for me to add the sleepable check. Should I
send a patch with this or am I making a mistake here ??
Sleepability for kprobe_multi was never checked (If it's meant to not be
sleepable, that is.)
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index 056bc50a0bbb..7b5f83ce543e 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -11815,6 +11835,25 @@ bpf_program__attach_kprobe_opts(const struct bpf_program *prog,
> offset = OPTS_GET(opts, offset, 0);
> pe_opts.bpf_cookie = OPTS_GET(opts, bpf_cookie, 0);
>
> + /* This provides backwards compatibility to programs using kprobe, but
> + * have been auto-upgraded to multi kprobe.
> + */
> + if (prog->expected_attach_type == BPF_TRACE_KPROBE_MULTI &&
> + offset == 0 && attach_mode == PROBE_ATTACH_MODE_DEFAULT) {
> + LIBBPF_OPTS(bpf_kprobe_multi_opts, multi_opts);
> + const char *syms[1] = { func_name };
> + __u64 bpf_cookie;
> +
> + multi_opts.retprobe = OPTS_GET(opts, retprobe, false);
> + multi_opts.syms = syms;
> + multi_opts.cnt = 1;
> + bpf_cookie = OPTS_GET(opts, bpf_cookie, 0);
> + if (bpf_cookie)
> + multi_opts.cookies = &bpf_cookie;
> +
> + return bpf_program__attach_kprobe_multi_opts(prog, NULL, &multi_opts);
> + }
> +
> + legacy = determine_kprobe_perf_type() < 0;
What happens when a program was auto-upgraded to
BPF_TRACE_KPROBE_MULTI but the caller passes offset != 0 or
attach_mode == PROBE_ATTACH_MODE_LINK?
The redirect above is skipped, and the code falls through to the
perf_event path, which eventually calls bpf_link_create() with
BPF_PERF_EVENT. The kernel's bpf_prog_attach_check_attach_type()
in link_create() rejects this:
kernel/bpf/syscall.c:bpf_prog_attach_check_attach_type() {
case BPF_PROG_TYPE_KPROBE:
if (prog->expected_attach_type == BPF_TRACE_KPROBE_MULTI &&
attach_type != BPF_TRACE_KPROBE_MULTI)
return -EINVAL;
}
This affects callers of the public API
bpf_program__attach_kprobe_opts() who have a SEC("kprobe") program
that was auto-upgraded but then manually attach with offset != 0
(the get_func_ip_test.c case) or with PROBE_ATTACH_MODE_LINK (the
test_attach_probe_manual.c case). The selftests were updated to use
.single, but external users of the API would get a confusing
-EINVAL with no diagnostic.
Would it make sense to add a pr_warn here for the fallthrough case
when expected_attach_type == BPF_TRACE_KPROBE_MULTI, suggesting
SEC("kprobe.single") as a workaround? Something like:
if (prog->expected_attach_type == BPF_TRACE_KPROBE_MULTI) {
pr_warn("prog '%s': kprobe was auto-upgraded to multi but "
"offset or attach_mode is incompatible; "
"use SEC(\"kprobe.single\") instead\n",
prog->name);
return libbpf_err_ptr(-ENOTSUP);
}
[ ... ]
> @@ -12269,14 +12308,24 @@ static int attach_kprobe(const struct bpf_program *prog, long cookie, struct bpf
> *link = NULL;
>
> /* no auto-attach for SEC("kprobe") and SEC("kretprobe") */
> + if (strcmp(prog->sec_name, "kprobe") == 0 ||
> + strcmp(prog->sec_name, "kretprobe") == 0 ||
> + strcmp(prog->sec_name, "kprobe.single") == 0 ||
> + strcmp(prog->sec_name, "kretprobe.single") == 0)
> return 0;
This isn't a bug, but the comment above still says SEC("kprobe")
and SEC("kretprobe") -- should it also mention the .single
variants now that they are handled here?
[ ... ]
---
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
On Mon, Mar 30, 2026 at 11:47:26AM +0000, bot+bpf-ci@kernel.org wrote:
> > + multi_opts.cookies = &bpf_cookie;
> > +
> > + return bpf_program__attach_kprobe_multi_opts(prog, NULL, &multi_opts);
> > + }
> > +
> > + legacy = determine_kprobe_perf_type() < 0;
>
> What happens when a program was auto-upgraded to
> BPF_TRACE_KPROBE_MULTI but the caller passes offset != 0 or
> attach_mode == PROBE_ATTACH_MODE_LINK?
>
> The redirect above is skipped, and the code falls through to the
> perf_event path, which eventually calls bpf_link_create() with
> BPF_PERF_EVENT. The kernel's bpf_prog_attach_check_attach_type()
> in link_create() rejects this:
>
> kernel/bpf/syscall.c:bpf_prog_attach_check_attach_type() {
> case BPF_PROG_TYPE_KPROBE:
> if (prog->expected_attach_type == BPF_TRACE_KPROBE_MULTI &&
> attach_type != BPF_TRACE_KPROBE_MULTI)
> return -EINVAL;
> }
>
> This affects callers of the public API
> bpf_program__attach_kprobe_opts() who have a SEC("kprobe") program
> that was auto-upgraded but then manually attach with offset != 0
> (the get_func_ip_test.c case) or with PROBE_ATTACH_MODE_LINK (the
> test_attach_probe_manual.c case). The selftests were updated to use
> .single, but external users of the API would get a confusing
> -EINVAL with no diagnostic.
>
> Would it make sense to add a pr_warn here for the fallthrough case
> when expected_attach_type == BPF_TRACE_KPROBE_MULTI, suggesting
> SEC("kprobe.single") as a workaround? Something like:
>
> if (prog->expected_attach_type == BPF_TRACE_KPROBE_MULTI) {
> pr_warn("prog '%s': kprobe was auto-upgraded to multi but "
> "offset or attach_mode is incompatible; "
> "use SEC(\"kprobe.single\") instead\n",
> prog->name);
> return libbpf_err_ptr(-ENOTSUP);
> }
Nice catch! I've added a check for that now.
> [ ... ]
>
> > @@ -12269,14 +12308,24 @@ static int attach_kprobe(const struct bpf_program *prog, long cookie, struct bpf
> > *link = NULL;
> >
> > /* no auto-attach for SEC("kprobe") and SEC("kretprobe") */
> > + if (strcmp(prog->sec_name, "kprobe") == 0 ||
> > + strcmp(prog->sec_name, "kretprobe") == 0 ||
> > + strcmp(prog->sec_name, "kprobe.single") == 0 ||
> > + strcmp(prog->sec_name, "kretprobe.single") == 0)
> > return 0;
>
> This isn't a bug, but the comment above still says SEC("kprobe")
> and SEC("kretprobe") -- should it also mention the .single
> variants now that they are handled here?
>
> [ ... ]
>
>
> ---
> 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.