* [PATCH net v3] selftests/net: test network sysctl extremes
@ 2026-10-25 16:09 Sahaj Chaudhari
2026-10-26 16:49 ` netdev-bot+sashiko
0 siblings, 1 reply; 2+ messages in thread
From: Sahaj Chaudhari @ 2026-10-25 16:09 UTC (permalink / raw)
To: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Shuah Khan
Cc: Hangbin Liu, netdev-bot+sashiko, linux-kernel, netdev, linux-kselftest
Probe the accepted lower and upper bounds of per-network-namespace TCP
receive and send memory sysctls, then exercise loopback traffic at each
bound.
Drop udp_rmem_min and udp_wmem_min: one-byte UDP traffic does not create
the memory pressure needed to test them. Keep UDP loopback only as a
transport smoke check.
Link: https://lore.kernel.org/r/20261014041446.2821085-1-sahaj123.sc@gmail.com
Tested: W=1 selftest build; x86_64 QEMU with CONFIG_NET_NS=y.
Signed-off-by: Sahaj Chaudhari <sahaj123.sc@gmail.com>
---
Changes in v3:
- Handle /dev/kmsg EPIPE as lost records and skip an incomplete splat check.
- Use the kselftest result/finish API for skips and final results.
- Drop UDP sysctls that the one-byte traffic did not exercise.
- Match kernel splat signatures without matching task-controlled "BUG:" text.
Scope: In tools/testing/selftests/net, my search found packetdrill and other
scripts that set these sysctls as setup, but no test that probes accepted
bounds, restores the original values, and checks loopback traffic together.
I kept this standalone for now; Eric, would you prefer it folded into an
existing test?
Testing:
- Built sysctl_net_test and nettest with W=1 against netdev/main.
- Ran in x86_64 QEMU with CONFIG_NET_NS=y. The test searched the accepted
lower and upper bounds of tcp_rmem and tcp_wmem, verified each write by
reading it back, restored the original values, sent 64-byte TCP and
one-byte UDP loopback traffic at each bound, and checked /dev/kmsg for
splats. It tested 2 sysctls and 6 values; TCP and UDP both succeeded
12/12 times. A task named "BUG:" produced a SIGSEGV record and stack-depth
notice without a false splat report.
- An ARM64 run on a vendor Linux 6.12.57 kernel accepted tcp_rmem[1]=1 and
timed out once in 12 TCP attempts; UDP succeeded 12/12. A separate local
packet-capture probe sent 64-byte payloads with tcp_rmem[1] set to 1, 4096,
and 131072. It timed out 5/5 times at 1 and delivered 5/5 times at both
larger values; at 1, SO_RCVBUF was 1 and the SYN/SYN-ACK advertised zero
windows. This was diagnostic testing, not a regression result for
netdev/main: that tree and the documentation require each tcp_rmem value
to be at least 4096.
- The ordinary host invocation skips because this environment cannot create
a network namespace.
---
tools/testing/selftests/net/.gitignore | 1 +
tools/testing/selftests/net/Makefile | 1 +
tools/testing/selftests/net/sysctl_net_test.c | 982 ++++++++++++++++++
3 files changed, 984 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 21412510ac26..b85cd287b2b7 100644
--- a/tools/testing/selftests/net/.gitignore
+++ b/tools/testing/selftests/net/.gitignore
@@ -44,6 +44,7 @@ so_netns_cookie
so_reserve_mem
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 df215e1a6262..6bb4226d02eb 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -200,6 +200,7 @@ TEST_GEN_PROGS := \
sk_so_peek_off \
so_incoming_cpu \
so_reserve_mem \
+ 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..81b9b32245f4
--- /dev/null
+++ b/tools/testing/selftests/net/sysctl_net_test.c
@@ -0,0 +1,982 @@
+// 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 bool kernel_log_incomplete;
+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",
+};
+
+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: possible circular locking dependency",
+ "BUG: unable to handle",
+ "------------[ cut here ]",
+ "Kernel panic - not syncing:",
+ "BUG: KASAN:",
+ "watchdog: BUG: soft lockup",
+ "watchdog: BUG: hard LOCKUP",
+ };
+ static const char * const error_prefixes[] = {
+ "Oops:",
+ "divide error:",
+ "general protection fault:",
+ "invalid opcode:",
+ };
+ 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;
+ bool splat = false;
+
+ if (n < 0 && errno == EINTR)
+ continue;
+ if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ return true;
+ if (n < 0 && errno == EPIPE) {
+ kernel_log_incomplete = true;
+ continue;
+ }
+ 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 (!strncmp(message, splats[i], strlen(splats[i]))) {
+ splat = true;
+ break;
+ }
+ }
+ if (!splat &&
+ !strncmp(message, "WARNING: CPU: ", strlen("WARNING: CPU: "))) {
+ char *p = message + strlen("WARNING: CPU: ");
+
+ if (isdigit((unsigned char)*p)) {
+ while (isdigit((unsigned char)*p))
+ p++;
+ if (!strncmp(p, " PID: ", strlen(" PID: "))) {
+ p += strlen(" PID: ");
+ splat = isdigit((unsigned char)*p);
+ }
+ }
+ }
+ for (i = 0; !splat && i < ARRAY_SIZE(error_prefixes); i++) {
+ const char *p;
+
+ if (strncmp(message, error_prefixes[i],
+ strlen(error_prefixes[i])))
+ continue;
+ p = message + strlen(error_prefixes[i]);
+ splat = *p == ' ' && isxdigit((unsigned char)p[1]);
+ }
+ if (!splat && !strncmp(message, "UBSAN: ", strlen("UBSAN: ")) &&
+ isalpha((unsigned char)message[strlen("UBSAN: ")]))
+ splat = true;
+ if (!splat &&
+ !strncmp(message, "INFO: task ", strlen("INFO: task ")) &&
+ strstr(message, " blocked for more than "))
+ splat = true;
+ if (splat) {
+ 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 is a transport smoke check, not udp_*_min pressure coverage. */
+ 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_test_result_skip("cannot create network namespace: %s\n",
+ strerror(errno));
+ ksft_finished();
+ }
+
+ kmsg_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ if (kmsg_fd < 0) {
+ ksft_test_result_skip("cannot read /dev/kmsg: %s\n",
+ strerror(errno));
+ ksft_finished();
+ }
+ if (lseek(kmsg_fd, 0, SEEK_END) < 0) {
+ ksft_test_result_skip("cannot seek /dev/kmsg: %s\n",
+ strerror(errno));
+ close(kmsg_fd);
+ ksft_finished();
+ }
+ if (set_loopback_up()) {
+ ksft_test_result_skip("cannot bring loopback up: %s\n",
+ strerror(errno));
+ close(kmsg_fd);
+ ksft_finished();
+ }
+ 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) {
+ close(kmsg_fd);
+ ksft_test_result_skip("no selected writable numeric network sysctls found\n");
+ ksft_finished();
+ }
+ 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;
+ if (kernel_log_incomplete) {
+ printf("# printk records were lost; kernel splat check is incomplete\n");
+ close(kmsg_fd);
+ ksft_test_result_skip("cannot complete kernel splat check\n");
+ ksft_finished();
+ }
+
+ printf("# tested %zu sysctls, %zu values; skipped %zu\n",
+ tested_files, tested_values, skipped_files);
+ close(kmsg_fd);
+ ksft_test_result_pass("TCP/UDP loopback smoke: TCP %zu/%zu, UDP %zu/%zu\n",
+ tcp_successes, tcp_attempts, udp_successes,
+ udp_attempts);
+ ksft_finished();
+
+fail:
+ close(kmsg_fd);
+ ksft_test_result_fail("%s\n", failure);
+ ksft_finished();
+}
--
2.43.0
^ permalink raw reply [flat|nested] 2+ messages in thread
* Re: [PATCH net v3] selftests/net: test network sysctl extremes
2026-10-25 16:09 [PATCH net v3] selftests/net: test network sysctl extremes Sahaj Chaudhari
@ 2026-10-26 16:49 ` netdev-bot+sashiko
0 siblings, 0 replies; 2+ messages in thread
From: netdev-bot+sashiko @ 2026-10-26 16:49 UTC (permalink / raw)
To: sahaj123.sc
Cc: davem, edumazet, kuba, pabeni, horms, shuah, hangbin.liu,
netdev-bot+sashiko, linux-kernel, netdev, linux-kselftest
Thank you for your contribution! Sashiko AI review found 7 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 4 · 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] Several splat patterns in check_kernel_log()
(tools/testing/selftests/net/sysctl_net_test.c) do not match what this…
- [Medium] The test says it exercises TCP and UDP loopback traffic at
every accepted lower and upper bound of tcp_rmem/tcp_wmem, but its…
- [Medium] Every whole-test skip path in main() calls
ksft_test_result_skip() and then ksft_finished().
- [Medium] check_kernel_log() can still be spoofed by text a task
controls, even though the v3 notes claim "Match kernel splat…
- [Low] The permanent commit message mixes revision history into the
change description, overstates scope, and leaves out behaviour that…
- [Low] The test's verdict depends on global kernel log activity that has
nothing to do with the sysctls under test.
- [Low] The boundary search has no expected value to compare against.
--
selftests/net: test network sysctl extremes
This adds a new kselftest, sysctl_net_test. It unshares a network
namespace, searches for the accepted lower and upper bounds of each value
in ipv4/tcp_rmem and ipv4/tcp_wmem, and runs TCP and UDP loopback traffic
at each bound. While it runs, it also watches /dev/kmsg for kernel splats.
Link: https://lore.kernel.org/r/20261014041446.2821085-1-sahaj123.sc@gmail.com
> selftests/net: test network sysctl extremes
>
> Probe the accepted lower and upper bounds of per-network-namespace TCP
> receive and send memory sysctls, then exercise loopback traffic at each
> bound.
>
> Drop udp_rmem_min and udp_wmem_min: one-byte UDP traffic does not create
> the memory pressure needed to test them. Keep UDP loopback only as a
> transport smoke check.
[ ... ]
> Tested: W=1 selftest build; x86_64 QEMU with CONFIG_NET_NS=y.
[Severity: Low]
This isn't a bug, but the "Drop udp_rmem_min and udp_wmem_min" paragraph
describes a change from an earlier posting. This commit creates
sysctl_net_test.c from scratch, so nothing is dropped in git history.
Should that text go below the --- line with the rest of the changelog?
The subject "test network sysctl extremes" also sounds broader than the
code. representative_sysctls[] only lists ipv4/tcp_rmem and
ipv4/tcp_wmem.
The body doesn't say that the test reads the global /dev/kmsg and fails
on any kernel splat. It also doesn't say that the test skips when
/dev/kmsg can't be opened or seeked, or when printk records are lost.
Could the commit message describe this?
Also, "Tested:" isn't a standard trailer.
> 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..81b9b32245f42
> --- /dev/null
> +++ b/tools/testing/selftests/net/sysctl_net_test.c
> @@ -0,0 +1,982 @@
[ ... ]
> +static bool check_kernel_log(void)
> +{
> + static const char * const splats[] = {
> + "WARNING: possible circular locking dependency",
> + "BUG: unable to handle",
> + "------------[ cut here ]",
> + "Kernel panic - not syncing:",
> + "BUG: KASAN:",
> + "watchdog: BUG: soft lockup",
> + "watchdog: BUG: hard LOCKUP",
> + };
> + static const char * const error_prefixes[] = {
> + "Oops:",
> + "divide error:",
> + "general protection fault:",
> + "invalid opcode:",
> + };
[Severity: Medium]
Do these patterns match what the kernel prints today? Several of them
don't seem to line up with the current format strings.
For WARN, __warn() in kernel/panic.c prints:
pr_warn("WARNING: %s:%d at %pS, CPU#%d: %s/%d\n", ...
pr_warn("WARNING: at %pS, CPU#%d: %s/%d\n", ...
So the "WARNING: CPU: <n> PID: <n>" parser later in check_kernel_log()
can never match. WARNs are only caught by the "cut here" entry.
For x86 oopses, __die_header() in arch/x86/kernel/dumpstack.c prints:
"Oops: %s: %04lx [#%d]%s%s%s%s\n", str, err & 0xffff,
page_fault_oops() passes "Oops" as str, so the record reads
"Oops: Oops: 0000 [#1] ...". The error_prefixes check:
splat = *p == ' ' && isxdigit((unsigned char)p[1]);
rejects that record ('O' is not a hex digit). It also rejects
"Oops: general protection fault: ..." and "Oops: invalid opcode: ...".
No record starts with the bare "general protection fault:" or
"invalid opcode:" prefixes. Only "Oops: divide error" matches, and only
because 'd' happens to be a hex digit.
On x86, a kernel NULL dereference goes through show_fault_oops() in
arch/x86/mm/fault.c:
pr_alert("BUG: kernel NULL pointer dereference, address: %px\n",
That string isn't in splats[], which only has the "BUG: unable to
handle" branch. The die() path prints no "cut here" line, so nothing
matches a NULL dereference.
On arm64, __die() in arch/arm64/kernel/traps.c prints
"Internal error: %s: %016lx [#%d] SMP", and die_kernel_fault() prints
"Unable to handle kernel %s at virtual address". Neither is matched,
even though the commit notes cite an arm64 run.
watchdog_hardlockup_check() in kernel/watchdog.c prints:
pr_emerg("CPU%u: Watchdog detected hard LOCKUP on cpu %u\n", ...);
So the "watchdog: BUG: hard LOCKUP" entry never matches anything.
Lockdep also prints "WARNING: possible recursive locking detected",
"WARNING: inconsistent lock state", "WARNING: suspicious RCU usage",
"WARNING: bad unlock balance detected!" and others with no "cut here"
line. Only the circular dependency report is listed.
An oops can happen in softirq, a timer or a kworker, for example in TCP
timers or loopback RX at an extreme rmem/wmem value. If it does, the
test keeps running and reports a pass. Could these patterns be updated
to match the current format strings?
[ ... ]
> + /* Ignore messages that userspace wrote to /dev/kmsg. */
> + prefix = strtoul(record, NULL, 10);
> + if (prefix >> 3 != 0)
> + continue;
[Severity: Medium]
Can a task still fake a splat here? The v3 notes say the matcher no
longer matches task-controlled "BUG:" text. However, some facility 0
kernel records start with the task comm, and those get past this
filter.
On x86, show_signal_msg() in arch/x86/mm/fault.c does:
printk("%s%s[%d]: segfault at %lx ip %px sp %px error %lx",
loglvl, tsk->comm, task_pid_nr(tsk), address,
show_unhandled_signals defaults to 1. On arm64, arm64_show_signal()
does:
pr_info("%s[%d]: unhandled exception: ", tsk->comm, ...);
TASK_COMM_LEN is 16, so a comm can be up to 15 characters. /dev/kmsg
isn't namespaced, so any unprivileged process on the host can do this:
prctl(PR_SET_NAME, "BUG: KASAN:");
/* then take an unhandled SIGSEGV */
The record "BUG: KASAN:[pid]: segfault at ..." matches the
"BUG: KASAN:" entry in splats[], so the test fails with "kernel splat
detected".
Other comms work the same way:
"Oops: 0" matches the "Oops:" prefix, since a space and a hex digit
follow it.
"divide error: a" is exactly 15 characters.
"UBSAN: x" passes the isalpha() check.
The validation in the notes only used the comm "BUG:", which is shorter
than every pattern, so it doesn't cover this case.
[ ... ]
> +static int find_bound(const char *path,
> + const struct sysctl_values *original, size_t index,
> + bool lower, bool unsigned_domain,
> + struct sysctl_number *bound)
> +{
[ ... ]
> + *bound = unsigned_domain ? number_from_unsigned(valid) :
> + number_from_signed(unordered_value(valid));
> + return 0;
> +}
[Severity: Low]
Should the bound found here be checked against an expected value?
find_bound() takes whatever value the kernel accepts as the correct
bound.
Documentation/networking/ip-sysctl.rst says each tcp_rmem value "cannot
be set below 4096". net/ipv4/sysctl_net_ipv4.c enforces this with:
.extra1 = &tcp_min_rcvbuf,
Suppose that minimum regresses. The notes describe a vendor 6.12.57
kernel that accepted tcp_rmem[1]=1 and sent zero-window SYN/SYN-ACKs.
The test would just find the new bound and run traffic there.
The verdict in main() only needs one success, so the arm64 run with TCP
11/12 would still pass.
[ ... ]
> +int main(int argc, char **argv)
> +{
[ ... ]
> + if (unshare(CLONE_NEWNET)) {
> + ksft_test_result_skip("cannot create network namespace: %s\n",
> + strerror(errno));
> + ksft_finished();
> + }
[Severity: Medium]
Does this exit with KSFT_PASS instead of KSFT_SKIP?
ksft_test_result_skip() increments ksft_cnt.ksft_xskip, and
ksft_finished() is defined as:
#define ksft_finished() \
ksft_exit(ksft_plan == \
ksft_cnt.ksft_pass + \
ksft_cnt.ksft_xpass + \
ksft_cnt.ksft_xfail + \
ksft_cnt.ksft_xskip)
With a plan of 1 and one skip, the condition is true and
ksft_exit_pass() exits with 0. run_one() in kselftest/runner.sh tells
pass from skip only by the exit code, so the run is recorded as a pass.
main() uses the same pattern for the /dev/kmsg open and seek failures,
for the loopback failure, and for the "no selected writable numeric
network sysctls found" case. It also uses it for the
kernel_log_incomplete case, which v3 added so that an incomplete splat
check would not count as a pass.
Would ksft_exit_skip() be the right call for these whole-test skips?
> +
> + kmsg_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
[Severity: Low]
Should the result of this test depend on the global kernel log?
unshare(CLONE_NEWNET) isolates the sysctls, but /dev/kmsg is
system-wide.
check_kernel_log() runs after every probe and traffic step. A WARN from
unrelated work running at the same time, such as other selftests,
drivers or kworkers, would fail this test with "kernel splat detected".
Also, a printk burst that causes EPIPE sets kernel_log_incomplete. The
whole run is then reported as a skip, even if every sysctl and traffic
check passed:
if (kernel_log_incomplete) {
...
ksft_test_result_skip("cannot complete kernel splat check\n");
netdev CI already scans dmesg after each test. Is a separate check
needed here?
[ ... ]
> + 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);
[Severity: Medium]
Is this check strict enough? When delivery fails,
run_loopback_traffic() only copies the first error into
tcp_delivery_failure or udp_delivery_failure. That string is printed as
a '#' comment, and set_failure() is never called.
So the test only fails here when every TCP attempt fails or every UDP
attempt fails. With TCP 1/12 or UDP 1/12, main() still reaches:
ksft_test_result_pass("TCP/UDP loopback smoke: TCP %zu/%zu, UDP %zu/%zu\n",
The arm64 run in the notes had one TCP timeout in 12 attempts at
tcp_rmem[1]=1, and it would be reported as a pass.
Should a delivery failure at any bound fail the test?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261025160927.3934600-1-sahaj123.sc%40gmail.com
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-26 16:49 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-25 16:09 [PATCH net v3] selftests/net: test network sysctl extremes Sahaj Chaudhari
2026-10-26 16:49 ` 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
all inboxes | Powered by JetHome®