[PATCH v2 9/9] kernel/api: add runtime verification selftest

Sasha Levin
2026-03-22T12:10:59Z

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 | 33 +
tools/testing/selftests/kapi/test_kapi.c | 1023 +++++++++++++++++
4 files changed, 1064 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 db2ba9a596eef..430a23994b314 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13814,6 +13814,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..e097c370542ad
--- /dev/null
+++ b/tools/testing/selftests/kapi/kapi_test_util.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * 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..3d9a3a05186d2
--- /dev/null
+++ b/tools/testing/selftests/kapi/test_kapi.c
@@ -0,0 +1,1023 @@
+// 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 <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