Skip to content

attachPerfEventLink formats the syscall error with %v, so errors.Is never matches #2088

Description

@FranciscoPLoureiro

What happens

attachPerfEventLink wraps the error from LinkCreatePerfEvent with %v rather than %w:

https://github.com/cilium/ebpf/blob/main/link/perf_event.go#L326

return nil, fmt.Errorf("cannot create bpf perf link: %v", err)

The errno is therefore unrecoverable by the caller. errors.Is(err, unix.EACCES) returns false for an error that is EACCES, and the only thing left to match on is the message text.

Minimal reproduction

A tracepoint program whose highest context read is past the end of the tracepoint's record. The verifier accepts it — tp_prog_is_valid_access allows any offset below PERF_MAX_TRACE_SIZE — and the kernel refuses it at attach, in perf_event_set_bpf_prog:

	if (is_tracepoint || is_syscall_tp) {
		int off = trace_event_get_offsets(event->tp_event);

		if (prog->aux->max_ctx_offset > off)
			return -EACCES;
	}

kernel/events/core.c

// oversized.bpf.c -- sched_process_fork is 48 bytes here; 256 is past the end
// on any kernel.
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

char _license[] SEC("license") = "GPL";

struct oversized_ctx {
	char pad[256];
	__u32 far;
};

SEC("tracepoint/sched/sched_process_fork")
int reads_past_the_end(struct oversized_ctx *ctx)
{
	return ctx->far ? 1 : 0;
}
coll, err := ebpf.NewCollection(spec)   // succeeds
prog := coll.Programs["reads_past_the_end"]

l, err := link.Tracepoint("sched", "sched_process_fork", prog, nil)
fmt.Printf("attach: %v\n", err)
fmt.Printf("  errors.Is(err, unix.EACCES): %v\n", errors.Is(err, unix.EACCES))

Output, running as root, on 6.6.87.2 with ebpf v0.22.0:

load: ok -- the verifier accepted it
attach: cannot create bpf perf link: permission denied
  errors.Is(err, unix.EACCES): false
  errors.Is(err, unix.EPERM):  false

Why this one is worth the message as well as the wrapping

The wrapping is the bug and the fix is %w. But this particular EACCES is also about as misleading as an error can be: it says permission denied to a process running as root with every capability, and the actual cause is that the program reads past the end of the tracepoint's context.

It is not an exotic case. Tracepoint field names are stable ABI and their offsets are not — sched_process_fork is a live example, where parent_comm went from char[16] to __data_loc char[] between 6.6 and 6.17 and moved every field after it. A program built against one layout attaches fine on the kernel it was built on and is refused on the other, with permission denied and nothing else to go on. That cost me two days before I looked at perf_event_set_bpf_prog.

So a second suggestion, separable from the fix: when LinkCreatePerfEvent returns EACCES for a tracepoint program, say what it usually means.

if err != nil {
	if errors.Is(err, unix.EACCES) {
		return nil, fmt.Errorf("cannot create bpf perf link: %w (the program may read "+
			"past the end of the tracepoint's context)", err)
	}
	return nil, fmt.Errorf("cannot create bpf perf link: %w", err)
}

Happy to send a PR for the %w on its own, or for both, whichever you prefer.

Environment

  • ebpf v0.22.0
  • Linux 6.6.87.2, x86-64
  • Go 1.26.5, clang 19.1.7

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions