#2100·ebpf

perf: Reader registers event FDs under wrong map keys when online CPU IDs are non-contiguous

Author: alahaiyoCreated Sep 10, 2026Updated Sep 14, 2026
Labelsbug

Describe the bug

perf.NewReaderWithOptions skips offline CPUs and appends the remaining rings and event FDs to compact slices. Reader.Resume() then uses the slice index as the perf-event-array key, although that index may no longer match the CPU on which the event was opened.

For a hypothetical minimal example, consider possible CPUs 0-2 with only CPUs 0,2 online:

Slice index CPU used to open the perf event Key used by Resume Expected key
0 0 0 0
1 2 1 2

A BPF program using bpf_perf_event_output(..., BPF_F_CURRENT_CPU, ...) on CPU 2 consequently looks up a slot that the reader has not populated. CPU 0 can still deliver events, so receiving some records does not establish that all online CPUs are covered.

Expected behavior: each event FD should be registered under its actual CPU ID, and events from every online CPU covered by the map should reach the reader.

Source analysis

The mismatch is present in the upstream source at commit 6ae24e9d375806943ab8f6052c826030f76467f1, which was the current main when checked:

  • Reader construction skips ENODEV and appends only successful entries.
  • Resume registers FDs using the compact slice index.
  • Pause deletes using that same index and must be updated together with Resume.

This appears related to #1510, which removed the nil placeholders for offline CPUs following #1503. Removing the placeholders loses the correspondence between slice position and CPU ID.

A possible minimal fix is to use the CPU ID already stored in the corresponding ring (pr.rings[i].cpu) for map operations in both Resume and Pause, and in their CPU-related error messages. The two slices are appended together. This is a proposed fix, not a tested patch.

How to reproduce

This is a source-level bug report. The following is a suggested independent reproduction / regression test derived from the public implementation, not a standalone reproducer that has already been executed.

  1. Use a Linux test VM with a stable, non-contiguous online CPU set before creating the reader. For a hypothetical minimal example, use possible CPUs 0-2 with only CPUs 0,2 online. Merely restricting process affinity while all CPUs remain online does not establish this condition.
  2. Create a fresh BPF_MAP_TYPE_PERF_EVENT_ARRAY with enough entries to cover the highest possible CPU ID (3 for this example), then create a reader through the public perf.NewReader API.
  3. Attach a small BPF program that emits a CPU ID and sequence number through bpf_perf_event_output with BPF_F_CURRENT_CPU. Record the helper's return value separately to distinguish failed output from ring-buffer overflow.
  4. Trigger the program from a thread pinned to each online CPU and read with a deadline. Verify the emitted CPU ID, sequence number, and Record.CPU.
  5. Based on the current source, the FD opened for CPU 2 is stored at map key 1, leaving key 2 unpopulated. CPU 0 should deliver events, while output on CPU 2 should fail. After a fix, both online CPUs should deliver.
  6. Drain pending records and repeat across Pause/Resume cycles to verify that all actual CPU slots are removed and restored correctly.

The proposed fix has not been runtime-validated. This report concerns offline CPUs at reader creation; dynamic CPU hotplug after reader creation is outside its scope.

Version information

main at 6ae24e9d375806943ab8f6052c826030f76467f1 (source inspection; not runtime-validated)