// SPDX-License-Identifier: GPL-2.0 #include #include #include #include #include #include #include #include "bpftool_helpers.h" #include "bpftool_ringbuf.skel.h" #define WAIT_STEPS 500 #define WAIT_US 10000 struct consumer { pid_t pid; int fd; }; 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); char path[PATH_MAX]; char *argv[12], id[16]; int out[2], ready[2], n = 0, err, err_fd; struct pollfd pfd; if (!ASSERT_OK(detect_bpftool_path(path, sizeof(path)), "bpftool path") || !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++] = 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; if (write(ready[1], &err, sizeof(err)) != sizeof(err)) _exit(126); _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 && !strcmp(option, "foobar") ? "{\"error\":\"what is 'foobar'?\"}" : 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_unknown_json")) test_reject(BPF_MAP_TYPE_RINGBUF, "foobar", false, true); if (test__start_subtest("reject_cpu_index_json")) test_reject(BPF_MAP_TYPE_RINGBUF, "cpu", true, true); }