From: Sasha Levin <sashal@kernel.org>
To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org
Cc: Sasha Levin <sashal@kernel.org>,
linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org,
linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org,
workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org,
Thomas Gleixner <tglx@kernel.org>,
"Paul E . McKenney" <paulmck@kernel.org>,
Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
Jonathan Corbet <corbet@lwn.net>,
Dmitry Vyukov <dvyukov@google.com>,
Randy Dunlap <rdunlap@infradead.org>,
Cyril Hrubis <chrubis@suse.cz>, Kees Cook <kees@kernel.org>,
Jake Edge <jake@lwn.net>,
David Laight <david.laight.linux@gmail.com>,
Gabriele Paoloni <gpaoloni@redhat.com>,
Mauro Carvalho Chehab <mchehab@kernel.org>,
Christian Brauner <brauner@kernel.org>,
Alexander Viro <viro@zeniv.linux.org.uk>,
Andrew Morton <akpm@linux-foundation.org>,
Masahiro Yamada <masahiroy@kernel.org>,
Shuah Khan <skhan@linuxfoundation.org>,
Arnd Bergmann <arnd@arndb.de>,
Nathan Chancellor <nathan@kernel.org>,
Steven Rostedt <rostedt@goodmis.org>,
Masami Hiramatsu <mhiramat@kernel.org>,
Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Subject: [PATCH v5 09/11] kernel/api: add runtime verification selftest
Date: Thu, 8 Oct 2026 04:49:49 -0400 [thread overview]
Message-ID: <20261008084956.2911790-10-sashal@kernel.org> (raw)
In-Reply-To: <20261008084956.2911790-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-8: Unknown open flag bits and a NULL path are rejected with EINVAL,
while mode bits above 07777 are accepted (the kernel masks them)
9-20: Boundary conditions and error paths (bad and closed fds, zero
counts, over-long paths, unmapped buffers, wrong access mode)
21-27: Pipe, file descriptor lifecycle and other error paths
28-30: A normal open/read/write/close sequence, and read()/write() with
a NULL buffer and a zero count returning 0
31: dmesg contains the expected KAPI warning strings
Tests that depend on CONFIG_KAPI_RUNTIME_CHECKS are skipped when an open()
with an unknown flag bit succeeds, which means the checks are not active.
Assisted-by: LLM
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
MAINTAINERS | 1 +
tools/testing/selftests/Makefile | 1 +
tools/testing/selftests/kapi/.gitignore | 2 +
tools/testing/selftests/kapi/Makefile | 7 +
tools/testing/selftests/kapi/kapi_test_util.h | 31 +
tools/testing/selftests/kapi/test_kapi.c | 1163 +++++++++++++++++
6 files changed, 1205 insertions(+)
create mode 100644 tools/testing/selftests/kapi/.gitignore
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 cd15c6a776ae3..1d9e55b6bf5ff 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14128,6 +14128,7 @@ F: include/linux/kernel_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/Makefile b/tools/testing/selftests/Makefile
index 273853937c254..d5f973eeefc83 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -60,6 +60,7 @@ TARGETS += intel_pstate
TARGETS += iommu
TARGETS += ipc
TARGETS += ir
+TARGETS += kapi
TARGETS += kcmp
TARGETS += kexec
TARGETS += kselftest_harness
diff --git a/tools/testing/selftests/kapi/.gitignore b/tools/testing/selftests/kapi/.gitignore
new file mode 100644
index 0000000000000..e60893d183d81
--- /dev/null
+++ b/tools/testing/selftests/kapi/.gitignore
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+test_kapi
diff --git a/tools/testing/selftests/kapi/Makefile b/tools/testing/selftests/kapi/Makefile
new file mode 100644
index 0000000000000..d1bb26541c0b8
--- /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 -Wextra -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..a09871207bfc6
--- /dev/null
+++ b/tools/testing/selftests/kapi/kapi_test_util.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Compatibility helpers for KAPI selftests.
+ *
+ * __NR_open does not exist on architectures using the generic syscall table
+ * (e.g. arm64, riscv); only __NR_openat does. There the wrapper falls back to
+ * openat(AT_FDCWD, ...), which does not go through sys_open, so the sys_open
+ * KAPI checks are not exercised.
+ */
+#ifndef KAPI_TEST_UTIL_H
+#define KAPI_TEST_UTIL_H
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/syscall.h>
+
+#ifndef __NR_open
+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..deccb075ee1f9
--- /dev/null
+++ b/tools/testing/selftests/kapi/test_kapi.c
@@ -0,0 +1,1163 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * 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 <stdbool.h>
+#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 "../kselftest.h"
+#include "kapi_test_util.h"
+
+#define NUM_TESTS 31
+
+/* Set from the SIGPIPE handler. */
+static volatile sig_atomic_t got_sigpipe;
+
+static void tap_ok(const char *desc)
+{
+ ksft_test_result_pass("%s\n", desc);
+}
+
+static void tap_fail(const char *desc, const char *reason)
+{
+ ksft_test_result_fail("%s: %s\n", desc, reason);
+}
+
+static void tap_skip(const char *desc, const char *reason)
+{
+ ksft_test_result_skip("%s: %s\n", desc, reason);
+}
+
+/*
+ * Return true when CONFIG_KAPI_RUNTIME_CHECKS is active: it rejects unknown
+ * open flag bits with EINVAL, while the kernel otherwise ignores them.
+ */
+static bool kapi_runtime_checks_active(void)
+{
+ long ret;
+
+ errno = 0;
+ ret = kapi_sys_open("/dev/null", 0x10000000, 0);
+ if (ret >= 0) {
+ syscall(__NR_close, (int)ret);
+ return false;
+ }
+
+ return errno == EINVAL;
+}
+
+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)
+{
+ long ret;
+
+ if (!kapi_runtime_checks_active()) {
+ tap_skip("open with invalid flags",
+ "CONFIG_KAPI_RUNTIME_CHECKS not enabled");
+ return;
+ }
+
+ errno = 0;
+ /*
+ * Use /dev/null (always present on any sane rootfs) so KAPI's flag
+ * validation is reached before a path-lookup ENOENT can mask it.
+ * 0x10000000 is outside the valid O_* mask.
+ */
+ ret = kapi_sys_open("/dev/null", 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 mode bits above S_IALLUGO
+ * The kernel masks mode with S_IALLUGO (07777), so KAPI must accept it.
+ */
+static void test_open_high_mode_bits(void)
+{
+ const char *desc = "open with mode bits above 07777 creates file";
+ char path[64];
+ struct stat st;
+ mode_t um, want;
+ long fd;
+
+ snprintf(path, sizeof(path), "/tmp/kapi_test_mode.%d", getpid());
+ unlink(path);
+
+ um = umask(0);
+ umask(um);
+ want = 07777 & ~um;
+
+ errno = 0;
+ fd = kapi_sys_open(path, O_CREAT | O_WRONLY | O_EXCL, 0xFFFF);
+ if (fd < 0) {
+ tap_fail(desc, strerror(errno));
+ return;
+ }
+
+ if (fstat((int)fd, &st) < 0) {
+ tap_fail(desc, strerror(errno));
+ } else if ((st.st_mode & 07777) != want) {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected mode %04o, got %04o",
+ (unsigned int)want, (unsigned int)(st.st_mode & 07777));
+ tap_fail(desc, msg);
+ } else {
+ tap_ok(desc);
+ }
+
+ syscall(__NR_close, (int)fd);
+ unlink(path);
+}
+
+/*
+ * 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)
+{
+ long ret;
+
+ if (!kapi_runtime_checks_active()) {
+ tap_skip("open with flag bit 30 (0x40000000) returns EINVAL",
+ "CONFIG_KAPI_RUNTIME_CHECKS not enabled");
+ return;
+ }
+
+ errno = 0;
+ 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 */
+ if (write(pipefd[1], "hello", 5) != 5) {
+ tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+ "pipe write failed");
+ close(pipefd[0]);
+ close(pipefd[1]);
+ return;
+ }
+
+ 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),
+ (int)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 non-directory 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 non-directory returns ENOTDIR");
+ } else if (ret >= 0) {
+ tap_fail("open O_DIRECTORY on non-directory",
+ "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 non-directory", 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: read with a NULL buffer and count=0 should return 0
+ * No access is performed, so KAPI must not reject the NULL pointer.
+ */
+static void test_read_null_zero_count(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+ if (fd < 0) {
+ tap_fail("read with NULL buffer and count=0 returns 0",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_read, (int)fd, NULL, 0);
+
+ if (ret == 0) {
+ tap_ok("read with NULL buffer and 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 NULL buffer and count=0 returns 0", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 30: write with a NULL buffer and count=0 should return 0
+ * No access is performed, so KAPI must not reject the NULL pointer.
+ */
+static void test_write_null_zero_count(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (fd < 0) {
+ tap_fail("write with NULL buffer and count=0 returns 0",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)fd, NULL, 0);
+
+ if (ret == 0) {
+ tap_ok("write with NULL buffer and 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 NULL buffer and count=0 returns 0", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 31: verify dmesg contains KAPI warnings for the invalid tests
+ */
+static void test_dmesg_warnings(void)
+{
+ int kmsg_fd;
+
+ if (!kapi_runtime_checks_active()) {
+ tap_skip("dmesg contains expected KAPI warnings",
+ "CONFIG_KAPI_RUNTIME_CHECKS not enabled");
+ return;
+ }
+
+ kmsg_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+ if (kmsg_fd < 0) {
+ tap_skip("dmesg contains expected KAPI warnings",
+ "cannot open /dev/kmsg");
+ return;
+ }
+
+ char line[4096];
+ int found_invalid_bits = 0;
+ int found_null = 0;
+ ssize_t n;
+
+ for (;;) {
+ n = read(kmsg_fd, line, sizeof(line) - 1);
+ if (n > 0) {
+ line[n] = '\0';
+ if (strstr(line, "contains invalid bits"))
+ found_invalid_bits++;
+ if (strstr(line, "NULL") && strstr(line, "not allowed"))
+ found_null++;
+ } else if (n == -1 && errno == EPIPE) {
+ /* Ring buffer wrapped, continue reading */
+ continue;
+ } else {
+ /* EAGAIN (no more messages) or other error */
+ break;
+ }
+ }
+
+ close(kmsg_fd);
+
+ if (found_invalid_bits >= 2 && found_null >= 1) {
+ tap_ok("dmesg contains expected KAPI warnings");
+ } else {
+ char msg[128];
+
+ snprintf(msg, sizeof(msg),
+ "expected invalid_bits>=2 null>=1, got invalid_bits=%d null=%d",
+ found_invalid_bits, found_null);
+ tap_fail("dmesg contains expected KAPI warnings", msg);
+ }
+}
+
+int main(void)
+{
+ ksft_print_header();
+ ksft_set_plan(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 validation (5-8) */
+ test_open_invalid_flags();
+ test_open_high_mode_bits();
+ 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-31) */
+ test_normal_path();
+ test_read_null_zero_count();
+ test_write_null_zero_count();
+ test_dmesg_warnings();
+
+ ksft_finished();
+ return 0;
+}
--
2.53.0
next prev parent reply other threads:[~2026-10-08 8:51 UTC|newest]
Thread overview: 21+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-08 8:49 [PATCH v5 00/11] Kernel API Specification Framework Sasha Levin
2026-10-08 8:49 ` [PATCH v5 01/11] kernel/api: introduce kernel API specification framework Sasha Levin
2026-10-08 8:49 ` [PATCH v5 02/11] kernel/api: enable kerneldoc-based API specifications Sasha Levin
2026-10-08 8:49 ` [PATCH v5 03/11] kernel/api: add debugfs interface for kernel " Sasha Levin
2026-10-08 8:49 ` [PATCH v5 04/11] tools/kapi: add kernel API specification extraction tool Sasha Levin
2026-10-08 8:49 ` [PATCH v5 05/11] kernel/api: add API specification for sys_open Sasha Levin
2026-10-08 12:49 ` Serge E. Hallyn
2026-10-08 13:13 ` Gregory Price
2026-10-08 14:20 ` Serge E. Hallyn
2026-10-08 14:37 ` Sasha Levin
2026-10-08 14:46 ` Serge E. Hallyn
2026-10-08 15:23 ` Sasha Levin
2026-10-08 16:12 ` David Laight
2026-10-08 16:16 ` Serge E. Hallyn
2026-10-08 22:47 ` Sasha Levin
2026-10-08 8:49 ` [PATCH v5 06/11] kernel/api: add API specification for sys_close Sasha Levin
2026-10-08 8:49 ` [PATCH v5 07/11] kernel/api: add API specification for sys_read Sasha Levin
2026-10-08 8:49 ` [PATCH v5 08/11] kernel/api: add API specification for sys_write Sasha Levin
2026-10-08 8:49 ` Sasha Levin [this message]
2026-10-08 8:49 ` [PATCH v5 10/11] kernel/api: add API specification for sys_madvise Sasha Levin
2026-10-08 8:49 ` [PATCH v5 11/11] kernel/api: add syscall enter/exit tracepoints Sasha Levin
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261008084956.2911790-10-sashal@kernel.org \
--to=sashal@kernel.org \
--cc=akpm@linux-foundation.org \
--cc=arnd@arndb.de \
--cc=brauner@kernel.org \
--cc=chrubis@suse.cz \
--cc=corbet@lwn.net \
--cc=david.laight.linux@gmail.com \
--cc=dvyukov@google.com \
--cc=gpaoloni@redhat.com \
--cc=gregkh@linuxfoundation.org \
--cc=jake@lwn.net \
--cc=kees@kernel.org \
--cc=linux-api@vger.kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kbuild@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=masahiroy@kernel.org \
--cc=mathieu.desnoyers@efficios.com \
--cc=mchehab@kernel.org \
--cc=mhiramat@kernel.org \
--cc=nathan@kernel.org \
--cc=paulmck@kernel.org \
--cc=rdunlap@infradead.org \
--cc=rostedt@goodmis.org \
--cc=skhan@linuxfoundation.org \
--cc=tglx@kernel.org \
--cc=tools@kernel.org \
--cc=viro@zeniv.linux.org.uk \
--cc=workflows@vger.kernel.org \
--cc=x86@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.