mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/4] seccomp: fix user-notification state transitions
@ 2026-11-04 10:37 Andrea Parri
  2026-11-04 10:37 ` [PATCH 1/4] seccomp: wake poll waiters after failed addfd send Andrea Parri
                   ` (3 more replies)
  0 siblings, 4 replies; 6+ messages in thread
From: Andrea Parri @ 2026-11-04 10:37 UTC (permalink / raw)
  To: Kees Cook
  Cc: Andrea Parri, Andy Lutomirski, Will Drewry, Shuah Khan,
	Rodrigo Campos, Christian Brauner, Tycho Andersen,
	Sargun Dhillon, Hui Peng, Bradley Morgan, Cong Wang,
	linux-kernel, linux-kselftest, bpf

An atomic SECCOMP_IOCTL_NOTIF_ADDFD request moves a notification from
SENT to REPLIED, so the listener stops reporting EPOLLOUT for it.  Two
paths move it back to SENT without waking poll waiters, so a supervisor
waiting for EPOLLOUT, with edge- or level-triggered epoll, poll() or
select(), can remain asleep, leaving the target blocked in the trapped
syscall.

Patch 1 fixes the failed descriptor install, present since commit
0ae71c7720e3a ("seccomp: Support atomic "addfd + send reply"").  Patch 2
fixes the restore on signal interruption, added by
commit 832b9b176be06 ("seccomp: restore knotif->state when
SECCOMP_ADDFD_FLAG_SEND is interrupted").  Patch 3 moves the state change
and the wakeup into one helper, with no functional change.

SECCOMP_IOCTL_NOTIF_RECV marks a notification SENT before copying it to
userspace.  Unless SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV is also set, a
signal during the copy makes the target return EINTR, even with
SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV, while the supervisor can receive
a withdrawn ID.  With it, the signal is held back as if the notification
had already been received.  Patch 4 adds a RECEIVING state and marks the
notification SENT only after the copy.

Patch 4 affects all filters, not only those using RESTART_BEFORE_RECV:
RECV now fails with ENOENT if the target withdraws the notification during
the copy.  It clears the buffer first, and
Documentation/userspace-api/seccomp_filter.rst already asks supervisors to
retry on ENOENT.

All four patches concern transitions into SENT; patch 4 takes its
post-copy transition through the helper added by patch 3.

Tested in vng on x86_64 with the full seccomp_bpf suite, using the seccomp
selftest config plus CHECKPOINT_RESTORE: 146 passed, 0 failed, 0 skipped.
Reverting the kernel/seccomp.c changes of the whole series makes
user_notification_addfd_rlimit and user_notification_addfd_send_interrupted
time out on their second epoll_wait() and all four during_receive variants
fail; reverting only those of patch 4 makes just the during_receive
variants fail.

The bugs were found and the patches were written with the help of an LLM.

---
Reviewer notes: 832b9b176be06 and f7952251ecc3, fixed by patches 2 and 4,
are in v7.4-rc1.  Patches 3 and 4 only need backporting to 7.4.x:
f7952251ecc3 adds a new flag that is not tagged for stable itself,
unlike 832b9b176be06, and patch 3 is tagged only because patch 4 needs
it.

Patches 1 and 2 do not depend on patches 3 and 4 and can be applied
first.

The series conflicts with Cong Wang's RFC "seccomp: report the result of
continued system calls" [1], which is based on v7.3.  After patch 4, a
task killed while its held notification is being received is in
RECEIVING, so the "killed unseen" check in seccomp_hold_result() would
need to become n.state <= SECCOMP_NOTIFY_RECEIVING for the result to
still be queued.

[1] https://lore.kernel.org/all/20261020001124.974063-1-xiyou.wangcong@gmail.com/

Andrea Parri (4):
  seccomp: wake poll waiters after failed addfd send
  seccomp: wake poll waiters after interrupted addfd send
  seccomp: add a helper to move a notification to SENT
  seccomp: publish user notifications after copying them to userspace

 kernel/seccomp.c                              |  79 +++++----
 tools/testing/selftests/seccomp/config        |   1 +
 tools/testing/selftests/seccomp/seccomp_bpf.c | 162 +++++++++++++++++-
 3 files changed, 204 insertions(+), 38 deletions(-)


base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4
-- 
2.53.0


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

* [PATCH 1/4] seccomp: wake poll waiters after failed addfd send
  2026-11-04 10:37 [PATCH 0/4] seccomp: fix user-notification state transitions Andrea Parri
@ 2026-11-04 10:37 ` Andrea Parri
  2026-11-04 16:19   ` Tycho Andersen
  2026-11-04 10:37 ` [PATCH 2/4] seccomp: wake poll waiters after interrupted " Andrea Parri
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 6+ messages in thread
From: Andrea Parri @ 2026-11-04 10:37 UTC (permalink / raw)
  To: Kees Cook
  Cc: Andrea Parri, Andy Lutomirski, Will Drewry, Shuah Khan,
	Rodrigo Campos, Christian Brauner, Tycho Andersen,
	Sargun Dhillon, Hui Peng, Bradley Morgan, Cong Wang,
	linux-kernel, linux-kselftest, bpf

An atomic SECCOMP_IOCTL_NOTIF_ADDFD request stops the listener from
reporting EPOLLOUT for its notification while the request is pending.  If
installing the file descriptor fails, the notification becomes writable
again, but poll waiters are not woken.  A supervisor waiting for EPOLLOUT
can then remain asleep, leaving the target blocked in the trapped syscall.

This affects edge-triggered epoll if the supervisor consumed the event
before the request, and level-triggered epoll, poll() and select() if a
concurrent waiter found no EPOLLOUT while the request was pending.

The atomic path changes the state from SENT to REPLIED before asking the
target to install the descriptor.  seccomp_handle_addfd() restores SENT on
failure, but unlike seccomp_notify_recv(), it does not wake poll waiters.

Wake EPOLLOUT waiters on that failure path.  Extend the RLIMIT_NOFILE
test to consume the initial edge and require another after a failed
ADDFD+SEND.

The bug was found and the fix was written with the help of an LLM.

Fixes: 0ae71c7720e3a ("seccomp: Support atomic "addfd + send reply"")
Cc: stable@vger.kernel.org
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
---
 kernel/seccomp.c                              |  7 +++++--
 tools/testing/selftests/seccomp/seccomp_bpf.c | 15 ++++++++++++++-
 2 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 3eecba5ccdef3..d74e66bcb5211 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -1125,7 +1125,9 @@ static u64 seccomp_next_notify_id(struct seccomp_filter *filter)
 	return filter->notif->next_id++;
 }
 
-static void seccomp_handle_addfd(struct seccomp_kaddfd *addfd, struct seccomp_knotif *n)
+static void seccomp_handle_addfd(struct seccomp_filter *filter,
+				 struct seccomp_kaddfd *addfd,
+				 struct seccomp_knotif *n)
 {
 	int fd;
 
@@ -1144,6 +1146,7 @@ static void seccomp_handle_addfd(struct seccomp_kaddfd *addfd, struct seccomp_kn
 		/* If we fail reset and return an error to the notifier */
 		if (fd < 0) {
 			n->state = SECCOMP_NOTIFY_SENT;
+			wake_up_poll(&filter->wqh, EPOLLOUT | EPOLLWRNORM);
 		} else {
 			/* Return the FD we just added */
 			n->flags = 0;
@@ -1224,7 +1227,7 @@ static int seccomp_do_user_notification(int this_syscall,
 						 struct seccomp_kaddfd, list);
 		/* Check if we were woken up by a addfd message */
 		if (addfd)
-			seccomp_handle_addfd(addfd, &n);
+			seccomp_handle_addfd(match, addfd, &n);
 
 	}  while (n.state != SECCOMP_NOTIFY_REPLIED);
 
diff --git a/tools/testing/selftests/seccomp/seccomp_bpf.c b/tools/testing/selftests/seccomp/seccomp_bpf.c
index 66bb53bad93bc..6dd51efe8dff1 100644
--- a/tools/testing/selftests/seccomp/seccomp_bpf.c
+++ b/tools/testing/selftests/seccomp/seccomp_bpf.c
@@ -22,6 +22,7 @@
 
 #include <errno.h>
 #include <linux/filter.h>
+#include <sys/epoll.h>
 #include <sys/prctl.h>
 #include <sys/ptrace.h>
 #include <sys/time.h>
@@ -4391,7 +4392,10 @@ TEST(user_notification_addfd_rlimit)
 {
 	pid_t pid;
 	long ret;
-	int status, listener, memfd;
+	int epollfd, status, listener, memfd;
+	struct epoll_event event = {
+		.events = EPOLLOUT | EPOLLET,
+	};
 	struct seccomp_notif_addfd addfd = {};
 	struct seccomp_notif req = {};
 	struct seccomp_notif_resp resp = {};
@@ -4434,9 +4438,17 @@ TEST(user_notification_addfd_rlimit)
 	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd), -1);
 	EXPECT_EQ(errno, EMFILE);
 
+	epollfd = epoll_create1(EPOLL_CLOEXEC);
+	ASSERT_GE(epollfd, 0);
+	ASSERT_EQ(epoll_ctl(epollfd, EPOLL_CTL_ADD, listener, &event), 0);
+	ASSERT_EQ(epoll_wait(epollfd, &event, 1, 1000), 1);
+	ASSERT_NE(event.events & EPOLLOUT, 0);
+
 	addfd.flags = SECCOMP_ADDFD_FLAG_SEND;
 	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd), -1);
 	EXPECT_EQ(errno, EMFILE);
+	EXPECT_EQ(epoll_wait(epollfd, &event, 1, 1000), 1);
+	EXPECT_NE(event.events & EPOLLOUT, 0);
 
 	addfd.newfd = 100;
 	addfd.flags = SECCOMP_ADDFD_FLAG_SETFD;
@@ -4454,6 +4466,7 @@ TEST(user_notification_addfd_rlimit)
 	EXPECT_EQ(true, WIFEXITED(status));
 	EXPECT_EQ(0, WEXITSTATUS(status));
 
+	close(epollfd);
 	close(memfd);
 }
 
-- 
2.53.0


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

* [PATCH 2/4] seccomp: wake poll waiters after interrupted addfd send
  2026-11-04 10:37 [PATCH 0/4] seccomp: fix user-notification state transitions Andrea Parri
  2026-11-04 10:37 ` [PATCH 1/4] seccomp: wake poll waiters after failed addfd send Andrea Parri
@ 2026-11-04 10:37 ` Andrea Parri
  2026-11-04 10:37 ` [PATCH 3/4] seccomp: add a helper to move a notification to SENT Andrea Parri
  2026-11-04 10:37 ` [PATCH 4/4] seccomp: publish user notifications after copying them to userspace Andrea Parri
  3 siblings, 0 replies; 6+ messages in thread
From: Andrea Parri @ 2026-11-04 10:37 UTC (permalink / raw)
  To: Kees Cook
  Cc: Andrea Parri, Andy Lutomirski, Will Drewry, Shuah Khan,
	Rodrigo Campos, Christian Brauner, Tycho Andersen,
	Sargun Dhillon, Hui Peng, Bradley Morgan, Cong Wang,
	linux-kernel, linux-kselftest, bpf

An atomic SECCOMP_IOCTL_NOTIF_ADDFD request stops the listener from
reporting EPOLLOUT for its notification while the request is pending.  If
a signal interrupts the request before the target dequeues it, the
notification becomes writable again, but poll waiters are not woken.  A
supervisor waiting for EPOLLOUT can then remain asleep, leaving the target
blocked in the trapped syscall.

This affects edge-triggered epoll if the supervisor consumed the event
before the request, and level-triggered epoll, poll() and select() if a
concurrent waiter found no EPOLLOUT while the request was pending.

seccomp_notify_addfd() restores the state from REPLIED to SENT on this
path, but unlike seccomp_notify_recv() and seccomp_handle_addfd(), it does
not wake poll waiters.

Wake EPOLLOUT waiters when restoring SENT.  Extend the interrupted
ADDFD+SEND test to consume the initial edge and require another after the
interrupted request.

The bug was found and the fix was written with the help of an LLM.

Fixes: 832b9b176be06 ("seccomp: restore knotif->state when SECCOMP_ADDFD_FLAG_SEND is interrupted")
Cc: stable@vger.kernel.org
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
---
 kernel/seccomp.c                              |  4 +++-
 tools/testing/selftests/seccomp/seccomp_bpf.c | 15 ++++++++++++++-
 2 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index d74e66bcb5211..ee94a071736ef 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -1824,8 +1824,10 @@ static long seccomp_notify_addfd(struct seccomp_filter *filter,
 		ret = kaddfd.ret;
 	} else {
 		list_del(&kaddfd.list);
-		if (addfd.flags & SECCOMP_ADDFD_FLAG_SEND)
+		if (addfd.flags & SECCOMP_ADDFD_FLAG_SEND) {
 			knotif->state = SECCOMP_NOTIFY_SENT;
+			wake_up_poll(&filter->wqh, EPOLLOUT | EPOLLWRNORM);
+		}
 	}
 
 out_unlock:
diff --git a/tools/testing/selftests/seccomp/seccomp_bpf.c b/tools/testing/selftests/seccomp/seccomp_bpf.c
index 6dd51efe8dff1..123eddb0c0d64 100644
--- a/tools/testing/selftests/seccomp/seccomp_bpf.c
+++ b/tools/testing/selftests/seccomp/seccomp_bpf.c
@@ -4485,6 +4485,7 @@ static void empty_handler(int signo)
  *      returning 0 from the trapped syscall without the FD installed.
  *   2. The supervisor can retry SECCOMP_IOCTL_NOTIF_ADDFD (or
  *      SECCOMP_IOCTL_NOTIF_SEND) instead of failing with -EINPROGRESS.
+ *   3. Poll waiters see a new EPOLLOUT edge for the notification.
  *
  * To deterministically hit the race window where the supervisor sleeps in
  * wait_for_completion_interruptible(&kaddfd.completion) after waking the
@@ -4504,7 +4505,10 @@ TEST(user_notification_addfd_send_interrupted)
 	 */
 	pid_t tracee_pid, sig_pid, parent_pid = getpid();
 	long ret;
-	int status, listener, memfd, err;
+	int epollfd, status, listener, memfd, err;
+	struct epoll_event event = {
+		.events = EPOLLOUT | EPOLLET,
+	};
 	struct seccomp_notif_addfd addfd = {};
 	struct seccomp_notif req = {};
 	struct sigaction sa = {};
@@ -4563,6 +4567,12 @@ TEST(user_notification_addfd_send_interrupted)
 	/* Wait for the tracee to trap in do_user_notif(). */
 	ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
 
+	epollfd = epoll_create1(EPOLL_CLOEXEC);
+	ASSERT_GE(epollfd, 0);
+	ASSERT_EQ(epoll_ctl(epollfd, EPOLL_CTL_ADD, listener, &event), 0);
+	ASSERT_EQ(epoll_wait(epollfd, &event, 1, 1000), 1);
+	ASSERT_NE(event.events & EPOLLOUT, 0);
+
 	/*
 	 * Demote the tracee to SCHED_IDLE and promote the supervisor to
 	 * SCHED_FIFO(99) on the same CPU.
@@ -4630,6 +4640,8 @@ TEST(user_notification_addfd_send_interrupted)
 	EXPECT_EQ(waitpid(sig_pid, &status, 0), sig_pid);
 	EXPECT_EQ(true, WIFEXITED(status));
 	EXPECT_EQ(0, WEXITSTATUS(status));
+	EXPECT_EQ(epoll_wait(epollfd, &event, 1, 1000), 1);
+	EXPECT_NE(event.events & EPOLLOUT, 0);
 
 	nanosleep(&delay, NULL);
 
@@ -4645,6 +4657,7 @@ TEST(user_notification_addfd_send_interrupted)
 	EXPECT_EQ(true, WIFEXITED(status));
 	EXPECT_EQ(0, WEXITSTATUS(status));
 
+	close(epollfd);
 	close(listener);
 	close(memfd);
 }
-- 
2.53.0


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

* [PATCH 3/4] seccomp: add a helper to move a notification to SENT
  2026-11-04 10:37 [PATCH 0/4] seccomp: fix user-notification state transitions Andrea Parri
  2026-11-04 10:37 ` [PATCH 1/4] seccomp: wake poll waiters after failed addfd send Andrea Parri
  2026-11-04 10:37 ` [PATCH 2/4] seccomp: wake poll waiters after interrupted " Andrea Parri
@ 2026-11-04 10:37 ` Andrea Parri
  2026-11-04 10:37 ` [PATCH 4/4] seccomp: publish user notifications after copying them to userspace Andrea Parri
  3 siblings, 0 replies; 6+ messages in thread
From: Andrea Parri @ 2026-11-04 10:37 UTC (permalink / raw)
  To: Kees Cook
  Cc: Andrea Parri, Andy Lutomirski, Will Drewry, Shuah Khan,
	Rodrigo Campos, Christian Brauner, Tycho Andersen,
	Sargun Dhillon, Hui Peng, Bradley Morgan, Cong Wang,
	linux-kernel, linux-kselftest, bpf

seccomp_notify_recv(), seccomp_handle_addfd() and seccomp_notify_addfd()
each move a user notification to the SENT state.  The notification then
accepts a reply, so each site must also wake EPOLLOUT waiters.  Both
addfd paths have missed that wakeup before.

Move the state change and the wakeup into seccomp_notify_set_sent() so a
new path cannot set SENT without waking poll waiters.  The helper asserts
that notify_lock is held, as all three callers already do.

No functional change intended.

The change was written with the help of an LLM.

Cc: stable@vger.kernel.org # 7.4.x
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
---
 kernel/seccomp.c | 21 +++++++++++++--------
 1 file changed, 13 insertions(+), 8 deletions(-)

diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index ee94a071736ef..75a758128db2d 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -1125,6 +1125,15 @@ static u64 seccomp_next_notify_id(struct seccomp_filter *filter)
 	return filter->notif->next_id++;
 }
 
+/* A notification in SENT state accepts a reply; tell poll waiters. */
+static void seccomp_notify_set_sent(struct seccomp_filter *filter,
+				    struct seccomp_knotif *n)
+{
+	lockdep_assert_held(&filter->notify_lock);
+	n->state = SECCOMP_NOTIFY_SENT;
+	wake_up_poll(&filter->wqh, EPOLLOUT | EPOLLWRNORM);
+}
+
 static void seccomp_handle_addfd(struct seccomp_filter *filter,
 				 struct seccomp_kaddfd *addfd,
 				 struct seccomp_knotif *n)
@@ -1145,8 +1154,7 @@ static void seccomp_handle_addfd(struct seccomp_filter *filter,
 	if (addfd->ioctl_flags & SECCOMP_ADDFD_FLAG_SEND) {
 		/* If we fail reset and return an error to the notifier */
 		if (fd < 0) {
-			n->state = SECCOMP_NOTIFY_SENT;
-			wake_up_poll(&filter->wqh, EPOLLOUT | EPOLLWRNORM);
+			seccomp_notify_set_sent(filter, n);
 		} else {
 			/* Return the FD we just added */
 			n->flags = 0;
@@ -1600,8 +1608,7 @@ static long seccomp_notify_recv(struct seccomp_filter *filter,
 	unotif.pid = task_pid_vnr(knotif->task);
 	unotif.data = *(knotif->data);
 
-	knotif->state = SECCOMP_NOTIFY_SENT;
-	wake_up_poll(&filter->wqh, EPOLLOUT | EPOLLWRNORM);
+	seccomp_notify_set_sent(filter, knotif);
 	ret = 0;
 out:
 	mutex_unlock(&filter->notify_lock);
@@ -1824,10 +1831,8 @@ static long seccomp_notify_addfd(struct seccomp_filter *filter,
 		ret = kaddfd.ret;
 	} else {
 		list_del(&kaddfd.list);
-		if (addfd.flags & SECCOMP_ADDFD_FLAG_SEND) {
-			knotif->state = SECCOMP_NOTIFY_SENT;
-			wake_up_poll(&filter->wqh, EPOLLOUT | EPOLLWRNORM);
-		}
+		if (addfd.flags & SECCOMP_ADDFD_FLAG_SEND)
+			seccomp_notify_set_sent(filter, knotif);
 	}
 
 out_unlock:
-- 
2.53.0


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

* [PATCH 4/4] seccomp: publish user notifications after copying them to userspace
  2026-11-04 10:37 [PATCH 0/4] seccomp: fix user-notification state transitions Andrea Parri
                   ` (2 preceding siblings ...)
  2026-11-04 10:37 ` [PATCH 3/4] seccomp: add a helper to move a notification to SENT Andrea Parri
@ 2026-11-04 10:37 ` Andrea Parri
  3 siblings, 0 replies; 6+ messages in thread
From: Andrea Parri @ 2026-11-04 10:37 UTC (permalink / raw)
  To: Kees Cook
  Cc: Andrea Parri, Andy Lutomirski, Will Drewry, Shuah Khan,
	Rodrigo Campos, Christian Brauner, Tycho Andersen,
	Sargun Dhillon, Hui Peng, Bradley Morgan, Cong Wang,
	linux-kernel, linux-kselftest, bpf

SECCOMP_FILTER_FLAG_RESTART_BEFORE_RECV restarts a syscall when a signal
interrupts its notification before the supervisor receives it. However,
seccomp_notify_recv() marks the notification SENT before copy_to_user().
Unless WAIT_KILLABLE_RECV is also set, a signal in that window makes the
target return EINTR while the supervisor can receive a stale notification
ID; with it, the signal is held back until the reply.

  CPU 1 (target)                   CPU 2 (supervisor)
  ------------------------------   ------------------------------
                                   mark notification SENT
  receive signal
  observe SENT                     copy notification to user
  remove notification
  return EINTR                     return success with stale ID

Cure this by reserving the notification in a new RECEIVING state. Move it
to SENT after copy_to_user() succeeds and it remains on the notification
list. The target's interrupted wait treats RECEIVING like INIT, while the
receiver returns ENOENT if the target withdraws the notification during
the copy.

When it returns ENOENT, the receiver clears the copied notification, so
userspace can retry with the same zeroed buffer, and returns EFAULT if
clearing fails.

The copy failure path no longer wakes a killable waiter. The target only
switches to a killable wait once its notification is SENT, which now
happens after the copy, and a SENT notification never returns to INIT.

Filters without RESTART_BEFORE_RECV see the same change. Without
WAIT_KILLABLE_RECV, the receiver fails with ENOENT instead of returning a
withdrawn ID. With it, a signal during the copy is handled normally, as
documented for signals sent before userspace receives the notification.

This should be safe because notify_lock protects every state transition
and the post-copy list lookup. seccomp_notify_recv() works from its
by-value copy across the unlocked copy and looks the notification up again
by ID, so it does not dereference the target's stack object after the
target can remove it.

Add a regression test which uses userfaultfd to stop copy_to_user() in the
race window and checks that the buffer is zeroed again on ENOENT. All four
flag variants fail without the kernel change and pass with it when run with
the following command.

  ./seccomp_bpf -t during_receive

The test needs CONFIG_USERFAULTFD, so add it to the seccomp selftest
config.

The race was found and the fix was written with the help of an LLM.

Fixes: f7952251ecc3 ("seccomp: allow restarting interrupted unreceived notifications")
Cc: stable@vger.kernel.org
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
---
 kernel/seccomp.c                              |  59 ++++----
 tools/testing/selftests/seccomp/config        |   1 +
 tools/testing/selftests/seccomp/seccomp_bpf.c | 132 ++++++++++++++++++
 3 files changed, 161 insertions(+), 31 deletions(-)

diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 75a758128db2d..7dfd5eec6d640 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -54,6 +54,7 @@
 
 enum notify_state {
 	SECCOMP_NOTIFY_INIT,
+	SECCOMP_NOTIFY_RECEIVING,
 	SECCOMP_NOTIFY_SENT,
 	SECCOMP_NOTIFY_REPLIED,
 };
@@ -74,11 +75,12 @@ struct seccomp_knotif {
 
 	/*
 	 * Notification states. When SECCOMP_RET_USER_NOTIF is returned, a
-	 * struct seccomp_knotif is created and starts out in INIT. Once the
-	 * handler reads the notification off of an FD, it transitions to SENT.
-	 * If a signal is received the state transitions back to INIT and
-	 * another message is sent. When the userspace handler replies, state
-	 * transitions to REPLIED.
+	 * struct seccomp_knotif is created and starts out in INIT. A receiver
+	 * reserves it by moving it to RECEIVING. Once the notification has been
+	 * copied to userspace, it transitions to SENT; if copying fails, it
+	 * returns to INIT. An interrupted wait may withdraw the notification in
+	 * any state, including RECEIVING. When the userspace handler replies,
+	 * state transitions to REPLIED.
 	 */
 	enum notify_state state;
 
@@ -1225,7 +1227,7 @@ static int seccomp_do_user_notification(int this_syscall,
 			 */
 			if (!(!wait_killable && should_sleep_killable(match, &n))) {
 				if (err == -ERESTARTSYS && match->restart_before_recv &&
-				    n.state == SECCOMP_NOTIFY_INIT)
+				    n.state <= SECCOMP_NOTIFY_RECEIVING)
 					err = -ERESTARTNOINTR;
 				goto interrupted;
 			}
@@ -1600,40 +1602,35 @@ static long seccomp_notify_recv(struct seccomp_filter *filter,
 	 * when we were able to acquire the rw lock.
 	 */
 	if (!knotif) {
-		ret = -ENOENT;
-		goto out;
+		mutex_unlock(&filter->notify_lock);
+		return -ENOENT;
 	}
 
 	unotif.id = knotif->id;
 	unotif.pid = task_pid_vnr(knotif->task);
 	unotif.data = *(knotif->data);
-
-	seccomp_notify_set_sent(filter, knotif);
-	ret = 0;
-out:
+	knotif->state = SECCOMP_NOTIFY_RECEIVING;
 	mutex_unlock(&filter->notify_lock);
 
-	if (ret == 0 && copy_to_user(buf, &unotif, sizeof(unotif))) {
-		ret = -EFAULT;
+	ret = copy_to_user(buf, &unotif, sizeof(unotif)) ? -EFAULT : 0;
 
-		/*
-		 * Userspace screwed up. To make sure that we keep this
-		 * notification alive, let's reset it back to INIT. It
-		 * may have died when we released the lock, so we need to make
-		 * sure it's still around.
-		 */
-		mutex_lock(&filter->notify_lock);
-		knotif = find_notification(filter, unotif.id);
-		if (knotif) {
-			/* Reset the process to make sure it's not stuck */
-			if (should_sleep_killable(filter, knotif))
-				complete(&knotif->ready);
-			knotif->state = SECCOMP_NOTIFY_INIT;
-			atomic_inc(&filter->notif->requests);
-			wake_up_poll(&filter->wqh, EPOLLIN | EPOLLRDNORM);
-		}
-		mutex_unlock(&filter->notify_lock);
+	mutex_lock(&filter->notify_lock);
+	knotif = find_notification(filter, unotif.id);
+	if (!knotif) {
+		if (!ret)
+			ret = -ENOENT;
+	} else if (!ret) {
+		seccomp_notify_set_sent(filter, knotif);
+	} else {
+		knotif->state = SECCOMP_NOTIFY_INIT;
+		atomic_inc(&filter->notif->requests);
+		wake_up_poll(&filter->wqh, EPOLLIN | EPOLLRDNORM);
 	}
+	mutex_unlock(&filter->notify_lock);
+
+	/* Leave the buffer zeroed so that userspace can retry unchanged. */
+	if (ret == -ENOENT && clear_user(buf, sizeof(unotif)))
+		ret = -EFAULT;
 
 	return ret;
 }
diff --git a/tools/testing/selftests/seccomp/config b/tools/testing/selftests/seccomp/config
index ad431a5178fbe..c6e14eb473fd9 100644
--- a/tools/testing/selftests/seccomp/config
+++ b/tools/testing/selftests/seccomp/config
@@ -1,4 +1,5 @@
 CONFIG_PID_NS=y
 CONFIG_SECCOMP=y
 CONFIG_SECCOMP_FILTER=y
+CONFIG_USERFAULTFD=y
 CONFIG_USER_NS=y
diff --git a/tools/testing/selftests/seccomp/seccomp_bpf.c b/tools/testing/selftests/seccomp/seccomp_bpf.c
index 123eddb0c0d64..1ac5b6960a1ce 100644
--- a/tools/testing/selftests/seccomp/seccomp_bpf.c
+++ b/tools/testing/selftests/seccomp/seccomp_bpf.c
@@ -22,6 +22,7 @@
 
 #include <errno.h>
 #include <linux/filter.h>
+#include <linux/userfaultfd.h>
 #include <sys/epoll.h>
 #include <sys/prctl.h>
 #include <sys/ptrace.h>
@@ -5137,6 +5138,137 @@ TEST_F(notification_restart, failed_receive)
 	notification_result(_metadata, self, -1, EAGAIN);
 }
 
+#ifdef __NR_userfaultfd
+struct notification_receive_args {
+	struct seccomp_notif *req;
+	int listener;
+	int ret;
+	int err;
+};
+
+static void *notification_receive(void *data)
+{
+	struct notification_receive_args *args = data;
+
+	args->ret = ioctl(args->listener, SECCOMP_IOCTL_NOTIF_RECV, args->req);
+	args->err = errno;
+	return NULL;
+}
+
+/*
+ * Write-protect the range through a new userfaultfd and return it, or
+ * return -1 with a skip reason if this kernel cannot report the fault.
+ */
+static int notification_uffd_wp(struct __test_metadata *_metadata,
+				const struct uffdio_range *range,
+				char *reason, size_t reason_len)
+{
+	struct uffdio_writeprotect wp = {};
+	struct uffdio_register reg = {};
+	struct uffdio_api api = {
+		.api = UFFD_API,
+		.features = UFFD_FEATURE_PAGEFAULT_FLAG_WP,
+	};
+	int ret, uffd;
+
+	/* Kernel-originated faults require a privileged userfaultfd. */
+	uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (uffd < 0) {
+		snprintf(reason, reason_len,
+			 "userfaultfd for kernel faults unavailable: %s",
+			 strerror(errno));
+		return -1;
+	}
+	ret = ioctl(uffd, UFFDIO_API, &api);
+	if (ret < 0 && errno == EINVAL) {
+		snprintf(reason, reason_len, "userfaultfd API is not available");
+		goto skip;
+	}
+	ASSERT_EQ(ret, 0);
+	if (!(api.features & UFFD_FEATURE_PAGEFAULT_FLAG_WP)) {
+		snprintf(reason, reason_len,
+			 "userfaultfd write protection is not available");
+		goto skip;
+	}
+
+	reg.range = *range;
+	reg.mode = UFFDIO_REGISTER_MODE_WP;
+	ASSERT_EQ(ioctl(uffd, UFFDIO_REGISTER, &reg), 0);
+	wp.range = *range;
+	wp.mode = UFFDIO_WRITEPROTECT_MODE_WP;
+	ASSERT_EQ(ioctl(uffd, UFFDIO_WRITEPROTECT, &wp), 0);
+	return uffd;
+
+skip:
+	ASSERT_EQ(close(uffd), 0);
+	return -1;
+}
+
+TEST_F(notification_restart, during_receive)
+{
+	struct notification_receive_args args = { .listener = self->listener };
+	struct seccomp_notif req = {};
+	struct seccomp_notif_resp resp = {};
+	struct uffdio_writeprotect wp = {};
+	struct uffdio_range range = {};
+	struct pollfd pfd = { .events = POLLIN };
+	struct uffd_msg msg;
+	char reason[128];
+	pthread_t thread;
+	long page_size;
+	int uffd;
+
+	page_size = sysconf(_SC_PAGESIZE);
+	ASSERT_GT(page_size, 0);
+	args.req = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(args.req, MAP_FAILED);
+	memset(args.req, 0, page_size);
+
+	range.start = (unsigned long)args.req;
+	range.len = page_size;
+	uffd = notification_uffd_wp(_metadata, &range, reason, sizeof(reason));
+	if (uffd < 0) {
+		ASSERT_EQ(munmap(args.req, page_size), 0);
+		SKIP(return, "%s", reason);
+	}
+
+	notification_restart_child(_metadata, self);
+	notification_pending(_metadata, self->listener);
+	ASSERT_EQ(pthread_create(&thread, NULL, notification_receive, &args), 0);
+	pfd.fd = uffd;
+	ASSERT_EQ(poll(&pfd, 1, 5000), 1);
+	ASSERT_EQ(read(uffd, &msg, sizeof(msg)), sizeof(msg));
+	ASSERT_EQ(msg.event, UFFD_EVENT_PAGEFAULT);
+	ASSERT_TRUE(msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_WP);
+
+	/* Withdraw the notification while copy_to_user() is stalled. */
+	notification_signal(_metadata, self);
+	wp.range = range;
+	wp.mode = 0;
+	ASSERT_EQ(ioctl(uffd, UFFDIO_WRITEPROTECT, &wp), 0);
+	ASSERT_EQ(pthread_join(thread, NULL), 0);
+	ASSERT_EQ(args.ret, -1);
+	EXPECT_EQ(args.err, ENOENT);
+	EXPECT_EQ(memcmp(args.req, &req, sizeof(req)), 0);
+	ASSERT_EQ(close(uffd), 0);
+	ASSERT_EQ(munmap(args.req, page_size), 0);
+
+	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);
+}
+#endif
+
 TEST_F(notification_restart, after_receive)
 {
 	struct seccomp_notif req = {};
-- 
2.53.0


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

* Re: [PATCH 1/4] seccomp: wake poll waiters after failed addfd send
  2026-11-04 10:37 ` [PATCH 1/4] seccomp: wake poll waiters after failed addfd send Andrea Parri
@ 2026-11-04 16:19   ` Tycho Andersen
  0 siblings, 0 replies; 6+ messages in thread
From: Tycho Andersen @ 2026-11-04 16:19 UTC (permalink / raw)
  To: Andrea Parri
  Cc: Kees Cook, Andy Lutomirski, Will Drewry, Shuah Khan,
	Rodrigo Campos, Christian Brauner, Sargun Dhillon, Hui Peng,
	Bradley Morgan, Cong Wang, linux-kernel, linux-kselftest, bpf

On Wed, Nov 04, 2026 at 11:37:31AM +0100, Andrea Parri wrote:
> An atomic SECCOMP_IOCTL_NOTIF_ADDFD request stops the listener from
> reporting EPOLLOUT for its notification while the request is pending.  If
> installing the file descriptor fails, the notification becomes writable
> again, but poll waiters are not woken.  A supervisor waiting for EPOLLOUT
> can then remain asleep, leaving the target blocked in the trapped syscall.
> 
> This affects edge-triggered epoll if the supervisor consumed the event
> before the request, and level-triggered epoll, poll() and select() if a
> concurrent waiter found no EPOLLOUT while the request was pending.
> 
> The atomic path changes the state from SENT to REPLIED before asking the
> target to install the descriptor.  seccomp_handle_addfd() restores SENT on
> failure, but unlike seccomp_notify_recv(), it does not wake poll waiters.
> 
> Wake EPOLLOUT waiters on that failure path.  Extend the RLIMIT_NOFILE
> test to consume the initial edge and require another after a failed
> ADDFD+SEND.

Hm, don't we return success or failure from
ioctl(SECCOMP_IOCTL_NOTIF_ADDFD) via kaddfd.ret in
seccomp_notify_addfd()?

I don't really understand why this is necessary unless you're ignoring
the return value from that call.

Tycho

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

end of thread, other threads:[~2026-11-04 16:19 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-04 10:37 [PATCH 0/4] seccomp: fix user-notification state transitions Andrea Parri
2026-11-04 10:37 ` [PATCH 1/4] seccomp: wake poll waiters after failed addfd send Andrea Parri
2026-11-04 16:19   ` Tycho Andersen
2026-11-04 10:37 ` [PATCH 2/4] seccomp: wake poll waiters after interrupted " Andrea Parri
2026-11-04 10:37 ` [PATCH 3/4] seccomp: add a helper to move a notification to SENT Andrea Parri
2026-11-04 10:37 ` [PATCH 4/4] seccomp: publish user notifications after copying them to userspace Andrea Parri

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®