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

From: Jiri Olsa

Date: Mon Mar 30 2026 - 11:01:37 EST


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@xxxxxxxxx>
> ---
> 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