* [PATCH 9/9] kernel/api: add runtime verification selftest
From: Sasha Levin @ 2026-03-13 15:09 UTC (permalink / raw)
To: linux-api, linux-kernel
Cc: linux-doc, linux-fsdevel, linux-kbuild, linux-kselftest,
workflows, tools, x86, Thomas Gleixner, Paul E. McKenney,
Greg Kroah-Hartman, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann, Sasha Levin
In-Reply-To: <20260313150928.2637368-1-sashal@kernel.org>
Add a selftest for CONFIG_KAPI_RUNTIME_CHECKS that exercises
sys_open/sys_read/sys_write/sys_close through raw syscall() and
verifies KAPI pre-validation catches invalid parameters while
allowing valid operations through.
Test cases (TAP output):
1-4: Valid open/read/write/close succeed
5-7: Invalid flags, mode bits, NULL path rejected with EINVAL
8: dmesg contains expected KAPI warning strings
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
MAINTAINERS | 1 +
tools/testing/selftests/kapi/Makefile | 7 +
tools/testing/selftests/kapi/kapi_test_util.h | 31 +
tools/testing/selftests/kapi/test_kapi.c | 1021 +++++++++++++++++
4 files changed, 1060 insertions(+)
create mode 100644 tools/testing/selftests/kapi/Makefile
create mode 100644 tools/testing/selftests/kapi/kapi_test_util.h
create mode 100644 tools/testing/selftests/kapi/test_kapi.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 6fa403d620aab..2bc938fa49759 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13820,6 +13820,7 @@ F: include/linux/syscall_api_spec.h
F: kernel/api/
F: tools/kapi/
F: tools/lib/python/kdoc/kdoc_apispec.py
+F: tools/testing/selftests/kapi/
KERNEL AUTOMOUNTER
M: Ian Kent <raven@themaw.net>
diff --git a/tools/testing/selftests/kapi/Makefile b/tools/testing/selftests/kapi/Makefile
new file mode 100644
index 0000000000000..5f3fdeddcae41
--- /dev/null
+++ b/tools/testing/selftests/kapi/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_GEN_PROGS := test_kapi
+
+CFLAGS += -static -Wall -O2 $(KHDR_INCLUDES)
+
+include ../lib.mk
diff --git a/tools/testing/selftests/kapi/kapi_test_util.h b/tools/testing/selftests/kapi/kapi_test_util.h
new file mode 100644
index 0000000000000..f18b44ff6239d
--- /dev/null
+++ b/tools/testing/selftests/kapi/kapi_test_util.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Compatibility helpers for KAPI selftests.
+ *
+ * __NR_open is not defined on aarch64 and riscv64 (only __NR_openat exists).
+ * Provide a wrapper that uses __NR_openat with AT_FDCWD to achieve the same
+ * behavior as __NR_open on architectures that lack it.
+ */
+#ifndef KAPI_TEST_UTIL_H
+#define KAPI_TEST_UTIL_H
+
+#include <fcntl.h>
+#include <sys/syscall.h>
+
+#ifndef __NR_open
+/*
+ * On architectures without __NR_open (e.g., aarch64, riscv64),
+ * use openat(AT_FDCWD, ...) which is equivalent.
+ */
+static inline long kapi_sys_open(const char *pathname, int flags, int mode)
+{
+ return syscall(__NR_openat, AT_FDCWD, pathname, flags, mode);
+}
+#else
+static inline long kapi_sys_open(const char *pathname, int flags, int mode)
+{
+ return syscall(__NR_open, pathname, flags, mode);
+}
+#endif
+
+#endif /* KAPI_TEST_UTIL_H */
diff --git a/tools/testing/selftests/kapi/test_kapi.c b/tools/testing/selftests/kapi/test_kapi.c
new file mode 100644
index 0000000000000..81aaa4f607073
--- /dev/null
+++ b/tools/testing/selftests/kapi/test_kapi.c
@@ -0,0 +1,1021 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Userspace selftest for KAPI runtime verification of syscall parameters.
+ *
+ * Exercises sys_open, sys_read, sys_write, and sys_close through raw
+ * syscall() to ensure KAPI pre-validation wrappers interact correctly
+ * with normal kernel error handling.
+ *
+ * Requires CONFIG_KAPI_RUNTIME_CHECKS=y for full coverage; many tests
+ * also pass without it.
+ *
+ * TAP output format.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <signal.h>
+#include <sys/syscall.h>
+#include <sys/stat.h>
+#include <linux/limits.h>
+#include "kapi_test_util.h"
+
+#define NUM_TESTS 29
+
+static int test_num;
+static int failures;
+static volatile sig_atomic_t got_sigpipe;
+
+static void tap_ok(const char *desc)
+{
+ printf("ok %d - %s\n", ++test_num, desc);
+}
+
+static void tap_fail(const char *desc, const char *reason)
+{
+ printf("not ok %d - %s # %s\n", ++test_num, desc, reason);
+ failures++;
+}
+
+static void sigpipe_handler(int sig)
+{
+ (void)sig;
+ got_sigpipe = 1;
+}
+
+/* ---- Valid operation tests ---- */
+
+/*
+ * Test 1: open a readable file
+ * Returns fd on success.
+ */
+static int test_open_valid(void)
+{
+ errno = 0;
+ long fd = kapi_sys_open("/etc/hostname", O_RDONLY, 0);
+
+ if (fd >= 0) {
+ tap_ok("open valid file");
+ } else {
+ /* /etc/hostname might not exist; try /etc/passwd */
+ errno = 0;
+ fd = kapi_sys_open("/etc/passwd", O_RDONLY, 0);
+ if (fd >= 0)
+ tap_ok("open valid file (fallback /etc/passwd)");
+ else
+ tap_fail("open valid file", strerror(errno));
+ }
+ return (int)fd;
+}
+
+/*
+ * Test 2: read from fd
+ */
+static void test_read_valid(int fd)
+{
+ char buf[256];
+
+ errno = 0;
+ long ret = syscall(__NR_read, fd, buf, sizeof(buf));
+
+ if (ret > 0)
+ tap_ok("read from valid fd");
+ else if (ret == 0)
+ tap_ok("read from valid fd (EOF)");
+ else
+ tap_fail("read from valid fd", strerror(errno));
+}
+
+/*
+ * Test 3: write to /dev/null
+ */
+static void test_write_valid(void)
+{
+ errno = 0;
+ long devnull = kapi_sys_open("/dev/null", O_WRONLY, 0);
+
+ if (devnull < 0) {
+ tap_fail("write to /dev/null (open failed)", strerror(errno));
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)devnull, "hello", 5);
+
+ if (ret == 5)
+ tap_ok("write to /dev/null");
+ else
+ tap_fail("write to /dev/null",
+ ret < 0 ? strerror(errno) : "short write");
+
+ syscall(__NR_close, (int)devnull);
+}
+
+/*
+ * Test 4: close fd
+ */
+static void test_close_valid(int fd)
+{
+ errno = 0;
+ long ret = syscall(__NR_close, fd);
+
+ if (ret == 0)
+ tap_ok("close valid fd");
+ else
+ tap_fail("close valid fd", strerror(errno));
+}
+
+/* ---- KAPI parameter rejection tests ---- */
+
+/*
+ * Test 5: open with invalid flag bits
+ * 0x10000000 is outside the valid O_* mask, KAPI should reject.
+ */
+static void test_open_invalid_flags(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open("/tmp/kapi_test", 0x10000000, 0);
+
+ if (ret == -1 && errno == EINVAL) {
+ tap_ok("open with invalid flags returns EINVAL");
+ } else if (ret >= 0) {
+ tap_fail("open with invalid flags", "expected EINVAL, got success");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EINVAL, got %s",
+ strerror(errno));
+ tap_fail("open with invalid flags", msg);
+ }
+}
+
+/*
+ * Test 6: open with invalid mode bits
+ * 0xFFFF has bits outside S_IALLUGO (07777), KAPI should reject.
+ */
+static void test_open_invalid_mode(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open("/tmp/kapi_test_mode",
+ O_CREAT | O_WRONLY, 0xFFFF);
+
+ if (ret == -1 && errno == EINVAL) {
+ tap_ok("open with invalid mode returns EINVAL");
+ } else if (ret >= 0) {
+ tap_fail("open with invalid mode", "expected EINVAL, got success");
+ syscall(__NR_close, (int)ret);
+ unlink("/tmp/kapi_test_mode");
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EINVAL, got %s",
+ strerror(errno));
+ tap_fail("open with invalid mode", msg);
+ }
+}
+
+/*
+ * Test 7: open with NULL path
+ * KAPI USER_PATH constraint should reject NULL.
+ */
+static void test_open_null_path(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open(NULL, O_RDONLY, 0);
+
+ if (ret == -1 && errno == EINVAL) {
+ tap_ok("open with NULL path returns EINVAL");
+ } else if (ret == -1 && errno == EFAULT) {
+ /* Kernel may catch this as EFAULT before KAPI */
+ tap_ok("open with NULL path returns EFAULT (acceptable)");
+ } else if (ret >= 0) {
+ tap_fail("open with NULL path", "expected error, got success");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "got %s", strerror(errno));
+ tap_fail("open with NULL path", msg);
+ }
+}
+
+/*
+ * Test 8: open with flag bit 30 set (0x40000000)
+ * This bit is outside the valid O_* mask, KAPI should reject with EINVAL.
+ */
+static void test_open_flag_bit30(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open("/dev/null", 0x40000000, 0);
+
+ if (ret == -1 && errno == EINVAL)
+ tap_ok("open with flag bit 30 (0x40000000) returns EINVAL");
+ else if (ret >= 0) {
+ tap_fail("open with flag bit 30 (0x40000000) returns EINVAL",
+ "expected EINVAL, got success");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EINVAL, got %s",
+ strerror(errno));
+ tap_fail("open with flag bit 30 (0x40000000) returns EINVAL",
+ msg);
+ }
+}
+
+/* ---- Boundary condition and error path tests ---- */
+
+/*
+ * Test 9: read with fd=-1 should return an error.
+ * With CONFIG_KAPI_RUNTIME_CHECKS=y, KAPI validates the fd first and
+ * rejects negative fds (other than AT_FDCWD) with EINVAL. Without
+ * KAPI, the kernel returns EBADF. Accept either.
+ */
+static void test_read_bad_fd(void)
+{
+ char buf[16];
+
+ errno = 0;
+ long ret = syscall(__NR_read, -1, buf, sizeof(buf));
+
+ if (ret == -1 && (errno == EBADF || errno == EINVAL))
+ tap_ok("read with fd=-1 returns error");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF/EINVAL, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("read with fd=-1 returns error", msg);
+ }
+}
+
+/*
+ * Test 10: read with count=0 should return 0
+ */
+static void test_read_zero_count(void)
+{
+ char buf[1];
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+ if (fd < 0) {
+ tap_fail("read with count=0 returns 0",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_read, (int)fd, buf, 0);
+
+ if (ret == 0)
+ tap_ok("read with count=0 returns 0");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+ ret, strerror(errno));
+ tap_fail("read with count=0 returns 0", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 11: write with count=0 should return 0
+ */
+static void test_write_zero_count(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (fd < 0) {
+ tap_fail("write with count=0 returns 0",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)fd, "x", 0);
+
+ if (ret == 0)
+ tap_ok("write with count=0 returns 0");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+ ret, strerror(errno));
+ tap_fail("write with count=0 returns 0", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 12: open with a path longer than PATH_MAX should fail
+ * Expect ENAMETOOLONG or EINVAL.
+ */
+static void test_open_long_path(void)
+{
+ char *longpath;
+ size_t len = PATH_MAX + 256;
+
+ longpath = malloc(len);
+ if (!longpath) {
+ tap_fail("open with path > PATH_MAX", "malloc failed");
+ return;
+ }
+
+ memset(longpath, 'A', len - 1);
+ longpath[0] = '/';
+ longpath[len - 1] = '\0';
+
+ errno = 0;
+ long ret = kapi_sys_open(longpath, O_RDONLY, 0);
+
+ if (ret == -1 && (errno == ENAMETOOLONG || errno == EINVAL))
+ tap_ok("open with path > PATH_MAX returns ENAMETOOLONG/EINVAL");
+ else if (ret >= 0) {
+ tap_fail("open with path > PATH_MAX",
+ "expected error, got success");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg),
+ "expected ENAMETOOLONG/EINVAL, got %s",
+ strerror(errno));
+ tap_fail("open with path > PATH_MAX", msg);
+ }
+
+ free(longpath);
+}
+
+/*
+ * Test 13: read with unmapped user pointer should return EFAULT or EINVAL.
+ * Use a pipe with data so the kernel actually tries to copy to the buffer.
+ */
+static void test_read_unmapped_buf(void)
+{
+ int pipefd[2];
+
+ if (pipe(pipefd) < 0) {
+ tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+ "pipe() failed");
+ return;
+ }
+
+ /* Write some data so read has something to copy */
+ (void)write(pipefd[1], "hello", 5);
+
+ errno = 0;
+ long ret = syscall(__NR_read, pipefd[0], (void *)0xDEAD0000, 16);
+
+ if (ret == -1 && (errno == EFAULT || errno == EINVAL))
+ tap_ok("read with unmapped buffer returns EFAULT/EINVAL");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg),
+ "expected EFAULT/EINVAL, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+ msg);
+ }
+
+ close(pipefd[0]);
+ close(pipefd[1]);
+}
+
+/*
+ * Test 14: write with unmapped user pointer should return EFAULT or EINVAL.
+ * Use a pipe so the kernel actually tries to copy from the buffer.
+ */
+static void test_write_unmapped_buf(void)
+{
+ int pipefd[2];
+
+ if (pipe(pipefd) < 0) {
+ tap_fail("write with unmapped buffer returns EFAULT/EINVAL",
+ "pipe() failed");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, pipefd[1], (void *)0xDEAD0000, 16);
+
+ if (ret == -1 && (errno == EFAULT || errno == EINVAL))
+ tap_ok("write with unmapped buffer returns EFAULT/EINVAL");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg),
+ "expected EFAULT/EINVAL, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("write with unmapped buffer returns EFAULT/EINVAL",
+ msg);
+ }
+
+ close(pipefd[0]);
+ close(pipefd[1]);
+}
+
+/*
+ * Test 15: close an already-closed fd should return EBADF
+ */
+static void test_close_already_closed(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+ if (fd < 0) {
+ tap_fail("close already-closed fd returns EBADF",
+ "cannot open /dev/null");
+ return;
+ }
+
+ /* Close it once - should succeed */
+ syscall(__NR_close, (int)fd);
+
+ /* Close it again - should fail with EBADF */
+ errno = 0;
+ long ret = syscall(__NR_close, (int)fd);
+
+ if (ret == -1 && errno == EBADF)
+ tap_ok("close already-closed fd returns EBADF");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+ ret == 0 ? "success" : strerror(errno));
+ tap_fail("close already-closed fd returns EBADF", msg);
+ }
+}
+
+/*
+ * Test 16: open /dev/null with O_RDONLY|O_CLOEXEC should succeed
+ */
+static void test_open_valid_cloexec(void)
+{
+ errno = 0;
+ long fd = kapi_sys_open("/dev/null", O_RDONLY | O_CLOEXEC, 0);
+
+ if (fd >= 0) {
+ tap_ok("open /dev/null with O_RDONLY|O_CLOEXEC succeeds");
+ syscall(__NR_close, (int)fd);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected success, got %s",
+ strerror(errno));
+ tap_fail("open /dev/null with O_RDONLY|O_CLOEXEC succeeds",
+ msg);
+ }
+}
+
+/*
+ * Test 17: write 0 bytes to /dev/null should return 0
+ */
+static void test_write_zero_devnull(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (fd < 0) {
+ tap_fail("write 0 bytes to /dev/null returns 0",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)fd, "", 0);
+
+ if (ret == 0)
+ tap_ok("write 0 bytes to /dev/null returns 0");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+ ret, strerror(errno));
+ tap_fail("write 0 bytes to /dev/null returns 0", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 18: read from a write-only fd should return EBADF
+ */
+static void test_read_writeonly_fd(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (fd < 0) {
+ tap_fail("read from write-only fd returns EBADF",
+ "cannot open /dev/null");
+ return;
+ }
+
+ char buf[16];
+
+ errno = 0;
+ long ret = syscall(__NR_read, (int)fd, buf, sizeof(buf));
+
+ if (ret == -1 && errno == EBADF)
+ tap_ok("read from write-only fd returns EBADF");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("read from write-only fd returns EBADF", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 19: write to a read-only fd should return EBADF
+ */
+static void test_write_readonly_fd(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+ if (fd < 0) {
+ tap_fail("write to read-only fd returns EBADF",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)fd, "hello", 5);
+
+ if (ret == -1 && errno == EBADF)
+ tap_ok("write to read-only fd returns EBADF");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("write to read-only fd returns EBADF", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 20: close fd 9999 (likely invalid) should return EBADF
+ */
+static void test_close_fd_9999(void)
+{
+ errno = 0;
+ long ret = syscall(__NR_close, 9999);
+
+ if (ret == -1 && errno == EBADF)
+ tap_ok("close fd 9999 returns EBADF");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+ ret == 0 ? "success" : strerror(errno));
+ tap_fail("close fd 9999 returns EBADF", msg);
+ }
+}
+
+/*
+ * Test 21: read from pipe after write end is closed returns 0 (EOF)
+ */
+static void test_read_closed_pipe(void)
+{
+ int pipefd[2];
+
+ if (pipe(pipefd) < 0) {
+ tap_fail("read from closed pipe returns 0 (EOF)",
+ "pipe() failed");
+ return;
+ }
+
+ /* Close write end */
+ close(pipefd[1]);
+
+ char buf[16];
+
+ errno = 0;
+ long ret = syscall(__NR_read, pipefd[0], buf, sizeof(buf));
+
+ if (ret == 0)
+ tap_ok("read from closed pipe returns 0 (EOF)");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+ ret, ret < 0 ? strerror(errno) : "n/a");
+ tap_fail("read from closed pipe returns 0 (EOF)", msg);
+ }
+
+ close(pipefd[0]);
+}
+
+/*
+ * Test 22: write to pipe after read end is closed returns EPIPE + SIGPIPE
+ */
+static void test_write_closed_pipe(void)
+{
+ int pipefd[2];
+ struct sigaction sa, old_sa;
+
+ if (pipe(pipefd) < 0) {
+ tap_fail("write to closed pipe returns EPIPE + SIGPIPE",
+ "pipe() failed");
+ return;
+ }
+
+ /* Install SIGPIPE handler */
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = sigpipe_handler;
+ sigemptyset(&sa.sa_mask);
+ sigaction(SIGPIPE, &sa, &old_sa);
+
+ got_sigpipe = 0;
+
+ /* Close read end */
+ close(pipefd[0]);
+
+ errno = 0;
+ long ret = syscall(__NR_write, pipefd[1], "hello", 5);
+
+ if (ret == -1 && errno == EPIPE && got_sigpipe)
+ tap_ok("write to closed pipe returns EPIPE + SIGPIPE");
+ else if (ret == -1 && errno == EPIPE)
+ tap_ok("write to closed pipe returns EPIPE (SIGPIPE not caught)");
+ else {
+ char msg[128];
+
+ snprintf(msg, sizeof(msg),
+ "expected EPIPE, got %s (sigpipe=%d)",
+ ret >= 0 ? "success" : strerror(errno),
+ got_sigpipe);
+ tap_fail("write to closed pipe returns EPIPE + SIGPIPE", msg);
+ }
+
+ /* Restore SIGPIPE handler */
+ sigaction(SIGPIPE, &old_sa, NULL);
+ close(pipefd[1]);
+}
+
+/*
+ * Test 23: open with O_DIRECTORY on a regular file returns ENOTDIR
+ */
+static void test_open_directory_on_file(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open("/dev/null", O_RDONLY | O_DIRECTORY, 0);
+
+ if (ret == -1 && errno == ENOTDIR)
+ tap_ok("open O_DIRECTORY on regular file returns ENOTDIR");
+ else if (ret >= 0) {
+ tap_fail("open O_DIRECTORY on regular file",
+ "expected ENOTDIR, got success");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected ENOTDIR, got %s",
+ strerror(errno));
+ tap_fail("open O_DIRECTORY on regular file", msg);
+ }
+}
+
+/*
+ * Test 24: open nonexistent file without O_CREAT returns ENOENT
+ */
+static void test_open_nonexistent(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open(
+ "/tmp/kapi_nonexistent_file_12345", O_RDONLY, 0);
+
+ if (ret == -1 && errno == ENOENT)
+ tap_ok("open nonexistent file without O_CREAT returns ENOENT");
+ else if (ret >= 0) {
+ tap_fail("open nonexistent file",
+ "expected ENOENT, got success (file exists?)");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected ENOENT, got %s",
+ strerror(errno));
+ tap_fail("open nonexistent file", msg);
+ }
+}
+
+/*
+ * Test 25: close stdin (fd 0) should succeed
+ * We dup it first so we can restore it.
+ */
+static void test_close_stdin(void)
+{
+ int saved_stdin = dup(0);
+
+ if (saved_stdin < 0) {
+ tap_fail("close stdin succeeds", "cannot dup stdin");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_close, 0);
+
+ if (ret == 0)
+ tap_ok("close stdin (fd 0) succeeds");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected success, got %s",
+ strerror(errno));
+ tap_fail("close stdin (fd 0) succeeds", msg);
+ }
+
+ /* Restore stdin */
+ dup2(saved_stdin, 0);
+ close(saved_stdin);
+}
+
+/*
+ * Test 26: read after close returns EBADF
+ */
+static void test_read_after_close(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+ if (fd < 0) {
+ tap_fail("read after close returns EBADF",
+ "cannot open /dev/null");
+ return;
+ }
+
+ syscall(__NR_close, (int)fd);
+
+ char buf[16];
+
+ errno = 0;
+ long ret = syscall(__NR_read, (int)fd, buf, sizeof(buf));
+
+ if (ret == -1 && errno == EBADF)
+ tap_ok("read after close returns EBADF");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("read after close returns EBADF", msg);
+ }
+}
+
+/*
+ * Test 27: write with large count
+ * Without KAPI: the kernel clamps count to MAX_RW_COUNT and succeeds.
+ * With KAPI: KAPI validates the buffer against the count and may
+ * return EFAULT/EINVAL since the buffer is smaller than count.
+ * Accept either success or EFAULT/EINVAL.
+ */
+static void test_write_large_count(void)
+{
+ long fd;
+ char buf[64] = "test data";
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (fd < 0) {
+ tap_fail("write with large count handled correctly",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)fd, buf, (size_t)0x7ffff000UL);
+
+ if (ret > 0)
+ tap_ok("write with large count succeeds (clamped, no KAPI)");
+ else if (ret == -1 && (errno == EFAULT || errno == EINVAL))
+ tap_ok("write with large count returns EFAULT/EINVAL (KAPI validates buffer)");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected success or EFAULT, got %s",
+ ret == 0 ? "zero" : strerror(errno));
+ tap_fail("write with large count handled correctly", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/* ---- Integration tests ---- */
+
+/*
+ * Test 28: full normal syscall path - open, read, write, close
+ * Verify KAPI does not interfere with normal operations.
+ */
+static void test_normal_path(void)
+{
+ long rd_fd, wr_fd;
+ char buf[128];
+ int ok = 1;
+ char reason[128] = "";
+
+ /* Open a readable file */
+ errno = 0;
+ rd_fd = kapi_sys_open("/etc/hostname", O_RDONLY, 0);
+ if (rd_fd < 0) {
+ errno = 0;
+ rd_fd = kapi_sys_open("/etc/passwd", O_RDONLY, 0);
+ }
+ if (rd_fd < 0) {
+ snprintf(reason, sizeof(reason), "open readable file: %s",
+ strerror(errno));
+ ok = 0;
+ }
+
+ /* Read from it */
+ if (ok) {
+ errno = 0;
+ long n = syscall(__NR_read, (int)rd_fd, buf, sizeof(buf));
+
+ if (n < 0) {
+ snprintf(reason, sizeof(reason), "read: %s",
+ strerror(errno));
+ ok = 0;
+ }
+ }
+
+ /* Open /dev/null for writing */
+ wr_fd = -1;
+ if (ok) {
+ errno = 0;
+ wr_fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (wr_fd < 0) {
+ snprintf(reason, sizeof(reason),
+ "open /dev/null: %s", strerror(errno));
+ ok = 0;
+ }
+ }
+
+ /* Write to /dev/null */
+ if (ok) {
+ errno = 0;
+ long n = syscall(__NR_write, (int)wr_fd, "test", 4);
+
+ if (n != 4) {
+ snprintf(reason, sizeof(reason), "write: %s",
+ n < 0 ? strerror(errno) : "short write");
+ ok = 0;
+ }
+ }
+
+ /* Close both fds */
+ if (rd_fd >= 0) {
+ errno = 0;
+ if (syscall(__NR_close, (int)rd_fd) != 0 && ok) {
+ snprintf(reason, sizeof(reason), "close read fd: %s",
+ strerror(errno));
+ ok = 0;
+ }
+ }
+
+ if (wr_fd >= 0) {
+ errno = 0;
+ if (syscall(__NR_close, (int)wr_fd) != 0 && ok) {
+ snprintf(reason, sizeof(reason), "close write fd: %s",
+ strerror(errno));
+ ok = 0;
+ }
+ }
+
+ if (ok)
+ tap_ok("normal syscall path (open/read/write/close) works");
+ else
+ tap_fail("normal syscall path (open/read/write/close) works",
+ reason);
+}
+
+/*
+ * Test 29: verify dmesg contains KAPI warnings for the invalid tests
+ */
+static void test_dmesg_warnings(void)
+{
+ int kmsg_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+ if (kmsg_fd < 0) {
+ tap_ok("dmesg check skipped (cannot open /dev/kmsg)");
+ return;
+ }
+
+ /* Read all available kmsg messages from the start */
+ lseek(kmsg_fd, 0, SEEK_DATA);
+
+ char line[4096];
+ int found_invalid_bits = 0;
+ int found_null = 0;
+ ssize_t n;
+
+ while ((n = read(kmsg_fd, line, sizeof(line) - 1)) > 0) {
+ line[n] = '\0';
+ if (strstr(line, "contains invalid bits"))
+ found_invalid_bits++;
+ if (strstr(line, "NULL") && strstr(line, "not allowed"))
+ found_null++;
+ }
+
+ close(kmsg_fd);
+
+ if (found_invalid_bits >= 2 && found_null >= 1)
+ tap_ok("dmesg contains expected KAPI warnings");
+ else if (found_invalid_bits >= 1 || found_null >= 1) {
+ char msg[128];
+
+ snprintf(msg, sizeof(msg),
+ "partial: invalid_bits=%d null=%d",
+ found_invalid_bits, found_null);
+ tap_ok(msg);
+ } else {
+ tap_fail("dmesg KAPI warnings",
+ "no KAPI warnings found in dmesg");
+ }
+}
+
+int main(void)
+{
+ printf("TAP version 13\n");
+ printf("1..%d\n", NUM_TESTS);
+
+ /* Valid operations (1-4) */
+ int fd = test_open_valid();
+
+ if (fd >= 0)
+ test_read_valid(fd);
+ else
+ tap_fail("read from valid fd", "no fd from open");
+
+ test_write_valid();
+
+ if (fd >= 0)
+ test_close_valid(fd);
+ else
+ tap_fail("close valid fd", "no fd from open");
+
+ /* KAPI parameter rejection (5-8) */
+ test_open_invalid_flags();
+ test_open_invalid_mode();
+ test_open_null_path();
+ test_open_flag_bit30();
+
+ /* Boundary conditions and error paths (9-20) */
+ test_read_bad_fd();
+ test_read_zero_count();
+ test_write_zero_count();
+ test_open_long_path();
+ test_read_unmapped_buf();
+ test_write_unmapped_buf();
+ test_close_already_closed();
+ test_open_valid_cloexec();
+ test_write_zero_devnull();
+ test_read_writeonly_fd();
+ test_write_readonly_fd();
+ test_close_fd_9999();
+
+ /* Pipe and lifecycle tests (21-27) */
+ test_read_closed_pipe();
+ test_write_closed_pipe();
+ test_open_directory_on_file();
+ test_open_nonexistent();
+ test_close_stdin();
+ test_read_after_close();
+ test_write_large_count();
+
+ /* Integration (28-29) */
+ test_normal_path();
+ test_dmesg_warnings();
+
+ if (failures)
+ fprintf(stderr, "# %d test(s) failed\n", failures);
+ else
+ fprintf(stderr, "# All tests passed\n");
+
+ return failures ? 1 : 0;
+}
--
2.51.0
^ permalink raw reply related
* Re: [RFC PATCH v4] futex: Introduce __vdso_robust_futex_unlock and __vdso_robust_pi_futex_try_unlock
From: Mathieu Desnoyers @ 2026-03-13 15:19 UTC (permalink / raw)
To: Thomas Weißschuh
Cc: André Almeida, linux-kernel, Carlos O'Donell,
Sebastian Andrzej Siewior, Peter Zijlstra, Florian Weimer,
Rich Felker, Torvald Riegel, Darren Hart, Thomas Gleixner,
Ingo Molnar, Davidlohr Bueso, Arnd Bergmann, Liam R . Howlett,
linux-api
In-Reply-To: <20260313150111-64c38feb-825d-433e-9c71-f4f109b8cbfb@linutronix.de>
On 2026-03-13 10:24, Thomas Weißschuh wrote:
> Hi Mathieu,
>
> some small remarks around the vDSO code.
>
> On Fri, Mar 13, 2026 at 09:39:03AM -0400, Mathieu Desnoyers wrote:
>
> (...)
>
>> The approach taken to fix this is to introduce two vDSO and extend the
>> x86 vDSO exception table to track the relevant ip ranges: one for non-PI
>> robust futexes, and one for PI robust futexes.
>
> One of the central points behind the vDSO so far was that it is only a
> performance optimization. It is never required for correctness.
> What are applications supposed to do when the vDSO is disabled?
Good point!
For non-PI futexes, we'd need to introduce a new robust_futex_unlock
system call to handle the scenario where vDSO is unavailable. This
system call would perform the equivalent of what is done within the
vDSO, but from within the kernel. This comes with a significant
performance overhead, as this would be called on _every_ robust
futex unlock.
For PI futexes, applications could either directly invoke the
futex_unlock_pi system call when the vDSO is not available, or
we could provide a new system call which does the equivalent of
the vDSO within the kernel. The advantage of the new system call
is that we would not hit the hb->lock in the uncontended case.
Another alternative is that we tweak the existing futex_unlock_pi
to handle the case where it is called when there are no waiters more
efficiently.
>
> (...)
>
>> diff --git a/arch/x86/entry/vdso/common/vfutex.c b/arch/x86/entry/vdso/common/vfutex.c
>> new file mode 100644
>> index 000000000000..336095b04952
>> --- /dev/null
>> +++ b/arch/x86/entry/vdso/common/vfutex.c
>> @@ -0,0 +1,90 @@
>> +// SPDX-License-Identifier: GPL-2.0-only
>> +/*
>> + * Copyright (C) 2026 Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
>> + */
>> +#include <linux/types.h>
>> +#include <linux/futex.h>
>
> This should be uapi/linux/futex.h. Headers from the linux/ namespace should
> not be used in vDSO code. The definitions from them may end up being wrong
> in the compat vDSO. Either use uapi/ or vdso/ headers. (linux/types.h is a bit
> of an exception for historic reasons, it could be replaced by uapi/linux/types.h)
OK, will fix.
>
>> +#include <vdso/futex.h>
>> +#include "extable.h"
>
>> +
>> +#ifdef CONFIG_X86_64
>
> This only works because of the ugly hacks in fake_32bit_build.h.
> Testing for '#ifdef __x86_64__' is simpler and nicer to read.
OK, will fix.
>
>> +# define ASM_PTR_BIT_SET "btsq "
>> +# define ASM_PTR_SET "movq "
>> +#else
>> +# define ASM_PTR_BIT_SET "btsl "
>> +# define ASM_PTR_SET "movl "
>> +#endif
>> +
>> +u32 __vdso_robust_futex_unlock(u32 *uaddr, struct robust_list_head *robust_list_head)
>> +{
>> + u32 val = 0;
>> +
>> + /*
>> + * Within the ip range identified by the futex exception table,
>> + * the register "eax" contains the value loaded by xchg. This is
>> + * expected by futex_vdso_exception() to check whether waiters
>> + * need to be woken up. This register state is transferred to
>> + * bit 1 (NEED_ACTION) of *op_pending_addr before the ip range
>> + * ends.
>> + */
>> + asm volatile (
>> + _ASM_VDSO_EXTABLE_FUTEX_HANDLE(1f, 3f)
>> + /* Exchange uaddr (store-release). */
>> + "xchg %[uaddr], %[val]\n\t"
>> + "1:\n\t"
>> + /* Test if FUTEX_WAITERS (0x80000000) is set. */
>> + "test %[val], %[val]\n\t"
>> + "js 2f\n\t"
>> + /* Clear *op_pending_addr if there are no waiters. */
>> + ASM_PTR_SET "$0, %[op_pending_addr]\n\t"
>> + "jmp 3f\n\t"
>> + "2:\n\t"
>> + /* Set bit 1 (NEED_ACTION) in *op_pending_addr. */
>> + ASM_PTR_BIT_SET "$1, %[op_pending_addr]\n\t"
>> + "3:\n\t"
>> + : [val] "+a" (val),
>> + [uaddr] "+m" (*uaddr)
>> + : [op_pending_addr] "m" (robust_list_head->list_op_pending)
>> + : "memory"
>> + );
>> + return val;
>> +}
>> +
>> +u32 robust_futex_unlock(u32 *, struct robust_list_head *)
>> + __attribute__((weak, alias("__vdso_robust_futex_unlock")));
>
> __weak and __alias() as per checkpatch.pl.
OK, will fix.
>
> The entries in the linkerscripts are missing.
Good catch, will fix for:
vdso/vdso64/vdsox32.lds.S
vdso/vdso64/vdso64.lds.S
vdso/vdso32/vdso32.lds.S
>
> (...)
>
>> --- /dev/null
>> +++ b/include/vdso/futex.h
>> @@ -0,0 +1,72 @@
>> +/* SPDX-License-Identifier: GPL-2.0 */
>> +/*
>> + * Copyright (C) 2026 Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
>> + */
>> +
>> +#ifndef _VDSO_FUTEX_H
>> +#define _VDSO_FUTEX_H
>> +
>> +#include <linux/types.h>
>> +#include <linux/futex.h>
>
> Same remarks about the linux/ namespace as before.
OK, fixed.
>
>> +/**
>> + * __vdso_robust_futex_unlock - Architecture-specific vDSO implementation of robust futex unlock.
>> + * @uaddr: Lock address (points to a 32-bit unsigned integer type).
>> + * @robust_list_head: The thread-specific robust list that has been registered with set_robust_list.
>> + *
>> + * This vDSO unlocks the robust futex by exchanging the content of
>> + * *@uaddr with 0 with a store-release semantic. If the futex has
>> + * waiters, it sets bit 1 of *@robust_list_head->list_op_pending, else
>> + * it clears *@robust_list_head->list_op_pending. Those operations are
>> + * within a code region known by the kernel, making them safe with
>> + * respect to asynchronous program termination either from thread
>> + * context or from a nested signal handler.
>> + *
>> + * Returns: The old value present at *@uaddr.
>> + *
>> + * Expected use of this vDSO:
>> + *
>> + * robust_list_head is the thread-specific robust list that has been
>> + * registered with set_robust_list.
>> + *
>> + * if ((__vdso_robust_futex_unlock((u32 *) &mutex->__data.__lock, robust_list_head)
>> + * & FUTEX_WAITERS) != 0)
>> + * futex_wake((u32 *) &mutex->__data.__lock, 1, private);
>> + * WRITE_ONCE(robust_list_head->list_op_pending, 0);
>> + */
>> +extern u32 __vdso_robust_futex_unlock(u32 *uaddr, struct robust_list_head *robust_list_head);
>
> Drop the extern.
OK, fixed.
Thanks,
Mathieu
>
> (...)
>
>> +#endif /* _VDSO_FUTEX_H */
>
> (...)
--
Mathieu Desnoyers
EfficiOS Inc.
https://www.efficios.com
^ permalink raw reply
* Re: [PATCH 3/9] kernel/api: add debugfs interface for kernel API specifications
From: Greg Kroah-Hartman @ 2026-03-13 15:32 UTC (permalink / raw)
To: Sasha Levin
Cc: linux-api, linux-kernel, linux-doc, linux-fsdevel, linux-kbuild,
linux-kselftest, workflows, tools, x86, Thomas Gleixner,
Paul E. McKenney, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann
In-Reply-To: <20260313150928.2637368-4-sashal@kernel.org>
On Fri, Mar 13, 2026 at 11:09:13AM -0400, Sasha Levin wrote:
> Add a debugfs interface to expose kernel API specifications at runtime.
> This allows tools and users to query the complete API specifications
> through the debugfs filesystem.
>
> The interface provides:
> - /sys/kernel/debug/kapi/list - lists all available API specifications
> - /sys/kernel/debug/kapi/specs/<name> - detailed info for each API
>
> Each specification file includes:
> - Function name, version, and descriptions
> - Execution context requirements and flags
> - Parameter details with types, flags, and constraints
> - Return value specifications and success conditions
> - Error codes with descriptions and conditions
> - Locking requirements and constraints
> - Signal handling specifications
> - Examples, notes, and deprecation status
>
> This enables runtime introspection of kernel APIs for documentation
> tools, static analyzers, and debugging purposes.
>
> Signed-off-by: Sasha Levin <sashal@kernel.org>
> ---
> Documentation/dev-tools/kernel-api-spec.rst | 88 ++--
You are removing stuff from the file you created earlier in this patch
series, is that ok?
> --- /dev/null
> +++ b/kernel/api/kapi_debugfs.c
> @@ -0,0 +1,503 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Kernel API specification debugfs interface
> + *
> + * This provides a debugfs interface to expose kernel API specifications
> + * at runtime, allowing tools and users to query the complete API specs.
> + */
No copyright line? :)
And this is, a totally and crazy interface with debugfs, I love it!
Two minor minor nits:
> +static int __init kapi_debugfs_init(void)
> +{
> + struct kernel_api_spec *spec;
> + struct dentry *spec_dir;
> +
> + /* Create main directory */
> + kapi_debugfs_root = debugfs_create_dir("kapi", NULL);
> +
> + /* Create list file */
> + debugfs_create_file("list", 0444, kapi_debugfs_root, NULL, &kapi_list_fops);
> +
> + /* Create specs subdirectory */
> + spec_dir = debugfs_create_dir("specs", kapi_debugfs_root);
> +
> + /* Create a file for each API spec */
> + for (spec = __start_kapi_specs; spec < __stop_kapi_specs; spec++) {
> + debugfs_create_file(spec->name, 0444, spec_dir, spec, &kapi_spec_fops);
> + }
No need for { }
> +
> + pr_info("Kernel API debugfs interface initialized\n");
When code is working properly, it should be quiet, no need for this as
initializing this can not fail.
thanks,
greg k-h
^ permalink raw reply
* Re: [PATCH 5/9] kernel/api: add API specification for sys_open
From: Greg Kroah-Hartman @ 2026-03-13 15:33 UTC (permalink / raw)
To: Sasha Levin
Cc: linux-api, linux-kernel, linux-doc, linux-fsdevel, linux-kbuild,
linux-kselftest, workflows, tools, x86, Thomas Gleixner,
Paul E. McKenney, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann
In-Reply-To: <20260313150928.2637368-6-sashal@kernel.org>
On Fri, Mar 13, 2026 at 11:09:15AM -0400, Sasha Levin wrote:
> Signed-off-by: Sasha Levin <sashal@kernel.org>
No changelog?
> + * since-version: 1.0
I think since older versions :)
Anyway, very nice documentation, will be good to have this as part of
the kerneldocs no matter what the result of this patch series is.
thanks,
greg k-h
^ permalink raw reply
* Re: [PATCH 6/9] kernel/api: add API specification for sys_close
From: Greg Kroah-Hartman @ 2026-03-13 15:49 UTC (permalink / raw)
To: Sasha Levin
Cc: linux-api, linux-kernel, linux-doc, linux-fsdevel, linux-kbuild,
linux-kselftest, workflows, tools, x86, Thomas Gleixner,
Paul E. McKenney, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann
In-Reply-To: <20260313150928.2637368-7-sashal@kernel.org>
On Fri, Mar 13, 2026 at 11:09:16AM -0400, Sasha Levin wrote:
> + * Calling close() on a file descriptor while another thread is using it
> + * (e.g., in a blocking read() or write()) has implementation-defined
> + * behavior. On Linux, the blocked operation continues on the underlying
> + * file and may complete even after close() returns.
I'm guessing this came from the man pages? This is Linux, so we are the
"implementation" here :)
thanks,
greg k-h
^ permalink raw reply
* Re: [PATCH 6/9] kernel/api: add API specification for sys_close
From: Greg Kroah-Hartman @ 2026-03-13 15:52 UTC (permalink / raw)
To: Sasha Levin
Cc: linux-api, linux-kernel, linux-doc, linux-fsdevel, linux-kbuild,
linux-kselftest, workflows, tools, x86, Thomas Gleixner,
Paul E. McKenney, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann
In-Reply-To: <20260313150928.2637368-7-sashal@kernel.org>
On Fri, Mar 13, 2026 at 11:09:16AM -0400, Sasha Levin wrote:
> + * notes: This syscall has subtle non-POSIX semantics: the fd is ALWAYS closed
> + * regardless of the return value. POSIX specifies that on EINTR, the state
> + * of the fd is unspecified, but Linux always closes it. HP-UX requires
> + * retrying close() on EINTR, but doing so on Linux may close an unrelated
> + * fd that was reassigned by another thread. For portable code, the safest
> + * approach is to check for errors but never retry close().
We don't care about HP-UX :)
> + * Error codes from the flush callback (EIO, ENOSPC, EDQUOT) indicate that
> + * previously written data may have been lost. These errors are particularly
> + * common on NFS where write errors are often deferred to close time.
What flush callback?
> + *
> + * The driver's release() callback errors are explicitly ignored by the
> + * kernel, so device driver cleanup errors are not propagated to userspace.
What "The driver" here? release() callbacks aren't really relevant
here.
thanks,
greg k-h
^ permalink raw reply
* Re: [PATCH 3/9] kernel/api: add debugfs interface for kernel API specifications
From: Sasha Levin @ 2026-03-13 16:27 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: linux-api, linux-kernel, linux-doc, linux-fsdevel, linux-kbuild,
linux-kselftest, workflows, tools, x86, Thomas Gleixner,
Paul E. McKenney, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann
In-Reply-To: <2026031301-duplicate-finalist-b7a5@gregkh>
On Fri, Mar 13, 2026 at 04:32:23PM +0100, Greg Kroah-Hartman wrote:
>On Fri, Mar 13, 2026 at 11:09:13AM -0400, Sasha Levin wrote:
>> Add a debugfs interface to expose kernel API specifications at runtime.
>> This allows tools and users to query the complete API specifications
>> through the debugfs filesystem.
>>
>> The interface provides:
>> - /sys/kernel/debug/kapi/list - lists all available API specifications
>> - /sys/kernel/debug/kapi/specs/<name> - detailed info for each API
>>
>> Each specification file includes:
>> - Function name, version, and descriptions
>> - Execution context requirements and flags
>> - Parameter details with types, flags, and constraints
>> - Return value specifications and success conditions
>> - Error codes with descriptions and conditions
>> - Locking requirements and constraints
>> - Signal handling specifications
>> - Examples, notes, and deprecation status
>>
>> This enables runtime introspection of kernel APIs for documentation
>> tools, static analyzers, and debugging purposes.
>>
>> Signed-off-by: Sasha Levin <sashal@kernel.org>
>> ---
>> Documentation/dev-tools/kernel-api-spec.rst | 88 ++--
>
>You are removing stuff from the file you created earlier in this patch
>series, is that ok?
Sorry, just a rebasing artifact from shuffling patches around. I'll fix it.
>> --- /dev/null
>> +++ b/kernel/api/kapi_debugfs.c
>> @@ -0,0 +1,503 @@
>> +// SPDX-License-Identifier: GPL-2.0
>> +/*
>> + * Kernel API specification debugfs interface
>> + *
>> + * This provides a debugfs interface to expose kernel API specifications
>> + * at runtime, allowing tools and users to query the complete API specs.
>> + */
>
>No copyright line? :)
I'll add one.
>And this is, a totally and crazy interface with debugfs, I love it!
Thanks :)
>Two minor minor nits:
>
>> +static int __init kapi_debugfs_init(void)
>> +{
>> + struct kernel_api_spec *spec;
>> + struct dentry *spec_dir;
>> +
>> + /* Create main directory */
>> + kapi_debugfs_root = debugfs_create_dir("kapi", NULL);
>> +
>> + /* Create list file */
>> + debugfs_create_file("list", 0444, kapi_debugfs_root, NULL, &kapi_list_fops);
>> +
>> + /* Create specs subdirectory */
>> + spec_dir = debugfs_create_dir("specs", kapi_debugfs_root);
>> +
>> + /* Create a file for each API spec */
>> + for (spec = __start_kapi_specs; spec < __stop_kapi_specs; spec++) {
>> + debugfs_create_file(spec->name, 0444, spec_dir, spec, &kapi_spec_fops);
>> + }
>
>No need for { }
ack
>> +
>> + pr_info("Kernel API debugfs interface initialized\n");
>
>When code is working properly, it should be quiet, no need for this as
>initializing this can not fail.
ack
--
Thanks,
Sasha
^ permalink raw reply
* Re: [PATCH 5/9] kernel/api: add API specification for sys_open
From: Sasha Levin @ 2026-03-13 16:42 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: linux-api, linux-kernel, linux-doc, linux-fsdevel, linux-kbuild,
linux-kselftest, workflows, tools, x86, Thomas Gleixner,
Paul E. McKenney, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann
In-Reply-To: <2026031343-raft-panhandle-0a21@gregkh>
On Fri, Mar 13, 2026 at 04:33:57PM +0100, Greg Kroah-Hartman wrote:
>On Fri, Mar 13, 2026 at 11:09:15AM -0400, Sasha Levin wrote:
>> Signed-off-by: Sasha Levin <sashal@kernel.org>
>
>No changelog?
I'll add something to all patches.
>> + * since-version: 1.0
>
>I think since older versions :)
Right. I guess that in my mind 1.0 was the first official "release". I'll
update it to 0.01.
>Anyway, very nice documentation, will be good to have this as part of
>the kerneldocs no matter what the result of this patch series is.
Thanks!
--
Thanks,
Sasha
^ permalink raw reply
* Re: [PATCH 6/9] kernel/api: add API specification for sys_close
From: Sasha Levin @ 2026-03-13 16:46 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: linux-api, linux-kernel, linux-doc, linux-fsdevel, linux-kbuild,
linux-kselftest, workflows, tools, x86, Thomas Gleixner,
Paul E. McKenney, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann
In-Reply-To: <2026031348-deceiving-calculate-0017@gregkh>
On Fri, Mar 13, 2026 at 04:49:28PM +0100, Greg Kroah-Hartman wrote:
>On Fri, Mar 13, 2026 at 11:09:16AM -0400, Sasha Levin wrote:
>> + * Calling close() on a file descriptor while another thread is using it
>> + * (e.g., in a blocking read() or write()) has implementation-defined
>> + * behavior. On Linux, the blocked operation continues on the underlying
>> + * file and may complete even after close() returns.
>
>I'm guessing this came from the man pages? This is Linux, so we are the
>"implementation" here :)
Right :)
I was just trying to make it a comparison to posix. I'll clarify the docs here.
--
Thanks,
Sasha
^ permalink raw reply
* Re: [PATCH 6/9] kernel/api: add API specification for sys_close
From: Sasha Levin @ 2026-03-13 16:55 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: linux-api, linux-kernel, linux-doc, linux-fsdevel, linux-kbuild,
linux-kselftest, workflows, tools, x86, Thomas Gleixner,
Paul E. McKenney, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann
In-Reply-To: <2026031321-steadfast-fang-ba42@gregkh>
On Fri, Mar 13, 2026 at 04:52:30PM +0100, Greg Kroah-Hartman wrote:
>On Fri, Mar 13, 2026 at 11:09:16AM -0400, Sasha Levin wrote:
>> + * notes: This syscall has subtle non-POSIX semantics: the fd is ALWAYS closed
>> + * regardless of the return value. POSIX specifies that on EINTR, the state
>> + * of the fd is unspecified, but Linux always closes it. HP-UX requires
>> + * retrying close() on EINTR, but doing so on Linux may close an unrelated
>> + * fd that was reassigned by another thread. For portable code, the safest
>> + * approach is to check for errors but never retry close().
>
>We don't care about HP-UX :)
Fair :) The original text was contrasting Linux's "always closed" behavior
with HP-UX. I'll just drop that reference.
>> + * Error codes from the flush callback (EIO, ENOSPC, EDQUOT) indicate that
>> + * previously written data may have been lost. These errors are particularly
>> + * common on NFS where write errors are often deferred to close time.
>
>What flush callback?
This was referring to f_op->flush, which filp_flush() calls during close.
But you're right, that's internal plumbing. I'll reworded to describe the
behavior without referencing internal callbacks:
Error codes like EIO, ENOSPC, and EDQUOT indicate that previously
buffered writes may have failed to reach storage.
>> + *
>> + * The driver's release() callback errors are explicitly ignored by the
>> + * kernel, so device driver cleanup errors are not propagated to userspace.
>
>What "The driver" here? release() callbacks aren't really relevant
>here.
The original text was noting that __fput() ignores the return value of
f_op->release(), so even if a driver's cleanup fails, userspace never
sees it via close(). But agreed, that's an internal implementation detail
not relevant to the syscall specification. Removed.
Thanks for the review!
--
Thanks,
Sasha
^ permalink raw reply
* Re: [RFC] Modernizing Linux authentication logs (lastlog, btmp, utmp, wtmp) with SQLite
From: Roman Bakshansky @ 2026-03-13 17:48 UTC (permalink / raw)
To: Adhemerval Zanella Netto, linux-api, Thorsten Kukuk
Cc: linux-kernel, audit, libc-alpha
In-Reply-To: <2d5de14a-17d2-4d08-992e-cbc5d430e231@linaro.org>
On 3/13/26 4:59 PM, Adhemerval Zanella Netto wrote:
> From the glibc standpoint my plan is just to make the accounting database
> function no-op [1] (I hopefully to get this in the next 2.44 release).
>
> And I think Thorsten Kukuk already adapted most of the usages in current
> distros [2][3] using similar strategy, along with a better systemd
> integration. I am not sure if/when distros are incorporating his work.
>
> [1] https://patchwork.sourceware.org/project/glibc/list/?series=37271
> [2] https://www.thkukuk.de/blog/Y2038_glibc_lastlog_64bit/
> [3] https://www.thkukuk.de/blog/Y2038_glibc_utmp_64bit/
Thank you for the links and information.
I have been following Thorsten Kukuk's work. I know that liblastlog2 is
already integrated into util-linux and successfully solves the Y2038
problem for lastlog. I am also aware of his wtmpdb project. These are
important steps in the right direction.
My RFC proposes to go further and provide a unified solution for all
four files: lastlog, btmp, utmp, wtmp. The idea is to create a set of
public libraries (liblastlog2, libbtmp2, libutmp2, libwtmp2) with a
consistent C interface, built on SQLite. This would provide 64-bit
timestamps, indexes for fast queries, ACID transactions, and schema
extensibility without breaking backward compatibility.
^ permalink raw reply
* Re: [RFC] Modernizing Linux authentication logs (lastlog, btmp, utmp, wtmp) with SQLite
From: Roman Bakshansky @ 2026-03-13 17:49 UTC (permalink / raw)
To: Vincent Lefevre, Adhemerval Zanella Netto, linux-api,
Thorsten Kukuk, linux-kernel, audit, libc-alpha
In-Reply-To: <20260313144508.GA5446@cventin.lip.ens-lyon.fr>
On 3/13/26 5:45 PM, Vincent Lefevre wrote:
> FYI, utmp has been reintroduced in Debian for libutempter (and thus
> applications that use this library), because systemd was not working
> or at least not sufficiently documented:
>
> https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1125682
Thank you for the link to the Debian bug. This example has further
convinced me that I am on the right track and that such an RFC is indeed
needed by the community.
^ permalink raw reply
* Re: [RFC] Modernizing Linux authentication logs (lastlog, btmp, utmp, wtmp) with SQLite
From: Adhemerval Zanella Netto @ 2026-03-13 18:03 UTC (permalink / raw)
To: Roman Bakshansky, linux-api, Thorsten Kukuk
Cc: linux-kernel, audit, libc-alpha
In-Reply-To: <948e83d4-4a34-41d5-a394-6c4ec17bc534@gmail.com>
On 13/03/26 14:48, Roman Bakshansky wrote:
> On 3/13/26 4:59 PM, Adhemerval Zanella Netto wrote:
>> From the glibc standpoint my plan is just to make the accounting database
>> function no-op [1] (I hopefully to get this in the next 2.44 release).
>>
>> And I think Thorsten Kukuk already adapted most of the usages in current
>> distros [2][3] using similar strategy, along with a better systemd
>> integration. I am not sure if/when distros are incorporating his work.
>>
>> [1] https://patchwork.sourceware.org/project/glibc/list/?series=37271
>> [2] https://www.thkukuk.de/blog/Y2038_glibc_lastlog_64bit/
>> [3] https://www.thkukuk.de/blog/Y2038_glibc_utmp_64bit/
> Thank you for the links and information.
>
> I have been following Thorsten Kukuk's work. I know that liblastlog2 is already integrated into util-linux and successfully solves the Y2038 problem for lastlog. I am also aware of his wtmpdb project. These are important steps in the right direction.
>
> My RFC proposes to go further and provide a unified solution for all four files: lastlog, btmp, utmp, wtmp. The idea is to create a set of public libraries (liblastlog2, libbtmp2, libutmp2, libwtmp2) with a consistent C interface, built on SQLite. This would provide 64-bit timestamps, indexes for fast queries, ACID transactions, and schema extensibility without breaking backward compatibility.
I think you will need to check with the systemd developers whether they are
willing to integrate this idea into their plans (if they haven't already). I
recall that systemd is extensive in some form. My understanding is that
eventually all distros, at least the ones that are systemd-based, will use
the systemd-provided framework instead of relying on different libraries.
From the glibc standpoint, providing this was always problematic, and I
shared Rich's view (musl creator) that the interface is insecure and legacy [1].
If I were to implement something not systemd-based, I would follow the
direction that utmps used[2]: a daemon as the only authority to handle the
files and all clients (utmp{x}.h} uses will just query through IPC.
It means that users should not care whether how or where the database is
placed (the _PATH_UTM and related macro should be handled as legacy and
deprecated), and the client should only care about the record structure
in the usual C form, along with a version (so no extra concepts on how the
record is placed in the file).
But I wouldn't put any extra effort into a different API as you did; rather,
I'd check how to adapt legacy programs to work better with systemd. These
interfaces are really legacy, and I think we should move away from them.
[1] https://www.openwall.com/lists/musl/2012/03/04/4
[2] https://skarnet.org/software/utmps/
^ permalink raw reply
* Re: [RFC] Modernizing Linux authentication logs (lastlog, btmp, utmp, wtmp) with SQLite
From: Florian Weimer @ 2026-03-13 18:51 UTC (permalink / raw)
To: Roman Bakshansky; +Cc: linux-api, linux-kernel, audit, libc-alpha
In-Reply-To: <660c10e6-f8b5-46e2-a424-e3e052992b3a@gmail.com>
* Roman Bakshansky:
> The full RFC, including preliminary database schemas and API drafts,
> is available in the discussion repository:
>
> https://github.com/bakshansky/linux-auth-logs
I don't understand how SQLite (without a daemon) addresses the locking
issue. WAL mode still uses fcntl locking.
^ permalink raw reply
* Re: [PATCH 0/9] Kernel API Specification Framework
From: Jakub Kicinski @ 2026-03-14 18:18 UTC (permalink / raw)
To: Sasha Levin
Cc: linux-api, linux-kernel, linux-doc, linux-fsdevel, linux-kbuild,
linux-kselftest, workflows, tools, x86, Thomas Gleixner,
Paul E. McKenney, Greg Kroah-Hartman, Jonathan Corbet,
Dmitry Vyukov, Randy Dunlap, Cyril Hrubis, Kees Cook, Jake Edge,
David Laight, Askar Safin, Gabriele Paoloni,
Mauro Carvalho Chehab, Christian Brauner, Alexander Viro,
Andrew Morton, Masahiro Yamada, Shuah Khan, Ingo Molnar,
Arnd Bergmann
In-Reply-To: <20260313150928.2637368-1-sashal@kernel.org>
On Fri, 13 Mar 2026 11:09:10 -0400 Sasha Levin wrote:
> This enables static analysis tools to verify userspace API usage at compile
> time, test generation based on formal specifications, consistent error handling
> validation, automated documentation generation, and formal verification of
> kernel interfaces.
Could you give some examples? We have machine readable descriptions for
Netlink interfaces, we approached syzbot folks and they did not really
seem to care for those.
^ permalink raw reply
* [PATCH net-next v2 00/14] tcp: preserve receive-window accounting across ratio drift
From: atwellwea @ 2026-03-14 20:13 UTC (permalink / raw)
To: netdev, davem, kuba, pabeni, edumazet, ncardwell
Cc: linux-kernel, linux-api, linux-doc, linux-kselftest,
linux-trace-kernel, mptcp, dsahern, horms, kuniyu, andrew+netdev,
willemdebruijn.kernel, jasowang, skhan, corbet, matttbe,
martineau, geliang, rostedt, mhiramat, mathieu.desnoyers,
0x7f454c46
From: Wesley Atwell <atwellwea@gmail.com>
This series keeps sender-visible TCP receive-window accounting tied to the
scaling basis that was in force when the window was advertised, even if
later receive-side truesize inflation lowers scaling_ratio or the live
receive window retracts below the largest right edge already exposed to the
sender.
After the receive-window retraction changes, the receive path needs to keep
track of two related pieces of sender-visible state:
1. the live advertised receive window
2. the maximum advertised right edge and the basis it was exposed with
This repost snapshots both, uses them to repair receive-buffer backing when
ratio drift would otherwise strand sender-visible space, extends
TCP_REPAIR_WINDOW so repair/restore can round-trip the new state, and adds
truesize-drift coverage through TUN packetdrill tests and netdevsim-based
selftests.
v2:
- repost to net-next and use the [PATCH net-next v2] prefix
- rebase the receive-window accounting changes on top of the retraction
model
- split the series more finely
- snapshot both the live rwnd basis and the max advertised-window basis
- extend TCP_REPAIR_WINDOW to preserve legacy, v1, and current layouts
- add TUN RX truesize injection and packetdrill coverage for ratio drift
- split the generic netdevsim PSP extension cleanup into its own final
patch after the peer RX truesize support
- add the requested ABI/runtime comments at the non-obvious review points
Testing:
- full runtime selftest coverage for netdevsim, tcp_ao, mptcp, and
packetdrill; all runtime suites completed successfully
- tcp_ao completed 24/24 top-level tests, covering 803 passing checks,
6 expected failures, 36 skips, and 0 unexpected failures
- mptcp completed 588 passing checks in aggregate, with 28 skips and
0 unexpected failures
- packetdrill completed 219/219 runtime cases with 0 failures,
including the new tests
- netdevsim completed 18/18 top-level runtime tests with 0 failures,
including the peer RX truesize and related netdevsim coverage used by
this series
Wesley Atwell (14):
tcp: factor receive-memory accounting helpers
tcp: snapshot advertise-time scaling for rcv_wnd
tcp: refresh rcv_wnd snapshots at TCP write sites
tcp: snapshot the maximum advertised receive window
tcp: grow rcvbuf to back scaled-window quantization slack
tcp: regrow rcvbuf when scaling_ratio drops after advertisement
tcp: honor the maximum advertised window after live retraction
tcp: extend TCP_REPAIR_WINDOW for live and max-window snapshots
mptcp: refresh TCP receive-window snapshots on subflows
tcp: expose rmem and backlog in tcp and mptcp rcvbuf_grow tracepoints
selftests: tcp_ao: cover legacy, v1, and retracted repair windows
tun/selftests: add RX truesize injection for TCP window tests
netdevsim: add peer RX truesize support for selftests
netdevsim: release pinned PSP ext on drop paths
.../networking/net_cachelines/tcp_sock.rst | 2 +
drivers/net/netdevsim/netdev.c | 156 ++++++-
drivers/net/netdevsim/netdevsim.h | 4 +
drivers/net/tun.c | 65 +++
include/linux/tcp.h | 2 +
include/net/tcp.h | 118 ++++-
include/trace/events/mptcp.h | 11 +-
include/trace/events/tcp.h | 12 +-
include/uapi/linux/if_tun.h | 4 +
include/uapi/linux/tcp.h | 8 +
net/ipv4/tcp.c | 75 ++-
net/ipv4/tcp_fastopen.c | 2 +-
net/ipv4/tcp_input.c | 160 ++++++-
net/ipv4/tcp_minisocks.c | 4 +-
net/ipv4/tcp_output.c | 25 +-
net/mptcp/options.c | 14 +-
net/mptcp/protocol.h | 14 +-
.../selftests/drivers/net/netdevsim/Makefile | 1 +
.../drivers/net/netdevsim/peer-rx-truesize.sh | 426 ++++++++++++++++++
.../tcp_rcv_neg_window_truesize.pkt | 143 ++++++
.../net/packetdrill/tcp_rcv_toobig.pkt | 35 ++
.../packetdrill/tcp_rcv_toobig_default.pkt | 97 ++++
.../tcp_rcv_toobig_default_truesize.pkt | 118 +++++
.../tcp_rcv_wnd_shrink_allowed_truesize.pkt | 49 ++
.../testing/selftests/net/tcp_ao/lib/aolib.h | 83 +++-
.../testing/selftests/net/tcp_ao/lib/repair.c | 18 +-
.../selftests/net/tcp_ao/self-connect.c | 201 ++++++++-
tools/testing/selftests/net/tun.c | 140 +++++-
28 files changed, 1911 insertions(+), 76 deletions(-)
create mode 100755 tools/testing/selftests/drivers/net/netdevsim/peer-rx-truesize.sh
create mode 100644 tools/testing/selftests/net/packetdrill/tcp_rcv_neg_window_truesize.pkt
create mode 100644 tools/testing/selftests/net/packetdrill/tcp_rcv_toobig.pkt
create mode 100644 tools/testing/selftests/net/packetdrill/tcp_rcv_toobig_default.pkt
create mode 100644 tools/testing/selftests/net/packetdrill/tcp_rcv_toobig_default_truesize.pkt
create mode 100644 tools/testing/selftests/net/packetdrill/tcp_rcv_wnd_shrink_allowed_truesize.pkt
base-commit: f807b5b9b89eb9220d034115c272c312251cbcac
--
2.43.0
^ permalink raw reply
* [PATCH net-next v2 01/14] tcp: factor receive-memory accounting helpers
From: atwellwea @ 2026-03-14 20:13 UTC (permalink / raw)
To: netdev, davem, kuba, pabeni, edumazet, ncardwell
Cc: linux-kernel, linux-api, linux-doc, linux-kselftest,
linux-trace-kernel, mptcp, dsahern, horms, kuniyu, andrew+netdev,
willemdebruijn.kernel, jasowang, skhan, corbet, matttbe,
martineau, geliang, rostedt, mhiramat, mathieu.desnoyers,
0x7f454c46
In-Reply-To: <20260314201348.1786972-1-atwellwea@gmail.com>
From: Wesley Atwell <atwellwea@gmail.com>
Factor the core receive-memory byte accounting into small helpers so
window selection, pressure checks, and prune decisions all start from
one set of quantities.
This is preparatory only. Later patches will use the same helpers when
tying sender-visible receive-window state back to hard memory admission.
Signed-off-by: Wesley Atwell <atwellwea@gmail.com>
---
include/net/tcp.h | 32 +++++++++++++++++++++++++++-----
net/ipv4/tcp_input.c | 2 +-
2 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/include/net/tcp.h b/include/net/tcp.h
index f87bdacb5a69..3a0060599afe 100644
--- a/include/net/tcp.h
+++ b/include/net/tcp.h
@@ -1751,12 +1751,34 @@ static inline void tcp_scaling_ratio_init(struct sock *sk)
tcp_sk(sk)->scaling_ratio = TCP_DEFAULT_SCALING_RATIO;
}
+/* TCP receive-side accounting reuses sk_rcvbuf as both a hard memory limit
+ * and as the source material for the advertised receive window after
+ * scaling_ratio conversion. Keep the byte accounting explicit so admission,
+ * pruning, and rwnd selection all start from the same quantities.
+ */
+static inline int tcp_rmem_used(const struct sock *sk)
+{
+ return atomic_read(&sk->sk_rmem_alloc);
+}
+
+static inline int tcp_rmem_avail(const struct sock *sk)
+{
+ return READ_ONCE(sk->sk_rcvbuf) - tcp_rmem_used(sk);
+}
+
+/* Sender-visible rwnd headroom also reserves bytes already queued on backlog.
+ * Those bytes are not free to advertise again until __release_sock() drains
+ * backlog and clears sk_backlog.len.
+ */
+static inline int tcp_rwnd_avail(const struct sock *sk)
+{
+ return tcp_rmem_avail(sk) - READ_ONCE(sk->sk_backlog.len);
+}
+
/* Note: caller must be prepared to deal with negative returns */
static inline int tcp_space(const struct sock *sk)
{
- return tcp_win_from_space(sk, READ_ONCE(sk->sk_rcvbuf) -
- READ_ONCE(sk->sk_backlog.len) -
- atomic_read(&sk->sk_rmem_alloc));
+ return tcp_win_from_space(sk, tcp_rwnd_avail(sk));
}
static inline int tcp_full_space(const struct sock *sk)
@@ -1799,7 +1821,7 @@ static inline bool tcp_rmem_pressure(const struct sock *sk)
rcvbuf = READ_ONCE(sk->sk_rcvbuf);
threshold = rcvbuf - (rcvbuf >> 3);
- return atomic_read(&sk->sk_rmem_alloc) > threshold;
+ return tcp_rmem_used(sk) > threshold;
}
static inline bool tcp_epollin_ready(const struct sock *sk, int target)
@@ -1949,7 +1971,7 @@ static inline void tcp_fast_path_check(struct sock *sk)
if (RB_EMPTY_ROOT(&tp->out_of_order_queue) &&
tp->rcv_wnd &&
- atomic_read(&sk->sk_rmem_alloc) < sk->sk_rcvbuf &&
+ tcp_rmem_avail(sk) > 0 &&
!tp->urg_data)
tcp_fast_path_on(tp);
}
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index e6b2f4be7723..b8e65e31255e 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -5959,7 +5959,7 @@ static int tcp_prune_queue(struct sock *sk, const struct sk_buff *in_skb)
struct tcp_sock *tp = tcp_sk(sk);
/* Do nothing if our queues are empty. */
- if (!atomic_read(&sk->sk_rmem_alloc))
+ if (!tcp_rmem_used(sk))
return -1;
NET_INC_STATS(sock_net(sk), LINUX_MIB_PRUNECALLED);
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v2 02/14] tcp: snapshot advertise-time scaling for rcv_wnd
From: atwellwea @ 2026-03-14 20:13 UTC (permalink / raw)
To: netdev, davem, kuba, pabeni, edumazet, ncardwell
Cc: linux-kernel, linux-api, linux-doc, linux-kselftest,
linux-trace-kernel, mptcp, dsahern, horms, kuniyu, andrew+netdev,
willemdebruijn.kernel, jasowang, skhan, corbet, matttbe,
martineau, geliang, rostedt, mhiramat, mathieu.desnoyers,
0x7f454c46
In-Reply-To: <20260314201348.1786972-1-atwellwea@gmail.com>
From: Wesley Atwell <atwellwea@gmail.com>
Track the scaling basis that was in force when tp->rcv_wnd was last
advertised, and provide helpers to refresh or interpret that snapshot.
Later patches use this live-window basis to preserve sender-visible rwnd
accounting when receive-side memory costs drift after advertisement.
Signed-off-by: Wesley Atwell <atwellwea@gmail.com>
---
.../networking/net_cachelines/tcp_sock.rst | 1 +
include/linux/tcp.h | 1 +
include/net/tcp.h | 52 ++++++++++++++++++-
net/ipv4/tcp.c | 1 +
4 files changed, 54 insertions(+), 1 deletion(-)
diff --git a/Documentation/networking/net_cachelines/tcp_sock.rst b/Documentation/networking/net_cachelines/tcp_sock.rst
index fecf61166a54..09ece1c59c2d 100644
--- a/Documentation/networking/net_cachelines/tcp_sock.rst
+++ b/Documentation/networking/net_cachelines/tcp_sock.rst
@@ -11,6 +11,7 @@ Type Name fastpath_tx_access fastpa
struct inet_connection_sock inet_conn
u16 tcp_header_len read_mostly read_mostly tcp_bound_to_half_wnd,tcp_current_mss(tx);tcp_rcv_established(rx)
u16 gso_segs read_mostly tcp_xmit_size_goal
+u8 rcv_wnd_scaling_ratio read_write read_mostly tcp_set_rcv_wnd,tcp_can_ingest,tcp_repair_set_window,do_tcp_getsockopt
__be32 pred_flags read_write read_mostly tcp_select_window(tx);tcp_rcv_established(rx)
u64 bytes_received read_write tcp_rcv_nxt_update(rx)
u32 segs_in read_write tcp_v6_rcv(rx)
diff --git a/include/linux/tcp.h b/include/linux/tcp.h
index 6982f10e826b..2ace563d59d6 100644
--- a/include/linux/tcp.h
+++ b/include/linux/tcp.h
@@ -297,6 +297,7 @@ struct tcp_sock {
est_ecnfield:2,/* ECN field for AccECN delivered estimates */
accecn_opt_demand:2,/* Demand AccECN option for n next ACKs */
prev_ecnfield:2; /* ECN bits from the previous segment */
+ u8 rcv_wnd_scaling_ratio; /* 0 if unknown, else tp->rcv_wnd basis */
__be32 pred_flags;
u64 tcp_clock_cache; /* cache last tcp_clock_ns() (see tcp_mstamp_refresh()) */
u64 tcp_mstamp; /* most recent packet received/sent */
diff --git a/include/net/tcp.h b/include/net/tcp.h
index 3a0060599afe..6fa7cdb0979e 100644
--- a/include/net/tcp.h
+++ b/include/net/tcp.h
@@ -1741,6 +1741,31 @@ static inline int tcp_space_from_win(const struct sock *sk, int win)
return __tcp_space_from_win(tcp_sk(sk)->scaling_ratio, win);
}
+static inline bool tcp_wnd_snapshot_valid(u8 scaling_ratio)
+{
+ return scaling_ratio != 0;
+}
+
+static inline bool tcp_space_from_wnd_snapshot(u8 scaling_ratio, int win,
+ int *space)
+{
+ if (!tcp_wnd_snapshot_valid(scaling_ratio))
+ return false;
+
+ *space = __tcp_space_from_win(scaling_ratio, win);
+ return true;
+}
+
+/* Rebuild hard receive-memory units for data already covered by tp->rcv_wnd if
+ * the advertise-time basis is known.
+ */
+static inline bool tcp_space_from_rcv_wnd(const struct tcp_sock *tp, int win,
+ int *space)
+{
+ return tcp_space_from_wnd_snapshot(tp->rcv_wnd_scaling_ratio, win,
+ space);
+}
+
/* Assume a 50% default for skb->len/skb->truesize ratio.
* This may be adjusted later in tcp_measure_rcv_mss().
*/
@@ -1748,7 +1773,32 @@ static inline int tcp_space_from_win(const struct sock *sk, int win)
static inline void tcp_scaling_ratio_init(struct sock *sk)
{
- tcp_sk(sk)->scaling_ratio = TCP_DEFAULT_SCALING_RATIO;
+ struct tcp_sock *tp = tcp_sk(sk);
+
+ tp->scaling_ratio = TCP_DEFAULT_SCALING_RATIO;
+ tp->rcv_wnd_scaling_ratio = TCP_DEFAULT_SCALING_RATIO;
+}
+
+/* tp->rcv_wnd is paired with the scaling_ratio that was in force when that
+ * window was last advertised. Callers can leave a zero snapshot when the
+ * advertise-time basis is unknown and refresh the pair on the next local
+ * window update.
+ */
+static inline void tcp_set_rcv_wnd_snapshot(struct tcp_sock *tp, u32 win,
+ u8 scaling_ratio)
+{
+ tp->rcv_wnd = win;
+ tp->rcv_wnd_scaling_ratio = scaling_ratio;
+}
+
+static inline void tcp_set_rcv_wnd(struct tcp_sock *tp, u32 win)
+{
+ tcp_set_rcv_wnd_snapshot(tp, win, tp->scaling_ratio);
+}
+
+static inline void tcp_set_rcv_wnd_unknown(struct tcp_sock *tp, u32 win)
+{
+ tcp_set_rcv_wnd_snapshot(tp, win, 0);
}
/* TCP receive-side accounting reuses sk_rcvbuf as both a hard memory limit
diff --git a/net/ipv4/tcp.c b/net/ipv4/tcp.c
index 516087c622ad..0383ee8d3b78 100644
--- a/net/ipv4/tcp.c
+++ b/net/ipv4/tcp.c
@@ -5275,6 +5275,7 @@ static void __init tcp_struct_check(void)
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, received_ce);
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, received_ecn_bytes);
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, app_limited);
+ CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, rcv_wnd_scaling_ratio);
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, rcv_wnd);
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, rcv_mwnd_seq);
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, rcv_tstamp);
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v2 03/14] tcp: refresh rcv_wnd snapshots at TCP write sites
From: atwellwea @ 2026-03-14 20:13 UTC (permalink / raw)
To: netdev, davem, kuba, pabeni, edumazet, ncardwell
Cc: linux-kernel, linux-api, linux-doc, linux-kselftest,
linux-trace-kernel, mptcp, dsahern, horms, kuniyu, andrew+netdev,
willemdebruijn.kernel, jasowang, skhan, corbet, matttbe,
martineau, geliang, rostedt, mhiramat, mathieu.desnoyers,
0x7f454c46
In-Reply-To: <20260314201348.1786972-1-atwellwea@gmail.com>
From: Wesley Atwell <atwellwea@gmail.com>
Refresh the live rwnd snapshot whenever TCP updates tp->rcv_wnd at the
normal write sites, including child setup, tcp_select_window(), and the
initial connect-time window selection.
This keeps the live sender-visible window paired with the scaling basis
that was actually advertised.
Signed-off-by: Wesley Atwell <atwellwea@gmail.com>
---
net/ipv4/tcp_minisocks.c | 2 +-
net/ipv4/tcp_output.c | 8 ++++++--
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/net/ipv4/tcp_minisocks.c b/net/ipv4/tcp_minisocks.c
index d350d794a959..1c02c9cd13fe 100644
--- a/net/ipv4/tcp_minisocks.c
+++ b/net/ipv4/tcp_minisocks.c
@@ -603,7 +603,7 @@ struct sock *tcp_create_openreq_child(const struct sock *sk,
newtp->rx_opt.sack_ok = ireq->sack_ok;
newtp->window_clamp = req->rsk_window_clamp;
newtp->rcv_ssthresh = req->rsk_rcv_wnd;
- newtp->rcv_wnd = req->rsk_rcv_wnd;
+ tcp_set_rcv_wnd(newtp, req->rsk_rcv_wnd);
newtp->rcv_mwnd_seq = newtp->rcv_wup + req->rsk_rcv_wnd;
newtp->rx_opt.wscale_ok = ireq->wscale_ok;
if (newtp->rx_opt.wscale_ok) {
diff --git a/net/ipv4/tcp_output.c b/net/ipv4/tcp_output.c
index 35c3b0ab5a0c..0b082726d7c4 100644
--- a/net/ipv4/tcp_output.c
+++ b/net/ipv4/tcp_output.c
@@ -291,7 +291,7 @@ static u16 tcp_select_window(struct sock *sk)
*/
if (unlikely(inet_csk(sk)->icsk_ack.pending & ICSK_ACK_NOMEM)) {
tp->pred_flags = 0;
- tp->rcv_wnd = 0;
+ tcp_set_rcv_wnd(tp, 0);
tp->rcv_wup = tp->rcv_nxt;
tcp_update_max_rcv_wnd_seq(tp);
return 0;
@@ -315,7 +315,7 @@ static u16 tcp_select_window(struct sock *sk)
}
}
- tp->rcv_wnd = new_win;
+ tcp_set_rcv_wnd(tp, new_win);
tp->rcv_wup = tp->rcv_nxt;
tcp_update_max_rcv_wnd_seq(tp);
@@ -4148,6 +4148,10 @@ static void tcp_connect_init(struct sock *sk)
READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_window_scaling),
&rcv_wscale,
rcv_wnd);
+ /* tcp_select_initial_window() filled tp->rcv_wnd through its out-param,
+ * so snapshot the scaling_ratio we will use for that initial rwnd.
+ */
+ tcp_set_rcv_wnd(tp, tp->rcv_wnd);
tp->rx_opt.rcv_wscale = rcv_wscale;
tp->rcv_ssthresh = tp->rcv_wnd;
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v2 04/14] tcp: snapshot the maximum advertised receive window
From: atwellwea @ 2026-03-14 20:13 UTC (permalink / raw)
To: netdev, davem, kuba, pabeni, edumazet, ncardwell
Cc: linux-kernel, linux-api, linux-doc, linux-kselftest,
linux-trace-kernel, mptcp, dsahern, horms, kuniyu, andrew+netdev,
willemdebruijn.kernel, jasowang, skhan, corbet, matttbe,
martineau, geliang, rostedt, mhiramat, mathieu.desnoyers,
0x7f454c46
In-Reply-To: <20260314201348.1786972-1-atwellwea@gmail.com>
From: Wesley Atwell <atwellwea@gmail.com>
Track the maximum sender-visible receive-window right edge separately
from the live rwnd, along with the scaling basis that was in force when
that larger window was advertised.
This gives later admission and restore paths enough information to
reason about retracted windows without losing the original sender-
visible bound.
Signed-off-by: Wesley Atwell <atwellwea@gmail.com>
---
.../networking/net_cachelines/tcp_sock.rst | 1 +
include/linux/tcp.h | 1 +
include/net/tcp.h | 21 ++++++++++++++++++-
net/ipv4/tcp.c | 1 +
net/ipv4/tcp_fastopen.c | 2 +-
net/ipv4/tcp_input.c | 4 ++--
net/ipv4/tcp_minisocks.c | 2 +-
net/ipv4/tcp_output.c | 2 +-
8 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/Documentation/networking/net_cachelines/tcp_sock.rst b/Documentation/networking/net_cachelines/tcp_sock.rst
index 09ece1c59c2d..d58a3b1eb55d 100644
--- a/Documentation/networking/net_cachelines/tcp_sock.rst
+++ b/Documentation/networking/net_cachelines/tcp_sock.rst
@@ -11,6 +11,7 @@ Type Name fastpath_tx_access fastpa
struct inet_connection_sock inet_conn
u16 tcp_header_len read_mostly read_mostly tcp_bound_to_half_wnd,tcp_current_mss(tx);tcp_rcv_established(rx)
u16 gso_segs read_mostly tcp_xmit_size_goal
+u8 rcv_mwnd_scaling_ratio read_write read_mostly tcp_init_max_rcv_wnd_seq,tcp_update_max_rcv_wnd_seq,tcp_repair_set_window,do_tcp_getsockopt
u8 rcv_wnd_scaling_ratio read_write read_mostly tcp_set_rcv_wnd,tcp_can_ingest,tcp_repair_set_window,do_tcp_getsockopt
__be32 pred_flags read_write read_mostly tcp_select_window(tx);tcp_rcv_established(rx)
u64 bytes_received read_write tcp_rcv_nxt_update(rx)
diff --git a/include/linux/tcp.h b/include/linux/tcp.h
index 2ace563d59d6..e5d7a65ac439 100644
--- a/include/linux/tcp.h
+++ b/include/linux/tcp.h
@@ -297,6 +297,7 @@ struct tcp_sock {
est_ecnfield:2,/* ECN field for AccECN delivered estimates */
accecn_opt_demand:2,/* Demand AccECN option for n next ACKs */
prev_ecnfield:2; /* ECN bits from the previous segment */
+ u8 rcv_mwnd_scaling_ratio; /* 0 if unknown, else tp->rcv_mwnd_seq basis */
u8 rcv_wnd_scaling_ratio; /* 0 if unknown, else tp->rcv_wnd basis */
__be32 pred_flags;
u64 tcp_clock_cache; /* cache last tcp_clock_ns() (see tcp_mstamp_refresh()) */
diff --git a/include/net/tcp.h b/include/net/tcp.h
index 6fa7cdb0979e..fc22ab6b80d5 100644
--- a/include/net/tcp.h
+++ b/include/net/tcp.h
@@ -947,13 +947,21 @@ static inline u32 tcp_max_receive_window(const struct tcp_sock *tp)
return (u32) win;
}
+static inline void tcp_init_max_rcv_wnd_seq(struct tcp_sock *tp)
+{
+ tp->rcv_mwnd_seq = tp->rcv_wup + tp->rcv_wnd;
+ tp->rcv_mwnd_scaling_ratio = tp->rcv_wnd_scaling_ratio;
+}
+
/* Check if we need to update the maximum receive window sequence number */
static inline void tcp_update_max_rcv_wnd_seq(struct tcp_sock *tp)
{
u32 wre = tp->rcv_wup + tp->rcv_wnd;
- if (after(wre, tp->rcv_mwnd_seq))
+ if (after(wre, tp->rcv_mwnd_seq)) {
tp->rcv_mwnd_seq = wre;
+ tp->rcv_mwnd_scaling_ratio = tp->rcv_wnd_scaling_ratio;
+ }
}
/* Choose a new window, without checks for shrinking, and without
@@ -1766,6 +1774,16 @@ static inline bool tcp_space_from_rcv_wnd(const struct tcp_sock *tp, int win,
space);
}
+/* Same as tcp_space_from_rcv_wnd(), but for the remembered maximum
+ * sender-visible receive window.
+ */
+static inline bool tcp_space_from_rcv_mwnd(const struct tcp_sock *tp, int win,
+ int *space)
+{
+ return tcp_space_from_wnd_snapshot(tp->rcv_mwnd_scaling_ratio, win,
+ space);
+}
+
/* Assume a 50% default for skb->len/skb->truesize ratio.
* This may be adjusted later in tcp_measure_rcv_mss().
*/
@@ -1776,6 +1794,7 @@ static inline void tcp_scaling_ratio_init(struct sock *sk)
struct tcp_sock *tp = tcp_sk(sk);
tp->scaling_ratio = TCP_DEFAULT_SCALING_RATIO;
+ tp->rcv_mwnd_scaling_ratio = TCP_DEFAULT_SCALING_RATIO;
tp->rcv_wnd_scaling_ratio = TCP_DEFAULT_SCALING_RATIO;
}
diff --git a/net/ipv4/tcp.c b/net/ipv4/tcp.c
index 0383ee8d3b78..66706dbb90f5 100644
--- a/net/ipv4/tcp.c
+++ b/net/ipv4/tcp.c
@@ -5275,6 +5275,7 @@ static void __init tcp_struct_check(void)
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, received_ce);
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, received_ecn_bytes);
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, app_limited);
+ CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, rcv_mwnd_scaling_ratio);
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, rcv_wnd_scaling_ratio);
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, rcv_wnd);
CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_write_txrx, rcv_mwnd_seq);
diff --git a/net/ipv4/tcp_fastopen.c b/net/ipv4/tcp_fastopen.c
index 4e389d609f91..56113cf2a165 100644
--- a/net/ipv4/tcp_fastopen.c
+++ b/net/ipv4/tcp_fastopen.c
@@ -377,7 +377,7 @@ static struct sock *tcp_fastopen_create_child(struct sock *sk,
tcp_rsk(req)->rcv_nxt = tp->rcv_nxt;
tp->rcv_wup = tp->rcv_nxt;
- tp->rcv_mwnd_seq = tp->rcv_wup + tp->rcv_wnd;
+ tcp_init_max_rcv_wnd_seq(tp);
/* tcp_conn_request() is sending the SYNACK,
* and queues the child into listener accept queue.
*/
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index b8e65e31255e..352f814a4ff6 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -6902,7 +6902,7 @@ static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
*/
WRITE_ONCE(tp->rcv_nxt, TCP_SKB_CB(skb)->seq + 1);
tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;
- tp->rcv_mwnd_seq = tp->rcv_wup + tp->rcv_wnd;
+ tcp_init_max_rcv_wnd_seq(tp);
/* RFC1323: The window in SYN & SYN/ACK segments is
* never scaled.
@@ -7015,7 +7015,7 @@ static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
WRITE_ONCE(tp->rcv_nxt, TCP_SKB_CB(skb)->seq + 1);
WRITE_ONCE(tp->copied_seq, tp->rcv_nxt);
tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;
- tp->rcv_mwnd_seq = tp->rcv_wup + tp->rcv_wnd;
+ tcp_init_max_rcv_wnd_seq(tp);
/* RFC1323: The window in SYN & SYN/ACK segments is
* never scaled.
diff --git a/net/ipv4/tcp_minisocks.c b/net/ipv4/tcp_minisocks.c
index 1c02c9cd13fe..85bd9580caf9 100644
--- a/net/ipv4/tcp_minisocks.c
+++ b/net/ipv4/tcp_minisocks.c
@@ -604,7 +604,7 @@ struct sock *tcp_create_openreq_child(const struct sock *sk,
newtp->window_clamp = req->rsk_window_clamp;
newtp->rcv_ssthresh = req->rsk_rcv_wnd;
tcp_set_rcv_wnd(newtp, req->rsk_rcv_wnd);
- newtp->rcv_mwnd_seq = newtp->rcv_wup + req->rsk_rcv_wnd;
+ tcp_init_max_rcv_wnd_seq(newtp);
newtp->rx_opt.wscale_ok = ireq->wscale_ok;
if (newtp->rx_opt.wscale_ok) {
newtp->rx_opt.snd_wscale = ireq->snd_wscale;
diff --git a/net/ipv4/tcp_output.c b/net/ipv4/tcp_output.c
index 0b082726d7c4..57a2a6daaad3 100644
--- a/net/ipv4/tcp_output.c
+++ b/net/ipv4/tcp_output.c
@@ -4171,7 +4171,7 @@ static void tcp_connect_init(struct sock *sk)
else
tp->rcv_tstamp = tcp_jiffies32;
tp->rcv_wup = tp->rcv_nxt;
- tp->rcv_mwnd_seq = tp->rcv_nxt + tp->rcv_wnd;
+ tcp_init_max_rcv_wnd_seq(tp);
WRITE_ONCE(tp->copied_seq, tp->rcv_nxt);
inet_csk(sk)->icsk_rto = tcp_timeout_init(sk);
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v2 05/14] tcp: grow rcvbuf to back scaled-window quantization slack
From: atwellwea @ 2026-03-14 20:13 UTC (permalink / raw)
To: netdev, davem, kuba, pabeni, edumazet, ncardwell
Cc: linux-kernel, linux-api, linux-doc, linux-kselftest,
linux-trace-kernel, mptcp, dsahern, horms, kuniyu, andrew+netdev,
willemdebruijn.kernel, jasowang, skhan, corbet, matttbe,
martineau, geliang, rostedt, mhiramat, mathieu.desnoyers,
0x7f454c46
In-Reply-To: <20260314201348.1786972-1-atwellwea@gmail.com>
From: Wesley Atwell <atwellwea@gmail.com>
Teach TCP to grow sk_rcvbuf when scale rounding would otherwise expose
more sender-visible window than the current hard receive-memory backing
can cover.
The new helper keeps backlog and memory-pressure limits in the same
units as the rest of the receive path, while __tcp_select_window()
backs any rounding slack before advertising it.
Signed-off-by: Wesley Atwell <atwellwea@gmail.com>
---
include/net/tcp.h | 12 ++++++++++++
net/ipv4/tcp_input.c | 36 ++++++++++++++++++++++++++++++++++--
net/ipv4/tcp_output.c | 15 +++++++++++++--
3 files changed, 59 insertions(+), 4 deletions(-)
diff --git a/include/net/tcp.h b/include/net/tcp.h
index fc22ab6b80d5..5b479ad44f89 100644
--- a/include/net/tcp.h
+++ b/include/net/tcp.h
@@ -397,6 +397,7 @@ int tcp_ioctl(struct sock *sk, int cmd, int *karg);
enum skb_drop_reason tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb);
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb);
void tcp_rcvbuf_grow(struct sock *sk, u32 newval);
+bool tcp_try_grow_rcvbuf(struct sock *sk, int needed);
void tcp_rcv_space_adjust(struct sock *sk);
int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp);
void tcp_twsk_destructor(struct sock *sk);
@@ -1844,6 +1845,17 @@ static inline int tcp_rwnd_avail(const struct sock *sk)
return tcp_rmem_avail(sk) - READ_ONCE(sk->sk_backlog.len);
}
+/* Passive children clone the listener's sk_socket until accept() grafts
+ * their own struct socket, so only sockets that point back to themselves
+ * should autotune receive-buffer backing.
+ */
+static inline bool tcp_rcvbuf_grow_allowed(const struct sock *sk)
+{
+ struct socket *sock = READ_ONCE(sk->sk_socket);
+
+ return sock && READ_ONCE(sock->sk) == sk;
+}
+
/* Note: caller must be prepared to deal with negative returns */
static inline int tcp_space(const struct sock *sk)
{
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index 352f814a4ff6..32256519a085 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -774,6 +774,38 @@ static void tcp_init_buffer_space(struct sock *sk)
(u32)TCP_INIT_CWND * tp->advmss);
}
+/* Try to grow sk_rcvbuf so the hard receive-memory limit covers @needed
+ * bytes beyond sk_rmem_alloc while preserving sender-visible headroom
+ * already consumed by sk_backlog.len.
+ */
+bool tcp_try_grow_rcvbuf(struct sock *sk, int needed)
+{
+ struct net *net = sock_net(sk);
+ int backlog;
+ int rmem2;
+ int target;
+
+ needed = max(needed, 0);
+ backlog = READ_ONCE(sk->sk_backlog.len);
+ target = tcp_rmem_used(sk) + backlog + needed;
+
+ if (target <= READ_ONCE(sk->sk_rcvbuf))
+ return true;
+
+ rmem2 = READ_ONCE(net->ipv4.sysctl_tcp_rmem[2]);
+ if (READ_ONCE(sk->sk_rcvbuf) >= rmem2 ||
+ (sk->sk_userlocks & SOCK_RCVBUF_LOCK) ||
+ tcp_under_memory_pressure(sk) ||
+ sk_memory_allocated(sk) >= sk_prot_mem_limits(sk, 0))
+ return false;
+
+ WRITE_ONCE(sk->sk_rcvbuf,
+ min_t(int, rmem2,
+ max_t(int, READ_ONCE(sk->sk_rcvbuf), target)));
+
+ return target <= READ_ONCE(sk->sk_rcvbuf);
+}
+
/* 4. Recalculate window clamp after socket hit its memory bounds. */
static void tcp_clamp_window(struct sock *sk)
{
@@ -785,14 +817,14 @@ static void tcp_clamp_window(struct sock *sk)
icsk->icsk_ack.quick = 0;
rmem2 = READ_ONCE(net->ipv4.sysctl_tcp_rmem[2]);
- if (sk->sk_rcvbuf < rmem2 &&
+ if (READ_ONCE(sk->sk_rcvbuf) < rmem2 &&
!(sk->sk_userlocks & SOCK_RCVBUF_LOCK) &&
!tcp_under_memory_pressure(sk) &&
sk_memory_allocated(sk) < sk_prot_mem_limits(sk, 0)) {
WRITE_ONCE(sk->sk_rcvbuf,
min(atomic_read(&sk->sk_rmem_alloc), rmem2));
}
- if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf)
+ if (atomic_read(&sk->sk_rmem_alloc) > READ_ONCE(sk->sk_rcvbuf))
tp->rcv_ssthresh = min(tp->window_clamp, 2U * tp->advmss);
}
diff --git a/net/ipv4/tcp_output.c b/net/ipv4/tcp_output.c
index 57a2a6daaad3..53781cf591d2 100644
--- a/net/ipv4/tcp_output.c
+++ b/net/ipv4/tcp_output.c
@@ -3375,13 +3375,24 @@ u32 __tcp_select_window(struct sock *sk)
* scaled window will not line up with the MSS boundary anyway.
*/
if (tp->rx_opt.rcv_wscale) {
+ int rcv_wscale = 1 << tp->rx_opt.rcv_wscale;
+
window = free_space;
/* Advertise enough space so that it won't get scaled away.
- * Import case: prevent zero window announcement if
+ * Important case: prevent zero-window announcement if
* 1<<rcv_wscale > mss.
*/
- window = ALIGN(window, (1 << tp->rx_opt.rcv_wscale));
+ window = ALIGN(window, rcv_wscale);
+
+ /* Back any scale-quantization slack before we expose it.
+ * Otherwise tcp_can_ingest() can reject data which is still
+ * within the sender-visible window.
+ */
+ if (window > free_space &&
+ (!tcp_rcvbuf_grow_allowed(sk) ||
+ !tcp_try_grow_rcvbuf(sk, tcp_space_from_win(sk, window))))
+ window = round_down(free_space, rcv_wscale);
} else {
window = tp->rcv_wnd;
/* Get the largest window that is a nice multiple of mss.
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v2 06/14] tcp: regrow rcvbuf when scaling_ratio drops after advertisement
From: atwellwea @ 2026-03-14 20:13 UTC (permalink / raw)
To: netdev, davem, kuba, pabeni, edumazet, ncardwell
Cc: linux-kernel, linux-api, linux-doc, linux-kselftest,
linux-trace-kernel, mptcp, dsahern, horms, kuniyu, andrew+netdev,
willemdebruijn.kernel, jasowang, skhan, corbet, matttbe,
martineau, geliang, rostedt, mhiramat, mathieu.desnoyers,
0x7f454c46
In-Reply-To: <20260314201348.1786972-1-atwellwea@gmail.com>
From: Wesley Atwell <atwellwea@gmail.com>
When tcp_measure_rcv_mss() lowers scaling_ratio after a window was
already advertised, grow sk_rcvbuf so the remaining live sender-visible
window still has matching hard receive-memory backing.
This repairs the live advertised window only. Retracted-window rescue is
handled separately in a later patch.
Signed-off-by: Wesley Atwell <atwellwea@gmail.com>
---
net/ipv4/tcp_input.c | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index 32256519a085..d76e4e4c0e57 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -221,6 +221,31 @@ static __cold void tcp_gro_dev_warn(const struct sock *sk, const struct sk_buff
rcu_read_unlock();
}
+/* If scaling_ratio drops after we already advertised tp->rcv_wnd, grow
+ * sk_rcvbuf so the remaining live window still maps back to hard memory
+ * units under the old advertise-time basis.
+ */
+static void tcp_try_grow_advertised_window(struct sock *sk,
+ const struct sk_buff *skb)
+{
+ struct tcp_sock *tp = tcp_sk(sk);
+ int needed;
+
+ /* Keep this repair aligned with tcp_rcvbuf_grow(): do not adjust
+ * receive-buffer backing for not-yet-accepted or orphaned sockets.
+ */
+ if (!tcp_rcvbuf_grow_allowed(sk))
+ return;
+
+ if (!tcp_receive_window(tp))
+ return;
+
+ if (!tcp_space_from_rcv_wnd(tp, tcp_receive_window(tp), &needed))
+ return;
+
+ tcp_try_grow_rcvbuf(sk, needed);
+}
+
/* Adapt the MSS value used to make delayed ack decision to the
* real world.
*/
@@ -251,6 +276,7 @@ static void tcp_measure_rcv_mss(struct sock *sk, const struct sk_buff *skb)
if (old_ratio != tcp_sk(sk)->scaling_ratio) {
struct tcp_sock *tp = tcp_sk(sk);
+ tcp_try_grow_advertised_window(sk, skb);
val = tcp_win_from_space(sk, sk->sk_rcvbuf);
tcp_set_window_clamp(sk, val);
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v2 07/14] tcp: honor the maximum advertised window after live retraction
From: atwellwea @ 2026-03-14 20:13 UTC (permalink / raw)
To: netdev, davem, kuba, pabeni, edumazet, ncardwell
Cc: linux-kernel, linux-api, linux-doc, linux-kselftest,
linux-trace-kernel, mptcp, dsahern, horms, kuniyu, andrew+netdev,
willemdebruijn.kernel, jasowang, skhan, corbet, matttbe,
martineau, geliang, rostedt, mhiramat, mathieu.desnoyers,
0x7f454c46
In-Reply-To: <20260314201348.1786972-1-atwellwea@gmail.com>
From: Wesley Atwell <atwellwea@gmail.com>
If receive-side accounting retracts the live rwnd below a larger
sender-visible window that was already advertised, allow one in-order
skb within that historical bound to repair its backing and reach the
normal receive path.
Hard receive-memory admission is still enforced through the existing
prune and collapse path. The rescue only changes how data already
inside sender-visible sequence space is classified and backed.
Signed-off-by: Wesley Atwell <atwellwea@gmail.com>
---
net/ipv4/tcp_input.c | 92 +++++++++++++++++++++++++++++++++++++++++---
1 file changed, 86 insertions(+), 6 deletions(-)
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index d76e4e4c0e57..4b9309c37e99 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -5376,24 +5376,86 @@ static void tcp_ofo_queue(struct sock *sk)
static bool tcp_prune_ofo_queue(struct sock *sk, const struct sk_buff *in_skb);
static int tcp_prune_queue(struct sock *sk, const struct sk_buff *in_skb);
+/* Sequence checks run against the sender-visible receive window before this
+ * point. If later receive-side accounting retracts the live receive window
+ * below the maximum right edge we already advertised, allow one in-order skb
+ * which still fits inside that sender-visible bound to reach the normal
+ * receive queue path.
+ *
+ * Keep receive-memory admission itself on the legacy hard-cap path so prune
+ * and collapse behavior stay aligned with the established retracted-window
+ * handling.
+ */
+static bool tcp_skb_in_retracted_window(const struct tcp_sock *tp,
+ const struct sk_buff *skb)
+{
+ u32 live_end = tp->rcv_nxt + tcp_receive_window(tp);
+ u32 max_end = tp->rcv_nxt + tcp_max_receive_window(tp);
+
+ return after(max_end, live_end) &&
+ after(TCP_SKB_CB(skb)->end_seq, live_end) &&
+ !after(TCP_SKB_CB(skb)->end_seq, max_end);
+}
+
static bool tcp_can_ingest(const struct sock *sk, const struct sk_buff *skb)
{
- unsigned int rmem = atomic_read(&sk->sk_rmem_alloc);
+ return tcp_rmem_used(sk) <= READ_ONCE(sk->sk_rcvbuf);
+}
+
+/* Caller already established that @skb extends into the retracted-but-still-
+ * valid sender-visible window. For in-order progress, regrow sk_rcvbuf before
+ * falling into prune/forced-mem handling.
+ *
+ * This path intentionally repairs backing for one in-order skb that is already
+ * within sender-visible sequence space, rather than treating it like ordinary
+ * receive-buffer autotuning.
+ *
+ * Keep this rescue bounded to the span accepted by this skb instead of the
+ * full historical tp->rcv_mwnd_seq. However, never grow below skb->truesize,
+ * because sk_rmem_schedule() still charges hard memory, not sender-visible
+ * window bytes.
+ */
+static void tcp_try_grow_retracted_skb(struct sock *sk,
+ const struct sk_buff *skb)
+{
+ struct tcp_sock *tp = tcp_sk(sk);
+ int needed = skb->truesize;
+ int span_space;
+ u32 span_win;
+
+ if (TCP_SKB_CB(skb)->seq != tp->rcv_nxt)
+ return;
+
+ span_win = TCP_SKB_CB(skb)->end_seq - tp->rcv_nxt;
+ if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
+ span_win--;
+
+ if (tcp_space_from_rcv_mwnd(tp, span_win, &span_space))
+ needed = max_t(int, needed, span_space);
- return rmem <= sk->sk_rcvbuf;
+ tcp_try_grow_rcvbuf(sk, needed);
}
+/* Sender-visible window rescue does not relax hard receive-memory admission.
+ * If growth did not make room, fall back to the established prune/collapse
+ * path.
+ */
static int tcp_try_rmem_schedule(struct sock *sk, const struct sk_buff *skb,
unsigned int size)
{
- if (!tcp_can_ingest(sk, skb) ||
- !sk_rmem_schedule(sk, skb, size)) {
+ bool can_ingest = tcp_can_ingest(sk, skb);
+ bool scheduled = can_ingest && sk_rmem_schedule(sk, skb, size);
+
+ if (!scheduled) {
+ int pruned = tcp_prune_queue(sk, skb);
- if (tcp_prune_queue(sk, skb) < 0)
+ if (pruned < 0)
return -1;
while (!sk_rmem_schedule(sk, skb, size)) {
- if (!tcp_prune_ofo_queue(sk, skb))
+ bool pruned_ofo = tcp_prune_ofo_queue(sk, skb);
+
+ if (!pruned_ofo)
return -1;
}
}
@@ -5629,6 +5691,7 @@ void tcp_data_ready(struct sock *sk)
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
+ bool retracted;
enum skb_drop_reason reason;
bool fragstolen;
int eaten;
@@ -5647,6 +5710,7 @@ static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
}
tcp_cleanup_skb(skb);
__skb_pull(skb, tcp_hdr(skb)->doff * 4);
+ retracted = skb->len && tcp_skb_in_retracted_window(tp, skb);
reason = SKB_DROP_REASON_NOT_SPECIFIED;
tp->rx_opt.dsack = 0;
@@ -5667,6 +5731,9 @@ static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
(TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN))
goto queue_and_out;
+ if (retracted)
+ goto queue_and_out;
+
reason = SKB_DROP_REASON_TCP_ZEROWINDOW;
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPZEROWINDOWDROP);
goto out_of_window;
@@ -5674,7 +5741,20 @@ static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
/* Ok. In sequence. In window. */
queue_and_out:
+ if (unlikely(retracted))
+ tcp_try_grow_retracted_skb(sk, skb);
+
if (tcp_try_rmem_schedule(sk, skb, skb->truesize)) {
+ /* If the live rwnd collapsed to zero while rescuing an
+ * skb that still fit in sender-visible sequence space,
+ * report zero-window rather than generic proto-mem.
+ */
+ if (unlikely(!tcp_receive_window(tp) && retracted)) {
+ reason = SKB_DROP_REASON_TCP_ZEROWINDOW;
+ NET_INC_STATS(sock_net(sk),
+ LINUX_MIB_TCPZEROWINDOWDROP);
+ goto out_of_window;
+ }
/* TODO: maybe ratelimit these WIN 0 ACK ? */
inet_csk(sk)->icsk_ack.pending |=
(ICSK_ACK_NOMEM | ICSK_ACK_NOW);
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v2 08/14] tcp: extend TCP_REPAIR_WINDOW for live and max-window snapshots
From: atwellwea @ 2026-03-14 20:13 UTC (permalink / raw)
To: netdev, davem, kuba, pabeni, edumazet, ncardwell
Cc: linux-kernel, linux-api, linux-doc, linux-kselftest,
linux-trace-kernel, mptcp, dsahern, horms, kuniyu, andrew+netdev,
willemdebruijn.kernel, jasowang, skhan, corbet, matttbe,
martineau, geliang, rostedt, mhiramat, mathieu.desnoyers,
0x7f454c46
In-Reply-To: <20260314201348.1786972-1-atwellwea@gmail.com>
From: Wesley Atwell <atwellwea@gmail.com>
Extend TCP_REPAIR_WINDOW so repair and restore can round-trip both the
live rwnd snapshot and the remembered maximum sender-visible window.
Keep the ABI append-only by accepting the legacy and v1 prefix lengths on
both get and set, rebuilding any missing max-window state from the live
window when older userspace restores a socket.
Signed-off-by: Wesley Atwell <atwellwea@gmail.com>
---
include/net/tcp.h | 13 +++----
include/uapi/linux/tcp.h | 8 +++++
net/ipv4/tcp.c | 73 ++++++++++++++++++++++++++++++++++++----
3 files changed, 81 insertions(+), 13 deletions(-)
diff --git a/include/net/tcp.h b/include/net/tcp.h
index 5b479ad44f89..12e62fea2aaf 100644
--- a/include/net/tcp.h
+++ b/include/net/tcp.h
@@ -1766,13 +1766,14 @@ static inline bool tcp_space_from_wnd_snapshot(u8 scaling_ratio, int win,
}
/* Rebuild hard receive-memory units for data already covered by tp->rcv_wnd if
- * the advertise-time basis is known.
+ * the advertise-time basis is known. Legacy TCP_REPAIR restores can only
+ * recover tp->rcv_wnd itself; callers must fall back when the snapshot is
+ * unknown.
*/
static inline bool tcp_space_from_rcv_wnd(const struct tcp_sock *tp, int win,
int *space)
{
- return tcp_space_from_wnd_snapshot(tp->rcv_wnd_scaling_ratio, win,
- space);
+ return tcp_space_from_wnd_snapshot(tp->rcv_wnd_scaling_ratio, win, space);
}
/* Same as tcp_space_from_rcv_wnd(), but for the remembered maximum
@@ -1800,9 +1801,9 @@ static inline void tcp_scaling_ratio_init(struct sock *sk)
}
/* tp->rcv_wnd is paired with the scaling_ratio that was in force when that
- * window was last advertised. Callers can leave a zero snapshot when the
- * advertise-time basis is unknown and refresh the pair on the next local
- * window update.
+ * window was last advertised. Legacy TCP_REPAIR restores can only recover the
+ * window value itself and use a zero snapshot until a fresh local window
+ * advertisement refreshes the pair.
*/
static inline void tcp_set_rcv_wnd_snapshot(struct tcp_sock *tp, u32 win,
u8 scaling_ratio)
diff --git a/include/uapi/linux/tcp.h b/include/uapi/linux/tcp.h
index 03772dd4d399..564a77f69130 100644
--- a/include/uapi/linux/tcp.h
+++ b/include/uapi/linux/tcp.h
@@ -152,6 +152,11 @@ struct tcp_repair_opt {
__u32 opt_val;
};
+/* Append-only repair ABI.
+ * Older userspace may stop at rcv_wup or rcv_wnd_scaling_ratio.
+ * The kernel accepts those prefix lengths and rebuilds any missing
+ * receive-window snapshot state on restore.
+ */
struct tcp_repair_window {
__u32 snd_wl1;
__u32 snd_wnd;
@@ -159,6 +164,9 @@ struct tcp_repair_window {
__u32 rcv_wnd;
__u32 rcv_wup;
+ __u32 rcv_wnd_scaling_ratio; /* 0 means live-window basis unknown */
+ __u32 rcv_mwnd_seq;
+ __u32 rcv_mwnd_scaling_ratio; /* 0 means max-window basis unknown */
};
enum {
diff --git a/net/ipv4/tcp.c b/net/ipv4/tcp.c
index 66706dbb90f5..39a1265876ea 100644
--- a/net/ipv4/tcp.c
+++ b/net/ipv4/tcp.c
@@ -3533,17 +3533,31 @@ static inline bool tcp_can_repair_sock(const struct sock *sk)
(sk->sk_state != TCP_LISTEN);
}
+/* Keep accepting the pre-extension TCP_REPAIR_WINDOW layout so legacy
+ * userspace can restore sockets without fabricating a snapshot basis.
+ */
+static inline int tcp_repair_window_legacy_size(void)
+{
+ return offsetof(struct tcp_repair_window, rcv_wnd_scaling_ratio);
+}
+
+static inline int tcp_repair_window_v1_size(void)
+{
+ return offsetof(struct tcp_repair_window, rcv_mwnd_seq);
+}
+
static int tcp_repair_set_window(struct tcp_sock *tp, sockptr_t optbuf, int len)
{
- struct tcp_repair_window opt;
+ struct tcp_repair_window opt = {};
if (!tp->repair)
return -EPERM;
- if (len != sizeof(opt))
+ if (len != tcp_repair_window_legacy_size() &&
+ len != tcp_repair_window_v1_size() && len != sizeof(opt))
return -EINVAL;
- if (copy_from_sockptr(&opt, optbuf, sizeof(opt)))
+ if (copy_from_sockptr(&opt, optbuf, len))
return -EFAULT;
if (opt.max_window < opt.snd_wnd)
@@ -3559,9 +3573,47 @@ static int tcp_repair_set_window(struct tcp_sock *tp, sockptr_t optbuf, int len)
tp->snd_wnd = opt.snd_wnd;
tp->max_window = opt.max_window;
- tp->rcv_wnd = opt.rcv_wnd;
+ if (len == tcp_repair_window_legacy_size()) {
+ /* Legacy repair UAPI has no advertise-time basis for tp->rcv_wnd.
+ * Mark the snapshot unknown until a fresh local advertisement
+ * re-establishes the pair.
+ */
+ tcp_set_rcv_wnd_unknown(tp, opt.rcv_wnd);
+ tp->rcv_wup = opt.rcv_wup;
+ tcp_init_max_rcv_wnd_seq(tp);
+ return 0;
+ }
+
+ if (opt.rcv_wnd_scaling_ratio > U8_MAX)
+ return -EINVAL;
+
+ tcp_set_rcv_wnd_snapshot(tp, opt.rcv_wnd, opt.rcv_wnd_scaling_ratio);
tp->rcv_wup = opt.rcv_wup;
- tp->rcv_mwnd_seq = opt.rcv_wup + opt.rcv_wnd;
+
+ if (len == tcp_repair_window_v1_size()) {
+ /* v1 repair can restore the live-window snapshot, but not a
+ * retracted max-window snapshot. Rebuild it from the live pair
+ * until a fresh local advertisement updates it again.
+ */
+ tcp_init_max_rcv_wnd_seq(tp);
+ return 0;
+ }
+
+ if (opt.rcv_mwnd_scaling_ratio > U8_MAX)
+ return -EINVAL;
+
+ /* Userspace may repair sequence-space values after checkpoint without
+ * also rebasing the remembered max advertised right edge. If the exact
+ * snapshot no longer covers the restored live window, treat it like
+ * v1 and rebuild the max-window side from the live pair.
+ */
+ if (after(opt.rcv_wup + opt.rcv_wnd, opt.rcv_mwnd_seq)) {
+ tcp_init_max_rcv_wnd_seq(tp);
+ return 0;
+ }
+
+ tp->rcv_mwnd_seq = opt.rcv_mwnd_seq;
+ tp->rcv_mwnd_scaling_ratio = opt.rcv_mwnd_scaling_ratio;
return 0;
}
@@ -4650,12 +4702,16 @@ int do_tcp_getsockopt(struct sock *sk, int level,
break;
case TCP_REPAIR_WINDOW: {
- struct tcp_repair_window opt;
+ struct tcp_repair_window opt = {};
if (copy_from_sockptr(&len, optlen, sizeof(int)))
return -EFAULT;
- if (len != sizeof(opt))
+ /* Mirror the accepted set-side prefix lengths so checkpoint
+ * tools can round-trip exactly the layout version they know.
+ */
+ if (len != tcp_repair_window_legacy_size() &&
+ len != tcp_repair_window_v1_size() && len != sizeof(opt))
return -EINVAL;
if (!tp->repair)
@@ -4666,6 +4722,9 @@ int do_tcp_getsockopt(struct sock *sk, int level,
opt.max_window = tp->max_window;
opt.rcv_wnd = tp->rcv_wnd;
opt.rcv_wup = tp->rcv_wup;
+ opt.rcv_wnd_scaling_ratio = tp->rcv_wnd_scaling_ratio;
+ opt.rcv_mwnd_seq = tp->rcv_mwnd_seq;
+ opt.rcv_mwnd_scaling_ratio = tp->rcv_mwnd_scaling_ratio;
if (copy_to_sockptr(optval, &opt, len))
return -EFAULT;
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v2 09/14] mptcp: refresh TCP receive-window snapshots on subflows
From: atwellwea @ 2026-03-14 20:13 UTC (permalink / raw)
To: netdev, davem, kuba, pabeni, edumazet, ncardwell
Cc: linux-kernel, linux-api, linux-doc, linux-kselftest,
linux-trace-kernel, mptcp, dsahern, horms, kuniyu, andrew+netdev,
willemdebruijn.kernel, jasowang, skhan, corbet, matttbe,
martineau, geliang, rostedt, mhiramat, mathieu.desnoyers,
0x7f454c46
In-Reply-To: <20260314201348.1786972-1-atwellwea@gmail.com>
From: Wesley Atwell <atwellwea@gmail.com>
When MPTCP resynchronizes the per-subflow TCP shadow window from the
mptcp-level receive state, refresh the live rwnd snapshot and the
remembered maximum-window snapshot along with it.
That keeps subflow TCP bookkeeping aligned with the sender-visible
window state tracked in the core TCP patches.
Signed-off-by: Wesley Atwell <atwellwea@gmail.com>
---
net/mptcp/options.c | 14 +++++++++-----
net/mptcp/protocol.h | 14 +++++++++++---
2 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/net/mptcp/options.c b/net/mptcp/options.c
index 8a1c5698983c..64cd637484a4 100644
--- a/net/mptcp/options.c
+++ b/net/mptcp/options.c
@@ -1073,9 +1073,12 @@ static void rwin_update(struct mptcp_sock *msk, struct sock *ssk,
return;
/* Some other subflow grew the mptcp-level rwin since rcv_wup,
- * resync.
+ * resync. Keep the TCP shadow window in its advertised u32 domain
+ * and refresh the advertise-time scaling snapshot while doing so.
*/
- tp->rcv_wnd += mptcp_rcv_wnd - subflow->rcv_wnd_sent;
+ tcp_set_rcv_wnd(tp, min_t(u64, (u64)tp->rcv_wnd +
+ (mptcp_rcv_wnd - subflow->rcv_wnd_sent),
+ U32_MAX));
tcp_update_max_rcv_wnd_seq(tp);
subflow->rcv_wnd_sent = mptcp_rcv_wnd;
}
@@ -1335,12 +1338,13 @@ static void mptcp_set_rwin(struct tcp_sock *tp, struct tcphdr *th)
if (rcv_wnd_new != rcv_wnd_old) {
raise_win:
/* The msk-level rcv wnd is after the tcp level one,
- * sync the latter.
+ * sync the latter and refresh its advertise-time scaling
+ * snapshot.
*/
rcv_wnd_new = rcv_wnd_old;
win = rcv_wnd_old - ack_seq;
- new_win = min_t(u64, win, U32_MAX);
- tp->rcv_wnd = new_win;
+ tcp_set_rcv_wnd(tp, min_t(u64, win, U32_MAX));
+ new_win = tp->rcv_wnd;
tcp_update_max_rcv_wnd_seq(tp);
/* Make sure we do not exceed the maximum possible
diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h
index 0bd1ee860316..4ea95c9c0c7a 100644
--- a/net/mptcp/protocol.h
+++ b/net/mptcp/protocol.h
@@ -408,11 +408,19 @@ static inline int mptcp_space_from_win(const struct sock *sk, int win)
return __tcp_space_from_win(mptcp_sk(sk)->scaling_ratio, win);
}
+/* MPTCP exposes window space from the mptcp-level receive queue, so it tracks
+ * a separate backlog counter from the subflow backlog embedded in struct sock.
+ */
+static inline int mptcp_rwnd_avail(const struct sock *sk)
+{
+ return READ_ONCE(sk->sk_rcvbuf) -
+ READ_ONCE(mptcp_sk(sk)->backlog_len) -
+ tcp_rmem_used(sk);
+}
+
static inline int __mptcp_space(const struct sock *sk)
{
- return mptcp_win_from_space(sk, READ_ONCE(sk->sk_rcvbuf) -
- READ_ONCE(mptcp_sk(sk)->backlog_len) -
- sk_rmem_alloc_get(sk));
+ return mptcp_win_from_space(sk, mptcp_rwnd_avail(sk));
}
static inline struct mptcp_data_frag *mptcp_send_head(const struct sock *sk)
--
2.43.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox