From: Kumar Kartikeya Dwivedi <memxor@gmail.com>
To: bpf@vger.kernel.org
Cc: Alexei Starovoitov <ast@kernel.org>,
Andrii Nakryiko <andrii@kernel.org>,
Daniel Borkmann <daniel@iogearbox.net>,
Eduard Zingerman <eddyz87@gmail.com>,
Emil Tsalapatis <emil@etsalapatis.com>, Tejun Heo <tj@kernel.org>,
kkd@meta.com, kernel-team@meta.com
Subject: [PATCH bpf-next v1 6/6] selftests/bpf: Test program stream file descriptors
Date: Sun, 30 Aug 2026 11:35:11 +0200 [thread overview]
Message-ID: <20260830093514.4105972-7-memxor@gmail.com> (raw)
In-Reply-To: <20260830093514.4105972-1-memxor@gmail.com>
Exercise validation and normal file semantics of BPF_PROG_STREAM_OPEN.
Cover read-only and close-on-exec state, write rejection, non-blocking
reads, partial reads, poll readiness, and a blocking reader woken by new
stream data.
Register the descriptor with EPOLLET, consume the initial data event
without draining buffered output, then release the program. Require a
second edge carrying POLLIN and POLLHUP, drain the buffered bytes, and
verify that poll continues to report HUP before read returns EOF. This
ensures program teardown notifies edge-triggered loops even when
readability never transitions.
Also block a reader on an empty live stream, release the program, and
verify that it wakes and returns EOF. This directly covers the teardown
wait condition and its no-lost-wakeup ordering.
Finally, attach a stream-writing program to a hardware perf event and
busy-poll its epoll registration until output arrives. This both drives the
per-thread event into NMI context and directly verifies that the deferred
irq_work notification reaches epoll, without a separate BSS completion
handshake.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
.../testing/selftests/bpf/prog_tests/stream.c | 376 ++++++++++++++++++
tools/testing/selftests/bpf/progs/stream.c | 10 +
2 files changed, 386 insertions(+)
diff --git a/tools/testing/selftests/bpf/prog_tests/stream.c b/tools/testing/selftests/bpf/prog_tests/stream.c
index 74bd15c4bfba..655aab35c03e 100644
--- a/tools/testing/selftests/bpf/prog_tests/stream.c
+++ b/tools/testing/selftests/bpf/prog_tests/stream.c
@@ -1,11 +1,17 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
#include <test_progs.h>
+#include <linux/perf_event.h>
+#include <poll.h>
+#include <sys/epoll.h>
#include <sys/mman.h>
+#include <sys/syscall.h>
#include "stream.skel.h"
#include "stream_fail.skel.h"
+#define NMI_TIMEOUT_NS (5ULL * 1000 * 1000 * 1000)
+
void test_stream_failure(void)
{
RUN_TESTS(stream_fail);
@@ -62,6 +68,376 @@ void test_stream_syscall(void)
stream__destroy(skel);
}
+static bool stream_fd_trigger(struct bpf_program *prog)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ int ret;
+
+ ret = bpf_prog_test_run_opts(bpf_program__fd(prog), &opts);
+ return ASSERT_OK(ret, "test_run") && ASSERT_OK(opts.retval, "retval");
+}
+
+static void test_stream_fd_open(void)
+{
+ LIBBPF_OPTS(bpf_prog_stream_open_opts, opts);
+ struct stream *skel;
+ int fd, prog_fd;
+
+ skel = stream__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "stream__open_and_load"))
+ return;
+
+ prog_fd = bpf_program__fd(skel->progs.stream_syscall);
+ fd = bpf_prog_stream_open(0, BPF_STREAM_STDOUT, NULL);
+ ASSERT_EQ(fd, -EINVAL, "bad_prog_fd");
+
+ fd = bpf_prog_stream_open(prog_fd, 0, NULL);
+ ASSERT_EQ(fd, -ENOENT, "bad_stream_id");
+
+ opts.flags = BPF_F_WRONLY;
+ fd = bpf_prog_stream_open(prog_fd, BPF_STREAM_STDOUT, &opts);
+ ASSERT_EQ(fd, -EINVAL, "writable");
+
+ opts.flags = 1U << 31;
+ fd = bpf_prog_stream_open(prog_fd, BPF_STREAM_STDOUT, &opts);
+ ASSERT_EQ(fd, -EINVAL, "unknown_flag");
+
+ opts.flags = BPF_F_RDONLY;
+ fd = bpf_prog_stream_open(prog_fd, BPF_STREAM_STDERR, &opts);
+ if (ASSERT_OK_FD(fd, "readonly"))
+ close(fd);
+
+ stream__destroy(skel);
+}
+
+static void test_stream_fd_nonblock(void)
+{
+ LIBBPF_OPTS(bpf_prog_stream_open_opts, opts,
+ .flags = BPF_F_RDONLY | BPF_F_STREAM_NONBLOCK,
+ );
+ struct pollfd pfd = { .events = POLLIN | POLLHUP };
+ struct stream *skel;
+ char buf[4] = {};
+ int fd, flags, ret;
+
+ skel = stream__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "stream__open_and_load"))
+ return;
+
+ fd = bpf_prog_stream_open(bpf_program__fd(skel->progs.stream_syscall),
+ BPF_STREAM_STDOUT, &opts);
+ if (!ASSERT_OK_FD(fd, "stream_open"))
+ goto out_destroy;
+ pfd.fd = fd;
+
+ flags = fcntl(fd, F_GETFD);
+ ASSERT_GE(flags, 0, "getfd");
+ ASSERT_NEQ(flags & FD_CLOEXEC, 0, "cloexec");
+ flags = fcntl(fd, F_GETFL);
+ ASSERT_GE(flags, 0, "getfl");
+ ASSERT_EQ(flags & O_ACCMODE, O_RDONLY, "readonly");
+ ASSERT_NEQ(flags & O_NONBLOCK, 0, "nonblock");
+
+ ret = write(fd, "x", 1);
+ ASSERT_EQ(ret, -1, "write");
+ ASSERT_EQ(errno, EBADF, "write_errno");
+
+ ret = poll(&pfd, 1, 0);
+ ASSERT_EQ(ret, 0, "poll_empty");
+ ret = read(fd, buf, sizeof(buf));
+ ASSERT_EQ(ret, -1, "read_empty");
+ ASSERT_EQ(errno, EAGAIN, "read_empty_errno");
+
+ if (!stream_fd_trigger(skel->progs.stream_syscall))
+ goto out_close;
+ ret = poll(&pfd, 1, 0);
+ ASSERT_EQ(ret, 1, "poll_data");
+ ASSERT_NEQ(pfd.revents & POLLIN, 0, "pollin");
+ ASSERT_EQ(pfd.revents & POLLHUP, 0, "no_pollhup");
+
+ ret = read(fd, buf, 2);
+ ASSERT_EQ(ret, 2, "read_first");
+ ASSERT_OK(memcmp(buf, "fo", 2), "read_first_data");
+ pfd.revents = 0;
+ ret = poll(&pfd, 1, 0);
+ ASSERT_EQ(ret, 1, "poll_partial");
+ ASSERT_NEQ(pfd.revents & POLLIN, 0, "pollin_partial");
+
+ ret = read(fd, buf, sizeof(buf));
+ ASSERT_EQ(ret, 1, "read_rest");
+ ASSERT_EQ(buf[0], 'o', "read_rest_data");
+ ret = read(fd, buf, sizeof(buf));
+ ASSERT_EQ(ret, -1, "read_drained");
+ ASSERT_EQ(errno, EAGAIN, "read_drained_errno");
+
+out_close:
+ close(fd);
+out_destroy:
+ stream__destroy(skel);
+}
+
+struct stream_fd_read_ctx {
+ int fd;
+ ssize_t ret;
+ char buf[4];
+};
+
+static void *stream_fd_read_thread(void *arg)
+{
+ struct stream_fd_read_ctx *ctx = arg;
+
+ ctx->ret = read(ctx->fd, ctx->buf, sizeof(ctx->buf));
+ return NULL;
+}
+
+static int stream_fd_timed_join(pthread_t thread)
+{
+ struct timespec timeout;
+
+ clock_gettime(CLOCK_REALTIME, &timeout);
+ timeout.tv_sec += 5;
+ return pthread_timedjoin_np(thread, NULL, &timeout);
+}
+
+static void test_stream_fd_blocking(void)
+{
+ struct stream_fd_read_ctx ctx = {};
+ struct stream *skel;
+ pthread_t thread;
+ bool thread_live = false;
+ int fd, flags, ret;
+
+ skel = stream__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "stream__open_and_load"))
+ return;
+
+ fd = bpf_prog_stream_open(bpf_program__fd(skel->progs.stream_syscall),
+ BPF_STREAM_STDOUT, NULL);
+ if (!ASSERT_OK_FD(fd, "stream_open"))
+ goto out_destroy;
+ ctx.fd = fd;
+
+ flags = fcntl(fd, F_GETFL);
+ ASSERT_GE(flags, 0, "getfl");
+ ASSERT_EQ(flags & O_NONBLOCK, 0, "blocking");
+
+ ret = pthread_create(&thread, NULL, stream_fd_read_thread, &ctx);
+ if (!ASSERT_OK(ret, "pthread_create"))
+ goto out_close;
+ thread_live = true;
+
+ usleep(50000);
+ ret = pthread_tryjoin_np(thread, NULL);
+ if (!ASSERT_EQ(ret, EBUSY, "read_blocks")) {
+ thread_live = ret != 0;
+ goto out_thread;
+ }
+ if (!stream_fd_trigger(skel->progs.stream_syscall))
+ goto out_thread;
+
+ ret = stream_fd_timed_join(thread);
+ if (!ASSERT_OK(ret, "pthread_join"))
+ goto out_thread;
+ thread_live = false;
+ ASSERT_EQ(ctx.ret, 3, "read_len");
+ ASSERT_OK(memcmp(ctx.buf, "foo", 3), "read_data");
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.fd = fd;
+ ret = pthread_create(&thread, NULL, stream_fd_read_thread, &ctx);
+ if (!ASSERT_OK(ret, "pthread_create_eof"))
+ goto out_close;
+ thread_live = true;
+
+ usleep(50000);
+ ret = pthread_tryjoin_np(thread, NULL);
+ if (!ASSERT_EQ(ret, EBUSY, "read_eof_blocks")) {
+ thread_live = ret != 0;
+ goto out_thread;
+ }
+
+ stream__destroy(skel);
+ skel = NULL;
+ ret = stream_fd_timed_join(thread);
+ if (!ASSERT_OK(ret, "pthread_join_eof"))
+ goto out_thread;
+ thread_live = false;
+ ASSERT_EQ(ctx.ret, 0, "read_eof");
+
+out_thread:
+ if (thread_live) {
+ stream__destroy(skel);
+ skel = NULL;
+ ret = stream_fd_timed_join(thread);
+ if (ret) {
+ pthread_cancel(thread);
+ pthread_join(thread, NULL);
+ }
+ }
+out_close:
+ close(fd);
+out_destroy:
+ stream__destroy(skel);
+}
+
+static void test_stream_fd_hup(void)
+{
+ LIBBPF_OPTS(bpf_prog_stream_open_opts, opts,
+ .flags = BPF_F_STREAM_NONBLOCK,
+ );
+ struct epoll_event event = {
+ .events = EPOLLIN | EPOLLET,
+ };
+ struct pollfd pfd = { .events = POLLIN | POLLHUP };
+ struct stream *skel;
+ char buf[4] = {};
+ int epfd = -1, fd, ret;
+
+ skel = stream__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "stream__open_and_load"))
+ return;
+
+ fd = bpf_prog_stream_open(bpf_program__fd(skel->progs.stream_syscall),
+ BPF_STREAM_STDOUT, &opts);
+ if (!ASSERT_OK_FD(fd, "stream_open"))
+ goto out_destroy;
+ pfd.fd = fd;
+ epfd = epoll_create1(EPOLL_CLOEXEC);
+ if (!ASSERT_OK_FD(epfd, "epoll_create"))
+ goto out_close;
+ event.data.fd = fd;
+ ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
+ if (!ASSERT_OK(ret, "epoll_ctl"))
+ goto out_close;
+
+ if (!stream_fd_trigger(skel->progs.stream_syscall))
+ goto out_close;
+ ret = epoll_wait(epfd, &event, 1, 5000);
+ if (!ASSERT_EQ(ret, 1, "epoll_wait_data"))
+ goto out_close;
+ ASSERT_NEQ(event.events & EPOLLIN, 0, "epollin");
+ ASSERT_EQ(event.events & EPOLLHUP, 0, "no_epollhup");
+
+ stream__destroy(skel);
+ skel = NULL;
+ event.events = 0;
+ ret = epoll_wait(epfd, &event, 1, 5000);
+ if (!ASSERT_EQ(ret, 1, "epoll_wait_hup"))
+ goto out_close;
+ ASSERT_NEQ(event.events & EPOLLIN, 0, "epollin_with_hup");
+ ASSERT_NEQ(event.events & EPOLLHUP, 0, "epollhup");
+
+ ret = read(fd, buf, sizeof(buf));
+ ASSERT_EQ(ret, 3, "read_buffered");
+ ASSERT_OK(memcmp(buf, "foo", 3), "read_buffered_data");
+ pfd.revents = 0;
+ ret = poll(&pfd, 1, 0);
+ ASSERT_EQ(ret, 1, "poll_drained_hup");
+ ASSERT_EQ(pfd.revents & POLLIN, 0, "no_pollin_after_drain");
+ ASSERT_NEQ(pfd.revents & POLLHUP, 0, "pollhup_after_drain");
+ ret = read(fd, buf, sizeof(buf));
+ ASSERT_EQ(ret, 0, "read_eof");
+
+out_close:
+ if (epfd >= 0)
+ close(epfd);
+ close(fd);
+out_destroy:
+ stream__destroy(skel);
+}
+
+static void test_stream_fd_nmi_epoll(void)
+{
+ LIBBPF_OPTS(bpf_prog_stream_open_opts, opts,
+ .flags = BPF_F_STREAM_NONBLOCK,
+ );
+ struct perf_event_attr attr = {
+ .size = sizeof(attr),
+ .type = PERF_TYPE_HARDWARE,
+ .config = PERF_COUNT_HW_CPU_CYCLES,
+ .freq = 1,
+ .sample_freq = 10,
+ };
+ struct epoll_event event = {
+ .events = EPOLLIN,
+ };
+ struct bpf_link *link = NULL;
+ struct stream *skel;
+ __u64 deadline;
+ char buf[4] = {};
+ int epfd = -1, fd = -1, pmu_fd = -1;
+ int ret;
+
+ skel = stream__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "stream__open_and_load"))
+ return;
+ fd = bpf_prog_stream_open(bpf_program__fd(skel->progs.stream_nmi),
+ BPF_STREAM_STDOUT, &opts);
+ if (!ASSERT_OK_FD(fd, "stream_open"))
+ goto out;
+
+ epfd = epoll_create1(EPOLL_CLOEXEC);
+ if (!ASSERT_OK_FD(epfd, "epoll_create"))
+ goto out;
+ event.data.fd = fd;
+ ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
+ if (!ASSERT_OK(ret, "epoll_ctl"))
+ goto out;
+
+ pmu_fd = syscall(__NR_perf_event_open, &attr, 0, -1, -1,
+ PERF_FLAG_FD_CLOEXEC);
+ if (pmu_fd < 0 && (errno == ENOENT || errno == EOPNOTSUPP)) {
+ printf("%s:SKIP:no PERF_COUNT_HW_CPU_CYCLES\n", __func__);
+ test__skip();
+ goto out;
+ }
+ if (!ASSERT_GE(pmu_fd, 0, "perf_event_open"))
+ goto out;
+
+ link = bpf_program__attach_perf_event(skel->progs.stream_nmi, pmu_fd);
+ if (!ASSERT_OK_PTR(link, "attach_perf_event")) {
+ link = NULL;
+ goto out;
+ }
+ pmu_fd = -1;
+
+ deadline = get_time_ns() + NMI_TIMEOUT_NS;
+ do {
+ ret = epoll_wait(epfd, &event, 1, 0);
+ } while (ret == 0 && get_time_ns() < deadline);
+ if (!ASSERT_EQ(ret, 1, "epoll_wait"))
+ goto out;
+ ASSERT_NEQ(event.events & EPOLLIN, 0, "epollin");
+
+ ret = read(fd, buf, sizeof(buf));
+ ASSERT_EQ(ret, 3, "read_len");
+ ASSERT_OK(memcmp(buf, "nmi", 3), "read_data");
+
+out:
+ bpf_link__destroy(link);
+ if (pmu_fd >= 0)
+ close(pmu_fd);
+ if (epfd >= 0)
+ close(epfd);
+ if (fd >= 0)
+ close(fd);
+ stream__destroy(skel);
+}
+
+void test_stream_fd(void)
+{
+ if (test__start_subtest("open"))
+ test_stream_fd_open();
+ if (test__start_subtest("nonblock"))
+ test_stream_fd_nonblock();
+ if (test__start_subtest("blocking"))
+ test_stream_fd_blocking();
+ if (test__start_subtest("hup"))
+ test_stream_fd_hup();
+ if (test__start_subtest("nmi_epoll"))
+ test_stream_fd_nmi_epoll();
+}
+
void test_stream_oversize(void)
{
LIBBPF_OPTS(bpf_test_run_opts, opts);
diff --git a/tools/testing/selftests/bpf/progs/stream.c b/tools/testing/selftests/bpf/progs/stream.c
index 12fc29e45487..00843127bca8 100644
--- a/tools/testing/selftests/bpf/progs/stream.c
+++ b/tools/testing/selftests/bpf/progs/stream.c
@@ -44,6 +44,7 @@ struct {
_X64 _X64 _X64 _X64 _X64 _X64 _X64 _X64)
int size;
+int nmi_stream_prints;
u64 fault_addr;
void *arena_ptr;
@@ -124,6 +125,15 @@ int stream_syscall(void *ctx)
return 0;
}
+SEC("perf_event")
+int stream_nmi(void *ctx)
+{
+ if (nmi_stream_prints)
+ return 0;
+ nmi_stream_prints = 1;
+ return bpf_stream_printk(BPF_STDOUT, "nmi");
+}
+
SEC("syscall")
__success __retval(0)
int stream_oversize(void *ctx)
--
2.53.0
prev parent reply other threads:[~2026-08-30 9:35 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-30 9:35 [PATCH bpf-next v1 0/6] File descriptor interface for BPF streams Kumar Kartikeya Dwivedi
2026-08-30 9:35 ` [PATCH bpf-next v1 1/6] bpf: Add file descriptor interface for program streams Kumar Kartikeya Dwivedi
2026-08-30 9:47 ` sashiko-bot
2026-08-30 10:47 ` bot+bpf-ci
2026-08-30 9:35 ` [PATCH bpf-next v1 2/6] bpf: Defer stream file notifications from NMI context Kumar Kartikeya Dwivedi
2026-08-30 9:35 ` [PATCH bpf-next v1 3/6] bpf: Separate stream readiness from capacity accounting Kumar Kartikeya Dwivedi
2026-08-30 9:45 ` sashiko-bot
2026-08-30 10:35 ` bot+bpf-ci
2026-08-30 9:35 ` [PATCH bpf-next v1 4/6] libbpf: Add bpf_prog_stream_open() Kumar Kartikeya Dwivedi
2026-08-30 9:35 ` [PATCH bpf-next v1 5/6] bpftool: Read program streams through file descriptors Kumar Kartikeya Dwivedi
2026-08-30 10:35 ` bot+bpf-ci
2026-08-30 9:35 ` Kumar Kartikeya Dwivedi [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260830093514.4105972-7-memxor@gmail.com \
--to=memxor@gmail.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=emil@etsalapatis.com \
--cc=kernel-team@meta.com \
--cc=kkd@meta.com \
--cc=tj@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox