This selftest exercises the tcp_bpf_recvmsg() and tcp_bpf_recvmsg_parser() functions, to ensure that they are properly handling spurious wakeups in tcp_msg_wait_data(). The expected behaviour is that: * Without a timeout - recvfrom() does not return an EAGAIN error. * With a timeout - recvfrom() returns EAGAIN, but only after the SO_RCVTIMEO timeout has expired. If the spurious wakeups are not correctly handled, the above assertions fail. Signed-off-by: Nnamdi Onyeyiri --- tools/testing/selftests/net/.gitignore | 2 + tools/testing/selftests/net/Makefile | 6 + .../selftests/net/sockmap_recvfrom.bpf.c | 31 ++ .../testing/selftests/net/sockmap_recvfrom.c | 288 ++++++++++++++++++ 4 files changed, 327 insertions(+) create mode 100644 tools/testing/selftests/net/sockmap_recvfrom.bpf.c create mode 100644 tools/testing/selftests/net/sockmap_recvfrom.c diff --git a/tools/testing/selftests/net/.gitignore b/tools/testing/selftests/net/.gitignore index c9f46031ac73..a1840a5d45f6 100644 --- a/tools/testing/selftests/net/.gitignore +++ b/tools/testing/selftests/net/.gitignore @@ -39,6 +39,8 @@ sk_connect_zero_addr sk_so_peek_off skf_net_off socket +sockmap_recvfrom +sockmap_recvfrom.bpf.o so_incoming_cpu so_netns_cookie so_rcv_listener diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile index 708d960ae07d..a05a4a924402 100644 --- a/tools/testing/selftests/net/Makefile +++ b/tools/testing/selftests/net/Makefile @@ -161,6 +161,7 @@ TEST_GEN_FILES := \ so_netns_cookie \ so_rcv_listener \ socket \ + sockmap_recvfrom.bpf.o \ stress_reuseport_listen \ tcp_fastopen_backup_key \ tcp_inq \ @@ -191,6 +192,7 @@ TEST_GEN_PROGS := \ sk_connect_zero_addr \ sk_so_peek_off \ so_incoming_cpu \ + sockmap_recvfrom \ tap \ tcp_port_share \ tls \ @@ -238,3 +240,7 @@ $(OUTPUT)/bind_bhash: LDLIBS += -lpthread $(OUTPUT)/io_uring_zerocopy_tx: CFLAGS += -I../../../include/ include bpf.mk + +$(OUTPUT)/sockmap_recvfrom: $(BPFOBJ) +$(OUTPUT)/sockmap_recvfrom: LDLIBS += $(BPFOBJ) -lelf -lz -lpthread +$(OUTPUT)/sockmap_recvfrom: CFLAGS += -I$(OUTPUT)/tools/include diff --git a/tools/testing/selftests/net/sockmap_recvfrom.bpf.c b/tools/testing/selftests/net/sockmap_recvfrom.bpf.c new file mode 100644 index 000000000000..fec470c738f1 --- /dev/null +++ b/tools/testing/selftests/net/sockmap_recvfrom.bpf.c @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0 +#include +#include + +#define AF_INET 2 + +char LICENSE[] SEC("license") = "GPL"; + +struct { + __uint(type, BPF_MAP_TYPE_SOCKHASH); + __uint(max_entries, 1024); + __type(key, __u64); + __type(value, __u64); + +} map_socks SEC(".maps"); + +SEC("sockops") int on_sockops(struct bpf_sock_ops *ctx) +{ + if (ctx->family == AF_INET && ctx->op == BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB) { + __u64 cookie = bpf_get_socket_cookie(ctx); + + bpf_sock_hash_update(ctx, &map_socks, &cookie, BPF_NOEXIST); + } + + return 0; +} + +SEC("sk_skb/stream_verdict") int on_recv(struct __sk_buff *ctx) +{ + return SK_PASS; +} diff --git a/tools/testing/selftests/net/sockmap_recvfrom.c b/tools/testing/selftests/net/sockmap_recvfrom.c new file mode 100644 index 000000000000..f96b4673c977 --- /dev/null +++ b/tools/testing/selftests/net/sockmap_recvfrom.c @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: GPL-2.0 +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kselftest_harness.h" + +#define MAX_ITERATIONS 100 +#define PAYLOAD_END 'e' + +static int start_listening(struct __test_metadata *_metadata, uint16_t *port) +{ + struct sockaddr_in addr; + socklen_t addrlen; + int fd; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + + fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + ASSERT_NE(fd, -1); + ASSERT_EQ(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), 0); + ASSERT_EQ(listen(fd, 5), 0); + + addrlen = sizeof(addr); + + ASSERT_EQ(getsockname(fd, (struct sockaddr *)&addr, &addrlen), 0); + + *port = addr.sin_port; + + return fd; +} + +static void process_client(struct __test_metadata *_metadata, int fd, atomic_int *running) +{ + char buf[1024]; + struct timeval timeo; + + timeo.tv_sec = 0; + timeo.tv_usec = 1000; + + EXPECT_EQ(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo)), 0); + + while (atomic_load(running)) { + ssize_t len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL); + + if (len == -1) { + EXPECT_TRUE(errno == EAGAIN || errno == EINTR); + continue; + } + + EXPECT_GE(len, 0); + + if (len <= 0 || buf[len - 1] == PAYLOAD_END) + break; + } + + write(fd, "test", 4); + + close(fd); +} + +struct bpf_t { + struct bpf_object *obj; + struct bpf_link *on_sockops; + struct bpf_link *on_recv; +}; + +static void setup_bpf(struct __test_metadata *_metadata, const char *path, bool recv, + struct bpf_t *bpf) +{ + struct bpf_program *prog; + int cgroup; + + memset(bpf, 0, sizeof(*bpf)); + bpf->obj = bpf_object__open_file(path, NULL); + + ASSERT_NE(bpf->obj, NULL); + ASSERT_EQ(bpf_object__load(bpf->obj), 0); + + prog = bpf_object__find_program_by_name(bpf->obj, "on_sockops"); + ASSERT_NE(prog, NULL); + + cgroup = open("/sys/fs/cgroup", O_RDONLY); + ASSERT_NE(cgroup, -1); + + bpf->on_sockops = bpf_program__attach_cgroup(prog, cgroup); + close(cgroup); + + ASSERT_NE(bpf->on_sockops, NULL); + + if (recv) { + struct bpf_map *map = bpf_object__find_map_by_name(bpf->obj, "map_socks"); + + ASSERT_NE(map, NULL); + + prog = bpf_object__find_program_by_name(bpf->obj, "on_recv"); + ASSERT_NE(prog, NULL); + + bpf->on_recv = bpf_program__attach_sockmap(prog, bpf_map__fd(map)); + ASSERT_NE(bpf->on_recv, NULL); + } +} + +struct server_t { + int fd; + atomic_int running; + pthread_t thread; + struct __test_metadata *metadata; +}; + +static void *run_server(void *arg) +{ + struct server_t *server = arg; + struct __test_metadata *_metadata = server->metadata; + + while (atomic_load(&server->running)) { + int client_fd = accept(server->fd, NULL, NULL); + + if (client_fd == -1) { + if (!atomic_load(&server->running)) + break; + + continue; + } + + process_client(_metadata, client_fd, &server->running); + } + + return NULL; +} + +static int send_payload(struct __test_metadata *_metadata, int fd, const char *buf, size_t len) +{ + size_t remaining = len; + + do { + ssize_t bytes = write(fd, buf + (len - remaining), remaining); + + if (bytes < 0) { + if (errno == EINTR) + continue; + + return -1; + } + + remaining -= bytes; + } while (remaining); + + return 0; +} + +FIXTURE(sockmap_recvfrom) +{ + struct server_t server; + struct sockaddr_in addr; + struct bpf_t bpf; + char *payload; + size_t payload_len; +}; + +FIXTURE_VARIANT(sockmap_recvfrom) +{ + bool with_recv; +}; + +FIXTURE_VARIANT_ADD(sockmap_recvfrom, recvmsg) +{ + .with_recv = false +}; + +FIXTURE_VARIANT_ADD(sockmap_recvfrom, recvmsg_parser) +{ + .with_recv = true +}; + +FIXTURE_SETUP(sockmap_recvfrom) +{ + memset(&self->addr, 0, sizeof(self->addr)); + + self->payload_len = 1024 * 1024 * 25; + self->payload = malloc(self->payload_len); + ASSERT_NE(self->payload, NULL); + + memset(self->payload, 0, self->payload_len); + self->payload[self->payload_len - 1] = PAYLOAD_END; + + setup_bpf(_metadata, "sockmap_recvfrom.bpf.o", variant->with_recv, &self->bpf); + atomic_store(&self->server.running, 1); + self->server.fd = start_listening(_metadata, &self->addr.sin_port); + self->server.metadata = _metadata; + + self->addr.sin_family = AF_INET; + self->addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + pthread_create(&self->server.thread, NULL, &run_server, &self->server); +} + +FIXTURE_TEARDOWN(sockmap_recvfrom) +{ + atomic_store(&self->server.running, 0); + + if (self->server.fd) { + shutdown(self->server.fd, SHUT_RD); + close(self->server.fd); + } + + if (self->server.thread) + pthread_join(self->server.thread, NULL); + + free(self->payload); + bpf_link__destroy(self->bpf.on_sockops); + + if (self->bpf.on_recv) + bpf_link__destroy(self->bpf.on_recv); + + bpf_object__close(self->bpf.obj); +} + +TEST_F(sockmap_recvfrom, no_timeout) +{ + char ignored[128]; + + for (int i = 0; i < MAX_ITERATIONS; ++i) { + int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + ASSERT_NE(fd, -1); + ASSERT_EQ(connect(fd, (struct sockaddr *)&self->addr, sizeof(self->addr)), 0); + + ASSERT_EQ(send_payload(_metadata, fd, self->payload, self->payload_len), 0); + + if (recvfrom(fd, ignored, sizeof(ignored), 0, NULL, NULL) < 0) + ASSERT_NE(errno, EAGAIN); + + close(fd); + } +} + +static int64_t to_nanos(struct timespec *time) +{ + return (time->tv_sec * 1000000000LL) + time->tv_nsec; +} + +TEST_F(sockmap_recvfrom, with_timeout) +{ + char ignored[128]; + struct timeval timeo; + + timeo.tv_sec = 0; + timeo.tv_usec = 5000; + + /* remove the payload end delimiter so the server never responds and recvfrom times out. */ + self->payload[self->payload_len - 1] = 0; + + for (int i = 0; i < MAX_ITERATIONS; ++i) { + struct timespec beg; + struct timespec end; + int err; + + int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + ASSERT_NE(fd, -1); + ASSERT_EQ(connect(fd, (struct sockaddr *)&self->addr, sizeof(self->addr)), 0); + + ASSERT_EQ(send_payload(_metadata, fd, self->payload, self->payload_len), 0); + + ASSERT_EQ(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo)), 0); + + clock_gettime(CLOCK_MONOTONIC, &beg); + ASSERT_EQ(recvfrom(fd, ignored, sizeof(ignored), 0, NULL, NULL), -1); + err = errno; + clock_gettime(CLOCK_MONOTONIC, &end); + + ASSERT_EQ(err, EAGAIN); + ASSERT_GE(to_nanos(&end) - to_nanos(&beg), timeo.tv_usec * 1000); + + close(fd); + } +} + +TEST_HARNESS_MAIN -- 2.52.0