blob: e9724c55d1dc638eb1196ede328c548edc1b5fe1 [file] [edit]
// 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_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);
}