Re: [PATCH] perf bpf-filter: Fix memory leak on bpf_link

From: Ian Rogers

Date: Tue Oct 20 2026 - 11:53:34 EST


On Mon, Oct 19, 2026 at 10:32 PM Namhyung Kim <namhyung@xxxxxxxxxx> wrote:
>
> Attaching BPF program returns bpf_link but it leaked like following.
>
> ==51328==ERROR: LeakSanitizer: detected memory leaks
>
> Direct leak of 1568 byte(s) in 28 object(s) allocated from:
> #0 0x7ffbe76ef24f in calloc (/lib64/libasan.so.8+0xef24f) (BuildId: 61e8165ca90f7a4f06b73f064a938c76deaae079)
> #1 0x000000625df7 in bpf_program__attach_perf_event_opts (tools/perf/perf+0x625df7) (BuildId: 5e6309fda155e58708e92f1c76d44cadd585d0eb)
> #2 0x000000beddc0 in perf_bpf_filter__prepare util/bpf-filter.c:507
> #3 0x0000008f3def in evlist__apply_filters util/evlist.c:1421
> #4 0x00000044bbcf in record__open tools/perf/builtin-record.c:1469
> #5 0x000000453f9e in __cmd_record tools/perf/builtin-record.c:2594
> #6 0x00000045dadb in cmd_record tools/perf/builtin-record.c:4427
> #7 0x0000005df56b in run_builtin tools/perf/perf.c:383
> #8 0x0000005dfa0e in handle_internal_command tools/perf/perf.c:436
> #9 0x0000005dfddc in run_argv tools/perf/perf.c:480
> #10 0x0000005e0333 in main tools/perf/perf.c:581
> #11 0x7ffbdd450680 in __libc_start_call_main (/lib64/libc.so.6+0x3680) (BuildId: 85075e2ae2e13352a1413d41c327dacf6461526c)
> #12 0x7ffbdd450797 in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x3797) (BuildId: 85075e2ae2e13352a1413d41c327dacf6461526c)
> #13 0x000000409c54 in _start (tools/perf/perf+0x409c54) (BuildId: 5e6309fda155e58708e92f1c76d44cadd585d0eb)
>
> SUMMARY: AddressSanitizer: 1568 byte(s) leaked in 28 allocation(s).
>
> Let's add an xyarray of bpf_link for the filters and release them at the
> end to fix the leaks.

As Sashiko noted, libbpf's bpf_program__attach_perf_event_opts() is
designed for callers that transfer ownership of the perf_event fd to
the returned bpf_link (such as bpf_program__attach_kprobe(), which
opens its own perf_event fd internally). When bpf_link__destroy() is
called, bpf_link_perf_detach() unconditionally disables and closes
perf_link->perf_event_fd:

static int bpf_link_perf_detach(struct bpf_link *link)
{
struct bpf_link_perf *perf_link = container_of(link,
struct bpf_link_perf, link);
int err = 0;

if (ioctl(perf_link->perf_event_fd,
PERF_EVENT_IOC_DISABLE, 0) < 0)
err = -errno;

if (perf_link->perf_event_fd != link->fd)
close(perf_link->perf_event_fd);
close(link->fd);
...

Since evsel->core.fd owns FD(evsel, x, y) and evlist__close() closes
those descriptors before evsel__exit() -> perf_bpf_filter__destroy()
runs, calling bpf_link__destroy() ends up closing FD(evsel, x, y) a
second time (and my v4 patch 2 has the same issue on FD(evsel, 0, 0)
by assigning skel->links.perf_tp_sample_filter = link).

Instead of using bpf_program__attach_perf_event_opts() and tracking
bpf_links, how about attaching the program directly with
ioctl(FD(evsel, x, y), PERF_EVENT_IOC_SET_BPF, fd), just like the
pinned filter path right above it in perf_bpf_filter__prepare()
already does?

fd = bpf_program__fd(skel->progs.perf_sample_filter);
for (x = 0; x < xyarray__max_x(evsel->core.fd); x++) {
for (y = 0; y < xyarray__max_y(evsel->core.fd); y++) {
ret = ioctl(FD(evsel, x, y),
PERF_EVENT_IOC_SET_BPF, fd);
if (ret < 0) {
pr_err("Failed to attach perf
sample-filter\n");
goto err;
}
}
}

That avoids allocating struct bpf_link_perf and extra BPF_PERF_EVENT
link fds altogether (fixing the leak without needing
evsel->bpf_links), avoids the double-close on FD(evsel, x, y) since
the kernel's _free_event() -> perf_event_free_bpf_prog() detaches the
program when evlist__close() closes the perf_event fd, and also lets
us drop pe_opts (.dont_enable = true) and the
LIBBPF_CURRENT_VERSION_GEQ(1, 7) guard.

For simplicity, I will include a patch doing this in v5 of my series.

Thanks,
Ian