* [PATCH net v2] selftests/net: test network sysctl extremes
@ 2026-10-14 4:14 Sahaj Chaudhari
2026-10-18 4:35 ` netdev-bot+sashiko
0 siblings, 1 reply; 2+ messages in thread
From: Sahaj Chaudhari @ 2026-10-14 4:14 UTC (permalink / raw)
To: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Shuah Khan
Cc: netdev, linux-kselftest, linux-kernel, Sahaj Chaudhari
Probe accepted bounds for writable network sysctls and exercise
TCP and UDP loopback traffic at each bound. Test representative
per-network-namespace TCP/UDP settings, with an option to select
from the set.
Tested: Focused selftest build and CLI checks. QEMU on Linux
7.3.0-rc7 passed (4 sysctls/8 values, TCP 16/16, UDP 16/16), also
while userspace wrote WARNING:/BUG: lines to /dev/kmsg. On an AArch64
Linux 6.12.57 board, 4 sysctls/8 values met aggregate criteria (TCP
15/16, UDP 16/16), with one TCP timeout at tcp_rmem[1]=1. No kernel
splats were found.
Link: https://lore.kernel.org/r/20261007054508.1059927-1-sahaj123.sc@gmail.com
Signed-off-by: Sahaj Chaudhari <sahaj123.sc@gmail.com>
---
Changes in v2:
- Remove --all and the full /proc/sys/net scan. It could enable
nf_hooks_lwtunnel, a global static key that cannot be disabled again,
so restoring the original value failed. The remaining sysctls are per
network namespace and restored by writing the original value back.
Document this constraint above the sysctl list. (Sashiko)
- The rounding issue in find_bound() applied to ms/jiffies sysctls that
only --all reached; the remaining four do not round. (Sashiko)
- Ignore /dev/kmsg records whose facility is not LOG_KERN. (Sashiko)
- Rebase on current master.
v1: https://lore.kernel.org/r/20261012155339.3799130-1-sahaj123.sc@gmail.com
Eric, v1 was narrower than the generic network-sysctl coverage you
suggested. Is this reduced scope acceptable, or should it be folded into
an existing selftest?
tools/testing/selftests/net/.gitignore | 1 +
tools/testing/selftests/net/Makefile | 1 +
tools/testing/selftests/net/sysctl_net_test.c | 925 ++++++++++++++++++
3 files changed, 927 insertions(+)
create mode 100644 tools/testing/selftests/net/sysctl_net_test.c
diff --git a/tools/testing/selftests/net/.gitignore b/tools/testing/selftests/net/.gitignore
index dacd36ed8455..842cb359bbf6 100644
--- a/tools/testing/selftests/net/.gitignore
+++ b/tools/testing/selftests/net/.gitignore
@@ -43,6 +43,7 @@ so_incoming_cpu
so_netns_cookie
so_rcv_listener
stress_reuseport_listen
+sysctl_net_test
tap
tcp_fastopen_backup_key
tcp_inq
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 590b33e16d9f..8926fcfef6e9 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -196,6 +196,7 @@ TEST_GEN_PROGS := \
sk_connect_zero_addr \
sk_so_peek_off \
so_incoming_cpu \
+ sysctl_net_test \
tap \
tcp_port_share \
tls \
diff --git a/tools/testing/selftests/net/sysctl_net_test.c b/tools/testing/selftests/net/sysctl_net_test.c
new file mode 100644
index 000000000000..6360616132c4
--- /dev/null
+++ b/tools/testing/selftests/net/sysctl_net_test.c
@@ -0,0 +1,925 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <arpa/inet.h>
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <limits.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <poll.h>
+#include <sched.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+
+#define SYSCTL_ROOT "/proc/sys/net"
+#define SYSCTL_TEXT_SIZE 4096
+#define SYSCTL_MAX_VALUES 256
+#define KMSG_TEXT_SIZE 4096
+#define FIRST_TEST_PORT 20000
+#define LAST_TEST_PORT 59998
+
+struct sysctl_number {
+ uint64_t magnitude;
+ bool negative;
+};
+
+struct sysctl_values {
+ struct sysctl_number value[SYSCTL_MAX_VALUES];
+ size_t count;
+};
+
+static int kmsg_fd = -1;
+static unsigned int next_tcp_port = FIRST_TEST_PORT;
+static unsigned int next_udp_port = FIRST_TEST_PORT;
+static size_t tested_files;
+static size_t skipped_files;
+static size_t tested_values;
+static size_t tcp_attempts;
+static size_t tcp_successes;
+static size_t udp_attempts;
+static size_t udp_successes;
+static char failure[256];
+static char tcp_delivery_failure[256];
+static char udp_delivery_failure[256];
+
+/*
+ * Only add sysctls that are per network namespace and are restored by
+ * writing their original value back. Some sysctls change global state that
+ * the private namespace does not isolate and that cannot be undone.
+ */
+static const char * const representative_sysctls[] = {
+ "ipv4/tcp_rmem",
+ "ipv4/tcp_wmem",
+ "ipv4/udp_rmem_min",
+ "ipv4/udp_wmem_min",
+};
+
+static bool selected_sysctls[ARRAY_SIZE(representative_sysctls)];
+
+static void set_failure(const char *fmt, ...)
+{
+ va_list args;
+
+ if (failure[0])
+ return;
+
+ va_start(args, fmt);
+ vsnprintf(failure, sizeof(failure), fmt, args);
+ va_end(args);
+}
+
+static struct sysctl_number number_from_signed(int64_t value)
+{
+ struct sysctl_number number = {};
+
+ if (value < 0) {
+ number.negative = true;
+ number.magnitude = (uint64_t)(-(value + 1)) + 1;
+ } else {
+ number.magnitude = value;
+ }
+ return number;
+}
+
+static struct sysctl_number number_from_unsigned(uint64_t value)
+{
+ struct sysctl_number number = {
+ .magnitude = value,
+ };
+
+ return number;
+}
+
+static int64_t number_to_signed(struct sysctl_number number)
+{
+ if (!number.negative)
+ return (int64_t)number.magnitude;
+ if (number.magnitude == (UINT64_C(1) << 63))
+ return INT64_MIN;
+ return -(int64_t)number.magnitude;
+}
+
+static bool numbers_equal(struct sysctl_number a, struct sysctl_number b)
+{
+ return a.magnitude == b.magnitude &&
+ (a.magnitude == 0 || a.negative == b.negative);
+}
+
+static bool format_number(struct sysctl_number number, char *text,
+ size_t text_size)
+{
+ int n = snprintf(text, text_size, number.negative ? "-%llu" : "%llu",
+ (unsigned long long)number.magnitude);
+
+ return n >= 0 && (size_t)n < text_size;
+}
+
+static bool parse_values(char *text, struct sysctl_values *values)
+{
+ char *pos = text;
+
+ values->count = 0;
+ while (*pos) {
+ char *end;
+ struct sysctl_number value;
+
+ while (isspace((unsigned char)*pos))
+ pos++;
+ if (!*pos)
+ break;
+ if (values->count == SYSCTL_MAX_VALUES)
+ return false;
+
+ errno = 0;
+ if (*pos == '-') {
+ long long signed_value = strtoll(pos, &end, 10);
+
+ if (end == pos || errno == ERANGE)
+ return false;
+ value = number_from_signed(signed_value);
+ } else {
+ unsigned long long unsigned_value = strtoull(pos, &end, 10);
+
+ if (end == pos || errno == ERANGE)
+ return false;
+ value = number_from_unsigned(unsigned_value);
+ }
+ if (*end && !isspace((unsigned char)*end))
+ return false;
+
+ values->value[values->count++] = value;
+ pos = end;
+ }
+
+ return values->count > 0;
+}
+
+static bool read_values(const char *path, struct sysctl_values *values)
+{
+ char text[SYSCTL_TEXT_SIZE];
+ size_t len = 0;
+ int fd;
+ bool ok = false;
+
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return false;
+
+ while (len < sizeof(text) - 1) {
+ ssize_t n = read(fd, text + len, sizeof(text) - 1 - len);
+
+ if (n < 0 && errno == EINTR)
+ continue;
+ if (n < 0)
+ goto out;
+ if (!n)
+ break;
+ len += n;
+ }
+ if (len == sizeof(text) - 1)
+ goto out;
+
+ text[len] = '\0';
+ ok = parse_values(text, values);
+out:
+ close(fd);
+ return ok;
+}
+
+static bool values_equal(const struct sysctl_values *a,
+ const struct sysctl_values *b)
+{
+ size_t i;
+
+ if (a->count != b->count)
+ return false;
+ for (i = 0; i < a->count; i++) {
+ if (!numbers_equal(a->value[i], b->value[i]))
+ return false;
+ }
+ return true;
+}
+
+static bool format_values(const struct sysctl_values *values, char *text,
+ size_t text_size)
+{
+ size_t i, len = 0;
+
+ for (i = 0; i < values->count; i++) {
+ char number[32];
+ int n;
+
+ if (!format_number(values->value[i], number, sizeof(number)))
+ return false;
+ n = snprintf(text + len, text_size - len, "%s%c", number,
+ i + 1 == values->count ? '\n' : ' ');
+ if (n < 0 || (size_t)n >= text_size - len)
+ return false;
+ len += n;
+ }
+ return len > 0;
+}
+
+static bool write_values(const char *path,
+ const struct sysctl_values *values)
+{
+ char text[SYSCTL_TEXT_SIZE];
+ size_t len;
+ ssize_t n;
+ int fd;
+ bool ok;
+
+ if (!format_values(values, text, sizeof(text)))
+ return false;
+ len = strlen(text);
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd < 0)
+ return false;
+ do {
+ n = write(fd, text, len);
+ } while (n < 0 && errno == EINTR);
+ ok = n == (ssize_t)len;
+ if (close(fd) < 0)
+ ok = false;
+ return ok;
+}
+
+static bool restore_values(const char *path,
+ const struct sysctl_values *original)
+{
+ struct sysctl_values current;
+
+ if (read_values(path, ¤t) && values_equal(original, ¤t))
+ return true;
+ return write_values(path, original) &&
+ read_values(path, ¤t) &&
+ values_equal(original, ¤t);
+}
+
+static bool check_kernel_log(void)
+{
+ static const char * const splats[] = {
+ "WARNING:",
+ "BUG:",
+ "Oops:",
+ "divide error",
+ "general protection fault",
+ "invalid opcode",
+ "kernel BUG at",
+ "Kernel panic",
+ "KASAN:",
+ "UBSAN:",
+ "soft lockup",
+ "hard LOCKUP",
+ "blocked for more than",
+ "possible circular locking dependency",
+ };
+ char record[KMSG_TEXT_SIZE + 1];
+
+ for (;;) {
+ ssize_t n = read(kmsg_fd, record, sizeof(record) - 1);
+ unsigned long prefix;
+ char *message;
+ size_t i;
+
+ if (n < 0 && errno == EINTR)
+ continue;
+ if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ return true;
+ if (n < 0) {
+ set_failure("cannot read /dev/kmsg: %s", strerror(errno));
+ return false;
+ }
+ if (!n)
+ return true;
+
+ record[n] = '\0';
+ message = strchr(record, ';');
+ if (!message)
+ continue;
+ /* Ignore messages that userspace wrote to /dev/kmsg. */
+ prefix = strtoul(record, NULL, 10);
+ if (prefix >> 3 != 0)
+ continue;
+ message++;
+ for (i = 0; i < ARRAY_SIZE(splats); i++) {
+ if (!strstr(message, splats[i]))
+ continue;
+ set_failure("kernel splat detected: %.180s", message);
+ return false;
+ }
+ }
+}
+
+static uint64_t ordered_value(int64_t value)
+{
+ /* Map signed integers to unsigned keys in numeric order. */
+ return (uint64_t)value ^ (UINT64_C(1) << 63);
+}
+
+static int64_t unordered_value(uint64_t key)
+{
+ uint64_t raw = key ^ (UINT64_C(1) << 63);
+
+ if (raw <= INT64_MAX)
+ return (int64_t)raw;
+ return -1 - (int64_t)(UINT64_MAX - raw);
+}
+
+static int probe_value(const char *path, const struct sysctl_values *original,
+ size_t index, struct sysctl_number candidate,
+ bool *accepted)
+{
+ struct sysctl_values expected = *original;
+ struct sysctl_values current;
+ char text[32];
+
+ expected.value[index] = candidate;
+ *accepted = false;
+
+ if (write_values(path, &expected) &&
+ read_values(path, ¤t) &&
+ values_equal(&expected, ¤t))
+ *accepted = true;
+
+ if (!restore_values(path, original)) {
+ format_number(candidate, text, sizeof(text));
+ set_failure("failed to restore %s after probing %s", path, text);
+ return -1;
+ }
+ if (!check_kernel_log())
+ return -1;
+ return 0;
+}
+
+static uint64_t next_step(uint64_t step)
+{
+ if (step > UINT64_MAX / 2)
+ return UINT64_MAX;
+ return step * 2;
+}
+
+static int find_bound(const char *path,
+ const struct sysctl_values *original, size_t index,
+ bool lower, bool unsigned_domain,
+ struct sysctl_number *bound)
+{
+ struct sysctl_number start = original->value[index];
+ uint64_t valid = unsigned_domain ? start.magnitude :
+ ordered_value(number_to_signed(start));
+ uint64_t step = 1;
+
+ for (;;) {
+ uint64_t candidate, invalid;
+ bool accepted;
+
+ if (lower) {
+ if (!valid)
+ break;
+ candidate = valid > step ? valid - step : 0;
+ } else {
+ if (valid == UINT64_MAX)
+ break;
+ candidate = UINT64_MAX - valid < step ?
+ UINT64_MAX : valid + step;
+ }
+ if (candidate == valid)
+ break;
+
+ if (probe_value(path, original, index,
+ unsigned_domain ? number_from_unsigned(candidate) :
+ number_from_signed(unordered_value(candidate)),
+ &accepted))
+ return -1;
+ if (accepted) {
+ valid = candidate;
+ step = next_step(step);
+ continue;
+ }
+
+ invalid = candidate;
+ while (lower ? valid - invalid > 1 : invalid - valid > 1) {
+ uint64_t middle = lower ?
+ invalid + (valid - invalid) / 2 :
+ valid + (invalid - valid) / 2;
+
+ if (probe_value(path, original, index,
+ unsigned_domain ?
+ number_from_unsigned(middle) :
+ number_from_signed(unordered_value(middle)),
+ &accepted))
+ return -1;
+ if (accepted)
+ valid = middle;
+ else
+ invalid = middle;
+ }
+ break;
+ }
+
+ *bound = unsigned_domain ? number_from_unsigned(valid) :
+ number_from_signed(unordered_value(valid));
+ return 0;
+}
+
+static int find_lower_bound(const char *path,
+ const struct sysctl_values *original,
+ size_t index, struct sysctl_number *bound)
+{
+ struct sysctl_number start = original->value[index];
+ bool unsigned_domain = !start.negative && start.magnitude > INT64_MAX;
+
+ return find_bound(path, original, index, true, unsigned_domain, bound);
+}
+
+static int find_upper_bound(const char *path,
+ const struct sysctl_values *original,
+ size_t index, struct sysctl_number *bound)
+{
+ bool unsigned_domain = !original->value[index].negative;
+
+ return find_bound(path, original, index, false, unsigned_domain, bound);
+}
+
+static int get_port_pair(bool tcp, uint16_t *server, uint16_t *client)
+{
+ unsigned int *next = tcp ? &next_tcp_port : &next_udp_port;
+
+ if (*next > LAST_TEST_PORT) {
+ errno = EADDRNOTAVAIL;
+ return -1;
+ }
+ *server = *next;
+ *client = *next + 1;
+ *next += 2;
+ return 0;
+}
+
+static int bind_loopback(int fd, uint16_t port)
+{
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_port = htons(port),
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+
+ return bind(fd, (struct sockaddr *)&addr, sizeof(addr));
+}
+
+static int wait_readable(int fd)
+{
+ struct pollfd pfd = {
+ .fd = fd,
+ .events = POLLIN,
+ };
+ int ret;
+
+ ret = poll(&pfd, 1, 1000);
+ if (ret < 0)
+ return -1;
+ if (!ret) {
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ if (!(pfd.revents & POLLIN)) {
+ errno = EIO;
+ return -1;
+ }
+ return 0;
+}
+
+static int tcp_loopback(void)
+{
+ struct timeval timeout = { .tv_sec = 1 };
+ uint16_t server_port, client_port;
+ char data[64] = { 1 };
+ char received[sizeof(data)];
+ int listener = -1, client = -1, server = -1;
+ int ret = -1;
+ ssize_t n;
+
+ if (get_port_pair(true, &server_port, &client_port))
+ return -1;
+ listener = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ client = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (listener < 0 || client < 0 ||
+ setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &timeout,
+ sizeof(timeout)) ||
+ bind_loopback(listener, server_port) ||
+ listen(listener, 1) || bind_loopback(client, client_port))
+ goto out;
+
+ {
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_port = htons(server_port),
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+
+ if (connect(client, (struct sockaddr *)&addr, sizeof(addr)))
+ goto out;
+ }
+
+ if (wait_readable(listener))
+ goto out;
+ server = accept4(listener, NULL, NULL, SOCK_CLOEXEC);
+ if (server < 0)
+ goto out;
+ n = send(client, data, sizeof(data), MSG_NOSIGNAL);
+ if (n <= 0) {
+ if (!n)
+ errno = EPIPE;
+ goto out;
+ }
+ if (wait_readable(server))
+ goto out;
+
+ n = recv(server, received, sizeof(received), 0);
+ if (n <= 0) {
+ if (!n)
+ errno = ECONNRESET;
+ goto out;
+ }
+ ret = 0;
+out:
+ if (server >= 0)
+ close(server);
+ if (client >= 0)
+ close(client);
+ if (listener >= 0)
+ close(listener);
+ return ret;
+}
+
+static int udp_loopback(void)
+{
+ uint16_t server_port, client_port;
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+ char data = 1, received;
+ int server = -1, client = -1;
+ int ret = -1;
+ ssize_t n;
+
+ if (get_port_pair(false, &server_port, &client_port))
+ return -1;
+ server = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ client = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (server < 0 || client < 0 || bind_loopback(server, server_port) ||
+ bind_loopback(client, client_port))
+ goto out;
+
+ addr.sin_port = htons(server_port);
+ n = sendto(client, &data, sizeof(data), 0, (struct sockaddr *)&addr,
+ sizeof(addr));
+ if (n != sizeof(data)) {
+ if (n >= 0)
+ errno = EIO;
+ goto out;
+ }
+ if (wait_readable(server))
+ goto out;
+
+ n = recv(server, &received, sizeof(received), 0);
+ if (n != sizeof(received)) {
+ if (n >= 0)
+ errno = EIO;
+ goto out;
+ }
+ ret = 0;
+out:
+ if (client >= 0)
+ close(client);
+ if (server >= 0)
+ close(server);
+ return ret;
+}
+
+static void run_loopback_traffic(const char *path, size_t index,
+ const char *value)
+{
+ int err;
+
+ tcp_attempts++;
+ if (!tcp_loopback()) {
+ tcp_successes++;
+ } else {
+ err = errno;
+ if (!tcp_delivery_failure[0])
+ snprintf(tcp_delivery_failure,
+ sizeof(tcp_delivery_failure),
+ "%s[%zu]=%s: %s", path, index, value,
+ strerror(err));
+ }
+
+ udp_attempts++;
+ if (!udp_loopback()) {
+ udp_successes++;
+ } else {
+ err = errno;
+ if (!udp_delivery_failure[0])
+ snprintf(udp_delivery_failure,
+ sizeof(udp_delivery_failure),
+ "%s[%zu]=%s: %s", path, index, value,
+ strerror(err));
+ }
+}
+
+static int exercise_value(const char *path,
+ const struct sysctl_values *original,
+ size_t index, struct sysctl_number value)
+{
+ struct sysctl_values expected = *original;
+ char text[32];
+ bool restore = !numbers_equal(value, original->value[index]);
+ bool applied = false;
+
+ if (!format_number(value, text, sizeof(text))) {
+ set_failure("cannot format boundary for %s", path);
+ return -1;
+ }
+ if (restore) {
+ struct sysctl_values current;
+
+ expected.value[index] = value;
+ applied = write_values(path, &expected) &&
+ read_values(path, ¤t) &&
+ values_equal(&expected, ¤t);
+ if (!applied) {
+ if (!restore_values(path, original)) {
+ set_failure("failed to restore %s after setting %s",
+ path, text);
+ return -1;
+ }
+ set_failure("%s no longer accepts boundary %s", path, text);
+ return -1;
+ }
+ }
+
+ check_kernel_log();
+ if (!failure[0])
+ run_loopback_traffic(path, index, text);
+
+ if (restore && !restore_values(path, original)) {
+ format_number(value, text, sizeof(text));
+ set_failure("failed to restore %s after traffic at %s", path,
+ text);
+ }
+ check_kernel_log();
+ return failure[0] ? -1 : 0;
+}
+
+static bool prepare_sysctl(const char *path,
+ const struct sysctl_values *original)
+{
+ struct sysctl_values current;
+ bool writable;
+
+ writable = write_values(path, original) &&
+ read_values(path, ¤t) &&
+ values_equal(original, ¤t);
+ if (!writable && !restore_values(path, original)) {
+ set_failure("failed to restore %s after initial write", path);
+ return false;
+ }
+ if (!check_kernel_log())
+ return false;
+ return writable;
+}
+
+static int test_sysctl(const char *path, const struct sysctl_values *original)
+{
+ size_t i;
+
+ if (!prepare_sysctl(path, original)) {
+ if (!failure[0]) {
+ skipped_files++;
+ printf("# SKIP %s: sysctl is not writable\n", path);
+ }
+ return failure[0] ? -1 : 0;
+ }
+
+ tested_files++;
+ for (i = 0; i < original->count; i++) {
+ struct sysctl_number lower, upper;
+
+ if (find_lower_bound(path, original, i, &lower) ||
+ find_upper_bound(path, original, i, &upper))
+ return -1;
+ if (exercise_value(path, original, i, lower) ||
+ (!numbers_equal(upper, lower) &&
+ exercise_value(path, original, i, upper)))
+ return -1;
+ tested_values++;
+ }
+ return 0;
+}
+
+static void print_usage(FILE *stream, const char *program)
+{
+ size_t i;
+
+ fprintf(stream, "Usage: %s [-s SYSCTL ...]\n", program);
+ fprintf(stream, " -s, --sysctl SYSCTL Test a representative sysctl (repeatable)\n");
+ fprintf(stream, " -h, --help Show this help\n");
+ fprintf(stream, "Default sysctls:\n");
+ for (i = 0; i < ARRAY_SIZE(representative_sysctls); i++)
+ fprintf(stream, " %s\n", representative_sysctls[i]);
+}
+
+static int parse_options(int argc, char **argv)
+{
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "sysctl", required_argument, NULL, 's' },
+ {}
+ };
+ bool selection_given = false;
+ int option;
+
+ while ((option = getopt_long(argc, argv, "hs:", options, NULL)) != -1) {
+ switch (option) {
+ case 's': {
+ size_t i;
+
+ selection_given = true;
+ for (i = 0; i < ARRAY_SIZE(representative_sysctls); i++) {
+ if (strcmp(optarg, representative_sysctls[i]))
+ continue;
+ selected_sysctls[i] = true;
+ break;
+ }
+ if (i == ARRAY_SIZE(representative_sysctls)) {
+ fprintf(stderr, "Unknown sysctl: %s\n", optarg);
+ goto invalid_options;
+ }
+ break;
+ }
+ case 'h':
+ print_usage(stdout, argv[0]);
+ return 1;
+ default:
+ goto invalid_options;
+ }
+ }
+ if (optind != argc)
+ goto invalid_options;
+
+ if (!selection_given) {
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(representative_sysctls); i++)
+ selected_sysctls[i] = true;
+ }
+ return 0;
+
+invalid_options:
+ print_usage(stderr, argv[0]);
+ return -1;
+}
+
+static int test_selected_sysctl(const char *name)
+{
+ char path[PATH_MAX];
+ struct sysctl_values values;
+ int n;
+
+ n = snprintf(path, sizeof(path), "%s/%s", SYSCTL_ROOT, name);
+ if (n < 0 || (size_t)n >= sizeof(path)) {
+ set_failure("sysctl path is too long: %s", name);
+ return -1;
+ }
+
+ if (access(path, F_OK)) {
+ if (errno == ENOENT || errno == ENOTDIR) {
+ skipped_files++;
+ printf("# SKIP %s: not available\n", name);
+ return 0;
+ }
+ set_failure("cannot inspect %s: %s", path, strerror(errno));
+ return -1;
+ }
+ if (access(path, R_OK)) {
+ int err = errno;
+
+ skipped_files++;
+ printf("# SKIP %s: not readable (%s)\n", name, strerror(err));
+ return 0;
+ }
+ if (!read_values(path, &values)) {
+ set_failure("%s is not a readable numeric sysctl", path);
+ return -1;
+ }
+ if (access(path, W_OK)) {
+ int err = errno;
+
+ skipped_files++;
+ printf("# SKIP %s: not writable (%s)\n", name, strerror(err));
+ return 0;
+ }
+
+ printf("# Testing %s\n", name);
+ return test_sysctl(path, &values);
+}
+
+static int set_loopback_up(void)
+{
+ struct ifreq ifr = {};
+ int fd, ret = -1;
+
+ fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (fd < 0)
+ return -1;
+ strncpy(ifr.ifr_name, "lo", IFNAMSIZ - 1);
+ if (ioctl(fd, SIOCGIFFLAGS, &ifr))
+ goto out;
+ ifr.ifr_flags |= IFF_UP;
+ ret = ioctl(fd, SIOCSIFFLAGS, &ifr);
+out:
+ close(fd);
+ return ret;
+}
+
+int main(int argc, char **argv)
+{
+ int option_status;
+ size_t i;
+
+ option_status = parse_options(argc, argv);
+ if (option_status > 0)
+ return KSFT_PASS;
+ if (option_status < 0)
+ return KSFT_FAIL;
+
+ ksft_print_header();
+ ksft_set_plan(1);
+
+ if (unshare(CLONE_NEWNET))
+ ksft_exit_skip("cannot create network namespace: %s\n",
+ strerror(errno));
+
+ kmsg_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ if (kmsg_fd < 0)
+ ksft_exit_skip("cannot read /dev/kmsg: %s\n", strerror(errno));
+ if (lseek(kmsg_fd, 0, SEEK_END) < 0)
+ ksft_exit_skip("cannot seek /dev/kmsg: %s\n", strerror(errno));
+ if (set_loopback_up())
+ ksft_exit_skip("cannot bring loopback up: %s\n", strerror(errno));
+ if (!check_kernel_log())
+ goto fail;
+
+ for (i = 0; i < ARRAY_SIZE(representative_sysctls); i++) {
+ if (!selected_sysctls[i])
+ continue;
+ if (test_selected_sysctl(representative_sysctls[i]))
+ break;
+ }
+ if (!failure[0] && !tested_files)
+ ksft_exit_skip("no selected writable numeric network sysctls found\n");
+ if (!failure[0] && (!tcp_successes || !udp_successes))
+ set_failure("loopback delivery incomplete: TCP %zu/%zu, UDP %zu/%zu",
+ tcp_successes, tcp_attempts,
+ udp_successes, udp_attempts);
+ if (!failure[0] && !check_kernel_log())
+ goto fail;
+ if (tcp_delivery_failure[0])
+ printf("# first TCP delivery failure: %s\n",
+ tcp_delivery_failure);
+ if (udp_delivery_failure[0])
+ printf("# first UDP delivery failure: %s\n",
+ udp_delivery_failure);
+ if (failure[0])
+ goto fail;
+
+ printf("# tested %zu sysctls, %zu values; skipped %zu\n",
+ tested_files, tested_values, skipped_files);
+ ksft_test_result_pass("TCP %zu/%zu, UDP %zu/%zu deliveries succeeded\n",
+ tcp_successes, tcp_attempts,
+ udp_successes, udp_attempts);
+ ksft_finished();
+ close(kmsg_fd);
+ return KSFT_PASS;
+
+fail:
+ ksft_test_result_fail("%s\n", failure);
+ ksft_finished();
+ close(kmsg_fd);
+ return KSFT_FAIL;
+}
--
2.43.0
^ permalink raw reply related [flat|nested] 2+ messages in thread* Re: [PATCH net v2] selftests/net: test network sysctl extremes
2026-10-14 4:14 [PATCH net v2] selftests/net: test network sysctl extremes Sahaj Chaudhari
@ 2026-10-18 4:35 ` netdev-bot+sashiko
0 siblings, 0 replies; 2+ messages in thread
From: netdev-bot+sashiko @ 2026-10-18 4:35 UTC (permalink / raw)
To: sahaj123.sc
Cc: davem, edumazet, kuba, pabeni, horms, shuah, netdev,
linux-kselftest, linux-kernel
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 3
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] selftests/net: unhandled EPIPE when reading /dev/kmsg causes
spurious test failures
- [Low] main() in tools/testing/selftests/net/sysctl_net_test.c uses the
kselftest.h API against its documented contract.
- [Low] The UDP part of the test cannot exercise the documented effect of
the UDP sysctls it selects, so its 'UDP 16/16' result says nothing…
- [Low] v2 adds a facility filter to check_kernel_log() (`prefix =
strtoul(record, NULL, 10); if (prefix >> 3 != 0) continue;`, lines…
--
selftests/net: test network sysctl extremes
This adds a new net selftest, sysctl_net_test. It finds the accepted lower
and upper bounds of four per-netns sysctls (tcp_rmem, tcp_wmem,
udp_rmem_min, udp_wmem_min) and runs TCP and UDP loopback traffic at each
bound. While it runs, it watches /dev/kmsg for kernel splats, and it
restores the original values after each probe.
Link: https://lore.kernel.org/r/20261007054508.1059927-1-sahaj123.sc@gmail.com
> diff --git a/tools/testing/selftests/net/sysctl_net_test.c b/tools/testing/selftests/net/sysctl_net_test.c
> new file mode 100644
> index 0000000000000..6360616132c41
> --- /dev/null
> +++ b/tools/testing/selftests/net/sysctl_net_test.c
> @@ -0,0 +1,925 @@
[ ... ]
> +static bool check_kernel_log(void)
> +{
[ ... ]
> + record[n] = '\0';
> + message = strchr(record, ';');
> + if (!message)
> + continue;
> + /* Ignore messages that userspace wrote to /dev/kmsg. */
> + prefix = strtoul(record, NULL, 10);
> + if (prefix >> 3 != 0)
> + continue;
> + message++;
> + for (i = 0; i < ARRAY_SIZE(splats); i++) {
> + if (!strstr(message, splats[i]))
> + continue;
> + set_failure("kernel splat detected: %.180s", message);
> + return false;
> + }
> + }
> +}
[Severity: Medium]
What happens in check_kernel_log() when the printk ring buffer wraps
past this reader between two calls?
devkmsg_read() reports that case once as -EPIPE. It then moves the
reader forward to the oldest record that is still available:
kernel/printk/printk.c:devkmsg_read() {
...
if (pmsg.dropped) {
/* our last seen message is gone, return error and reset */
atomic64_set(&user->seq, pmsg.seq);
ret = -EPIPE;
goto out;
}
...
}
The loop here handles EINTR and EAGAIN/EWOULDBLOCK. Any other errno
goes to set_failure() and returns false.
So on a busy machine where something else logs heavily during the run,
the test fails with "cannot read /dev/kmsg: Broken pipe". Nothing in
the network stack went wrong in that case. probe_value(),
prepare_sysctl() and exercise_value() all turn this into a failure.
main() then stops testing the remaining sysctls. That makes the test
flaky on noisy CI hosts.
The next read() after -EPIPE succeeds, so could this case simply
continue the loop? If you want the output to show that records were
missed, and a splat could therefore have been lost, a "# " diagnostic
line might be enough. Or do you prefer to treat lost records as a
failure?
[Severity: Low]
The commit message says the test passed "also while userspace wrote
WARNING:/BUG: lines to /dev/kmsg". This facility check covers that case,
because devkmsg_write() forces LOG_USER on those records.
Does it keep all user-controlled text out of the match, though? Records
that the kernel itself generates still carry facility 0 even when they
embed user strings. Also, strstr() matches anywhere in the message body.
For example, on x86 show_signal_msg() puts the task comm at the start of
the line:
arch/x86/mm/fault.c:show_signal_msg() {
...
printk("%s%s[%d]: segfault at %lx ip %px sp %px error %lx",
loglvl, tsk->comm, task_pid_nr(tsk), address,
...
}
Suppose an unprivileged process calls prctl(PR_SET_NAME, "BUG:") and then
segfaults while this test runs. The record "BUG:[1234]: segfault at ..."
passes the prefix >> 3 check and matches "BUG:". The test then reports a
kernel splat and fails.
Would it help to match the markers only at the start of the message
(for example "WARNING: CPU:", "------------[ cut here ]", or "BUG: " at
offset 0)?
[ ... ]
> +static int udp_loopback(void)
> +{
[ ... ]
> + addr.sin_port = htons(server_port);
> + n = sendto(client, &data, sizeof(data), 0, (struct sockaddr *)&addr,
> + sizeof(addr));
[Severity: Low]
How much does the "UDP 16/16" result say about udp_rmem_min and
udp_wmem_min?
udp_loopback() sends and receives a single byte, which normally stays
below the protocol memory limit. In that case __sk_mem_raise_allocated()
returns before it reads udp_rmem_min:
net/core/sock.c:__sk_mem_raise_allocated() {
...
/* Under limit. */
if (allocated <= sk_prot_mem_limits(sk, 0)) {
sk_leave_memory_pressure(sk);
return 1;
}
...
if (kind == SK_MEM_RECV) {
if (atomic_read(&sk->sk_rmem_alloc) < sk_get_rmem0(sk, prot))
return 1;
...
}
Documentation/networking/ip-sysctl.rst also says udp_rmem_min applies
"even if total pages of UDP sockets exceed udp_mem pressure". For
udp_wmem_min it says "this tunable has no effect".
The test never creates UDP memory pressure. Unless the system is already
under pressure, wouldn't both bounds of both UDP sysctls pass whether or
not the minimum buffer handling works?
> + if (n != sizeof(data)) {
> + if (n >= 0)
> + errno = EIO;
> + goto out;
> + }
[ ... ]
> +int main(int argc, char **argv)
> +{
[ ... ]
> + ksft_print_header();
> + ksft_set_plan(1);
> +
> + if (unshare(CLONE_NEWNET))
> + ksft_exit_skip("cannot create network namespace: %s\n",
> + strerror(errno));
> +
> + kmsg_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
> + if (kmsg_fd < 0)
> + ksft_exit_skip("cannot read /dev/kmsg: %s\n", strerror(errno));
> + if (lseek(kmsg_fd, 0, SEEK_END) < 0)
> + ksft_exit_skip("cannot seek /dev/kmsg: %s\n", strerror(errno));
> + if (set_loopback_up())
> + ksft_exit_skip("cannot bring loopback up: %s\n", strerror(errno));
[ ... ]
> + if (!failure[0] && !tested_files)
> + ksft_exit_skip("no selected writable numeric network sysctls found\n");
[ ... ]
> + ksft_test_result_pass("TCP %zu/%zu, UDP %zu/%zu deliveries succeeded\n",
> + tcp_successes, tcp_attempts,
> + udp_successes, udp_attempts);
> + ksft_finished();
> + close(kmsg_fd);
> + return KSFT_PASS;
> +
> +fail:
> + ksft_test_result_fail("%s\n", failure);
> + ksft_finished();
> + close(kmsg_fd);
> + return KSFT_FAIL;
> +}
[Severity: Low]
Can the close(kmsg_fd) and return statements after ksft_finished() ever
run? ksft_finished() expands to ksft_exit(), and that always calls either
ksft_exit_pass() or ksft_exit_fail(). Both are __noreturn.
So the exit status comes from the kselftest counters, not from the
KSFT_PASS or KSFT_FAIL returned here. The same applies to the fail: path.
Separately, ksft_exit_skip() is called after ksft_set_plan(1). This
happens on the unshare(), /dev/kmsg open and seek, loopback, and "no
selected writable numeric network sysctls" paths. kselftest.h calls this
pattern a misuse:
tools/testing/selftests/kselftest.h:ksft_exit_skip() {
...
/*
* FIXME: several tests misuse ksft_exit_skip so produce
* something sensible if some tests have already been run
* or a plan has been printed. Those tests should use
* ksft_test_result_skip or ksft_exit_fail_msg instead.
*/
...
}
The header works around this today and prints "ok 1 # SKIP", so the TAP
output is still valid. Would ksft_test_result_skip() followed by
ksft_finished() be a better fit on these paths?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261014041446.2821085-1-sahaj123.sc%40gmail.com
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-18 4:35 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-14 4:14 [PATCH net v2] selftests/net: test network sysctl extremes Sahaj Chaudhari
2026-10-18 4:35 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox