mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/3] seccomp: opt in to restarting notifications before receipt
@ 2026-09-24 20:42 Cong Wang
  2026-09-24 20:42 ` [PATCH 1/3] seccomp: allow restarting interrupted unreceived notifications Cong Wang
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Cong Wang @ 2026-09-24 20:42 UTC (permalink / raw)
  To: Kees Cook
  Cc: linux-kernel, Will Drewry, Christian Brauner, Andy Lutomirski,
	Jonathan Corbet, Shuah Khan, linux-doc, linux-kselftest

From: Cong Wang <cwang@multikernel.io>

Sandlock is an unprivileged Linux process sandbox that uses Landlock and
seccomp to confine untrusted programs.

While using seccomp user notifications to enforce process limits,
sandlock encountered intermittent shell pipeline failures: SIGCHLD can
interrupt a pending fork notification and make fork return EINTR before
it executes [1]. The same pre-execution interruption can leave an
intercepted close returning EINTR with its descriptor still open.

Receiving notifications eagerly only narrows the race, and
WAIT_KILLABLE_RECV protects the task only after receipt. This series
adds SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV so applications can opt in
to restarting these unexecuted syscalls after the signal handler
returns. Existing signal-driven cancellation remains unchanged unless
the flag is enabled. The series includes regression tests and
documentation.

[1] https://github.com/multikernel/sandlock/issues/235

Cong Wang (3):
  seccomp: allow restarting interrupted unreceived notifications
  selftests/seccomp: cover restart of unreceived notifications
  docs/seccomp: describe the SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV
    flag

 .../userspace-api/seccomp_filter.rst          |  29 ++
 include/linux/seccomp.h                       |   3 +-
 include/uapi/linux/seccomp.h                  |   1 +
 kernel/seccomp.c                              |  16 +-
 tools/include/uapi/linux/seccomp.h            |   1 +
 tools/testing/selftests/seccomp/seccomp_bpf.c | 354 ++++++++++++++++++
 6 files changed, 399 insertions(+), 5 deletions(-)


base-commit: f2c53ea949c5048f96b3dbb5a5ee7131ce4ff2de
-- 
2.43.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH 1/3] seccomp: allow restarting interrupted unreceived notifications
  2026-09-24 20:42 [PATCH 0/3] seccomp: opt in to restarting notifications before receipt Cong Wang
@ 2026-09-24 20:42 ` Cong Wang
  2026-09-24 20:42 ` [PATCH 2/3] selftests/seccomp: cover restart of " Cong Wang
  2026-09-24 20:42 ` [PATCH 3/3] docs/seccomp: describe the SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV flag Cong Wang
  2 siblings, 0 replies; 4+ messages in thread
From: Cong Wang @ 2026-09-24 20:42 UTC (permalink / raw)
  To: Kees Cook
  Cc: linux-kernel, Will Drewry, Christian Brauner, Andy Lutomirski,
	Jonathan Corbet, Shuah Khan, linux-doc, linux-kselftest

From: Cong Wang <cwang@multikernel.io>

An interrupted user notification wait returns ERESTARTSYS before the
syscall body has run. If the notifying task's handler for the delivered
signal was installed without SA_RESTART, that task sees EINTR even for
calls whose callers do not expect it. This can make fork fail
unexpectedly or make close report EINTR while leaving the descriptor
open. A leaked pipe write end can prevent readers from seeing EOF.

The supervisor cannot repair this result once the interrupted task
removes the notification. If removal happens before receipt, the
supervisor never receives that request; a reply using its ID would fail
with ENOENT. Receiving notifications eagerly only narrows the scheduling
window. WAIT_KILLABLE_RECV protects supervisor processing after receipt,
but deliberately leaves the pre-receive wait interruptible.

A sandbox also cannot transparently fix this in the target. It cannot
require arbitrary workloads to retry calls such as fork and close.
Retrying close on EINTR is unsafe when the native syscall has already
released the descriptor. Forcing SA_RESTART on application handlers
would change cancellation behavior for other blocking calls.

Sandlock encounters this while mediating process creation to enforce
process limits. For example, dash installs its SIGCHLD handler without
SA_RESTART. While dash is creating a pipeline, an earlier child can exit
and generate SIGCHLD while dash waits for a fork notification to be
received. The interrupted wait then makes fork return EINTR, causing
dash to report "Cannot fork". Removing fork from notification mediation
would bypass the process-limit enforcement that sandlock needs.

Add SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV, requiring NEW_LISTENER.
Under notify_lock, convert an interrupted wait's ERESTARTSYS to
ERESTARTNOINTR only while the notification remains INIT. The notifying
task's signal handler still runs; if it returns normally, syscall entry
and the filter are evaluated again.

Keep this opt-in because commit c2aa2dfef243 ("seccomp: Add
wait_killable semantic to seccomp user notifier") deliberately preserved
pre-receipt interruption so workloads could abandon requests before the
supervisor starts processing them. The flag can be combined with
WAIT_KILLABLE_RECV to defer non-fatal signals after receipt. Neither
supervisor-supplied errors nor the native syscall's restart behavior is
changed.

Assisted-by: Codex:gpt-6
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 include/linux/seccomp.h            |  3 ++-
 include/uapi/linux/seccomp.h       |  1 +
 kernel/seccomp.c                   | 16 ++++++++++++----
 tools/include/uapi/linux/seccomp.h |  1 +
 4 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/include/linux/seccomp.h b/include/linux/seccomp.h
index fcb3eb9825e5..3b6cc376fff5 100644
--- a/include/linux/seccomp.h
+++ b/include/linux/seccomp.h
@@ -10,7 +10,8 @@
 					 SECCOMP_FILTER_FLAG_SPEC_ALLOW | \
 					 SECCOMP_FILTER_FLAG_NEW_LISTENER | \
 					 SECCOMP_FILTER_FLAG_TSYNC_ESRCH | \
-					 SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV)
+					 SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV | \
+					 SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV)
 
 /* sizeof() the first published struct seccomp_notif_addfd */
 #define SECCOMP_NOTIFY_ADDFD_SIZE_VER0 24
diff --git a/include/uapi/linux/seccomp.h b/include/uapi/linux/seccomp.h
index dbfc9b37fcae..30b76aa48355 100644
--- a/include/uapi/linux/seccomp.h
+++ b/include/uapi/linux/seccomp.h
@@ -25,6 +25,7 @@
 #define SECCOMP_FILTER_FLAG_TSYNC_ESRCH		(1UL << 4)
 /* Received notifications wait in killable state (only respond to fatal signals) */
 #define SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV	(1UL << 5)
+#define SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV	(1UL << 6)
 
 /*
  * All BPF programs must return a 32-bit value.
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 86cf4460d69e..0d83cd848036 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -205,6 +205,7 @@ static inline void seccomp_cache_prepare(struct seccomp_filter *sfilter)
  * @log: true if all actions except for SECCOMP_RET_ALLOW should be logged
  * @wait_killable_recv: Put notifying process in killable state once the
  *			notification is received by the userspace listener.
+ * @restart_before_recv: Restart interrupted syscalls before notification receipt.
  * @prev: points to a previously installed, or inherited, filter
  * @prog: the BPF program to evaluate
  * @notif: the struct that holds all notification related information
@@ -226,6 +227,7 @@ struct seccomp_filter {
 	refcount_t users;
 	bool log;
 	bool wait_killable_recv;
+	bool restart_before_recv;
 	struct action_cache cache;
 	struct seccomp_filter *prev;
 	struct bpf_prog *prog;
@@ -953,6 +955,8 @@ static long seccomp_attach_filter(unsigned int flags,
 	/* Set wait killable flag, if present. */
 	if (flags & SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV)
 		filter->wait_killable_recv = true;
+	if (flags & SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV)
+		filter->restart_before_recv = true;
 
 	/*
 	 * If there is an existing filter, make it the prev and don't drop its
@@ -1208,8 +1212,12 @@ static int seccomp_do_user_notification(int this_syscall,
 			 * Check to see whether we should switch to wait
 			 * killable. Only return the interrupted error if not.
 			 */
-			if (!(!wait_killable && should_sleep_killable(match, &n)))
+			if (!(!wait_killable && should_sleep_killable(match, &n))) {
+				if (err == -ERESTARTSYS && match->restart_before_recv &&
+				    n.state == SECCOMP_NOTIFY_INIT)
+					err = -ERESTARTNOINTR;
 				goto interrupted;
+			}
 		}
 
 		addfd = list_first_entry_or_null(&n.addfd,
@@ -1977,10 +1985,10 @@ static long seccomp_set_mode_filter(unsigned int flags,
 		return -EINVAL;
 
 	/*
-	 * The SECCOMP_FILTER_FLAG_WAIT_KILLABLE_SENT flag doesn't make sense
-	 * without the SECCOMP_FILTER_FLAG_NEW_LISTENER flag.
+	 * Notification wait flags require a userspace listener.
 	 */
-	if ((flags & SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV) &&
+	if ((flags & (SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV |
+		      SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV)) &&
 	    ((flags & SECCOMP_FILTER_FLAG_NEW_LISTENER) == 0))
 		return -EINVAL;
 
diff --git a/tools/include/uapi/linux/seccomp.h b/tools/include/uapi/linux/seccomp.h
index dbfc9b37fcae..30b76aa48355 100644
--- a/tools/include/uapi/linux/seccomp.h
+++ b/tools/include/uapi/linux/seccomp.h
@@ -25,6 +25,7 @@
 #define SECCOMP_FILTER_FLAG_TSYNC_ESRCH		(1UL << 4)
 /* Received notifications wait in killable state (only respond to fatal signals) */
 #define SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV	(1UL << 5)
+#define SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV	(1UL << 6)
 
 /*
  * All BPF programs must return a 32-bit value.
-- 
2.43.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH 2/3] selftests/seccomp: cover restart of unreceived notifications
  2026-09-24 20:42 [PATCH 0/3] seccomp: opt in to restarting notifications before receipt Cong Wang
  2026-09-24 20:42 ` [PATCH 1/3] seccomp: allow restarting interrupted unreceived notifications Cong Wang
@ 2026-09-24 20:42 ` Cong Wang
  2026-09-24 20:42 ` [PATCH 3/3] docs/seccomp: describe the SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV flag Cong Wang
  2 siblings, 0 replies; 4+ messages in thread
From: Cong Wang @ 2026-09-24 20:42 UTC (permalink / raw)
  To: Kees Cook
  Cc: linux-kernel, Will Drewry, Christian Brauner, Andy Lutomirski,
	Jonathan Corbet, Shuah Khan, linux-doc, linux-kselftest

From: Cong Wang <cwang@multikernel.io>

Exercise restart and killable-wait flag combinations before receipt,
after a failed receive, and after receipt. Cover repeated interruptions,
listener closure, fatal signals, flag validation and supervisor errors.

Verify that restarted fork creates exactly one child, close releases its
descriptor, and denied fork returns EAGAIN. Preserve compatibility
checks with restart disabled. Synchronize pre-receipt signals through a
socket and check post-receipt response preservation without task-state
polling.

All 136 seccomp selftests passed before the polling cleanup. After
that change, all four revised post-receipt variants and the existing
wait-killable test passed.

Assisted-by: Codex:gpt-6
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 tools/testing/selftests/seccomp/seccomp_bpf.c | 354 ++++++++++++++++++
 1 file changed, 354 insertions(+)

diff --git a/tools/testing/selftests/seccomp/seccomp_bpf.c b/tools/testing/selftests/seccomp/seccomp_bpf.c
index 0622bc2acad4..6635340c0428 100644
--- a/tools/testing/selftests/seccomp/seccomp_bpf.c
+++ b/tools/testing/selftests/seccomp/seccomp_bpf.c
@@ -307,6 +307,10 @@ struct seccomp_notif_addfd_big {
 #define SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV (1UL << 5)
 #endif
 
+#ifndef SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV
+#define SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV (1UL << 6)
+#endif
+
 #ifndef seccomp
 int seccomp(unsigned int op, unsigned int flags, void *args)
 {
@@ -4639,6 +4643,356 @@ static long get_proc_syscall(struct __test_metadata *_metadata, int pid)
 	return ret;
 }
 
+
+static void notification_restart_handler(int sig)
+{
+	char c;
+	int saved_errno = errno;
+
+	if (write(handled, "s", 1) != 1 || read(handled, &c, 1) != 1)
+		_exit(1);
+	errno = saved_errno;
+}
+
+FIXTURE(notification_restart) {
+	int listener;
+	int sync[2];
+	pid_t pid;
+};
+
+FIXTURE_VARIANT(notification_restart) {
+	bool restart;
+	bool killable;
+};
+
+FIXTURE_VARIANT_ADD(notification_restart, neither) {
+	.restart = false, .killable = false,
+};
+FIXTURE_VARIANT_ADD(notification_restart, restart) {
+	.restart = true, .killable = false,
+};
+FIXTURE_VARIANT_ADD(notification_restart, killable) {
+	.restart = false, .killable = true,
+};
+FIXTURE_VARIANT_ADD(notification_restart, both) {
+	.restart = true, .killable = true,
+};
+
+FIXTURE_SETUP(notification_restart)
+{
+	unsigned int flags = SECCOMP_FILTER_FLAG_NEW_LISTENER;
+
+	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);
+	ASSERT_GE(self->listener, 0);
+}
+
+FIXTURE_TEARDOWN(notification_restart)
+{
+	if (self->pid > 0) {
+		kill(self->pid, SIGKILL);
+		waitpid(self->pid, NULL, 0);
+	}
+	close(self->listener);
+	close(self->sync[0]);
+	close(self->sync[1]);
+}
+
+static void notification_restart_child(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;
+
+	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;
+	if (write(handled, result, sizeof(result)) != sizeof(result))
+		_exit(1);
+	_exit(0);
+}
+
+static void notification_pending(struct __test_metadata *_metadata, int fd)
+{
+	struct pollfd pfd = { .fd = fd, .events = POLLIN };
+
+	ASSERT_EQ(poll(&pfd, 1, 5000), 1);
+	ASSERT_TRUE(pfd.revents & POLLIN);
+}
+
+static void notification_signal(struct __test_metadata *_metadata,
+				struct _test_data_notification_restart *self)
+{
+	struct pollfd pfd = { .fd = self->sync[0], .events = POLLIN };
+	char c;
+
+	ASSERT_EQ(kill(self->pid, SIGUSR1), 0);
+	ASSERT_EQ(poll(&pfd, 1, 5000), 1);
+	ASSERT_EQ(read(self->sync[0], &c, 1), 1);
+	ASSERT_EQ(c, 's');
+	/* The handler holds the task until the abandoned request is checked. */
+	pfd.fd = self->listener;
+	ASSERT_EQ(poll(&pfd, 1, 0), 0);
+	ASSERT_EQ(write(self->sync[0], "r", 1), 1);
+}
+
+static void notification_result(struct __test_metadata *_metadata,
+				struct _test_data_notification_restart *self,
+				long value, int error)
+{
+	long result[2];
+	int status;
+
+	ASSERT_EQ(read(self->sync[0], result, sizeof(result)), sizeof(result));
+	EXPECT_EQ(result[0], value);
+	if (value == -1)
+		EXPECT_EQ(result[1], error);
+	ASSERT_EQ(waitpid(self->pid, &status, 0), self->pid);
+	self->pid = -1;
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_F(notification_restart, before_receive)
+{
+	struct seccomp_notif req = {};
+	struct seccomp_notif_resp resp = {};
+	int i;
+
+	notification_restart_child(_metadata, self);
+	for (i = 0; i < 3; i++) {
+		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);
+	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, getpid(), 0);
+}
+
+TEST_F(notification_restart, failed_receive)
+{
+	struct seccomp_notif_resp resp = {};
+	struct seccomp_notif req = {};
+	void *buf;
+
+	buf = mmap(NULL, sizeof(req), PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(buf, MAP_FAILED);
+	notification_restart_child(_metadata, self);
+	notification_pending(_metadata, self->listener);
+	ASSERT_EQ(ioctl(self->listener, SECCOMP_IOCTL_NOTIF_RECV, buf), -1);
+	ASSERT_EQ(errno, EFAULT);
+	ASSERT_EQ(munmap(buf, sizeof(req)), 0);
+	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);
+	resp.id = req.id;
+	resp.error = -EAGAIN;
+	ASSERT_EQ(ioctl(self->listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+	notification_result(_metadata, self, -1, EAGAIN);
+}
+
+TEST_F(notification_restart, after_receive)
+{
+	struct seccomp_notif req = {};
+	struct seccomp_notif_resp resp = {};
+	char c;
+
+	notification_restart_child(_metadata, self);
+	notification_pending(_metadata, self->listener);
+	ASSERT_EQ(ioctl(self->listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+	if (!variant->killable) {
+		notification_signal(_metadata, self);
+		notification_result(_metadata, self, -1, EINTR);
+		ASSERT_EQ(ioctl(self->listener, SECCOMP_IOCTL_NOTIF_ID_VALID, &req.id), -1);
+		EXPECT_EQ(errno, ENOENT);
+		return;
+	}
+	ASSERT_EQ(kill(self->pid, SIGUSR1), 0);
+	/* Either ordering of signal delivery and reply must preserve the response. */
+	resp.id = req.id;
+	resp.val = USER_NOTIF_MAGIC;
+	ASSERT_EQ(ioctl(self->listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+	ASSERT_EQ(read(self->sync[0], &c, 1), 1);
+	ASSERT_EQ(c, 's');
+	ASSERT_EQ(write(self->sync[0], "r", 1), 1);
+	notification_result(_metadata, self, USER_NOTIF_MAGIC, 0);
+}
+
+TEST_F(notification_restart, fork_and_close)
+{
+	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 };
+	struct msghdr msg = {
+		.msg_iov = &iov, .msg_iovlen = 1,
+		.msg_control = control, .msg_controllen = sizeof(control),
+	};
+	struct cmsghdr *cmsg;
+	unsigned int flags = SECCOMP_FILTER_FLAG_NEW_LISTENER;
+	int i, fd, listener, status;
+	long result[2] = {};
+	pid_t child;
+
+	if (variant->restart)
+		flags |= SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV;
+	if (variant->killable)
+		flags |= SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV;
+	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);
+		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 = fork();
+		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(fork(), -1);
+		ASSERT_EQ(errno, EAGAIN);
+		ASSERT_EQ(write(handled, result, sizeof(result)), sizeof(result));
+		_exit(0);
+	}
+	ASSERT_EQ(recvmsg(self->sync[0], &msg, 0), 1);
+	ASSERT_FALSE(msg.msg_flags & MSG_CTRUNC);
+	cmsg = CMSG_FIRSTHDR(&msg);
+	ASSERT_NE(cmsg, NULL);
+	ASSERT_EQ(cmsg->cmsg_level, SOL_SOCKET);
+	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));
+
+	for (i = 0; i < 3; i++) {
+		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_result(_metadata, self, 0, 0);
+}
+
+TEST_F(notification_restart, fatal_signal)
+{
+	int status;
+
+	notification_restart_child(_metadata, self);
+	notification_pending(_metadata, self->listener);
+	ASSERT_EQ(kill(self->pid, SIGKILL), 0);
+	ASSERT_EQ(waitpid(self->pid, &status, 0), self->pid);
+	self->pid = -1;
+	ASSERT_TRUE(WIFSIGNALED(status));
+	EXPECT_EQ(WTERMSIG(status), SIGKILL);
+}
+
+TEST_F(notification_restart, listener_closed)
+{
+	notification_restart_child(_metadata, self);
+	notification_pending(_metadata, self->listener);
+	ASSERT_EQ(close(self->listener), 0);
+	self->listener = -1;
+	notification_result(_metadata, self, -1, ENOSYS);
+}
+
+TEST(user_notification_restart_requires_listener)
+{
+	ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
+	EXPECT_EQ(user_notif_syscall(__NR_getppid,
+				   SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV), -1);
+	EXPECT_EQ(errno, EINVAL);
+}
+
 /* Ensure non-fatal signals prior to receive are unmodified */
 TEST(user_notification_wait_killable_pre_notification)
 {
-- 
2.43.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH 3/3] docs/seccomp: describe the SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV flag
  2026-09-24 20:42 [PATCH 0/3] seccomp: opt in to restarting notifications before receipt Cong Wang
  2026-09-24 20:42 ` [PATCH 1/3] seccomp: allow restarting interrupted unreceived notifications Cong Wang
  2026-09-24 20:42 ` [PATCH 2/3] selftests/seccomp: cover restart of " Cong Wang
@ 2026-09-24 20:42 ` Cong Wang
  2 siblings, 0 replies; 4+ messages in thread
From: Cong Wang @ 2026-09-24 20:42 UTC (permalink / raw)
  To: Kees Cook
  Cc: linux-kernel, Will Drewry, Christian Brauner, Andy Lutomirski,
	Jonathan Corbet, Shuah Khan, linux-doc, linux-kselftest

From: Cong Wang <cwang@multikernel.io>

Document the new opt-in flag, its listener requirement and interaction
with WAIT_KILLABLE_RECV. Explain syscall re-evaluation and the
cancellation, timeout, fairness and argument-mutation tradeoffs.

Assisted-by: Codex:gpt-6
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 .../userspace-api/seccomp_filter.rst          | 29 +++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/Documentation/userspace-api/seccomp_filter.rst b/Documentation/userspace-api/seccomp_filter.rst
index cff0fa7f3175..b6875ce54fe2 100644
--- a/Documentation/userspace-api/seccomp_filter.rst
+++ b/Documentation/userspace-api/seccomp_filter.rst
@@ -281,6 +281,35 @@ process will ignore non-fatal signals until the response is sent. Signals that
 are sent prior to the notification being received by userspace are handled
 normally.
 
+``SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV`` can be set at filter installation
+to restart a syscall interrupted while its notification is still awaiting
+receipt, even if the signal handler was installed without ``SA_RESTART``.
+The handler runs, and if it returns normally, syscall entry and the seccomp
+filter are evaluated again. The abandoned notification is removed; a new
+notification is queued if the filter again returns ``SECCOMP_RET_USER_NOTIF``.
+This avoids returning ``EINTR`` before the syscall has executed, including
+for calls such as ``close`` where callers do not retry on ``EINTR``.
+A failed notification receive that resets the notification to its initial
+state is also eligible for restart.
+
+The flag requires ``SECCOMP_FILTER_FLAG_NEW_LISTENER`` and can be used with
+or without ``SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV``. Using both flags allows
+handlers to run before receipt and defers non-fatal signals during supervisor
+processing after receipt. Without ``SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV``,
+interruptions after receipt retain the existing ``SA_RESTART`` behavior.
+Fatal signals still terminate the notifying process. Neither flag changes
+supervisor-supplied errors or the native syscall's restart behavior after a
+``SECCOMP_USER_NOTIF_FLAG_CONTINUE`` response.
+
+Unconditional restart before receipt is opt-in: a normally returning signal
+handler can no longer cancel that wait with ``EINTR``. Native syscall timeout
+and signal-mask handling have not started during mediation. Repeated signals
+can therefore extend elapsed time and, as restarted notifications join the
+tail of the queue, delay receipt indefinitely. Handlers can also modify memory
+referenced by syscall arguments, so the restarted call must be authorized
+afresh; this flag does not provide an argument snapshot or prevent TOCTOU.
+Existing behavior is unchanged when the flag is absent.
+
 It is worth noting that ``struct seccomp_data`` contains the values of register
 arguments to the syscall, but does not contain pointers to memory. The task's
 memory is accessible to suitably privileged traces via ``ptrace()`` or
-- 
2.43.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-09-24 20:42 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-24 20:42 [PATCH 0/3] seccomp: opt in to restarting notifications before receipt Cong Wang
2026-09-24 20:42 ` [PATCH 1/3] seccomp: allow restarting interrupted unreceived notifications Cong Wang
2026-09-24 20:42 ` [PATCH 2/3] selftests/seccomp: cover restart of " Cong Wang
2026-09-24 20:42 ` [PATCH 3/3] docs/seccomp: describe the SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV flag Cong Wang

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®