mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [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®