* [PATCH bpf-next 0/2] bpftool: Consume ring buffer maps with event_pipe
@ 2026-09-06 17:07 Tianyi Chen
2026-09-06 17:07 ` [PATCH bpf-next 1/2] bpftool: Read " Tianyi Chen
` (2 more replies)
0 siblings, 3 replies; 9+ messages in thread
From: Tianyi Chen @ 2026-09-06 17:07 UTC (permalink / raw)
To: bpf
Cc: Tianyi Chen, Quentin Monnet, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
Emil Tsalapatis, Ihor Solodrai, Shuah Khan, linux-kernel,
linux-kselftest
This series extends map event_pipe to BPF ring buffer maps through
libbpf's ring buffer manager. Records contain a size and raw bytes in
plain or JSON output. CPU/index selectors remain perf-array specific.
The documentation explains the shared, destructive consumer position.
The existing perf event array path is retained. Signal handlers only set
a stop flag; messages and JSON closure happen in the main path. Poll
and output errors return failure instead of silently ending a stream.
Related request:
https://github.com/libbpf/bpftool/issues/54
The series is based on bpf-next and is independent of the flags and
batch-dump series sent alongside it.
Validation:
- Full bpftool and focused BPF selftests builds with LLVM 20, plus
bpftool-map man page generation and Bash completion syntax checks.
- All 12 bpftool_ringbuf subtests passed in an x86-64 KVM guest running
Linux 7.3.0-rc1, against a bpftool built with only this series.
- The perf event array regression also passed against unmodified bpftool.
- Injecting EIO into epoll_wait returned failure with an errno diagnostic
on stderr and a complete empty JSON array on stdout.
Only focused tests were run; this does not claim a full BPF selftest
suite run. Strict checkpatch has no errors or checks. Remaining warnings
are the userspace volatile sig_atomic_t signal flag, readable multiline
expected-output strings, and files covered by existing MAINTAINERS
patterns.
Integration check: these three independent series also applied and built
together with my previously posted recursive map-dump v2 series. The
combined 64 subtests passed with no skips or failures in the same guest.
Tianyi Chen (2):
bpftool: Read ring buffer maps with event_pipe
selftests/bpf: Cover bpftool ring buffer event_pipe
.../bpf/bpftool/Documentation/bpftool-map.rst | 15 +-
tools/bpf/bpftool/bash-completion/bpftool | 4 +-
tools/bpf/bpftool/map_perf_ring.c | 87 +++-
.../bpf/prog_tests/bpftool_ringbuf.c | 381 ++++++++++++++++++
.../selftests/bpf/progs/bpftool_ringbuf.c | 47 +++
5 files changed, 510 insertions(+), 24 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c
create mode 100644 tools/testing/selftests/bpf/progs/bpftool_ringbuf.c
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH bpf-next 1/2] bpftool: Read ring buffer maps with event_pipe
2026-09-06 17:07 [PATCH bpf-next 0/2] bpftool: Consume ring buffer maps with event_pipe Tianyi Chen
@ 2026-09-06 17:07 ` Tianyi Chen
2026-09-06 17:07 ` [PATCH bpf-next 2/2] selftests/bpf: Cover bpftool ring buffer event_pipe Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v2 0/2] bpftool: Consume ring buffer maps with event_pipe Tianyi Chen
2 siblings, 0 replies; 9+ messages in thread
From: Tianyi Chen @ 2026-09-06 17:07 UTC (permalink / raw)
To: bpf
Cc: Tianyi Chen, Quentin Monnet, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
Emil Tsalapatis, Ihor Solodrai, Shuah Khan, linux-kernel,
linux-kselftest
Allow map event_pipe to consume BPF_MAP_TYPE_RINGBUF maps using
libbpf's ring buffer manager. Print each record's size and raw bytes
in plain or JSON output, and flush output as records arrive.
Reject CPU and index selectors for ring buffers and retain the existing
perf event array path. Keep signal handlers limited to setting a stop
flag, reset that flag for each command, and print the stopping message
on stderr from the main path. Close the JSON array on poll errors and
preserve output errors returned by the ring buffer callback.
Document that consuming a ring buffer advances its shared consumer
position and add ring buffer maps to event_pipe completion.
Link: https://github.com/libbpf/bpftool/issues/54
Assisted-by: Codex:GPT-6
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
.../bpf/bpftool/Documentation/bpftool-map.rst | 15 +++-
tools/bpf/bpftool/bash-completion/bpftool | 4 +-
tools/bpf/bpftool/map_perf_ring.c | 87 ++++++++++++++-----
3 files changed, 82 insertions(+), 24 deletions(-)
diff --git a/tools/bpf/bpftool/Documentation/bpftool-map.rst b/tools/bpf/bpftool/Documentation/bpftool-map.rst
index 5daf3de5c74..c44e6f797e5 100644
--- a/tools/bpf/bpftool/Documentation/bpftool-map.rst
+++ b/tools/bpf/bpftool/Documentation/bpftool-map.rst
@@ -120,7 +120,8 @@ bpftool map pin *MAP* *FILE*
character ('.'), which is reserved for future extensions of *bpffs*.
bpftool map event_pipe *MAP* [cpu *N* index *M*]
- Read events from a **BPF_MAP_TYPE_PERF_EVENT_ARRAY** map.
+ Read events from a **BPF_MAP_TYPE_PERF_EVENT_ARRAY** or
+ **BPF_MAP_TYPE_RINGBUF** map.
Install perf rings into a perf event array map and dump output of any
**bpf_perf_event_output**\ () call in the kernel. By default read the
@@ -134,6 +135,18 @@ bpftool map event_pipe *MAP* [cpu *N* index *M*]
existing ring. Any other application will stop receiving events if it
installed its rings earlier.
+ For a ring buffer map, consume records submitted by BPF programs, including
+ records already queued before the command starts. **cpu** and **index**
+ are not supported. Each record is printed as raw bytes, including embedded
+ zero bytes. Plain output reports the record size followed by hexadecimal
+ bytes; JSON output contains **size** and **data** fields, with **data** an
+ array of byte values. Ring buffer records have no implicit CPU or timestamp.
+
+ Ring buffers support a single consumer. This command advances the shared
+ consumer position and must not run alongside another consumer of the same
+ map; it does not provide a passive view of events. **BPF_MAP_TYPE_USER_RINGBUF**
+ maps are not supported.
+
bpftool map peek *MAP*
Peek next value in the queue or stack.
diff --git a/tools/bpf/bpftool/bash-completion/bpftool b/tools/bpf/bpftool/bash-completion/bpftool
index 75cbcb512eb..1750b488c9e 100644
--- a/tools/bpf/bpftool/bash-completion/bpftool
+++ b/tools/bpf/bpftool/bash-completion/bpftool
@@ -878,11 +878,11 @@ _bpftool()
return 0
;;
id)
- _bpftool_get_map_ids_for_type perf_event_array
+ _bpftool_get_map_ids_for_type '"type": "\(perf_event_array\|ringbuf\)"'
return 0
;;
name)
- _bpftool_get_map_names_for_type perf_event_array
+ _bpftool_get_map_names_for_type '"type": "\(perf_event_array\|ringbuf\)"'
return 0
;;
cpu)
diff --git a/tools/bpf/bpftool/map_perf_ring.c b/tools/bpf/bpftool/map_perf_ring.c
index bcb767e2d67..7d555331f44 100644
--- a/tools/bpf/bpftool/map_perf_ring.c
+++ b/tools/bpf/bpftool/map_perf_ring.c
@@ -27,7 +27,7 @@
#define MMAP_PAGE_CNT 16
-static volatile bool stop;
+static volatile sig_atomic_t stop;
struct perf_event_sample {
struct perf_event_header header;
@@ -44,7 +44,6 @@ struct perf_event_lost {
static void int_exit(int signo)
{
- fprintf(stderr, "Stopping...\n");
stop = true;
}
@@ -107,6 +106,27 @@ print_bpf_output(void *private_data, int cpu, struct perf_event_header *event)
return LIBBPF_PERF_EVENT_CONT;
}
+static int print_ringbuf_output(void *ctx, void *data, size_t size)
+{
+ if (json_output) {
+ jsonw_start_object(json_wtr);
+ jsonw_uint_field(json_wtr, "size", size);
+ jsonw_name(json_wtr, "data");
+ print_data_json(data, size);
+ jsonw_end_object(json_wtr);
+ } else {
+ printf("== size: %zu =====\n", size);
+ fprint_hex(stdout, data, size, " ");
+ printf("\n");
+ }
+
+ if (fflush(stdout))
+ return errno ? -errno : -EIO;
+
+ /* A producer can keep poll() busy even after a signal arrives. */
+ return stop ? -EINTR : 0;
+}
+
int do_event_pipe(int argc, char **argv)
{
struct perf_event_attr perf_attr = {
@@ -123,18 +143,26 @@ int do_event_pipe(int argc, char **argv)
.cpu = -1,
.idx = -1,
};
- struct perf_buffer *pb;
+ struct perf_buffer *pb = NULL;
+ struct ring_buffer *rb = NULL;
__u32 map_info_len;
int err, map_fd;
+ stop = false;
map_info_len = sizeof(map_info);
map_fd = map_parse_fd_and_info(&argc, &argv, &map_info, &map_info_len,
0);
if (map_fd < 0)
return -1;
- if (map_info.type != BPF_MAP_TYPE_PERF_EVENT_ARRAY) {
- p_err("map is not a perf event array");
+ if (map_info.type != BPF_MAP_TYPE_PERF_EVENT_ARRAY &&
+ map_info.type != BPF_MAP_TYPE_RINGBUF) {
+ p_err("map is not a perf event array or ring buffer");
+ goto err_close_map;
+ }
+
+ if (map_info.type == BPF_MAP_TYPE_RINGBUF && argc) {
+ p_err("ring buffer maps do not support cpu or index arguments");
goto err_close_map;
}
@@ -184,15 +212,24 @@ int do_event_pipe(int argc, char **argv)
ctx.idx = 0;
}
- opts.cpu_cnt = ctx.all_cpus ? 0 : 1;
- opts.cpus = &ctx.cpu;
- opts.map_keys = &ctx.idx;
- pb = perf_buffer__new_raw(map_fd, MMAP_PAGE_CNT, &perf_attr,
- print_bpf_output, &ctx, &opts);
- if (!pb) {
- p_err("failed to create perf buffer: %s (%d)",
- strerror(errno), errno);
- goto err_close_map;
+ if (map_info.type == BPF_MAP_TYPE_RINGBUF) {
+ rb = ring_buffer__new(map_fd, print_ringbuf_output, NULL, NULL);
+ if (!rb) {
+ p_err("failed to create ring buffer: %s (%d)",
+ strerror(errno), errno);
+ goto err_close_map;
+ }
+ } else {
+ opts.cpu_cnt = ctx.all_cpus ? 0 : 1;
+ opts.cpus = &ctx.cpu;
+ opts.map_keys = &ctx.idx;
+ pb = perf_buffer__new_raw(map_fd, MMAP_PAGE_CNT, &perf_attr,
+ print_bpf_output, &ctx, &opts);
+ if (!pb) {
+ p_err("failed to create perf buffer: %s (%d)",
+ strerror(errno), errno);
+ goto err_close_map;
+ }
}
signal(SIGINT, int_exit);
@@ -202,25 +239,33 @@ int do_event_pipe(int argc, char **argv)
if (json_output)
jsonw_start_array(json_wtr);
+ err = 0;
while (!stop) {
- err = perf_buffer__poll(pb, 200);
+ err = rb ? ring_buffer__poll(rb, 200) : perf_buffer__poll(pb, 200);
if (err < 0 && err != -EINTR) {
- p_err("perf buffer polling failed: %s (%d)",
- strerror(errno), errno);
- goto err_close_pb;
+ fprintf(stderr, "Error: %s buffer polling failed: %s (%d)\n",
+ rb ? "ring" : "perf", strerror(-err), -err);
+ break;
}
+ err = 0;
}
+ if (stop)
+ fprintf(stderr, "Stopping...\n");
if (json_output)
jsonw_end_array(json_wtr);
+ if (fflush(stdout)) {
+ fprintf(stderr, "Error: failed to write events: %s\n", strerror(errno));
+ err = -1;
+ }
+ ring_buffer__free(rb);
perf_buffer__free(pb);
+ /* Both buffer managers borrow map_fd. */
close(map_fd);
- return 0;
+ return err < 0 ? -1 : 0;
-err_close_pb:
- perf_buffer__free(pb);
err_close_map:
close(map_fd);
return -1;
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH bpf-next 2/2] selftests/bpf: Cover bpftool ring buffer event_pipe
2026-09-06 17:07 [PATCH bpf-next 0/2] bpftool: Consume ring buffer maps with event_pipe Tianyi Chen
2026-09-06 17:07 ` [PATCH bpf-next 1/2] bpftool: Read " Tianyi Chen
@ 2026-09-06 17:07 ` Tianyi Chen
2026-09-06 17:18 ` sashiko-bot
2026-09-07 1:21 ` [PATCH bpf-next v2 0/2] bpftool: Consume ring buffer maps with event_pipe Tianyi Chen
2 siblings, 1 reply; 9+ messages in thread
From: Tianyi Chen @ 2026-09-06 17:07 UTC (permalink / raw)
To: bpf
Cc: Tianyi Chen, Quentin Monnet, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
Emil Tsalapatis, Ihor Solodrai, Shuah Khan, linux-kernel,
linux-kselftest
Produce known ring buffer records and check complete plain, JSON and
pretty JSON output. Exercise ID and pinned map selection, SIGINT and
SIGTERM shutdown, empty streams and invalid map types or selectors.
Also produce a perf event sample and check its existing header and raw
payload output. Use a payload whose size plus the raw sample length
field is aligned to eight bytes so the expected bytes exclude implicit
perf padding.
Assisted-by: Codex:GPT-6
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
.../bpf/prog_tests/bpftool_ringbuf.c | 381 ++++++++++++++++++
.../selftests/bpf/progs/bpftool_ringbuf.c | 47 +++
2 files changed, 428 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c
create mode 100644 tools/testing/selftests/bpf/progs/bpftool_ringbuf.c
diff --git a/tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c b/tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c
new file mode 100644
index 00000000000..2099bf10209
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c
@@ -0,0 +1,381 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <ctype.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <signal.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <test_progs.h>
+#include "bpftool_ringbuf.skel.h"
+
+#define WAIT_STEPS 500
+#define WAIT_US 10000
+
+struct consumer {
+ pid_t pid;
+ int fd;
+};
+
+static const char *bpftool_path(void)
+{
+ const char *path = getenv("BPFTOOL");
+
+ if (path)
+ return access(path, X_OK) ? NULL : path;
+ if (!access("./tools/sbin/bpftool", X_OK))
+ return "./tools/sbin/bpftool";
+ if (!access("../tools/sbin/bpftool", X_OK))
+ return "../tools/sbin/bpftool";
+ return NULL;
+}
+
+static void consumer_cleanup(struct consumer *child)
+{
+ if (child->pid > 0) {
+ kill(child->pid, SIGKILL);
+ while (waitpid(child->pid, NULL, 0) < 0 && errno == EINTR)
+ ;
+ child->pid = -1;
+ }
+ if (child->fd >= 0) {
+ close(child->fd);
+ child->fd = -1;
+ }
+}
+
+static bool consumer_start(struct consumer *child, int map_fd, bool json,
+ const char *option, bool pair, bool capture_errors)
+{
+ struct bpf_map_info info = {};
+ __u32 len = sizeof(info);
+ const char *path = bpftool_path();
+ char *argv[12], id[16];
+ int out[2], ready[2], n = 0, err, err_fd;
+ struct pollfd pfd;
+
+ if (!ASSERT_OK_PTR(path, "bpftool path (set BPFTOOL to override)") ||
+ !ASSERT_OK(bpf_map_get_info_by_fd(map_fd, &info, &len), "map info"))
+ return false;
+ snprintf(id, sizeof(id), "%u", info.id);
+ argv[n++] = (char *)path;
+ if (json)
+ argv[n++] = "-j";
+ argv[n++] = "map";
+ argv[n++] = "event_pipe";
+ argv[n++] = "id";
+ argv[n++] = id;
+ if (option) {
+ argv[n++] = (char *)option;
+ argv[n++] = "0";
+ if (pair) {
+ argv[n++] = "index";
+ argv[n++] = "0";
+ }
+ }
+ argv[n] = NULL;
+ if (!ASSERT_OK(pipe2(out, O_CLOEXEC), "output pipe"))
+ return false;
+ if (!ASSERT_OK(pipe2(ready, O_CLOEXEC), "exec pipe")) {
+ close(out[0]);
+ close(out[1]);
+ return false;
+ }
+ child->pid = fork();
+ if (!child->pid) {
+ close(out[0]);
+ close(ready[0]);
+ err_fd = capture_errors ? out[1] : open("/dev/null", O_WRONLY);
+ if (dup2(out[1], STDOUT_FILENO) < 0 ||
+ dup2(err_fd, STDERR_FILENO) < 0)
+ goto exec_fail;
+ if (!capture_errors)
+ close(err_fd);
+ close(out[1]);
+ execv(path, argv);
+exec_fail:
+ err = errno;
+ write(ready[1], &err, sizeof(err));
+ _exit(127);
+ }
+ close(out[1]);
+ close(ready[1]);
+ child->fd = out[0];
+ pfd = (struct pollfd) { .fd = ready[0], .events = POLLIN };
+ /* EOF on the close-on-exec pipe distinguishes exec from inherited handlers. */
+ err = child->pid > 0 ? poll(&pfd, 1, WAIT_STEPS * WAIT_US / 1000) : -1;
+ if (!ASSERT_GT(child->pid, 0, "fork") ||
+ !ASSERT_GT(err, 0, "exec timeout") ||
+ !ASSERT_EQ(read(ready[0], &err, sizeof(err)), 0, "exec")) {
+ close(ready[0]);
+ consumer_cleanup(child);
+ return false;
+ }
+ close(ready[0]);
+ return true;
+}
+
+static bool consumer_finish(struct consumer *child, int signo, bool success,
+ char *output, size_t size)
+{
+ int status = 0, i;
+ pid_t ret = 0;
+ ssize_t n;
+ size_t used = 0;
+
+ if (!ASSERT_OK(fcntl(child->fd, F_SETFL, O_NONBLOCK), "nonblocking output"))
+ return false;
+ if (signo && !ASSERT_OK(kill(child->pid, signo), "signal consumer"))
+ return false;
+ for (i = 0; i < WAIT_STEPS; i++) {
+ while (used < size - 1 &&
+ (n = read(child->fd, output + used, size - 1 - used)) > 0)
+ used += n;
+ ret = waitpid(child->pid, &status, WNOHANG);
+ if (ret == child->pid)
+ break;
+ if (ret < 0 && errno != EINTR)
+ break;
+ usleep(WAIT_US);
+ }
+ if (!ASSERT_EQ(ret, child->pid, "bounded consumer exit"))
+ return false;
+ child->pid = -1;
+ while (used < size - 1 && (n = read(child->fd, output + used, size - 1 - used)) > 0)
+ used += n;
+ output[used] = '\0';
+ return ASSERT_TRUE(WIFEXITED(status), "normal exit") &&
+ ASSERT_EQ(WEXITSTATUS(status) == 0, success, "exit status");
+}
+
+static bool consumer_ready(struct consumer *child)
+{
+ unsigned long long caught;
+ char path[64], line[256];
+ int i;
+ FILE *f;
+
+ snprintf(path, sizeof(path), "/proc/%d/status", child->pid);
+ for (i = 0; i < WAIT_STEPS; i++) {
+ f = fopen(path, "r");
+ if (!f)
+ break;
+ while (fgets(line, sizeof(line), f)) {
+ if (sscanf(line, "SigCgt: %llx", &caught) == 1 &&
+ (caught & (1ULL << (SIGINT - 1))) &&
+ (caught & (1ULL << (SIGTERM - 1)))) {
+ fclose(f);
+ return true;
+ }
+ }
+ fclose(f);
+ usleep(WAIT_US);
+ }
+ return ASSERT_TRUE(false, "consumer signal handlers ready");
+}
+
+static bool emit_record(struct bpftool_ringbuf *skel, int record)
+{
+ char packet[64] = {};
+
+ LIBBPF_OPTS(bpf_test_run_opts, opts,
+ .data_in = packet,
+ .data_size_in = sizeof(packet),
+ );
+
+ skel->bss->record = record;
+ return ASSERT_OK(bpf_prog_test_run_opts(bpf_program__fd(skel->progs.produce),
+ &opts), "produce record") &&
+ ASSERT_OK(skel->bss->output_err, "ringbuf output");
+}
+
+static bool consumed(unsigned long *position, unsigned long expected)
+{
+ int i;
+
+ for (i = 0; i < WAIT_STEPS; i++) {
+ if (__atomic_load_n(position, __ATOMIC_ACQUIRE) == expected)
+ return true;
+ usleep(WAIT_US);
+ }
+ return ASSERT_EQ(*position, expected, "consumer position");
+}
+
+static void check_json(char *output, const char *expected)
+{
+ char *src = output, *dst = output;
+ bool quoted = false, escaped = false;
+
+ /* Ignore formatting whitespace while checking the entire JSON document. */
+ while (*src) {
+ if (quoted || !isspace((unsigned char)*src))
+ *dst++ = *src;
+ if (!escaped && *src == '"')
+ quoted = !quoted;
+ escaped = !escaped && quoted && *src == '\\';
+ src++;
+ }
+ *dst = '\0';
+ ASSERT_STREQ(output, expected, "JSON records");
+}
+
+static void test_consumer(bool json, bool idle, int signo)
+{
+ struct consumer child = { .pid = -1, .fd = -1 };
+ struct bpftool_ringbuf *skel;
+ unsigned long *position = MAP_FAILED;
+ int page_size = getpagesize(), fd;
+ char output[4096];
+ struct pollfd pfd;
+
+ skel = bpftool_ringbuf__open();
+ if (!ASSERT_OK_PTR(skel, "open"))
+ return;
+ bpf_map__set_max_entries(skel->maps.ringbuf, page_size);
+ if (!ASSERT_OK(bpftool_ringbuf__load(skel), "load"))
+ goto out;
+ fd = bpf_map__fd(skel->maps.ringbuf);
+ position = mmap(NULL, page_size, PROT_READ, MAP_SHARED, fd, 0);
+ if (!ASSERT_NEQ(position, MAP_FAILED, "consumer mmap"))
+ goto out;
+ if (!idle && (!emit_record(skel, 0) || !emit_record(skel, 1)))
+ goto out;
+ if (!consumer_start(&child, fd, json, NULL, false, false) ||
+ !consumer_ready(&child))
+ goto out;
+ if (!idle) {
+ /* Both prefilled records occupy 16 bytes including their headers. */
+ if (!consumed(position, 32))
+ goto out;
+ pfd = (struct pollfd) { .fd = child.fd, .events = POLLIN };
+ if (!ASSERT_GT(poll(&pfd, 1, WAIT_STEPS * WAIT_US / 1000), 0,
+ "records flushed before exit") ||
+ !ASSERT_TRUE(pfd.revents & POLLIN, "record output readable") ||
+ !emit_record(skel, 2) || !consumed(position, 64))
+ goto out;
+ }
+ if (!consumer_finish(&child, signo, true, output, sizeof(output)))
+ goto out;
+ if (json)
+ check_json(output, idle ? "[]" :
+ "[{\"size\":2,\"data\":[0,255]},"
+ "{\"size\":5,\"data\":[1,2,3,4,5]},"
+ "{\"size\":17,\"data\":[16,17,18,19,20,21,22,23,"
+ "24,25,26,27,28,29,30,31,32]}]");
+ else
+ ASSERT_STREQ(output, idle ? "" :
+ "== size: 2 =====\n00 ff\n"
+ "== size: 5 =====\n01 02 03 04 05\n"
+ "== size: 17 =====\n10 11 12 13 14 15 16 17 "
+ "18 19 1a 1b 1c 1d 1e 1f\n20\n", "plain records");
+out:
+ consumer_cleanup(&child);
+ if (position != MAP_FAILED)
+ munmap(position, page_size);
+ bpftool_ringbuf__destroy(skel);
+}
+
+static void test_perf_consumer(void)
+{
+ struct consumer child = { .pid = -1, .fd = -1 };
+ unsigned long long seconds, nanoseconds;
+ struct bpftool_ringbuf *skel;
+ char output[16384], expected[16384];
+ int nr_cpus, cpu, index, offset = 0, i, used = 0, fields;
+ struct pollfd pfd;
+
+ nr_cpus = libbpf_num_possible_cpus();
+ if (!ASSERT_GT(nr_cpus, 0, "possible cpus"))
+ return;
+ skel = bpftool_ringbuf__open();
+ if (!ASSERT_OK_PTR(skel, "open"))
+ return;
+ bpf_map__set_max_entries(skel->maps.ringbuf, getpagesize());
+ bpf_map__set_max_entries(skel->maps.perfbuf, nr_cpus);
+ if (!ASSERT_OK(bpftool_ringbuf__load(skel), "load") ||
+ !consumer_start(&child, bpf_map__fd(skel->maps.perfbuf), false,
+ NULL, false, false) || !consumer_ready(&child) ||
+ !emit_record(skel, 3))
+ goto out;
+ /* One large record flushes the existing buffered perf output callback. */
+ pfd = (struct pollfd) { .fd = child.fd, .events = POLLIN };
+ if (!ASSERT_GT(poll(&pfd, 1, WAIT_STEPS * WAIT_US / 1000), 0,
+ "perf record output") ||
+ !ASSERT_TRUE(pfd.revents & POLLIN, "perf output readable") ||
+ !consumer_finish(&child, SIGINT, true, output, sizeof(output)))
+ goto out;
+ fields = sscanf(output, "== @%llu.%llu CPU: %d index: %d =====\n%n",
+ &seconds, &nanoseconds, &cpu, &index, &offset);
+ if (!ASSERT_EQ(fields, 4, "perf header") ||
+ !ASSERT_GT(offset, 0, "perf payload offset"))
+ goto out;
+ ASSERT_GT(seconds * 1000000000ULL + nanoseconds, 0, "perf timestamp");
+ ASSERT_LT(nanoseconds, 1000000000ULL, "perf timestamp nanoseconds");
+ ASSERT_GE(cpu, 0, "perf cpu");
+ ASSERT_LT(cpu, nr_cpus, "perf cpu range");
+ ASSERT_EQ(index, cpu, "perf index");
+ for (i = 0; i < sizeof(skel->rodata->perf_payload); i++) {
+ const char *separator = !i ? "" : !(i % 16) ? "\n" :
+ !(i % 8) ? " " : " ";
+
+ used += snprintf(expected + used, sizeof(expected) - used,
+ "%s%02x", separator, i == 1 ? 0xff : 0);
+ }
+ snprintf(expected + used, sizeof(expected) - used, "\n");
+ ASSERT_STREQ(output + offset, expected, "perf payload");
+out:
+ consumer_cleanup(&child);
+ bpftool_ringbuf__destroy(skel);
+}
+
+static void test_reject(enum bpf_map_type type, const char *option, bool pair, bool json)
+{
+ struct consumer child = { .pid = -1, .fd = -1 };
+ bool ring = type == BPF_MAP_TYPE_RINGBUF || type == BPF_MAP_TYPE_USER_RINGBUF;
+ const char *expected = option ?
+ "{\"error\":\"ring buffer maps do not support cpu or index arguments\"}" :
+ "{\"error\":\"map is not a perf event array or ring buffer\"}";
+ char output[4096];
+ int fd;
+
+ fd = bpf_map_create(type, NULL, ring ? 0 : 4, ring ? 0 : 4,
+ ring ? getpagesize() : 1, NULL);
+ if (!ASSERT_GE(fd, 0, "create map"))
+ return;
+ if (consumer_start(&child, fd, json, option, pair, true) &&
+ consumer_finish(&child, 0, false, output, sizeof(output))) {
+ if (json)
+ check_json(output, expected);
+ else
+ ASSERT_GT(strlen(output), 0, "error diagnostic");
+ }
+ consumer_cleanup(&child);
+ close(fd);
+}
+
+void test_bpftool_ringbuf(void)
+{
+ if (test__start_subtest("perf_event_array"))
+ test_perf_consumer();
+ if (test__start_subtest("plain"))
+ test_consumer(false, false, SIGINT);
+ if (test__start_subtest("json"))
+ test_consumer(true, false, SIGTERM);
+ if (test__start_subtest("idle_sigint"))
+ test_consumer(false, true, SIGINT);
+ if (test__start_subtest("idle_sigterm_json"))
+ test_consumer(true, true, SIGTERM);
+ if (test__start_subtest("reject_array"))
+ test_reject(BPF_MAP_TYPE_ARRAY, NULL, false, false);
+ if (test__start_subtest("reject_user_ringbuf"))
+ test_reject(BPF_MAP_TYPE_USER_RINGBUF, NULL, false, false);
+ if (test__start_subtest("reject_cpu"))
+ test_reject(BPF_MAP_TYPE_RINGBUF, "cpu", false, false);
+ if (test__start_subtest("reject_index"))
+ test_reject(BPF_MAP_TYPE_RINGBUF, "index", false, false);
+ if (test__start_subtest("reject_cpu_index"))
+ test_reject(BPF_MAP_TYPE_RINGBUF, "cpu", true, false);
+ if (test__start_subtest("reject_user_ringbuf_json"))
+ test_reject(BPF_MAP_TYPE_USER_RINGBUF, NULL, false, true);
+ if (test__start_subtest("reject_cpu_index_json"))
+ test_reject(BPF_MAP_TYPE_RINGBUF, "cpu", true, true);
+}
diff --git a/tools/testing/selftests/bpf/progs/bpftool_ringbuf.c b/tools/testing/selftests/bpf/progs/bpftool_ringbuf.c
new file mode 100644
index 00000000000..bbbc93c30a8
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/bpftool_ringbuf.c
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+
+struct {
+ __uint(type, BPF_MAP_TYPE_RINGBUF);
+} ringbuf SEC(".maps");
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
+ __uint(key_size, sizeof(__u32));
+ __uint(value_size, sizeof(__u32));
+ __uint(max_entries, 1);
+} perfbuf SEC(".maps");
+
+/*
+ * Include the u32 raw size in perf's 8-byte alignment to avoid padding.
+ * Keep plain output large enough to flush stdio before the consumer exits.
+ */
+const unsigned char perf_payload[4092] = { 0x00, 0xff };
+
+int record;
+int output_err;
+
+SEC("socket")
+int produce(struct __sk_buff *skb)
+{
+ unsigned char first[] = { 0x00, 0xff };
+ unsigned char second[] = { 1, 2, 3, 4, 5 };
+ unsigned char third[] = {
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
+ };
+
+ if (record == 0)
+ output_err = bpf_ringbuf_output(&ringbuf, first, sizeof(first), 0);
+ else if (record == 1)
+ output_err = bpf_ringbuf_output(&ringbuf, second, sizeof(second), 0);
+ else if (record == 2)
+ output_err = bpf_ringbuf_output(&ringbuf, third, sizeof(third), 0);
+ else
+ output_err = bpf_perf_event_output(skb, &perfbuf, BPF_F_CURRENT_CPU,
+ (void *)perf_payload, sizeof(perf_payload));
+ return 0;
+}
+
+char LICENSE[] SEC("license") = "GPL";
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH bpf-next 2/2] selftests/bpf: Cover bpftool ring buffer event_pipe
2026-09-06 17:07 ` [PATCH bpf-next 2/2] selftests/bpf: Cover bpftool ring buffer event_pipe Tianyi Chen
@ 2026-09-06 17:18 ` sashiko-bot
0 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-09-06 17:18 UTC (permalink / raw)
To: Tianyi Chen; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The commit message claims to test 'pinned map selection', but it is never implemented or exercised in the test code.
- [Medium] The commit message claims to check 'pretty JSON output', but the test never invokes bpftool with the pretty print flag.
--
commit 7060e09603777e15c3fff0503b5038d8629bb63d
Author: Tianyi Chen <hi@tychen.cc>
selftests/bpf: Cover bpftool ring buffer event_pipe
This commit adds selftests for bpftool's ring buffer and perf event pipe
output. It aims to verify plain, JSON, and pretty JSON output formats, as
well as map selection methods and error handling.
> diff --git a/tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c b/tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c
> new file mode 100644
> index 0000000000000..2099bf1020930
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c
[ ... ]
> @@ -0,0 +1,381 @@
[ ... ]
> +static bool consumer_start(struct consumer *child, int map_fd, bool json,
> + const char *option, bool pair, bool capture_errors)
> +{
> + struct bpf_map_info info = {};
> + __u32 len = sizeof(info);
> + const char *path = bpftool_path();
> + char *argv[12], id[16];
> + int out[2], ready[2], n = 0, err, err_fd;
> + struct pollfd pfd;
> +
> + if (!ASSERT_OK_PTR(path, "bpftool path (set BPFTOOL to override)") ||
> + !ASSERT_OK(bpf_map_get_info_by_fd(map_fd, &info, &len), "map info"))
> + return false;
> + snprintf(id, sizeof(id), "%u", info.id);
> + argv[n++] = (char *)path;
> + if (json)
> + argv[n++] = "-j";
[Severity: Medium]
The commit message states the test will check "complete plain, JSON and
pretty JSON output", but in consumer_start() the test only ever appends the
"-j" flag.
Is there a missing test case or flag assignment here to exercise the "-p"
(pretty JSON) bpftool output path as promised?
> + argv[n++] = "map";
> + argv[n++] = "event_pipe";
> + argv[n++] = "id";
> + argv[n++] = id;
[Severity: Medium]
The commit message claims to "Exercise ID and pinned map selection", but
consumer_start() hardcodes "id" for the map selection argument.
Is the logic to use "pinned" paths missing from this test implementation?
> + if (option) {
> + argv[n++] = (char *)option;
> + argv[n++] = "0";
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260906170747.1212378-1-hi@tychen.cc?part=2
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH bpf-next v2 0/2] bpftool: Consume ring buffer maps with event_pipe
2026-09-06 17:07 [PATCH bpf-next 0/2] bpftool: Consume ring buffer maps with event_pipe Tianyi Chen
2026-09-06 17:07 ` [PATCH bpf-next 1/2] bpftool: Read " Tianyi Chen
2026-09-06 17:07 ` [PATCH bpf-next 2/2] selftests/bpf: Cover bpftool ring buffer event_pipe Tianyi Chen
@ 2026-09-07 1:21 ` Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v2 1/2] bpftool: Read " Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v2 2/2] selftests/bpf: Cover bpftool ring buffer event_pipe Tianyi Chen
2 siblings, 2 replies; 9+ messages in thread
From: Tianyi Chen @ 2026-09-07 1:21 UTC (permalink / raw)
To: bpf; +Cc: Tianyi Chen, Quentin Monnet, linux-kselftest
Extend map event_pipe to BPF ring buffer maps using libbpf's ring buffer
manager. Output records in plain or JSON format, reject CPU/index
selectors for ring buffers, and document the shared consumer position.
Changes in v2:
- Add actual -p invocations and pinned map selection, which the v1
selftest commit message described without implementing those cases.
- Check the complete pretty-printed document, test plain/JSON/pretty
output through pinned paths, and test pretty output for an empty stream.
- Clean up pins and their unique directories on success and failure.
Validation:
- All 17 ringbuf subtests passed, including the existing perf event array
regression, in the integration run described below.
- The event_pipe implementation is unchanged from v1.
v1: https://lore.kernel.org/r/20260906170747.1212378-1-hi@tychen.cc
Request: https://github.com/libbpf/bpftool/issues/54
Integration validation: the flags, ring-buffer and batch v2 series plus
recursive dump v3 built together with LLVM 20. All 69 focused BPF subtests
passed in an x86-64 KVM guest running Linux 7.3.0-rc1, with no skips or
failures. The bpftool-map man page also built successfully. This is a
focused run, not the full BPF selftest suite.
Tianyi Chen (2):
bpftool: Read ring buffer maps with event_pipe
selftests/bpf: Cover bpftool ring buffer event_pipe
.../bpf/bpftool/Documentation/bpftool-map.rst | 15 +-
tools/bpf/bpftool/bash-completion/bpftool | 4 +-
tools/bpf/bpftool/map_perf_ring.c | 87 +++-
.../bpf/prog_tests/bpftool_ringbuf.c | 425 ++++++++++++++++++
.../selftests/bpf/progs/bpftool_ringbuf.c | 47 ++
5 files changed, 554 insertions(+), 24 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c
create mode 100644 tools/testing/selftests/bpf/progs/bpftool_ringbuf.c
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH bpf-next v2 1/2] bpftool: Read ring buffer maps with event_pipe
2026-09-07 1:21 ` [PATCH bpf-next v2 0/2] bpftool: Consume ring buffer maps with event_pipe Tianyi Chen
@ 2026-09-07 1:21 ` Tianyi Chen
2026-09-07 2:20 ` bot+bpf-ci
2026-09-07 1:21 ` [PATCH bpf-next v2 2/2] selftests/bpf: Cover bpftool ring buffer event_pipe Tianyi Chen
1 sibling, 1 reply; 9+ messages in thread
From: Tianyi Chen @ 2026-09-07 1:21 UTC (permalink / raw)
To: bpf; +Cc: Tianyi Chen, Quentin Monnet, linux-kselftest
Allow map event_pipe to consume BPF_MAP_TYPE_RINGBUF maps using
libbpf's ring buffer manager. Print each record's size and raw bytes
in plain or JSON output, and flush output as records arrive.
Reject CPU and index selectors for ring buffers and retain the existing
perf event array path. Keep signal handlers limited to setting a stop
flag, reset that flag for each command, and print the stopping message
on stderr from the main path. Close the JSON array on poll errors and
preserve output errors returned by the ring buffer callback.
Document that consuming a ring buffer advances its shared consumer
position and add ring buffer maps to event_pipe completion.
Link: https://github.com/libbpf/bpftool/issues/54
Assisted-by: Codex:GPT-6
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
.../bpf/bpftool/Documentation/bpftool-map.rst | 15 +++-
tools/bpf/bpftool/bash-completion/bpftool | 4 +-
tools/bpf/bpftool/map_perf_ring.c | 87 ++++++++++++++-----
3 files changed, 82 insertions(+), 24 deletions(-)
diff --git a/tools/bpf/bpftool/Documentation/bpftool-map.rst b/tools/bpf/bpftool/Documentation/bpftool-map.rst
index 5daf3de5c74..c44e6f797e5 100644
--- a/tools/bpf/bpftool/Documentation/bpftool-map.rst
+++ b/tools/bpf/bpftool/Documentation/bpftool-map.rst
@@ -120,7 +120,8 @@ bpftool map pin *MAP* *FILE*
character ('.'), which is reserved for future extensions of *bpffs*.
bpftool map event_pipe *MAP* [cpu *N* index *M*]
- Read events from a **BPF_MAP_TYPE_PERF_EVENT_ARRAY** map.
+ Read events from a **BPF_MAP_TYPE_PERF_EVENT_ARRAY** or
+ **BPF_MAP_TYPE_RINGBUF** map.
Install perf rings into a perf event array map and dump output of any
**bpf_perf_event_output**\ () call in the kernel. By default read the
@@ -134,6 +135,18 @@ bpftool map event_pipe *MAP* [cpu *N* index *M*]
existing ring. Any other application will stop receiving events if it
installed its rings earlier.
+ For a ring buffer map, consume records submitted by BPF programs, including
+ records already queued before the command starts. **cpu** and **index**
+ are not supported. Each record is printed as raw bytes, including embedded
+ zero bytes. Plain output reports the record size followed by hexadecimal
+ bytes; JSON output contains **size** and **data** fields, with **data** an
+ array of byte values. Ring buffer records have no implicit CPU or timestamp.
+
+ Ring buffers support a single consumer. This command advances the shared
+ consumer position and must not run alongside another consumer of the same
+ map; it does not provide a passive view of events. **BPF_MAP_TYPE_USER_RINGBUF**
+ maps are not supported.
+
bpftool map peek *MAP*
Peek next value in the queue or stack.
diff --git a/tools/bpf/bpftool/bash-completion/bpftool b/tools/bpf/bpftool/bash-completion/bpftool
index 75cbcb512eb..1750b488c9e 100644
--- a/tools/bpf/bpftool/bash-completion/bpftool
+++ b/tools/bpf/bpftool/bash-completion/bpftool
@@ -878,11 +878,11 @@ _bpftool()
return 0
;;
id)
- _bpftool_get_map_ids_for_type perf_event_array
+ _bpftool_get_map_ids_for_type '"type": "\(perf_event_array\|ringbuf\)"'
return 0
;;
name)
- _bpftool_get_map_names_for_type perf_event_array
+ _bpftool_get_map_names_for_type '"type": "\(perf_event_array\|ringbuf\)"'
return 0
;;
cpu)
diff --git a/tools/bpf/bpftool/map_perf_ring.c b/tools/bpf/bpftool/map_perf_ring.c
index bcb767e2d67..7d555331f44 100644
--- a/tools/bpf/bpftool/map_perf_ring.c
+++ b/tools/bpf/bpftool/map_perf_ring.c
@@ -27,7 +27,7 @@
#define MMAP_PAGE_CNT 16
-static volatile bool stop;
+static volatile sig_atomic_t stop;
struct perf_event_sample {
struct perf_event_header header;
@@ -44,7 +44,6 @@ struct perf_event_lost {
static void int_exit(int signo)
{
- fprintf(stderr, "Stopping...\n");
stop = true;
}
@@ -107,6 +106,27 @@ print_bpf_output(void *private_data, int cpu, struct perf_event_header *event)
return LIBBPF_PERF_EVENT_CONT;
}
+static int print_ringbuf_output(void *ctx, void *data, size_t size)
+{
+ if (json_output) {
+ jsonw_start_object(json_wtr);
+ jsonw_uint_field(json_wtr, "size", size);
+ jsonw_name(json_wtr, "data");
+ print_data_json(data, size);
+ jsonw_end_object(json_wtr);
+ } else {
+ printf("== size: %zu =====\n", size);
+ fprint_hex(stdout, data, size, " ");
+ printf("\n");
+ }
+
+ if (fflush(stdout))
+ return errno ? -errno : -EIO;
+
+ /* A producer can keep poll() busy even after a signal arrives. */
+ return stop ? -EINTR : 0;
+}
+
int do_event_pipe(int argc, char **argv)
{
struct perf_event_attr perf_attr = {
@@ -123,18 +143,26 @@ int do_event_pipe(int argc, char **argv)
.cpu = -1,
.idx = -1,
};
- struct perf_buffer *pb;
+ struct perf_buffer *pb = NULL;
+ struct ring_buffer *rb = NULL;
__u32 map_info_len;
int err, map_fd;
+ stop = false;
map_info_len = sizeof(map_info);
map_fd = map_parse_fd_and_info(&argc, &argv, &map_info, &map_info_len,
0);
if (map_fd < 0)
return -1;
- if (map_info.type != BPF_MAP_TYPE_PERF_EVENT_ARRAY) {
- p_err("map is not a perf event array");
+ if (map_info.type != BPF_MAP_TYPE_PERF_EVENT_ARRAY &&
+ map_info.type != BPF_MAP_TYPE_RINGBUF) {
+ p_err("map is not a perf event array or ring buffer");
+ goto err_close_map;
+ }
+
+ if (map_info.type == BPF_MAP_TYPE_RINGBUF && argc) {
+ p_err("ring buffer maps do not support cpu or index arguments");
goto err_close_map;
}
@@ -184,15 +212,24 @@ int do_event_pipe(int argc, char **argv)
ctx.idx = 0;
}
- opts.cpu_cnt = ctx.all_cpus ? 0 : 1;
- opts.cpus = &ctx.cpu;
- opts.map_keys = &ctx.idx;
- pb = perf_buffer__new_raw(map_fd, MMAP_PAGE_CNT, &perf_attr,
- print_bpf_output, &ctx, &opts);
- if (!pb) {
- p_err("failed to create perf buffer: %s (%d)",
- strerror(errno), errno);
- goto err_close_map;
+ if (map_info.type == BPF_MAP_TYPE_RINGBUF) {
+ rb = ring_buffer__new(map_fd, print_ringbuf_output, NULL, NULL);
+ if (!rb) {
+ p_err("failed to create ring buffer: %s (%d)",
+ strerror(errno), errno);
+ goto err_close_map;
+ }
+ } else {
+ opts.cpu_cnt = ctx.all_cpus ? 0 : 1;
+ opts.cpus = &ctx.cpu;
+ opts.map_keys = &ctx.idx;
+ pb = perf_buffer__new_raw(map_fd, MMAP_PAGE_CNT, &perf_attr,
+ print_bpf_output, &ctx, &opts);
+ if (!pb) {
+ p_err("failed to create perf buffer: %s (%d)",
+ strerror(errno), errno);
+ goto err_close_map;
+ }
}
signal(SIGINT, int_exit);
@@ -202,25 +239,33 @@ int do_event_pipe(int argc, char **argv)
if (json_output)
jsonw_start_array(json_wtr);
+ err = 0;
while (!stop) {
- err = perf_buffer__poll(pb, 200);
+ err = rb ? ring_buffer__poll(rb, 200) : perf_buffer__poll(pb, 200);
if (err < 0 && err != -EINTR) {
- p_err("perf buffer polling failed: %s (%d)",
- strerror(errno), errno);
- goto err_close_pb;
+ fprintf(stderr, "Error: %s buffer polling failed: %s (%d)\n",
+ rb ? "ring" : "perf", strerror(-err), -err);
+ break;
}
+ err = 0;
}
+ if (stop)
+ fprintf(stderr, "Stopping...\n");
if (json_output)
jsonw_end_array(json_wtr);
+ if (fflush(stdout)) {
+ fprintf(stderr, "Error: failed to write events: %s\n", strerror(errno));
+ err = -1;
+ }
+ ring_buffer__free(rb);
perf_buffer__free(pb);
+ /* Both buffer managers borrow map_fd. */
close(map_fd);
- return 0;
+ return err < 0 ? -1 : 0;
-err_close_pb:
- perf_buffer__free(pb);
err_close_map:
close(map_fd);
return -1;
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH bpf-next v2 2/2] selftests/bpf: Cover bpftool ring buffer event_pipe
2026-09-07 1:21 ` [PATCH bpf-next v2 0/2] bpftool: Consume ring buffer maps with event_pipe Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v2 1/2] bpftool: Read " Tianyi Chen
@ 2026-09-07 1:21 ` Tianyi Chen
1 sibling, 0 replies; 9+ messages in thread
From: Tianyi Chen @ 2026-09-07 1:21 UTC (permalink / raw)
To: bpf; +Cc: Tianyi Chen, Quentin Monnet, linux-kselftest
Produce known ring buffer records and check complete plain, JSON and
pretty JSON output. Exercise ID and pinned map selection, SIGINT and
SIGTERM shutdown, empty streams and invalid map types or selectors.
Also produce a perf event sample and check its existing header and raw
payload output. Use a payload whose size plus the raw sample length
field is aligned to eight bytes so the expected bytes exclude implicit
perf padding.
Assisted-by: Codex:GPT-6
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
.../bpf/prog_tests/bpftool_ringbuf.c | 425 ++++++++++++++++++
.../selftests/bpf/progs/bpftool_ringbuf.c | 47 ++
2 files changed, 472 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c
create mode 100644 tools/testing/selftests/bpf/progs/bpftool_ringbuf.c
diff --git a/tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c b/tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c
new file mode 100644
index 00000000000..1a4ae88c05a
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/bpftool_ringbuf.c
@@ -0,0 +1,425 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <ctype.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <signal.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <test_progs.h>
+#include "bpftool_ringbuf.skel.h"
+
+#define WAIT_STEPS 500
+#define WAIT_US 10000
+
+struct consumer {
+ pid_t pid;
+ int fd;
+};
+
+static const char *bpftool_path(void)
+{
+ const char *path = getenv("BPFTOOL");
+
+ if (path)
+ return access(path, X_OK) ? NULL : path;
+ if (!access("./tools/sbin/bpftool", X_OK))
+ return "./tools/sbin/bpftool";
+ if (!access("../tools/sbin/bpftool", X_OK))
+ return "../tools/sbin/bpftool";
+ return NULL;
+}
+
+static void consumer_cleanup(struct consumer *child)
+{
+ if (child->pid > 0) {
+ kill(child->pid, SIGKILL);
+ while (waitpid(child->pid, NULL, 0) < 0 && errno == EINTR)
+ ;
+ child->pid = -1;
+ }
+ if (child->fd >= 0) {
+ close(child->fd);
+ child->fd = -1;
+ }
+}
+
+static bool consumer_start(struct consumer *child, int map_fd, const char *format,
+ const char *pin_path, const char *option,
+ bool pair, bool capture_errors)
+{
+ struct bpf_map_info info = {};
+ __u32 len = sizeof(info);
+ const char *path = bpftool_path();
+ char *argv[12], id[16];
+ int out[2], ready[2], n = 0, err, err_fd;
+ struct pollfd pfd;
+
+ if (!ASSERT_OK_PTR(path, "bpftool path (set BPFTOOL to override)") ||
+ !ASSERT_OK(bpf_map_get_info_by_fd(map_fd, &info, &len), "map info"))
+ return false;
+ snprintf(id, sizeof(id), "%u", info.id);
+ argv[n++] = (char *)path;
+ if (format)
+ argv[n++] = (char *)format;
+ argv[n++] = "map";
+ argv[n++] = "event_pipe";
+ argv[n++] = pin_path ? "pinned" : "id";
+ argv[n++] = pin_path ? (char *)pin_path : id;
+ if (option) {
+ argv[n++] = (char *)option;
+ argv[n++] = "0";
+ if (pair) {
+ argv[n++] = "index";
+ argv[n++] = "0";
+ }
+ }
+ argv[n] = NULL;
+ if (!ASSERT_OK(pipe2(out, O_CLOEXEC), "output pipe"))
+ return false;
+ if (!ASSERT_OK(pipe2(ready, O_CLOEXEC), "exec pipe")) {
+ close(out[0]);
+ close(out[1]);
+ return false;
+ }
+ child->pid = fork();
+ if (!child->pid) {
+ close(out[0]);
+ close(ready[0]);
+ err_fd = capture_errors ? out[1] : open("/dev/null", O_WRONLY);
+ if (dup2(out[1], STDOUT_FILENO) < 0 ||
+ dup2(err_fd, STDERR_FILENO) < 0)
+ goto exec_fail;
+ if (!capture_errors)
+ close(err_fd);
+ close(out[1]);
+ execv(path, argv);
+exec_fail:
+ err = errno;
+ write(ready[1], &err, sizeof(err));
+ _exit(127);
+ }
+ close(out[1]);
+ close(ready[1]);
+ child->fd = out[0];
+ pfd = (struct pollfd) { .fd = ready[0], .events = POLLIN };
+ /* EOF on the close-on-exec pipe distinguishes exec from inherited handlers. */
+ err = child->pid > 0 ? poll(&pfd, 1, WAIT_STEPS * WAIT_US / 1000) : -1;
+ if (!ASSERT_GT(child->pid, 0, "fork") ||
+ !ASSERT_GT(err, 0, "exec timeout") ||
+ !ASSERT_EQ(read(ready[0], &err, sizeof(err)), 0, "exec")) {
+ close(ready[0]);
+ consumer_cleanup(child);
+ return false;
+ }
+ close(ready[0]);
+ return true;
+}
+
+static bool consumer_finish(struct consumer *child, int signo, bool success,
+ char *output, size_t size)
+{
+ int status = 0, i;
+ pid_t ret = 0;
+ ssize_t n;
+ size_t used = 0;
+
+ if (!ASSERT_OK(fcntl(child->fd, F_SETFL, O_NONBLOCK), "nonblocking output"))
+ return false;
+ if (signo && !ASSERT_OK(kill(child->pid, signo), "signal consumer"))
+ return false;
+ for (i = 0; i < WAIT_STEPS; i++) {
+ while (used < size - 1 &&
+ (n = read(child->fd, output + used, size - 1 - used)) > 0)
+ used += n;
+ ret = waitpid(child->pid, &status, WNOHANG);
+ if (ret == child->pid)
+ break;
+ if (ret < 0 && errno != EINTR)
+ break;
+ usleep(WAIT_US);
+ }
+ if (!ASSERT_EQ(ret, child->pid, "bounded consumer exit"))
+ return false;
+ child->pid = -1;
+ while (used < size - 1 && (n = read(child->fd, output + used, size - 1 - used)) > 0)
+ used += n;
+ output[used] = '\0';
+ return ASSERT_TRUE(WIFEXITED(status), "normal exit") &&
+ ASSERT_EQ(WEXITSTATUS(status) == 0, success, "exit status");
+}
+
+static bool consumer_ready(struct consumer *child)
+{
+ unsigned long long caught;
+ char path[64], line[256];
+ int i;
+ FILE *f;
+
+ snprintf(path, sizeof(path), "/proc/%d/status", child->pid);
+ for (i = 0; i < WAIT_STEPS; i++) {
+ f = fopen(path, "r");
+ if (!f)
+ break;
+ while (fgets(line, sizeof(line), f)) {
+ if (sscanf(line, "SigCgt: %llx", &caught) == 1 &&
+ (caught & (1ULL << (SIGINT - 1))) &&
+ (caught & (1ULL << (SIGTERM - 1)))) {
+ fclose(f);
+ return true;
+ }
+ }
+ fclose(f);
+ usleep(WAIT_US);
+ }
+ return ASSERT_TRUE(false, "consumer signal handlers ready");
+}
+
+static bool emit_record(struct bpftool_ringbuf *skel, int record)
+{
+ char packet[64] = {};
+
+ LIBBPF_OPTS(bpf_test_run_opts, opts,
+ .data_in = packet,
+ .data_size_in = sizeof(packet),
+ );
+
+ skel->bss->record = record;
+ return ASSERT_OK(bpf_prog_test_run_opts(bpf_program__fd(skel->progs.produce),
+ &opts), "produce record") &&
+ ASSERT_OK(skel->bss->output_err, "ringbuf output");
+}
+
+static bool consumed(unsigned long *position, unsigned long expected)
+{
+ int i;
+
+ for (i = 0; i < WAIT_STEPS; i++) {
+ if (__atomic_load_n(position, __ATOMIC_ACQUIRE) == expected)
+ return true;
+ usleep(WAIT_US);
+ }
+ return ASSERT_EQ(*position, expected, "consumer position");
+}
+
+static void check_json(char *output, const char *expected)
+{
+ char *src = output, *dst = output;
+ bool quoted = false, escaped = false;
+
+ /* Ignore formatting whitespace while checking the entire JSON document. */
+ while (*src) {
+ if (quoted || !isspace((unsigned char)*src))
+ *dst++ = *src;
+ if (!escaped && *src == '"')
+ quoted = !quoted;
+ escaped = !escaped && quoted && *src == '\\';
+ src++;
+ }
+ *dst = '\0';
+ ASSERT_STREQ(output, expected, "JSON records");
+}
+
+static void test_consumer(const char *format, bool idle, int signo, bool pinned)
+{
+ struct consumer child = { .pid = -1, .fd = -1 };
+ struct bpftool_ringbuf *skel;
+ unsigned long *position = MAP_FAILED;
+ int page_size = getpagesize(), fd;
+ char pin_dir[] = "/sys/fs/bpf/bpftool_ringbuf_XXXXXX";
+ char pin_path[sizeof(pin_dir) + sizeof("/map")];
+ bool dir_created = false, map_pinned = false;
+ char output[4096];
+ struct pollfd pfd;
+
+ skel = bpftool_ringbuf__open();
+ if (!ASSERT_OK_PTR(skel, "open"))
+ return;
+ bpf_map__set_max_entries(skel->maps.ringbuf, page_size);
+ if (!ASSERT_OK(bpftool_ringbuf__load(skel), "load"))
+ goto out;
+ fd = bpf_map__fd(skel->maps.ringbuf);
+ if (pinned) {
+ if (!ASSERT_OK_PTR(mkdtemp(pin_dir), "create pin directory"))
+ goto out;
+ dir_created = true;
+ snprintf(pin_path, sizeof(pin_path), "%s/map", pin_dir);
+ if (!ASSERT_OK(bpf_obj_pin(fd, pin_path), "pin ringbuf"))
+ goto out;
+ map_pinned = true;
+ }
+ position = mmap(NULL, page_size, PROT_READ, MAP_SHARED, fd, 0);
+ if (!ASSERT_NEQ(position, MAP_FAILED, "consumer mmap"))
+ goto out;
+ if (!idle && (!emit_record(skel, 0) || !emit_record(skel, 1)))
+ goto out;
+ if (!consumer_start(&child, fd, format, pinned ? pin_path : NULL,
+ NULL, false, false) ||
+ !consumer_ready(&child))
+ goto out;
+ if (!idle) {
+ /* Both prefilled records occupy 16 bytes including their headers. */
+ if (!consumed(position, 32))
+ goto out;
+ pfd = (struct pollfd) { .fd = child.fd, .events = POLLIN };
+ if (!ASSERT_GT(poll(&pfd, 1, WAIT_STEPS * WAIT_US / 1000), 0,
+ "records flushed before exit") ||
+ !ASSERT_TRUE(pfd.revents & POLLIN, "record output readable") ||
+ !emit_record(skel, 2) || !consumed(position, 64))
+ goto out;
+ }
+ if (!consumer_finish(&child, signo, true, output, sizeof(output)))
+ goto out;
+ if (format && !strcmp(format, "-p"))
+ ASSERT_STREQ(output, idle ? "[]\n" :
+ "[{\n"
+ " \"size\": 2,\n"
+ " \"data\": [0,255\n"
+ " ]\n"
+ " },{\n"
+ " \"size\": 5,\n"
+ " \"data\": [1,2,3,4,5\n"
+ " ]\n"
+ " },{\n"
+ " \"size\": 17,\n"
+ " \"data\": [16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32\n"
+ " ]\n"
+ " }\n"
+ "]\n", "pretty JSON records");
+ else if (format)
+ check_json(output, idle ? "[]" :
+ "[{\"size\":2,\"data\":[0,255]},"
+ "{\"size\":5,\"data\":[1,2,3,4,5]},"
+ "{\"size\":17,\"data\":[16,17,18,19,20,21,22,23,"
+ "24,25,26,27,28,29,30,31,32]}]");
+ else
+ ASSERT_STREQ(output, idle ? "" :
+ "== size: 2 =====\n00 ff\n"
+ "== size: 5 =====\n01 02 03 04 05\n"
+ "== size: 17 =====\n10 11 12 13 14 15 16 17 "
+ "18 19 1a 1b 1c 1d 1e 1f\n20\n", "plain records");
+out:
+ consumer_cleanup(&child);
+ if (position != MAP_FAILED)
+ munmap(position, page_size);
+ if (map_pinned)
+ ASSERT_OK(unlink(pin_path), "unpin ringbuf");
+ if (dir_created)
+ ASSERT_OK(rmdir(pin_dir), "remove pin directory");
+ bpftool_ringbuf__destroy(skel);
+}
+
+static void test_perf_consumer(void)
+{
+ struct consumer child = { .pid = -1, .fd = -1 };
+ unsigned long long seconds, nanoseconds;
+ struct bpftool_ringbuf *skel;
+ char output[16384], expected[16384];
+ int nr_cpus, cpu, index, offset = 0, i, used = 0, fields;
+ struct pollfd pfd;
+
+ nr_cpus = libbpf_num_possible_cpus();
+ if (!ASSERT_GT(nr_cpus, 0, "possible cpus"))
+ return;
+ skel = bpftool_ringbuf__open();
+ if (!ASSERT_OK_PTR(skel, "open"))
+ return;
+ bpf_map__set_max_entries(skel->maps.ringbuf, getpagesize());
+ bpf_map__set_max_entries(skel->maps.perfbuf, nr_cpus);
+ if (!ASSERT_OK(bpftool_ringbuf__load(skel), "load") ||
+ !consumer_start(&child, bpf_map__fd(skel->maps.perfbuf), NULL,
+ NULL, NULL, false, false) || !consumer_ready(&child) ||
+ !emit_record(skel, 3))
+ goto out;
+ /* One large record flushes the existing buffered perf output callback. */
+ pfd = (struct pollfd) { .fd = child.fd, .events = POLLIN };
+ if (!ASSERT_GT(poll(&pfd, 1, WAIT_STEPS * WAIT_US / 1000), 0,
+ "perf record output") ||
+ !ASSERT_TRUE(pfd.revents & POLLIN, "perf output readable") ||
+ !consumer_finish(&child, SIGINT, true, output, sizeof(output)))
+ goto out;
+ fields = sscanf(output, "== @%llu.%llu CPU: %d index: %d =====\n%n",
+ &seconds, &nanoseconds, &cpu, &index, &offset);
+ if (!ASSERT_EQ(fields, 4, "perf header") ||
+ !ASSERT_GT(offset, 0, "perf payload offset"))
+ goto out;
+ ASSERT_GT(seconds * 1000000000ULL + nanoseconds, 0, "perf timestamp");
+ ASSERT_LT(nanoseconds, 1000000000ULL, "perf timestamp nanoseconds");
+ ASSERT_GE(cpu, 0, "perf cpu");
+ ASSERT_LT(cpu, nr_cpus, "perf cpu range");
+ ASSERT_EQ(index, cpu, "perf index");
+ for (i = 0; i < sizeof(skel->rodata->perf_payload); i++) {
+ const char *separator = !i ? "" : !(i % 16) ? "\n" :
+ !(i % 8) ? " " : " ";
+
+ used += snprintf(expected + used, sizeof(expected) - used,
+ "%s%02x", separator, i == 1 ? 0xff : 0);
+ }
+ snprintf(expected + used, sizeof(expected) - used, "\n");
+ ASSERT_STREQ(output + offset, expected, "perf payload");
+out:
+ consumer_cleanup(&child);
+ bpftool_ringbuf__destroy(skel);
+}
+
+static void test_reject(enum bpf_map_type type, const char *option, bool pair, bool json)
+{
+ struct consumer child = { .pid = -1, .fd = -1 };
+ bool ring = type == BPF_MAP_TYPE_RINGBUF || type == BPF_MAP_TYPE_USER_RINGBUF;
+ const char *expected = option ?
+ "{\"error\":\"ring buffer maps do not support cpu or index arguments\"}" :
+ "{\"error\":\"map is not a perf event array or ring buffer\"}";
+ char output[4096];
+ int fd;
+
+ fd = bpf_map_create(type, NULL, ring ? 0 : 4, ring ? 0 : 4,
+ ring ? getpagesize() : 1, NULL);
+ if (!ASSERT_GE(fd, 0, "create map"))
+ return;
+ if (consumer_start(&child, fd, json ? "-j" : NULL, NULL, option, pair, true) &&
+ consumer_finish(&child, 0, false, output, sizeof(output))) {
+ if (json)
+ check_json(output, expected);
+ else
+ ASSERT_GT(strlen(output), 0, "error diagnostic");
+ }
+ consumer_cleanup(&child);
+ close(fd);
+}
+
+void test_bpftool_ringbuf(void)
+{
+ if (test__start_subtest("perf_event_array"))
+ test_perf_consumer();
+ if (test__start_subtest("plain"))
+ test_consumer(NULL, false, SIGINT, false);
+ if (test__start_subtest("json"))
+ test_consumer("-j", false, SIGTERM, false);
+ if (test__start_subtest("pretty_json"))
+ test_consumer("-p", false, SIGINT, false);
+ if (test__start_subtest("pinned_plain"))
+ test_consumer(NULL, false, SIGTERM, true);
+ if (test__start_subtest("pinned_json"))
+ test_consumer("-j", false, SIGINT, true);
+ if (test__start_subtest("pinned_pretty_json"))
+ test_consumer("-p", false, SIGTERM, true);
+ if (test__start_subtest("idle_sigint"))
+ test_consumer(NULL, true, SIGINT, false);
+ if (test__start_subtest("idle_sigterm_json"))
+ test_consumer("-j", true, SIGTERM, false);
+ if (test__start_subtest("idle_sigint_pretty_json"))
+ test_consumer("-p", true, SIGINT, false);
+ if (test__start_subtest("reject_array"))
+ test_reject(BPF_MAP_TYPE_ARRAY, NULL, false, false);
+ if (test__start_subtest("reject_user_ringbuf"))
+ test_reject(BPF_MAP_TYPE_USER_RINGBUF, NULL, false, false);
+ if (test__start_subtest("reject_cpu"))
+ test_reject(BPF_MAP_TYPE_RINGBUF, "cpu", false, false);
+ if (test__start_subtest("reject_index"))
+ test_reject(BPF_MAP_TYPE_RINGBUF, "index", false, false);
+ if (test__start_subtest("reject_cpu_index"))
+ test_reject(BPF_MAP_TYPE_RINGBUF, "cpu", true, false);
+ if (test__start_subtest("reject_user_ringbuf_json"))
+ test_reject(BPF_MAP_TYPE_USER_RINGBUF, NULL, false, true);
+ if (test__start_subtest("reject_cpu_index_json"))
+ test_reject(BPF_MAP_TYPE_RINGBUF, "cpu", true, true);
+}
diff --git a/tools/testing/selftests/bpf/progs/bpftool_ringbuf.c b/tools/testing/selftests/bpf/progs/bpftool_ringbuf.c
new file mode 100644
index 00000000000..bbbc93c30a8
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/bpftool_ringbuf.c
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+
+struct {
+ __uint(type, BPF_MAP_TYPE_RINGBUF);
+} ringbuf SEC(".maps");
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
+ __uint(key_size, sizeof(__u32));
+ __uint(value_size, sizeof(__u32));
+ __uint(max_entries, 1);
+} perfbuf SEC(".maps");
+
+/*
+ * Include the u32 raw size in perf's 8-byte alignment to avoid padding.
+ * Keep plain output large enough to flush stdio before the consumer exits.
+ */
+const unsigned char perf_payload[4092] = { 0x00, 0xff };
+
+int record;
+int output_err;
+
+SEC("socket")
+int produce(struct __sk_buff *skb)
+{
+ unsigned char first[] = { 0x00, 0xff };
+ unsigned char second[] = { 1, 2, 3, 4, 5 };
+ unsigned char third[] = {
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
+ };
+
+ if (record == 0)
+ output_err = bpf_ringbuf_output(&ringbuf, first, sizeof(first), 0);
+ else if (record == 1)
+ output_err = bpf_ringbuf_output(&ringbuf, second, sizeof(second), 0);
+ else if (record == 2)
+ output_err = bpf_ringbuf_output(&ringbuf, third, sizeof(third), 0);
+ else
+ output_err = bpf_perf_event_output(skb, &perfbuf, BPF_F_CURRENT_CPU,
+ (void *)perf_payload, sizeof(perf_payload));
+ return 0;
+}
+
+char LICENSE[] SEC("license") = "GPL";
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH bpf-next v2 1/2] bpftool: Read ring buffer maps with event_pipe
2026-09-07 1:21 ` [PATCH bpf-next v2 1/2] bpftool: Read " Tianyi Chen
@ 2026-09-07 2:20 ` bot+bpf-ci
2026-09-07 5:15 ` Tianyi Chen
0 siblings, 1 reply; 9+ messages in thread
From: bot+bpf-ci @ 2026-09-07 2:20 UTC (permalink / raw)
To: hi, bpf
Cc: hi, qmo, linux-kselftest, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 6588 bytes --]
> Allow map event_pipe to consume BPF_MAP_TYPE_RINGBUF maps using
> libbpf's ring buffer manager. Print each record's size and raw bytes
> in plain or JSON output, and flush output as records arrive.
>
> Reject CPU and index selectors for ring buffers and retain the existing
> perf event array path. Keep signal handlers limited to setting a stop
> flag, reset that flag for each command, and print the stopping message
> on stderr from the main path. Close the JSON array on poll errors and
> preserve output errors returned by the ring buffer callback.
This isn't a bug, but could the changelog lead with why this is needed
(ring buffers are now the common output channel but event_pipe has no way
to read them) and keep only the parts of the mechanism that are not
obvious from the diff, such as the -EINTR return from the callback? The
second and third paragraphs walk through what individual hunks do rather
than explaining the rationale. A reader can learn what each hunk does but
not why the feature is wanted; the motivation is only reachable through
the Link: tag.
> diff --git a/tools/bpf/bpftool/Documentation/bpftool-map.rst b/tools/bpf/bpftool/Documentation/bpftool-map.rst
> index 5daf3de5c744..c44e6f797e57 100644
> --- a/tools/bpf/bpftool/Documentation/bpftool-map.rst
> +++ b/tools/bpf/bpftool/Documentation/bpftool-map.rst
[ ... ]
> @@ -134,6 +135,18 @@ bpftool map event_pipe *MAP* [cpu *N* index *M*]
> existing ring. Any other application will stop receiving events if it
> installed its rings earlier.
>
> + For a ring buffer map, consume records submitted by BPF programs, including
> + records already queued before the command starts. **cpu** and **index**
> + are not supported. Each record is printed as raw bytes, including embedded
> + zero bytes. Plain output reports the record size followed by hexadecimal
> + bytes; JSON output contains **size** and **data** fields, with **data** an
> + array of byte values. Ring buffer records have no implicit CPU or timestamp.
> +
> + Ring buffers support a single consumer. This command advances the shared
> + consumer position and must not run alongside another consumer of the same
> + map; it does not provide a passive view of events. **BPF_MAP_TYPE_USER_RINGBUF**
> + maps are not supported.
This isn't a bug, but would it read more tightly to state the
consumer-position effect once, e.g. "Consuming a ring buffer advances its
shared consumer position, so this command must not run alongside another
consumer of the same map", and to say "printed in full, including embedded
zero bytes" rather than "raw bytes" just before describing the hex output?
The single-consumer point is made four times in three sentences: "support
a single consumer", "advances the shared consumer position", "must not run
alongside another consumer of the same map", and "it does not provide a
passive view of events". The paragraph above has a similar redundancy in
the other direction: "Each record is printed as raw bytes, including
embedded zero bytes" is immediately followed by "Plain output reports the
record size followed by hexadecimal bytes", so "raw bytes" reads as a
contradiction of the hex encoding described one sentence later.
> diff --git a/tools/bpf/bpftool/bash-completion/bpftool b/tools/bpf/bpftool/bash-completion/bpftool
> index 75cbcb512eba..1750b488c9e3 100644
> --- a/tools/bpf/bpftool/bash-completion/bpftool
> +++ b/tools/bpf/bpftool/bash-completion/bpftool
> @@ -878,11 +878,11 @@ _bpftool()
> return 0
> ;;
> id)
> - _bpftool_get_map_ids_for_type perf_event_array
> + _bpftool_get_map_ids_for_type '"type": "\(perf_event_array\|ringbuf\)"'
> return 0
> ;;
> name)
> - _bpftool_get_map_names_for_type perf_event_array
> + _bpftool_get_map_names_for_type '"type": "\(perf_event_array\|ringbuf\)"'
> return 0
> ;;
> cpu)
This isn't a bug, but the other _for_type callers here pass a bare map
type (e.g. stack / queue on two lines). Would anchoring the "type": "$type"
match inside _bpftool_get_map_ids_for_type()/_bpftool_get_map_names_for_type()
and then calling them twice read more consistently than passing a grep
expression from the call site? The two new call sites pass a raw grep BRE
to helpers that every other caller in this file feeds a bare map type
name, and the call site now encodes the helper's internal implementation
detail (that it greps the pretty-printed JSON for a "type": "..." line).
The file's existing idiom for a two-type selection is two consecutive
calls (lines 682-683 and 700-701).
> diff --git a/tools/bpf/bpftool/map_perf_ring.c b/tools/bpf/bpftool/map_perf_ring.c
> index bcb767e2d673..7d555331f443 100644
> --- a/tools/bpf/bpftool/map_perf_ring.c
> +++ b/tools/bpf/bpftool/map_perf_ring.c
> @@ -27,7 +27,7 @@
>
> #define MMAP_PAGE_CNT 16
>
> -static volatile bool stop;
> +static volatile sig_atomic_t stop;
>
> struct perf_event_sample {
> struct perf_event_header header;
> @@ -44,7 +44,6 @@ struct perf_event_lost {
>
> static void int_exit(int signo)
> {
> - fprintf(stderr, "Stopping...\n");
> stop = true;
> }
Would it be worth splitting the signal-handler hardening (volatile bool ->
volatile sig_atomic_t, and moving the "Stopping..." print out of int_exit())
into a small preparatory patch? It looks like a standalone async-signal-safety
fix for the existing perf-event-array path, and separating it would make both
halves easier to review and backport. Two of the changes in this hunk are
async-signal-safety fixes to pre-existing code rather than part of the new
ring buffer support: widening stop from volatile bool to volatile sig_atomic_t,
and dropping the fprintf call out of the handler (fprintf is not
async-signal-safe). Both stand on their own and apply equally to the existing
perf-event-array path, whereas the rest of the patch (the stop = false reset,
the -EINTR return from the ring buffer callback, the jsonw_end_array() on the
error path) is genuinely needed for ring buffers.
[ ... ]
---
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/34074185878
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH bpf-next v2 1/2] bpftool: Read ring buffer maps with event_pipe
2026-09-07 2:20 ` bot+bpf-ci
@ 2026-09-07 5:15 ` Tianyi Chen
0 siblings, 0 replies; 9+ messages in thread
From: Tianyi Chen @ 2026-09-07 5:15 UTC (permalink / raw)
To: bpf; +Cc: qmo, linux-kselftest, hi
Thanks for the review.
I have rewritten the local commit message around the missing capability:
map event_pipe currently accepts only perf event arrays, leaving no
built-in way to inspect records produced through the BPF ring buffer
API. It now explains the callback's -EINTR return as well: a producer
can keep ring_buffer__poll() busy after a stop signal, so checking only
the outer loop is insufficient. These message changes are ready for
the next full revision.
In the manual, "raw bytes" describes the payload without schema/BTF
decoding; hexadecimal is the stated plain-text representation of those
bytes. It is not intended to mean binary stdout. Likewise, the shared
consumer position explains why using this command affects another
consumer, while the passive-view warning states the user-visible risk.
The wording could be shorter, but I do not see a semantic contradiction
and am retaining the explicit consumption warning for now.
For completion, exact matching is intentional: a bare ringbuf pattern
would also match user_ringbuf, which event_pipe rejects. The current
expression matches the complete JSON type field and avoids that problem.
Moving exact matching into both common helpers is a reasonable cleanup,
but changes all their callers; using two calls also lists the maps twice.
I am keeping this feature's localized, single-listing change rather than
expanding it into a shared-helper refactor.
I agree that using sig_atomic_t and removing fprintf() from the handler
also harden the existing perf path, and could form a preparatory patch.
The current combined patch keeps the shared handler safe while retaining
the stopping diagnostic in normal control flow. I am keeping that
organization for now; I can split it if the maintainer prefers separate
review/backport handling. There is no remaining unsafe fprintf() in the
handler in the submitted patch.
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-09-07 5:15 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-06 17:07 [PATCH bpf-next 0/2] bpftool: Consume ring buffer maps with event_pipe Tianyi Chen
2026-09-06 17:07 ` [PATCH bpf-next 1/2] bpftool: Read " Tianyi Chen
2026-09-06 17:07 ` [PATCH bpf-next 2/2] selftests/bpf: Cover bpftool ring buffer event_pipe Tianyi Chen
2026-09-06 17:18 ` sashiko-bot
2026-09-07 1:21 ` [PATCH bpf-next v2 0/2] bpftool: Consume ring buffer maps with event_pipe Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v2 1/2] bpftool: Read " Tianyi Chen
2026-09-07 2:20 ` bot+bpf-ci
2026-09-07 5:15 ` Tianyi Chen
2026-09-07 1:21 ` [PATCH bpf-next v2 2/2] selftests/bpf: Cover bpftool ring buffer event_pipe Tianyi Chen
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox