* [PATCH] selftests/seccomp: Split fork_and_close into clone and close tests
@ 2026-10-06 9:06 Kees Cook
0 siblings, 0 replies; only message in thread
From: Kees Cook @ 2026-10-06 9:06 UTC (permalink / raw)
To: Cong Wang
Cc: Kees Cook, Andy Lutomirski, Will Drewry, linux-kernel, linux-hardening
fork_and_close checked three things against one listener: that a
restarted clone3() creates exactly one child, that a restarted close()
releases its descriptor, and that a supervisor error reaches the caller
after a restart. A three-pass loop chose what each pass did for each
variant, and a failure in the first check kept the others from running.
Split it into notification_restart.clone and notification_restart.close,
each with one mediated syscall, one signal, and one expected result per
variant. They share helpers that fork the filtered child, receive its
listener, and interrupt its notification. notification_restart_child()
now uses the same fork and report helpers, and the filter flags are
computed in one place. Drop the supervisor error check, which
failed_receive already covers.
Tests run on ARCH=x86_64 defconfig with CONFIG_PROVE_LOCKING=y,
CONFIG_DEBUG_ATOMIC_SLEEP=y, and CONFIG_DEBUG_LIST=y under QEMU, built
with GCC 16.2.0: seccomp_bpf passes 137/137 with 5 skips, and the 28
notification_restart tests pass in 25 of 25 runs at -smp 2 and -smp 4.
On v7.3-rc2 the restart and both variants fail and the others pass.
Assisted-by: LLM
Signed-off-by: Kees Cook <kees@kernel.org>
---
tools/testing/selftests/seccomp/seccomp_bpf.c | 258 ++++++++++--------
1 file changed, 148 insertions(+), 110 deletions(-)
diff --git a/tools/testing/selftests/seccomp/seccomp_bpf.c b/tools/testing/selftests/seccomp/seccomp_bpf.c
index b902d52cc917..730aaa21ee2d 100644
--- a/tools/testing/selftests/seccomp/seccomp_bpf.c
+++ b/tools/testing/selftests/seccomp/seccomp_bpf.c
@@ -4916,20 +4916,27 @@ FIXTURE_VARIANT_ADD(notification_restart, both) {
.restart = true, .killable = true,
};
-FIXTURE_SETUP(notification_restart)
+static unsigned int
+notification_restart_flags(const FIXTURE_VARIANT(notification_restart) *variant)
{
unsigned int flags = SECCOMP_FILTER_FLAG_NEW_LISTENER;
+ if (variant->restart)
+ flags |= SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV;
+ if (variant->killable)
+ flags |= SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV;
+ return flags;
+}
+
+FIXTURE_SETUP(notification_restart)
+{
self->pid = -1;
self->listener = -1;
self->sync[0] = self->sync[1] = -1;
ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, self->sync), 0);
- if (variant->restart)
- flags |= SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV;
- if (variant->killable)
- flags |= SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV;
- self->listener = user_notif_syscall(__NR_getppid, flags);
+ self->listener = user_notif_syscall(__NR_getppid,
+ notification_restart_flags(variant));
ASSERT_GE(self->listener, 0);
}
@@ -4944,29 +4951,50 @@ FIXTURE_TEARDOWN(notification_restart)
close(self->sync[1]);
}
-static void notification_restart_child(struct __test_metadata *_metadata,
- struct _test_data_notification_restart *self)
+/*
+ * Fork the notifying child with SIGUSR1 handled. Returns true in the
+ * child, which sends its result with notification_report().
+ */
+static bool notification_restart_fork(struct __test_metadata *_metadata,
+ struct _test_data_notification_restart *self)
{
struct sigaction action = { .sa_handler = notification_restart_handler };
- long result[2];
self->pid = fork();
ASSERT_GE(self->pid, 0);
if (self->pid)
- return;
+ return false;
- close(self->listener);
close(self->sync[0]);
handled = self->sync[1];
if (sigemptyset(&action.sa_mask) || sigaction(SIGUSR1, &action, NULL))
_exit(1);
- result[0] = syscall(__NR_getppid);
- result[1] = errno;
+ return true;
+}
+
+/* Send the child's syscall result to notification_result(), and exit. */
+static __noreturn void notification_report(long ret, int err)
+{
+ long result[2] = { ret, err };
+
if (write(handled, result, sizeof(result)) != sizeof(result))
_exit(1);
_exit(0);
}
+static void notification_restart_child(struct __test_metadata *_metadata,
+ struct _test_data_notification_restart *self)
+{
+ long ret;
+
+ if (!notification_restart_fork(_metadata, self))
+ return;
+
+ close(self->listener);
+ ret = syscall(__NR_getppid);
+ notification_report(ret, errno);
+}
+
static void notification_pending(struct __test_metadata *_metadata, int fd)
{
struct pollfd pfd = { .fd = fd, .events = POLLIN };
@@ -5084,28 +5112,17 @@ TEST_F(notification_restart, after_receive)
notification_result(_metadata, self, USER_NOTIF_MAGIC, 0);
}
-TEST_F(notification_restart, fork_and_close)
+/*
+ * Fork a child that installs its own filter notifying on @nr, so that the
+ * test process is not mediated, and receive the child's listener. Returns
+ * true in the child.
+ */
+static bool
+notification_restart_filtered(struct __test_metadata *_metadata,
+ struct _test_data_notification_restart *self,
+ const FIXTURE_VARIANT(notification_restart) *variant,
+ int nr)
{
- struct sigaction action = { .sa_handler = notification_restart_handler };
- struct sock_filter filter[] = {
- BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
-#ifdef __NR_fork
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fork, 0, 1),
- BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),
-#endif
-#ifdef __NR_clone
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clone, 0, 1),
- BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),
-#endif
-#ifdef __NR_clone3
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clone3, 0, 1),
- BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),
-#endif
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_close, 0, 1),
- BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),
- BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
- };
- struct sock_fprog prog = { .len = ARRAY_SIZE(filter), .filter = filter };
char control[CMSG_SPACE(sizeof(int))] = {};
char c = 'f';
struct iovec iov = { .iov_base = &c, .iov_len = 1 };
@@ -5114,73 +5131,25 @@ TEST_F(notification_restart, fork_and_close)
.msg_control = control, .msg_controllen = sizeof(control),
};
struct cmsghdr *cmsg;
- /*
- * glibc's fork() blocks all signals across clone(), so the
- * notification wait could not be interrupted: call clone3()
- * directly instead.
- */
- struct __clone_args args = { .exit_signal = SIGCHLD };
- unsigned int flags = SECCOMP_FILTER_FLAG_NEW_LISTENER;
- int i, fd, listener, status;
- long result[2] = {};
- pid_t child;
-
- if (__NR_clone3 < 0)
- SKIP(return, "Test not built with clone3 support");
- /* Some container profiles reject clone3() with ENOSYS. */
- if (sys_clone3(NULL, 0) == -1 && errno == ENOSYS)
- SKIP(return, "clone3() is not available");
+ int listener;
- if (variant->restart)
- flags |= SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV;
- if (variant->killable)
- flags |= SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV;
+ /* A filter chain may hold only one listener. */
ASSERT_EQ(close(self->listener), 0);
self->listener = -1;
- self->pid = fork();
- ASSERT_GE(self->pid, 0);
- if (!self->pid) {
- close(self->sync[0]);
- handled = self->sync[1];
- ASSERT_EQ(sigemptyset(&action.sa_mask), 0);
- ASSERT_EQ(sigaction(SIGUSR1, &action, NULL), 0);
- fd = open("/dev/null", O_RDONLY);
- ASSERT_GE(fd, 0);
- listener = seccomp(SECCOMP_SET_MODE_FILTER, flags, &prog);
- ASSERT_GE(listener, 0);
+ if (notification_restart_fork(_metadata, self)) {
+ listener = user_notif_syscall(nr, notification_restart_flags(variant));
+ if (listener < 0)
+ _exit(1);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(listener));
memcpy(CMSG_DATA(cmsg), &listener, sizeof(listener));
- ASSERT_EQ(sendmsg(handled, &msg, 0), 1);
-
- child = sys_clone3(&args, sizeof(args));
- if (!child)
- _exit(0);
- if (variant->restart) {
- ASSERT_GT(child, 0);
- ASSERT_EQ(waitpid(child, &status, 0), child);
- ASSERT_TRUE(WIFEXITED(status));
- ASSERT_EQ(WEXITSTATUS(status), 0);
- ASSERT_EQ(waitpid(-1, &status, WNOHANG), -1);
- ASSERT_EQ(errno, ECHILD);
- ASSERT_EQ(close(fd), 0);
- ASSERT_EQ(fcntl(fd, F_GETFD), -1);
- ASSERT_EQ(errno, EBADF);
- } else {
- ASSERT_EQ(child, -1);
- ASSERT_EQ(errno, EINTR);
- ASSERT_EQ(close(fd), -1);
- ASSERT_EQ(errno, EINTR);
- ASSERT_GE(fcntl(fd, F_GETFD), 0);
- }
-
- ASSERT_EQ(sys_clone3(&args, sizeof(args)), -1);
- ASSERT_EQ(errno, EAGAIN);
- ASSERT_EQ(write(handled, result, sizeof(result)), sizeof(result));
- _exit(0);
+ if (sendmsg(handled, &msg, 0) != 1)
+ _exit(1);
+ return true;
}
+
ASSERT_EQ(recvmsg(self->sync[0], &msg, 0), 1);
ASSERT_FALSE(msg.msg_flags & MSG_CTRUNC);
cmsg = CMSG_FIRSTHDR(&msg);
@@ -5189,30 +5158,99 @@ TEST_F(notification_restart, fork_and_close)
ASSERT_EQ(cmsg->cmsg_type, SCM_RIGHTS);
ASSERT_EQ(cmsg->cmsg_len, CMSG_LEN(sizeof(listener)));
memcpy(&self->listener, CMSG_DATA(cmsg), sizeof(self->listener));
+ return false;
+}
- for (i = 0; i < 3; i++) {
- struct seccomp_notif req = {};
- struct seccomp_notif_resp resp = {};
+/*
+ * Interrupt the child's notification before it is received. Without the
+ * restart flag the syscall fails with EINTR. With it, the restarted
+ * syscall is notified again, allowed to continue, and succeeds.
+ */
+static void
+notification_restart_interrupt(struct __test_metadata *_metadata,
+ struct _test_data_notification_restart *self,
+ const FIXTURE_VARIANT(notification_restart) *variant)
+{
+ struct seccomp_notif req = {};
+ struct seccomp_notif_resp resp = {};
- notification_pending(_metadata, self->listener);
- if (i < 2 || variant->restart) {
- notification_signal(_metadata, self);
- if (!variant->restart)
- continue;
- notification_pending(_metadata, self->listener);
- }
- ASSERT_EQ(ioctl(self->listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
- EXPECT_EQ(req.pid, self->pid);
- resp.id = req.id;
- if (i == 2)
- resp.error = -EAGAIN;
- else
- resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
- ASSERT_EQ(ioctl(self->listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+ notification_pending(_metadata, self->listener);
+ notification_signal(_metadata, self);
+ if (!variant->restart) {
+ notification_result(_metadata, self, -1, EINTR);
+ return;
}
+ notification_pending(_metadata, self->listener);
+ ASSERT_EQ(ioctl(self->listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+ EXPECT_EQ(req.pid, self->pid);
+ resp.id = req.id;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
+ ASSERT_EQ(ioctl(self->listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
notification_result(_metadata, self, 0, 0);
}
+/* A successful clone3() must have created exactly one child. */
+static __noreturn void notification_restart_clone_child(void)
+{
+ /*
+ * glibc's fork() blocks all signals across clone(), so the
+ * notification wait could not be interrupted: call clone3()
+ * directly instead.
+ */
+ struct __clone_args args = { .exit_signal = SIGCHLD };
+ int status;
+ long ret;
+
+ ret = sys_clone3(&args, sizeof(args));
+ if (ret == 0)
+ _exit(0);
+ if (ret > 0) {
+ if (waitpid(ret, &status, 0) != ret || !WIFEXITED(status) ||
+ WEXITSTATUS(status) != 0)
+ _exit(2);
+ if (waitpid(-1, NULL, WNOHANG) != -1 || errno != ECHILD)
+ _exit(3);
+ ret = 0;
+ }
+ notification_report(ret, errno);
+}
+
+/* close() must release the descriptor exactly when it succeeds. */
+static __noreturn void notification_restart_close_child(void)
+{
+ int fd, err;
+ long ret;
+
+ fd = open("/dev/null", O_RDONLY);
+ if (fd < 0)
+ _exit(1);
+ ret = close(fd);
+ err = errno;
+ if ((fcntl(fd, F_GETFD) == -1) != (ret == 0))
+ _exit(2);
+ notification_report(ret, err);
+}
+
+TEST_F(notification_restart, clone)
+{
+ if (__NR_clone3 < 0)
+ SKIP(return, "Test not built with clone3 support");
+ /* Some container profiles reject clone3() with ENOSYS. */
+ if (sys_clone3(NULL, 0) == -1 && errno == ENOSYS)
+ SKIP(return, "clone3() is not available");
+
+ if (notification_restart_filtered(_metadata, self, variant, __NR_clone3))
+ notification_restart_clone_child();
+ notification_restart_interrupt(_metadata, self, variant);
+}
+
+TEST_F(notification_restart, close)
+{
+ if (notification_restart_filtered(_metadata, self, variant, __NR_close))
+ notification_restart_close_child();
+ notification_restart_interrupt(_metadata, self, variant);
+}
+
TEST_F(notification_restart, fatal_signal)
{
int status;
--
2.55.0
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-10-06 9:06 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-06 9:06 [PATCH] selftests/seccomp: Split fork_and_close into clone and close tests Kees Cook
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®