* [RFC PATCH 0/9] seccomp: report the result of continued system calls
@ 2026-10-20 0:11 Cong Wang
2026-10-20 0:11 ` [RFC PATCH 1/9] seccomp: add result records to the notification listener Cong Wang
` (8 more replies)
0 siblings, 9 replies; 10+ messages in thread
From: Cong Wang @ 2026-10-20 0:11 UTC (permalink / raw)
To: linux-kernel; +Cc: Andy Lutomirski, Kees Cook, Will Drewry, Christian Brauner
From: Cong Wang <cwang@multikernel.io>
A seccomp user notification supervisor that continues a fork never
learns whether it succeeded or which task it created, so it cannot track
the tasks below it. Today the only way is ptrace fork events, which take
the one tracer slot from every task, may be forbidden by Yama or an LSM,
report nothing for a failed fork, interfere with signals and waitpid(),
and cost several stops per fork.
Sandlock [1] hits exactly this. It is an unprivileged process sandbox
built on seccomp user notification, with no cgroups to count tasks, so
it must see every fork itself and is left with ptrace and its problems.
This series lets the supervisor ask for the result of a continued
system call:
- SECCOMP_USER_NOTIF_FLAG_RESULT, given with CONTINUE, queues a one-way
result record on the listener once the call completes. It is read
with SECCOMP_IOCTL_NOTIF_RECV like any notification, flagged with
SECCOMP_NOTIF_FLAG_RESULT, and carries the id of the continued
notification and the call's return value.
- For the fork family, the record also carries the new task's pid in
the listener's pid namespace and its pidfs inode number, which is
never reused. The supervisor can open a pidfd for exactly that task
with open_by_handle_at(FD_PIDFS_ROOT, ...), unprivileged, or learn
that it has already exited. The record is queued before the new task
first runs and records are received before notifications, so the
supervisor always hears of a task before it hears from it.
- SECCOMP_USER_NOTIF_FLAG_HOLD, given with RESULT, reports the result
as a notification the supervisor must answer, and the task waits
until it does. For a fork, the new task does not run until then
either, so the supervisor can act on it before its first
instruction.
The interface is generic; only the fork family is supported so far.
Results are reported from task work on the way back to user space,
which needs nothing per system call. A successful fork is the one
exception: it is reported in kernel_clone() just before the child is
woken, which is the only point where both ordering and the child's
identity can be guaranteed. Every other system call is refused with
EINVAL, which is also what older kernels return, so the flags are easy
to probe and can be extended to more calls without new uAPI.
struct seccomp_notif keeps its size: the result shares a union with
struct seccomp_data, so the ioctl numbers and SECCOMP_GET_NOTIF_SIZES
are unchanged. The record is allocated by SEND and charged to the
supervisor, so the system call itself never allocates and a record can
never be lost after the point of no return.
Tested on x86_64 in QEMU with lockdep, KASAN and kmemleak: the full
seccomp_bpf selftest, the new tests as an unprivileged user, and fork
result records from an ia32 task.
[1] https://github.com/multikernel/sandlock
---
Cong Wang (9):
seccomp: add result records to the notification listener
seccomp: add __NR_seccomp_* aliases for the fork family
seccomp: report the result of a continued system call
seccomp: report the result of a continued fork
seccomp: factor out the user notification queue and wait
seccomp: add SECCOMP_USER_NOTIF_FLAG_HOLD
selftests/seccomp: test fork result records
selftests/seccomp: test held fork results
Documentation: seccomp: describe system call result records
.../userspace-api/seccomp_filter.rst | 117 +++-
arch/Kconfig | 6 +
arch/x86/include/asm/seccomp.h | 4 +
include/asm-generic/seccomp.h | 26 +
include/linux/seccomp.h | 4 +
include/linux/seccomp_types.h | 6 +
include/uapi/linux/seccomp.h | 57 +-
kernel/fork.c | 3 +
kernel/seccomp.c | 461 ++++++++++++--
tools/testing/selftests/seccomp/seccomp_bpf.c | 579 ++++++++++++++++++
10 files changed, 1218 insertions(+), 45 deletions(-)
base-commit: c03c1a14507dfaf8b856c2608f9ac216021fc222
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 1/9] seccomp: add result records to the notification listener
2026-10-20 0:11 [RFC PATCH 0/9] seccomp: report the result of continued system calls Cong Wang
@ 2026-10-20 0:11 ` Cong Wang
2026-10-20 0:11 ` [RFC PATCH 2/9] seccomp: add __NR_seccomp_* aliases for the fork family Cong Wang
` (7 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Cong Wang @ 2026-10-20 0:11 UTC (permalink / raw)
To: linux-kernel; +Cc: Andy Lutomirski, Kees Cook, Will Drewry, Christian Brauner
From: Cong Wang <cwang@multikernel.io>
A supervisor that continues a system call with
SECCOMP_USER_NOTIF_FLAG_CONTINUE never learns how the call turned out.
For the fork family that means it cannot learn about the new task at
all: a child that is killed before its first notified system call is
never seen, and a fork the kernel rejected after CONTINUE looks the same
as one that succeeded.
Prepare a second kind of entry on the listener: a one-way record of a
continued call's outcome. It is received with the existing
SECCOMP_IOCTL_NOTIF_RECV, carries SECCOMP_NOTIF_FLAG_RESULT in flags
(the field was never set before) and is never answered.
struct seccomp_notif cannot grow, because its size is encoded in the
RECV ioctl number and a program rebuilt against new headers would get
EINVAL from every old kernel. The payload therefore shares storage with
the seccomp_data in an anonymous union. struct seccomp_notif_result is
32 bytes, the seccomp_data 64, so the layout, the ioctl numbers and
SECCOMP_GET_NOTIF_SIZES stay the same and existing source keeps
compiling.
Records sit on their own list and RECV hands them out before any
pending notification. A record is meant to be queued before the task it
describes can run, so delivering records first guarantees that the
supervisor hears about a task before it hears from it. The notification
state machine is left alone.
A record names a new task by its pidfs inode number as well as by pid,
because the pid may be reused before the supervisor reads the record.
Opening a pidfd by inode number only works for a struct pid that is
registered with pidfs, so RECV registers the child. Registration may
fail with ENOMEM, in which case the record stays queued and RECV can be
retried.
Nothing queues records yet.
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
include/uapi/linux/seccomp.h | 30 +++++++++-
kernel/seccomp.c | 111 +++++++++++++++++++++++++++++++++++
2 files changed, 140 insertions(+), 1 deletion(-)
diff --git a/include/uapi/linux/seccomp.h b/include/uapi/linux/seccomp.h
index dbfc9b37fcae..67f7b2f45ab6 100644
--- a/include/uapi/linux/seccomp.h
+++ b/include/uapi/linux/seccomp.h
@@ -72,11 +72,39 @@ struct seccomp_notif_sizes {
__u16 seccomp_data;
};
+/**
+ * struct seccomp_notif_result - the outcome of a continued system call
+ * @nr: the system call number, as in struct seccomp_data
+ * @arch: the system call convention, as in struct seccomp_data
+ * @val: the return value the task saw
+ * @child_pid: for the fork family, the new task in the listener's pid
+ * namespace, or 0 if the call failed
+ * @__reserved: always 0
+ * @child_ino: for the fork family, the pidfs inode number of the new task,
+ * or 0 if the call failed. Unlike @child_pid it is never
+ * reused, so it names the task race free.
+ */
+struct seccomp_notif_result {
+ __s32 nr;
+ __u32 arch;
+ __s64 val;
+ __u32 child_pid;
+ __u32 __reserved;
+ __u64 child_ino;
+};
+
+/* Valid flags for struct seccomp_notif */
+#define SECCOMP_NOTIF_FLAG_RESULT (1UL << 0) /* @result is valid, not @data */
+#define SECCOMP_NOTIF_FLAG_THREAD (1UL << 1) /* the new task is a thread */
+
struct seccomp_notif {
__u64 id;
__u32 pid;
__u32 flags;
- struct seccomp_data data;
+ union {
+ struct seccomp_data data;
+ struct seccomp_notif_result result;
+ };
};
/*
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 86cf4460d69e..5641ac8b2f2e 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -38,6 +38,7 @@
#include <linux/file.h>
#include <linux/filter.h>
#include <linux/pid.h>
+#include <linux/pidfs.h>
#include <linux/ptrace.h>
#include <linux/capability.h>
#include <linux/uaccess.h>
@@ -132,6 +133,30 @@ struct seccomp_kaddfd {
struct list_head list;
};
+/**
+ * struct seccomp_kresult - a one-way record of a continued system call's
+ * outcome
+ *
+ * @id: The id of the notification whose reply asked for this record.
+ * @task: The task that made the system call.
+ * @child: The new task, for a successful call in the fork family.
+ * @nr: The system call number.
+ * @arch: The system call convention.
+ * @val: The system call's return value.
+ * @flags: SECCOMP_NOTIF_FLAG_* flags to report.
+ * @list: list_head for chaining onto notification->results.
+ */
+struct seccomp_kresult {
+ u64 id;
+ struct pid *task;
+ struct pid *child;
+ int nr;
+ u32 arch;
+ long val;
+ u32 flags;
+ struct list_head list;
+};
+
/**
* struct notification - container for seccomp userspace notifications. Since
* most seccomp filters will not have notification listeners attached and this
@@ -144,6 +169,8 @@ struct seccomp_kaddfd {
* @flags: A set of SECCOMP_USER_NOTIF_FD_* flags.
* @next_id: The id of the next request.
* @notifications: A list of struct seccomp_knotif elements.
+ * @results: A list of struct seccomp_kresult elements, delivered before any
+ * notification.
*/
struct notification {
@@ -151,6 +178,7 @@ struct notification {
u32 flags;
u64 next_id;
struct list_head notifications;
+ struct list_head results;
};
#ifdef SECCOMP_ARCH_NATIVE
@@ -1446,8 +1474,19 @@ static long seccomp_set_mode_strict(void)
}
#ifdef CONFIG_SECCOMP_FILTER
+static void seccomp_kresult_free(struct seccomp_kresult *kres)
+{
+ put_pid(kres->task);
+ put_pid(kres->child);
+ kfree(kres);
+}
+
static void seccomp_notify_free(struct seccomp_filter *filter)
{
+ struct seccomp_kresult *kres, *tmp;
+
+ list_for_each_entry_safe(kres, tmp, &filter->notif->results, list)
+ seccomp_kresult_free(kres);
kfree(filter->notif);
filter->notif = NULL;
}
@@ -1547,10 +1586,62 @@ static int recv_wait_event(struct seccomp_filter *filter)
return 0;
}
+static int seccomp_fill_result(struct seccomp_notif *unotif,
+ struct seccomp_kresult *kres)
+{
+ int ret;
+
+ /*
+ * A pidfd can only be opened by inode number once the struct pid is
+ * registered with pidfs. A child that is already reaped cannot be
+ * registered anymore; the supervisor finds out when opening fails.
+ */
+ ret = pidfs_register_pid(kres->child);
+ if (ret && ret != -ESRCH)
+ return ret;
+
+ unotif->id = kres->id;
+ unotif->pid = pid_vnr(kres->task);
+ unotif->flags = SECCOMP_NOTIF_FLAG_RESULT | kres->flags;
+ unotif->result.nr = kres->nr;
+ unotif->result.arch = kres->arch;
+ unotif->result.val = kres->val;
+ if (kres->child) {
+ unotif->result.child_pid = pid_vnr(kres->child);
+ unotif->result.child_ino = kres->child->ino;
+ }
+ return 0;
+}
+
+static long seccomp_copy_result(struct seccomp_filter *filter,
+ struct seccomp_kresult *kres,
+ const struct seccomp_notif *unotif,
+ void __user *buf)
+{
+ if (!copy_to_user(buf, unotif, sizeof(*unotif))) {
+ seccomp_kresult_free(kres);
+ return 0;
+ }
+
+ /* Put the record back at the head so that no record is lost. */
+ mutex_lock(&filter->notify_lock);
+ if (filter->notif) {
+ list_add(&kres->list, &filter->notif->results);
+ atomic_inc(&filter->notif->requests);
+ wake_up_poll(&filter->wqh, EPOLLIN | EPOLLRDNORM);
+ kres = NULL;
+ }
+ mutex_unlock(&filter->notify_lock);
+ if (kres)
+ seccomp_kresult_free(kres);
+ return -EFAULT;
+}
+
static long seccomp_notify_recv(struct seccomp_filter *filter,
void __user *buf)
{
struct seccomp_knotif *knotif = NULL, *cur;
+ struct seccomp_kresult *kres;
struct seccomp_notif unotif;
ssize_t ret;
@@ -1568,6 +1659,19 @@ static long seccomp_notify_recv(struct seccomp_filter *filter,
return ret;
mutex_lock(&filter->notify_lock);
+ kres = list_first_entry_or_null(&filter->notif->results,
+ struct seccomp_kresult, list);
+ if (kres) {
+ ret = seccomp_fill_result(&unotif, kres);
+ if (ret < 0) {
+ atomic_inc(&filter->notif->requests);
+ kres = NULL;
+ } else {
+ list_del(&kres->list);
+ }
+ goto out;
+ }
+
list_for_each_entry(cur, &filter->notif->notifications, list) {
if (cur->state == SECCOMP_NOTIFY_INIT) {
knotif = cur;
@@ -1595,6 +1699,9 @@ static long seccomp_notify_recv(struct seccomp_filter *filter,
out:
mutex_unlock(&filter->notify_lock);
+ if (kres)
+ return seccomp_copy_result(filter, kres, &unotif, buf);
+
if (ret == 0 && copy_to_user(buf, &unotif, sizeof(unotif))) {
ret = -EFAULT;
@@ -1862,6 +1969,9 @@ static __poll_t seccomp_notify_poll(struct file *file,
if (mutex_lock_interruptible(&filter->notify_lock) < 0)
return EPOLLERR;
+ if (!list_empty(&filter->notif->results))
+ ret |= EPOLLIN | EPOLLRDNORM;
+
list_for_each_entry(cur, &filter->notif->notifications, list) {
if (cur->state == SECCOMP_NOTIFY_INIT)
ret |= EPOLLIN | EPOLLRDNORM;
@@ -1897,6 +2007,7 @@ static struct file *init_listener(struct seccomp_filter *filter)
filter->notif->next_id = get_random_u64();
INIT_LIST_HEAD(&filter->notif->notifications);
+ INIT_LIST_HEAD(&filter->notif->results);
ret = anon_inode_getfile("seccomp notify", &seccomp_notify_ops,
filter, O_RDWR);
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 2/9] seccomp: add __NR_seccomp_* aliases for the fork family
2026-10-20 0:11 [RFC PATCH 0/9] seccomp: report the result of continued system calls Cong Wang
2026-10-20 0:11 ` [RFC PATCH 1/9] seccomp: add result records to the notification listener Cong Wang
@ 2026-10-20 0:11 ` Cong Wang
2026-10-20 0:11 ` [RFC PATCH 3/9] seccomp: report the result of a continued system call Cong Wang
` (6 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Cong Wang @ 2026-10-20 0:11 UTC (permalink / raw)
To: linux-kernel; +Cc: Andy Lutomirski, Kees Cook, Will Drewry, Christian Brauner
From: Cong Wang <cwang@multikernel.io>
The existing __NR_seccomp_* aliases name only the strict mode syscalls
(read, write, exit, sigreturn). Add aliases for the task creation
family, clone, clone3, fork and vfork, so that seccomp can recognise
those syscalls by number, natively and for compat tasks.
The native aliases get generic defaults from <asm/unistd.h>. A syscall
the arch does not have is named -1, which never matches a valid syscall
number.
The compat aliases have no generic default, because a wrong compat
number cannot be told from a right one. An arch provides them when it
wants compat tasks covered; x86 does so for ia32. x32 needs nothing:
its syscall numbers carry __X32_SYSCALL_BIT and match none of the
native aliases.
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
arch/Kconfig | 6 ++++++
arch/x86/include/asm/seccomp.h | 4 ++++
include/asm-generic/seccomp.h | 26 ++++++++++++++++++++++++++
3 files changed, 36 insertions(+)
diff --git a/arch/Kconfig b/arch/Kconfig
index 45c657772362..61040529a7da 100644
--- a/arch/Kconfig
+++ b/arch/Kconfig
@@ -641,6 +641,12 @@ config HAVE_ARCH_SECCOMP
- __NR_seccomp_write_32
- __NR_seccomp_exit_32
- __NR_seccomp_sigreturn_32
+ and, optionally, the compat fork family, for features that need to
+ recognise it (there is no generic default for these):
+ - __NR_seccomp_clone_32
+ - __NR_seccomp_clone3_32
+ - __NR_seccomp_fork_32
+ - __NR_seccomp_vfork_32
config HAVE_ARCH_SECCOMP_FILTER
bool
diff --git a/arch/x86/include/asm/seccomp.h b/arch/x86/include/asm/seccomp.h
index 42bcd42d70d1..767d047050a1 100644
--- a/arch/x86/include/asm/seccomp.h
+++ b/arch/x86/include/asm/seccomp.h
@@ -14,6 +14,10 @@
#define __NR_seccomp_write_32 __NR_ia32_write
#define __NR_seccomp_exit_32 __NR_ia32_exit
#define __NR_seccomp_sigreturn_32 __NR_ia32_sigreturn
+#define __NR_seccomp_clone_32 __NR_ia32_clone
+#define __NR_seccomp_clone3_32 __NR_ia32_clone3
+#define __NR_seccomp_fork_32 __NR_ia32_fork
+#define __NR_seccomp_vfork_32 __NR_ia32_vfork
#endif
#ifdef CONFIG_X86_64
diff --git a/include/asm-generic/seccomp.h b/include/asm-generic/seccomp.h
index 6b6f42bc58f9..5acdeb62c56c 100644
--- a/include/asm-generic/seccomp.h
+++ b/include/asm-generic/seccomp.h
@@ -26,6 +26,32 @@
#define __NR_seccomp_sigreturn __NR_rt_sigreturn
#endif
+/* The fork family; -1 names a syscall the arch does not have. */
+#ifndef __NR_seccomp_clone
+#define __NR_seccomp_clone __NR_clone
+#endif
+#ifndef __NR_seccomp_clone3
+#ifdef __NR_clone3
+#define __NR_seccomp_clone3 __NR_clone3
+#else
+#define __NR_seccomp_clone3 (-1)
+#endif
+#endif
+#ifndef __NR_seccomp_fork
+#ifdef __NR_fork
+#define __NR_seccomp_fork __NR_fork
+#else
+#define __NR_seccomp_fork (-1)
+#endif
+#endif
+#ifndef __NR_seccomp_vfork
+#ifdef __NR_vfork
+#define __NR_seccomp_vfork __NR_vfork
+#else
+#define __NR_seccomp_vfork (-1)
+#endif
+#endif
+
#ifdef CONFIG_COMPAT
#ifndef get_compat_mode1_syscalls
static inline const int *get_compat_mode1_syscalls(void)
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 3/9] seccomp: report the result of a continued system call
2026-10-20 0:11 [RFC PATCH 0/9] seccomp: report the result of continued system calls Cong Wang
2026-10-20 0:11 ` [RFC PATCH 1/9] seccomp: add result records to the notification listener Cong Wang
2026-10-20 0:11 ` [RFC PATCH 2/9] seccomp: add __NR_seccomp_* aliases for the fork family Cong Wang
@ 2026-10-20 0:11 ` Cong Wang
2026-10-20 0:11 ` [RFC PATCH 4/9] seccomp: report the result of a continued fork Cong Wang
` (5 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Cong Wang @ 2026-10-20 0:11 UTC (permalink / raw)
To: linux-kernel; +Cc: Andy Lutomirski, Kees Cook, Will Drewry, Christian Brauner
From: Cong Wang <cwang@multikernel.io>
A supervisor that continues a system call with
SECCOMP_USER_NOTIF_FLAG_CONTINUE never learns how the call turned out.
Add SECCOMP_USER_NOTIF_FLAG_RESULT: given together with CONTINUE, it
asks the kernel to queue a result record for the call on the listener,
carrying the call's return value.
The record is reported from task work, run as the task returns to user
space. That is one place for every system call: nothing has to be added
to the system calls themselves, and it works on every architecture.
The value is read from the task's registers after any ptrace syscall
exit stop, so it is what the task sees. The work is queued with
TWA_RESUME, so that the system call does not see a pending signal;
get_signal() still runs task work before it delivers a signal or
restarts the call.
Every reply that asked for a record gets exactly one. If a signal
interrupts the notification after the reply was sent, the record
reports the error the call fails with. If the task exits before
returning to user space, which happens after seccomp has released its
filters, the record is reported at release with -ESRCH. If the listener
is gone by then, the record is dropped.
SEND allocates the record, so that a failed allocation fails the ioctl,
which can be retried, and the system call never allocates.
The interface does not depend on the system call, but which calls are
supported is decided per call, and so far none is. Unsupported calls
are refused with EINVAL, which is also what older kernels return, so
the flag is easy to probe and can be extended without a new flag. The
next patch adds the fork family.
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
include/linux/seccomp_types.h | 6 ++
include/uapi/linux/seccomp.h | 10 +++
kernel/seccomp.c | 133 +++++++++++++++++++++++++++++++---
3 files changed, 140 insertions(+), 9 deletions(-)
diff --git a/include/linux/seccomp_types.h b/include/linux/seccomp_types.h
index cf0a0355024f..158bac728344 100644
--- a/include/linux/seccomp_types.h
+++ b/include/linux/seccomp_types.h
@@ -7,6 +7,7 @@
#ifdef CONFIG_SECCOMP
struct seccomp_filter;
+struct seccomp_kresult;
/**
* struct seccomp - the state of a seccomp'ed process
*
@@ -18,11 +19,16 @@ struct seccomp_filter;
*
* @filter must only be accessed from the context of current as there
* is no read locking.
+ * @result: a result record requested by a listener for the system call in
+ * progress, owned by current until it is reported.
+ * @result_work: reports @result when current returns to user space.
*/
struct seccomp {
int mode;
atomic_t filter_count;
struct seccomp_filter *filter;
+ struct seccomp_kresult *result;
+ struct callback_head result_work;
};
#else
diff --git a/include/uapi/linux/seccomp.h b/include/uapi/linux/seccomp.h
index 67f7b2f45ab6..799c93c8eed2 100644
--- a/include/uapi/linux/seccomp.h
+++ b/include/uapi/linux/seccomp.h
@@ -136,6 +136,16 @@ struct seccomp_notif {
*/
#define SECCOMP_USER_NOTIF_FLAG_CONTINUE (1UL << 0)
+/*
+ * SECCOMP_USER_NOTIF_FLAG_RESULT, together with
+ * SECCOMP_USER_NOTIF_FLAG_CONTINUE, asks for the outcome of the continued
+ * system call. It is reported once, as a notification with
+ * SECCOMP_NOTIF_FLAG_RESULT set and the same id, by the time the task
+ * returns to user space, and that notification is not answered. A system
+ * call for which results are not supported is refused with EINVAL.
+ */
+#define SECCOMP_USER_NOTIF_FLAG_RESULT (1UL << 1)
+
struct seccomp_notif_resp {
__u64 id;
__s64 val;
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 5641ac8b2f2e..246205e85d58 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -28,6 +28,7 @@
#include <linux/slab.h>
#include <linux/syscalls.h>
#include <linux/sysctl.h>
+#include <linux/task_work.h>
#include <asm/syscall.h>
@@ -98,6 +99,9 @@ struct seccomp_knotif {
/* outstanding addfd requests */
struct list_head addfd;
+
+ /* The result record the reply asked for, handed over with the reply */
+ struct seccomp_kresult *kres;
};
/**
@@ -137,6 +141,8 @@ struct seccomp_kaddfd {
* struct seccomp_kresult - a one-way record of a continued system call's
* outcome
*
+ * @filter: The filter whose listener asked for this record. Only used while
+ * the record is pending in the task, which keeps its filters alive.
* @id: The id of the notification whose reply asked for this record.
* @task: The task that made the system call.
* @child: The new task, for a successful call in the fork family.
@@ -147,6 +153,7 @@ struct seccomp_kaddfd {
* @list: list_head for chaining onto notification->results.
*/
struct seccomp_kresult {
+ struct seccomp_filter *filter;
u64 id;
struct pid *task;
struct pid *child;
@@ -549,6 +556,36 @@ static inline pid_t seccomp_can_sync_threads(void)
return 0;
}
+static void seccomp_kresult_free(struct seccomp_kresult *kres)
+{
+ put_pid(kres->task);
+ put_pid(kres->child);
+ kfree(kres);
+}
+
+static void seccomp_queue_result(struct seccomp_filter *filter,
+ struct seccomp_kresult *kres)
+{
+ lockdep_assert_held(&filter->notify_lock);
+
+ if (!filter->notif) {
+ seccomp_kresult_free(kres);
+ return;
+ }
+ list_add_tail(&kres->list, &filter->notif->results);
+ atomic_inc(&filter->notif->requests);
+ wake_up_poll(&filter->wqh, EPOLLIN | EPOLLRDNORM);
+}
+
+static void seccomp_report_result(struct seccomp_kresult *kres)
+{
+ struct seccomp_filter *filter = kres->filter;
+
+ mutex_lock(&filter->notify_lock);
+ seccomp_queue_result(filter, kres);
+ mutex_unlock(&filter->notify_lock);
+}
+
static inline void seccomp_filter_free(struct seccomp_filter *filter)
{
if (filter) {
@@ -602,6 +639,15 @@ void seccomp_filter_release(struct task_struct *tsk)
if (WARN_ON((tsk->flags & PF_EXITING) == 0))
return;
+ /* The filter is gone by the time task work runs; report now. */
+ if (tsk->seccomp.result) {
+ struct seccomp_kresult *kres = tsk->seccomp.result;
+
+ tsk->seccomp.result = NULL;
+ kres->val = -ESRCH;
+ seccomp_report_result(kres);
+ }
+
if (READ_ONCE(tsk->seccomp.filter) == NULL)
return;
@@ -1189,6 +1235,39 @@ static bool should_sleep_killable(struct seccomp_filter *match,
return match->wait_killable_recv && n->state >= SECCOMP_NOTIFY_SENT;
}
+/* Report the system call's outcome on the way back to user space. */
+static void seccomp_result_work(struct callback_head *work)
+{
+ struct seccomp_kresult *kres = current->seccomp.result;
+ struct pt_regs *regs = current_pt_regs();
+ long err;
+
+ if (!kres)
+ return;
+ current->seccomp.result = NULL;
+
+ err = syscall_get_error(current, regs);
+ kres->val = err ? err : syscall_get_return_value(current, regs);
+ seccomp_report_result(kres);
+}
+
+static void seccomp_result_pending(struct seccomp_kresult *kres)
+{
+ current->seccomp.result = kres;
+ init_task_work(¤t->seccomp.result_work, seccomp_result_work);
+ /*
+ * TWA_RESUME rather than TWA_SIGNAL: the latter would make the system
+ * call see a pending signal. The work still runs before a signal is
+ * delivered or the call restarted, as get_signal() runs task work
+ * first.
+ */
+ if (task_work_add(current, ¤t->seccomp.result_work, TWA_RESUME)) {
+ current->seccomp.result = NULL;
+ kres->val = -ESRCH;
+ seccomp_report_result(kres);
+ }
+}
+
static int seccomp_do_user_notification(int this_syscall,
struct seccomp_filter *match,
const struct seccomp_data *sd)
@@ -1276,6 +1355,13 @@ static int seccomp_do_user_notification(int this_syscall,
out:
mutex_unlock(&match->notify_lock);
+ /*
+ * Even if a signal beat the reply, the record is due: it then reports
+ * the call as failed with the error set below.
+ */
+ if (n.kres)
+ seccomp_result_pending(n.kres);
+
/* Userspace requests to continue the syscall. */
if (flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE)
return 0;
@@ -1474,13 +1560,6 @@ static long seccomp_set_mode_strict(void)
}
#ifdef CONFIG_SECCOMP_FILTER
-static void seccomp_kresult_free(struct seccomp_kresult *kres)
-{
- put_pid(kres->task);
- put_pid(kres->child);
- kfree(kres);
-}
-
static void seccomp_notify_free(struct seccomp_filter *filter)
{
struct seccomp_kresult *kres, *tmp;
@@ -1727,26 +1806,46 @@ static long seccomp_notify_recv(struct seccomp_filter *filter,
return ret;
}
+/* Which system calls can report their result. */
+static bool seccomp_result_supported(const struct seccomp_data *sd)
+{
+ return false;
+}
+
static long seccomp_notify_send(struct seccomp_filter *filter,
void __user *buf)
{
struct seccomp_notif_resp resp = {};
+ struct seccomp_kresult *kres = NULL;
struct seccomp_knotif *knotif;
long ret;
if (copy_from_user(&resp, buf, sizeof(resp)))
return -EFAULT;
- if (resp.flags & ~SECCOMP_USER_NOTIF_FLAG_CONTINUE)
+ if (resp.flags & ~(SECCOMP_USER_NOTIF_FLAG_CONTINUE |
+ SECCOMP_USER_NOTIF_FLAG_RESULT))
return -EINVAL;
if ((resp.flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE) &&
(resp.error || resp.val))
return -EINVAL;
+ if (resp.flags & SECCOMP_USER_NOTIF_FLAG_RESULT) {
+ if (!(resp.flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE))
+ return -EINVAL;
+ /*
+ * Allocate here, where failing is harmless, so that the
+ * system call never has to.
+ */
+ kres = kzalloc_obj(*kres, GFP_KERNEL_ACCOUNT);
+ if (!kres)
+ return -ENOMEM;
+ }
+
ret = mutex_lock_interruptible(&filter->notify_lock);
if (ret < 0)
- return ret;
+ goto out_free;
knotif = find_notification(filter, resp.id);
if (!knotif) {
@@ -1760,6 +1859,20 @@ static long seccomp_notify_send(struct seccomp_filter *filter,
goto out;
}
+ if (kres) {
+ if (!seccomp_result_supported(knotif->data)) {
+ ret = -EINVAL;
+ goto out;
+ }
+ kres->filter = filter;
+ kres->id = knotif->id;
+ kres->task = get_task_pid(knotif->task, PIDTYPE_PID);
+ kres->nr = knotif->data->nr;
+ kres->arch = knotif->data->arch;
+ knotif->kres = kres;
+ kres = NULL;
+ }
+
ret = 0;
knotif->state = SECCOMP_NOTIFY_REPLIED;
knotif->error = resp.error;
@@ -1771,6 +1884,8 @@ static long seccomp_notify_send(struct seccomp_filter *filter,
complete(&knotif->ready);
out:
mutex_unlock(&filter->notify_lock);
+out_free:
+ kfree(kres);
return ret;
}
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 4/9] seccomp: report the result of a continued fork
2026-10-20 0:11 [RFC PATCH 0/9] seccomp: report the result of continued system calls Cong Wang
` (2 preceding siblings ...)
2026-10-20 0:11 ` [RFC PATCH 3/9] seccomp: report the result of a continued system call Cong Wang
@ 2026-10-20 0:11 ` Cong Wang
2026-10-20 0:11 ` [RFC PATCH 5/9] seccomp: factor out the user notification queue and wait Cong Wang
` (4 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Cong Wang @ 2026-10-20 0:11 UTC (permalink / raw)
To: linux-kernel; +Cc: Andy Lutomirski, Kees Cook, Will Drewry, Christian Brauner
From: Cong Wang <cwang@multikernel.io>
Support SECCOMP_USER_NOTIF_FLAG_RESULT for fork, vfork, clone and
clone3. A successful fork is reported with the new task's pid in the
listener's pid namespace and its pidfs inode number, and with
SECCOMP_NOTIF_FLAG_THREAD if the new task is a thread.
With this a supervisor learns about every task created below it, even
one that is killed before it makes a single notified system call, and
it learns about forks that the kernel rejected after CONTINUE. Once it
knows the new task it can open a pidfd for it by inode number, without
privileges and without racing pid reuse, and the pidfd reports the
task's exit however it happens. Today the only complete alternative for
an unprivileged supervisor is ptrace fork events.
Reporting on the way back to user space is too late for a successful
fork: by then the child may be running, may have triggered
notifications, or may be gone with its pid reused. So kernel_clone()
reports the child just before wake_up_new_task(), holding a reference
to its struct pid. Records are delivered before notifications, so the
supervisor always hears of a new task before it can hear from it. The
forking task does not wait for the supervisor. A failed fork, wherever
it fails, is reported on exit like any other system call, so no other
fork path needs a hook. The new task does not inherit the pending
record.
The fork family is recognised through the __NR_seccomp_* aliases:
natively on every arch that defines SECCOMP_ARCH_NATIVE, and for compat
tasks where the arch provides the compat aliases, so far ia32. x32 is
refused, since its numbers carry __X32_SYSCALL_BIT and never match.
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
include/linux/seccomp.h | 4 +++
include/uapi/linux/seccomp.h | 4 +++
kernel/fork.c | 3 +++
kernel/seccomp.c | 48 +++++++++++++++++++++++++++++++++++-
4 files changed, 58 insertions(+), 1 deletion(-)
diff --git a/include/linux/seccomp.h b/include/linux/seccomp.h
index fcb3eb9825e5..91d6edaf5eb7 100644
--- a/include/linux/seccomp.h
+++ b/include/linux/seccomp.h
@@ -75,6 +75,7 @@ static inline int seccomp_mode(struct seccomp *s)
#ifdef CONFIG_SECCOMP_FILTER
extern void seccomp_filter_release(struct task_struct *tsk);
extern void get_seccomp_filter(struct task_struct *tsk);
+extern void seccomp_report_fork(struct task_struct *child, pid_t nr);
#else /* CONFIG_SECCOMP_FILTER */
static inline void seccomp_filter_release(struct task_struct *tsk)
{
@@ -84,6 +85,9 @@ static inline void get_seccomp_filter(struct task_struct *tsk)
{
return;
}
+static inline void seccomp_report_fork(struct task_struct *child, pid_t nr)
+{
+}
#endif /* CONFIG_SECCOMP_FILTER */
#if defined(CONFIG_SECCOMP_FILTER) && defined(CONFIG_CHECKPOINT_RESTORE)
diff --git a/include/uapi/linux/seccomp.h b/include/uapi/linux/seccomp.h
index 799c93c8eed2..223577fea182 100644
--- a/include/uapi/linux/seccomp.h
+++ b/include/uapi/linux/seccomp.h
@@ -143,6 +143,10 @@ struct seccomp_notif {
* SECCOMP_NOTIF_FLAG_RESULT set and the same id, by the time the task
* returns to user space, and that notification is not answered. A system
* call for which results are not supported is refused with EINVAL.
+ *
+ * So far fork, vfork, clone and clone3 are supported. For these a new task
+ * is reported before it runs, so the record reaches the listener before
+ * any notification the new task can trigger.
*/
#define SECCOMP_USER_NOTIF_FLAG_RESULT (1UL << 1)
diff --git a/kernel/fork.c b/kernel/fork.c
index 33760ece3bb3..637eec6a6cb3 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -1815,6 +1815,8 @@ static void copy_seccomp(struct task_struct *p)
/* Ref-count the new filter user, and assign it. */
get_seccomp_filter(current);
p->seccomp = current->seccomp;
+ /* The result of this fork is reported by the parent only. */
+ p->seccomp.result = NULL;
/*
* Explicitly enable no_new_privs here in case it got set
@@ -2809,6 +2811,7 @@ pid_t kernel_clone(struct kernel_clone_args *args)
task_unlock(p);
}
+ seccomp_report_fork(p, nr);
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 246205e85d58..336a7c28e1d6 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -1371,6 +1371,30 @@ static int seccomp_do_user_notification(int this_syscall,
return -1;
}
+/**
+ * seccomp_report_fork - report a new task to the listener that asked for it
+ * @child: the new task, not woken up yet
+ * @nr: its pid, the value the fork returns
+ *
+ * Exit is too late for a successful fork: the child would already run, and
+ * might be gone with its pid reused. Report it before it is woken instead.
+ * A failed fork is reported on exit, like any other system call.
+ */
+void seccomp_report_fork(struct task_struct *child, pid_t nr)
+{
+ struct seccomp_kresult *kres = current->seccomp.result;
+
+ if (likely(!kres))
+ return;
+ current->seccomp.result = NULL;
+
+ kres->val = nr;
+ kres->child = get_task_pid(child, PIDTYPE_PID);
+ if (!thread_group_leader(child))
+ kres->flags |= SECCOMP_NOTIF_FLAG_THREAD;
+ seccomp_report_result(kres);
+}
+
static bool __seccomp_filter(int this_syscall, const bool recheck_after_trace)
{
u32 filter_ret, action;
@@ -1806,10 +1830,32 @@ static long seccomp_notify_recv(struct seccomp_filter *filter,
return ret;
}
+static bool seccomp_is_fork_syscall(const struct seccomp_data *sd)
+{
+ /* The aliases use -1 for a syscall the arch lacks. */
+ if (sd->nr < 0)
+ return false;
+#if defined(SECCOMP_ARCH_COMPAT) && defined(__NR_seccomp_clone_32)
+ if (sd->arch == SECCOMP_ARCH_COMPAT)
+ return sd->nr == __NR_seccomp_clone_32 ||
+ sd->nr == __NR_seccomp_clone3_32 ||
+ sd->nr == __NR_seccomp_fork_32 ||
+ sd->nr == __NR_seccomp_vfork_32;
+#endif
+#if defined(SECCOMP_ARCH_NATIVE) && defined(__NR_seccomp_clone)
+ if (sd->arch == SECCOMP_ARCH_NATIVE)
+ return sd->nr == __NR_seccomp_clone ||
+ sd->nr == __NR_seccomp_clone3 ||
+ sd->nr == __NR_seccomp_fork ||
+ sd->nr == __NR_seccomp_vfork;
+#endif
+ return false;
+}
+
/* Which system calls can report their result. */
static bool seccomp_result_supported(const struct seccomp_data *sd)
{
- return false;
+ return seccomp_is_fork_syscall(sd);
}
static long seccomp_notify_send(struct seccomp_filter *filter,
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 5/9] seccomp: factor out the user notification queue and wait
2026-10-20 0:11 [RFC PATCH 0/9] seccomp: report the result of continued system calls Cong Wang
` (3 preceding siblings ...)
2026-10-20 0:11 ` [RFC PATCH 4/9] seccomp: report the result of a continued fork Cong Wang
@ 2026-10-20 0:11 ` Cong Wang
2026-10-20 0:11 ` [RFC PATCH 6/9] seccomp: add SECCOMP_USER_NOTIF_FLAG_HOLD Cong Wang
` (3 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Cong Wang @ 2026-10-20 0:11 UTC (permalink / raw)
To: linux-kernel; +Cc: Andy Lutomirski, Kees Cook, Will Drewry, Christian Brauner
From: Cong Wang <cwang@multikernel.io>
Split seccomp_do_user_notification() into helpers that queue a
notification, wait for its reply and take it off the list again. The
next patch queues a second kind of notification where a system call's
result is reported, and reuses all three.
No functional change.
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
kernel/seccomp.c | 153 ++++++++++++++++++++++++++++-------------------
1 file changed, 92 insertions(+), 61 deletions(-)
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 336a7c28e1d6..793efcf0e626 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -1235,6 +1235,91 @@ static bool should_sleep_killable(struct seccomp_filter *match,
return match->wait_killable_recv && n->state >= SECCOMP_NOTIFY_SENT;
}
+static void seccomp_notify_queue(struct seccomp_filter *match,
+ struct seccomp_knotif *n)
+{
+ lockdep_assert_held(&match->notify_lock);
+
+ n->state = SECCOMP_NOTIFY_INIT;
+ init_completion(&n->ready);
+ list_add_tail(&n->list, &match->notif->notifications);
+ INIT_LIST_HEAD(&n->addfd);
+
+ atomic_inc(&match->notif->requests);
+ if (match->notif->flags & SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP)
+ wake_up_poll_on_current_cpu(&match->wqh, EPOLLIN | EPOLLRDNORM);
+ else
+ wake_up_poll(&match->wqh, EPOLLIN | EPOLLRDNORM);
+}
+
+/* Returns 0 once @n is replied to, or the error that interrupted the wait. */
+static int seccomp_notify_wait(struct seccomp_filter *match,
+ struct seccomp_knotif *n)
+{
+ struct seccomp_kaddfd *addfd;
+ int err;
+
+ lockdep_assert_held(&match->notify_lock);
+
+ do {
+ bool wait_killable = should_sleep_killable(match, n);
+
+ mutex_unlock(&match->notify_lock);
+ if (wait_killable)
+ err = wait_for_completion_killable(&n->ready);
+ else
+ err = wait_for_completion_interruptible(&n->ready);
+ mutex_lock(&match->notify_lock);
+
+ if (err != 0) {
+ /*
+ * 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)))
+ return err;
+ }
+
+ addfd = list_first_entry_or_null(&n->addfd,
+ struct seccomp_kaddfd, list);
+ /* Check if we were woken up by a addfd message */
+ if (addfd)
+ seccomp_handle_addfd(addfd, n);
+
+ } while (n->state != SECCOMP_NOTIFY_REPLIED);
+
+ return 0;
+}
+
+static void seccomp_notify_dequeue(struct seccomp_filter *match,
+ struct seccomp_knotif *n)
+{
+ struct seccomp_kaddfd *addfd, *tmp;
+
+ lockdep_assert_held(&match->notify_lock);
+
+ /* If there were any pending addfd calls, clear them out */
+ list_for_each_entry_safe(addfd, tmp, &n->addfd, list) {
+ /* The process went away before we got a chance to handle it */
+ addfd->ret = -ESRCH;
+ list_del_init(&addfd->list);
+ complete(&addfd->completion);
+ }
+
+ /*
+ * Note that it's possible the listener died in between the time when
+ * we were notified of a response (or a signal) and when we were able to
+ * re-acquire the lock, so only delete from the list if the
+ * notification actually exists.
+ *
+ * Also note that this test is only valid because there's no way to
+ * *reattach* to a notifier right now. If one is added, we'll need to
+ * keep track of the notif itself and make sure they match here.
+ */
+ if (match->notif)
+ list_del(&n->list);
+}
+
/* Report the system call's outcome on the way back to user space. */
static void seccomp_result_work(struct callback_head *work)
{
@@ -1276,7 +1361,6 @@ static int seccomp_do_user_notification(int this_syscall,
u32 flags = 0;
long ret = 0;
struct seccomp_knotif n = {};
- struct seccomp_kaddfd *addfd, *tmp;
mutex_lock(&match->notify_lock);
err = -ENOSYS;
@@ -1284,74 +1368,21 @@ static int seccomp_do_user_notification(int this_syscall,
goto out;
n.task = current;
- n.state = SECCOMP_NOTIFY_INIT;
n.data = sd;
n.id = seccomp_next_notify_id(match);
- init_completion(&n.ready);
- list_add_tail(&n.list, &match->notif->notifications);
- INIT_LIST_HEAD(&n.addfd);
-
- atomic_inc(&match->notif->requests);
- if (match->notif->flags & SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP)
- wake_up_poll_on_current_cpu(&match->wqh, EPOLLIN | EPOLLRDNORM);
- else
- wake_up_poll(&match->wqh, EPOLLIN | EPOLLRDNORM);
+ seccomp_notify_queue(match, &n);
/*
* This is where we wait for a reply from userspace.
*/
- do {
- bool wait_killable = should_sleep_killable(match, &n);
-
- mutex_unlock(&match->notify_lock);
- if (wait_killable)
- err = wait_for_completion_killable(&n.ready);
- else
- err = wait_for_completion_interruptible(&n.ready);
- mutex_lock(&match->notify_lock);
-
- if (err != 0) {
- /*
- * 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)))
- goto interrupted;
- }
-
- addfd = list_first_entry_or_null(&n.addfd,
- struct seccomp_kaddfd, list);
- /* Check if we were woken up by a addfd message */
- if (addfd)
- seccomp_handle_addfd(addfd, &n);
-
- } while (n.state != SECCOMP_NOTIFY_REPLIED);
-
- ret = n.val;
- err = n.error;
- flags = n.flags;
-
-interrupted:
- /* If there were any pending addfd calls, clear them out */
- list_for_each_entry_safe(addfd, tmp, &n.addfd, list) {
- /* The process went away before we got a chance to handle it */
- addfd->ret = -ESRCH;
- list_del_init(&addfd->list);
- complete(&addfd->completion);
+ err = seccomp_notify_wait(match, &n);
+ if (!err) {
+ ret = n.val;
+ err = n.error;
+ flags = n.flags;
}
- /*
- * Note that it's possible the listener died in between the time when
- * we were notified of a response (or a signal) and when we were able to
- * re-acquire the lock, so only delete from the list if the
- * notification actually exists.
- *
- * Also note that this test is only valid because there's no way to
- * *reattach* to a notifier right now. If one is added, we'll need to
- * keep track of the notif itself and make sure they match here.
- */
- if (match->notif)
- list_del(&n.list);
+ seccomp_notify_dequeue(match, &n);
out:
mutex_unlock(&match->notify_lock);
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 6/9] seccomp: add SECCOMP_USER_NOTIF_FLAG_HOLD
2026-10-20 0:11 [RFC PATCH 0/9] seccomp: report the result of continued system calls Cong Wang
` (4 preceding siblings ...)
2026-10-20 0:11 ` [RFC PATCH 5/9] seccomp: factor out the user notification queue and wait Cong Wang
@ 2026-10-20 0:11 ` Cong Wang
2026-10-20 0:11 ` [RFC PATCH 7/9] selftests/seccomp: test fork result records Cong Wang
` (2 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Cong Wang @ 2026-10-20 0:11 UTC (permalink / raw)
To: linux-kernel; +Cc: Andy Lutomirski, Kees Cook, Will Drewry, Christian Brauner
From: Cong Wang <cwang@multikernel.io>
A result record is one-way: the task does not wait for the supervisor to
read it. That is enough for accounting, but not when the supervisor must
act on a result before the task builds on it. For a fork, the new task
may already be running by the time the supervisor reads the record, and
where tasks share memory one could rewrite another's arguments without
making a system call; today such a supervisor needs ptrace fork events.
With SECCOMP_USER_NOTIF_FLAG_HOLD added to CONTINUE | RESULT, the result
is reported as an ordinary notification instead, with
SECCOMP_NOTIF_FLAG_RESULT | SECCOMP_NOTIF_FLAG_HELD, and the task waits
at the point where the record would be queued until the supervisor
answers it with CONTINUE. That point is the same for every system call,
the task work on the way back to user space, so HOLD needs nothing of
its own and is supported wherever RESULT is. For a successful fork it
is just before wake_up_new_task(), so the new task has provably not run
when the supervisor learns of it either. For vfork the parent then
wakes the child and waits for it as usual.
The held notification reuses the user notification machinery: it has
the id of the call's notification, ID_VALID works on it, and a dying
listener releases it. It accepts only a plain CONTINUE and no ADDFD,
since the call has completed and there is nothing to decide.
The task sleeps killable, because the call has completed and cannot be
restarted, and freezable, because the supervisor may be frozen before it
answers. A held fork child is in TASK_NEW and cannot be frozen, so a
held fork still holds off the freezer until it is answered. If the task
is killed before the supervisor received the held notification, the
result is queued as a one-way record instead, so the supervisor still
learns of it. A result reported because the task is exiting is never
held.
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
include/uapi/linux/seccomp.h | 13 ++++++
kernel/seccomp.c | 86 +++++++++++++++++++++++++++++++++---
2 files changed, 92 insertions(+), 7 deletions(-)
diff --git a/include/uapi/linux/seccomp.h b/include/uapi/linux/seccomp.h
index 223577fea182..aa3902be9e72 100644
--- a/include/uapi/linux/seccomp.h
+++ b/include/uapi/linux/seccomp.h
@@ -96,6 +96,7 @@ struct seccomp_notif_result {
/* Valid flags for struct seccomp_notif */
#define SECCOMP_NOTIF_FLAG_RESULT (1UL << 0) /* @result is valid, not @data */
#define SECCOMP_NOTIF_FLAG_THREAD (1UL << 1) /* the new task is a thread */
+#define SECCOMP_NOTIF_FLAG_HELD (1UL << 2) /* answer to release the task */
struct seccomp_notif {
__u64 id;
@@ -150,6 +151,18 @@ struct seccomp_notif {
*/
#define SECCOMP_USER_NOTIF_FLAG_RESULT (1UL << 1)
+/*
+ * SECCOMP_USER_NOTIF_FLAG_HOLD, together with SECCOMP_USER_NOTIF_FLAG_RESULT,
+ * also keeps the task from returning to user space until the listener has
+ * seen the result. The result is then reported with SECCOMP_NOTIF_FLAG_HELD
+ * set, and the task waits until that notification is answered with
+ * SECCOMP_USER_NOTIF_FLAG_CONTINUE (and nothing else). For the fork family,
+ * a new task does not run until then either. If the task is killed before
+ * the listener received the notification, it is reported without
+ * SECCOMP_NOTIF_FLAG_HELD instead. HOLD is supported wherever RESULT is.
+ */
+#define SECCOMP_USER_NOTIF_FLAG_HOLD (1UL << 2)
+
struct seccomp_notif_resp {
__u64 id;
__s64 val;
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 793efcf0e626..1f33449ce571 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -102,6 +102,9 @@ struct seccomp_knotif {
/* The result record the reply asked for, handed over with the reply */
struct seccomp_kresult *kres;
+
+ /* The result this notification reports, holding the task until answered */
+ struct seccomp_kresult *held;
};
/**
@@ -150,6 +153,7 @@ struct seccomp_kaddfd {
* @arch: The system call convention.
* @val: The system call's return value.
* @flags: SECCOMP_NOTIF_FLAG_* flags to report.
+ * @hold: Whether the listener asked to hold the task until it answers.
* @list: list_head for chaining onto notification->results.
*/
struct seccomp_kresult {
@@ -161,6 +165,7 @@ struct seccomp_kresult {
u32 arch;
long val;
u32 flags;
+ bool hold;
struct list_head list;
};
@@ -1232,6 +1237,9 @@ static void seccomp_handle_addfd(struct seccomp_kaddfd *addfd, struct seccomp_kn
static bool should_sleep_killable(struct seccomp_filter *match,
struct seccomp_knotif *n)
{
+ /* A held call has completed and cannot be restarted. */
+ if (n->held)
+ return true;
return match->wait_killable_recv && n->state >= SECCOMP_NOTIFY_SENT;
}
@@ -1265,7 +1273,11 @@ static int seccomp_notify_wait(struct seccomp_filter *match,
bool wait_killable = should_sleep_killable(match, n);
mutex_unlock(&match->notify_lock);
- if (wait_killable)
+ /* The supervisor may be frozen before it can answer. */
+ if (n->held)
+ err = wait_for_completion_state(&n->ready,
+ TASK_KILLABLE | TASK_FREEZABLE);
+ else if (wait_killable)
err = wait_for_completion_killable(&n->ready);
else
err = wait_for_completion_interruptible(&n->ready);
@@ -1320,6 +1332,40 @@ static void seccomp_notify_dequeue(struct seccomp_filter *match,
list_del(&n->list);
}
+static void seccomp_hold_result(struct seccomp_filter *filter,
+ struct seccomp_kresult *kres)
+{
+ struct seccomp_knotif n = {};
+
+ n.task = current;
+ n.id = kres->id;
+ n.held = kres;
+ seccomp_notify_queue(filter, &n);
+
+ if (seccomp_notify_wait(filter, &n) &&
+ n.state == SECCOMP_NOTIFY_INIT) {
+ /* Killed unseen: the listener must still learn the result. */
+ seccomp_queue_result(filter, kres);
+ kres = NULL;
+ }
+
+ seccomp_notify_dequeue(filter, &n);
+ if (kres)
+ seccomp_kresult_free(kres);
+}
+
+static void seccomp_deliver_result(struct seccomp_kresult *kres)
+{
+ struct seccomp_filter *filter = kres->filter;
+
+ mutex_lock(&filter->notify_lock);
+ if (kres->hold && filter->notif)
+ seccomp_hold_result(filter, kres);
+ else
+ seccomp_queue_result(filter, kres);
+ mutex_unlock(&filter->notify_lock);
+}
+
/* Report the system call's outcome on the way back to user space. */
static void seccomp_result_work(struct callback_head *work)
{
@@ -1333,7 +1379,7 @@ static void seccomp_result_work(struct callback_head *work)
err = syscall_get_error(current, regs);
kres->val = err ? err : syscall_get_return_value(current, regs);
- seccomp_report_result(kres);
+ seccomp_deliver_result(kres);
}
static void seccomp_result_pending(struct seccomp_kresult *kres)
@@ -1423,7 +1469,7 @@ void seccomp_report_fork(struct task_struct *child, pid_t nr)
kres->child = get_task_pid(child, PIDTYPE_PID);
if (!thread_group_leader(child))
kres->flags |= SECCOMP_NOTIF_FLAG_THREAD;
- seccomp_report_result(kres);
+ seccomp_deliver_result(kres);
}
static bool __seccomp_filter(int this_syscall, const bool recheck_after_trace)
@@ -1823,9 +1869,18 @@ static long seccomp_notify_recv(struct seccomp_filter *filter,
goto out;
}
- unotif.id = knotif->id;
- unotif.pid = task_pid_vnr(knotif->task);
- unotif.data = *(knotif->data);
+ if (knotif->held) {
+ ret = seccomp_fill_result(&unotif, knotif->held);
+ if (ret < 0) {
+ atomic_inc(&filter->notif->requests);
+ goto out;
+ }
+ unotif.flags |= SECCOMP_NOTIF_FLAG_HELD;
+ } else {
+ unotif.id = knotif->id;
+ unotif.pid = task_pid_vnr(knotif->task);
+ unotif.data = *(knotif->data);
+ }
knotif->state = SECCOMP_NOTIFY_SENT;
wake_up_poll(&filter->wqh, EPOLLOUT | EPOLLWRNORM);
@@ -1901,7 +1956,12 @@ static long seccomp_notify_send(struct seccomp_filter *filter,
return -EFAULT;
if (resp.flags & ~(SECCOMP_USER_NOTIF_FLAG_CONTINUE |
- SECCOMP_USER_NOTIF_FLAG_RESULT))
+ SECCOMP_USER_NOTIF_FLAG_RESULT |
+ SECCOMP_USER_NOTIF_FLAG_HOLD))
+ return -EINVAL;
+
+ if ((resp.flags & SECCOMP_USER_NOTIF_FLAG_HOLD) &&
+ !(resp.flags & SECCOMP_USER_NOTIF_FLAG_RESULT))
return -EINVAL;
if ((resp.flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE) &&
@@ -1936,6 +1996,11 @@ static long seccomp_notify_send(struct seccomp_filter *filter,
goto out;
}
+ if (knotif->held && resp.flags != SECCOMP_USER_NOTIF_FLAG_CONTINUE) {
+ ret = -EINVAL;
+ goto out;
+ }
+
if (kres) {
if (!seccomp_result_supported(knotif->data)) {
ret = -EINVAL;
@@ -1946,6 +2011,7 @@ static long seccomp_notify_send(struct seccomp_filter *filter,
kres->task = get_task_pid(knotif->task, PIDTYPE_PID);
kres->nr = knotif->data->nr;
kres->arch = knotif->data->arch;
+ kres->hold = resp.flags & SECCOMP_USER_NOTIF_FLAG_HOLD;
knotif->kres = kres;
kres = NULL;
}
@@ -2064,6 +2130,12 @@ static long seccomp_notify_addfd(struct seccomp_filter *filter,
goto out_unlock;
}
+ /* A held call has completed; there is nothing to add to. */
+ if (knotif->held) {
+ ret = -EINVAL;
+ goto out_unlock;
+ }
+
if (addfd.flags & SECCOMP_ADDFD_FLAG_SEND) {
/*
* Disallow queuing an atomic addfd + send reply while there are
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 7/9] selftests/seccomp: test fork result records
2026-10-20 0:11 [RFC PATCH 0/9] seccomp: report the result of continued system calls Cong Wang
` (5 preceding siblings ...)
2026-10-20 0:11 ` [RFC PATCH 6/9] seccomp: add SECCOMP_USER_NOTIF_FLAG_HOLD Cong Wang
@ 2026-10-20 0:11 ` Cong Wang
2026-10-20 0:11 ` [RFC PATCH 8/9] selftests/seccomp: test held fork results Cong Wang
2026-10-20 0:11 ` [RFC PATCH 9/9] Documentation: seccomp: describe system call result records Cong Wang
8 siblings, 0 replies; 10+ messages in thread
From: Cong Wang @ 2026-10-20 0:11 UTC (permalink / raw)
To: linux-kernel; +Cc: Andy Lutomirski, Kees Cook, Will Drewry, Christian Brauner
From: Cong Wang <cwang@multikernel.io>
Cover SECCOMP_USER_NOTIF_FLAG_RESULT:
- a successful fork is reported with the entry's id, the child's pid
and pidfs inode number, the child can be opened as a pidfd by that
inode number, and its first notification arrives only after the
record, even when it was queued first;
- forks rejected by kernel_clone(), copy_process() and clone3() are
reported with their error, and an unread record is freed with the
listener;
- a new thread is flagged as one;
- the flag is refused for a non-fork system call and without CONTINUE.
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
tools/testing/selftests/seccomp/seccomp_bpf.c | 389 ++++++++++++++++++
1 file changed, 389 insertions(+)
diff --git a/tools/testing/selftests/seccomp/seccomp_bpf.c b/tools/testing/selftests/seccomp/seccomp_bpf.c
index 0622bc2acad4..d5dd5bc22faa 100644
--- a/tools/testing/selftests/seccomp/seccomp_bpf.c
+++ b/tools/testing/selftests/seccomp/seccomp_bpf.c
@@ -47,6 +47,7 @@
#include <sys/ioctl.h>
#include <linux/kcmp.h>
#include <sys/resource.h>
+#include <sys/stat.h>
#include <sys/capability.h>
#include <linux/perf_event.h>
@@ -4964,6 +4965,394 @@ TEST(user_notification_wait_killable_after_reply)
EXPECT_EQ(0, WEXITSTATUS(status));
}
+#ifndef FD_PIDFS_ROOT
+#define FD_PIDFS_ROOT -10002
+#endif
+
+#ifndef FILEID_KERNFS
+#define FILEID_KERNFS 0xfe
+#endif
+
+static int user_notif_fork_filter(void)
+{
+ struct sock_filter filter[] = {
+ BPF_STMT(BPF_LD|BPF_W|BPF_ABS,
+ offsetof(struct seccomp_data, nr)),
+ BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, __NR_clone, 2, 0),
+ BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, __NR_clone3, 1, 0),
+ BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, __NR_getppid, 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 = (unsigned short)ARRAY_SIZE(filter),
+ .filter = filter,
+ };
+
+ return seccomp(SECCOMP_SET_MODE_FILTER,
+ SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog);
+}
+
+static int send_fd(int sock, int fd)
+{
+ char cbuf[CMSG_SPACE(sizeof(int))] = {};
+ char c = 0;
+ struct iovec iov = { .iov_base = &c, .iov_len = 1 };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = cbuf,
+ .msg_controllen = sizeof(cbuf),
+ };
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+ memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
+
+ return sendmsg(sock, &msg, 0) == 1 ? 0 : -1;
+}
+
+static int recv_fd(int sock)
+{
+ char cbuf[CMSG_SPACE(sizeof(int))] = {};
+ char c;
+ struct iovec iov = { .iov_base = &c, .iov_len = 1 };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = cbuf,
+ .msg_controllen = sizeof(cbuf),
+ };
+ struct cmsghdr *cmsg;
+ int fd;
+
+ if (recvmsg(sock, &msg, 0) != 1)
+ return -1;
+ cmsg = CMSG_FIRSTHDR(&msg);
+ if (!cmsg || cmsg->cmsg_type != SCM_RIGHTS)
+ return -1;
+ memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
+ return fd;
+}
+
+/*
+ * Fork a child that notifies clone, clone3 and getppid to a listener,
+ * hands the listener to us and then exits with what @fn returns.
+ */
+static pid_t fork_result_child(struct __test_metadata *_metadata,
+ int (*fn)(void), int *listener)
+{
+ int sk[2];
+ pid_t pid;
+
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sk), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+
+ if (pid == 0) {
+ int fd;
+
+ close(sk[0]);
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
+ _exit(100);
+ fd = user_notif_fork_filter();
+ if (fd < 0 || send_fd(sk[1], fd))
+ _exit(101);
+ close(fd);
+ close(sk[1]);
+ _exit(fn());
+ }
+
+ close(sk[1]);
+ *listener = recv_fd(sk[0]);
+ close(sk[0]);
+ ASSERT_GE(*listener, 0);
+ return pid;
+}
+
+static bool is_fork_nr(int nr)
+{
+ return nr == __NR_clone || nr == __NR_clone3;
+}
+
+/* Receive a fork family notification and continue it, asking for a record. */
+static void continue_with_result(struct __test_metadata *_metadata,
+ int listener, pid_t pid,
+ struct seccomp_notif *req)
+{
+ struct seccomp_notif_resp resp = {};
+
+ memset(req, 0, sizeof(*req));
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, req), 0);
+ ASSERT_EQ(req->flags, 0);
+ ASSERT_EQ(req->pid, pid);
+ ASSERT_TRUE(is_fork_nr(req->data.nr));
+
+ resp.id = req->id;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE |
+ SECCOMP_USER_NOTIF_FLAG_RESULT;
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0) {
+ if (errno == EINVAL)
+ SKIP(return, "Kernel does not support SECCOMP_USER_NOTIF_FLAG_RESULT");
+ }
+}
+
+static void recv_result(struct __test_metadata *_metadata, int listener,
+ const struct seccomp_notif *entry,
+ struct seccomp_notif *rec)
+{
+ memset(rec, 0, sizeof(*rec));
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, rec), 0);
+ ASSERT_NE(rec->flags & SECCOMP_NOTIF_FLAG_RESULT, 0);
+ EXPECT_EQ(rec->id, entry->id);
+ EXPECT_EQ(rec->pid, entry->pid);
+ EXPECT_EQ(rec->result.nr, entry->data.nr);
+ EXPECT_EQ(rec->result.arch, entry->data.arch);
+ EXPECT_EQ(rec->result.__reserved, 0);
+}
+
+static int pidfd_open_by_ino(__u64 ino, int flags)
+{
+ /* struct file_handle with room for a pidfs handle */
+ struct {
+ unsigned int handle_bytes;
+ int handle_type;
+ __u64 ino;
+ } fh = {
+ .handle_bytes = sizeof(ino),
+ .handle_type = FILEID_KERNFS,
+ .ino = ino,
+ };
+
+ return open_by_handle_at(FD_PIDFS_ROOT, (struct file_handle *)&fh,
+ flags);
+}
+
+static int fork_result_fork(void)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ return 1;
+ if (pid == 0)
+ _exit(syscall(__NR_getppid) != USER_NOTIF_MAGIC);
+ if (waitpid(pid, &status, 0) != pid)
+ return 2;
+ return !WIFEXITED(status) || WEXITSTATUS(status);
+}
+
+TEST(user_notification_fork_result)
+{
+ struct seccomp_notif entry, rec, req = {};
+ struct seccomp_notif_resp resp = {};
+ struct pollfd pollfd;
+ int status, listener, pidfd;
+ struct stat st;
+ pid_t pid;
+
+ pid = fork_result_child(_metadata, fork_result_fork, &listener);
+ continue_with_result(_metadata, listener, pid, &entry);
+
+ /* Let the new child queue its getppid() notification first. */
+ usleep(100000);
+
+ recv_result(_metadata, listener, &entry, &rec);
+ EXPECT_EQ(rec.flags, SECCOMP_NOTIF_FLAG_RESULT);
+ ASSERT_GT(rec.result.val, 0);
+ EXPECT_EQ(rec.result.child_pid, rec.result.val);
+ ASSERT_NE(rec.result.child_ino, 0);
+
+ /* The record names the child race free, without privileges. */
+ pidfd = pidfd_open_by_ino(rec.result.child_ino, 0);
+ ASSERT_GE(pidfd, 0);
+ ASSERT_EQ(fstat(pidfd, &st), 0);
+ EXPECT_EQ(st.st_ino, rec.result.child_ino);
+
+ /* The child is only heard from after its record. */
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+ EXPECT_EQ(req.flags, 0);
+ EXPECT_EQ(req.pid, rec.result.child_pid);
+ EXPECT_EQ(req.data.nr, __NR_getppid);
+
+ resp.id = req.id;
+ resp.val = USER_NOTIF_MAGIC;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+ pollfd.fd = pidfd;
+ pollfd.events = POLLIN;
+ EXPECT_EQ(poll(&pollfd, 1, -1), 1);
+ close(pidfd);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_EQ(true, WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+ close(listener);
+}
+
+static int fork_result_fail(void)
+{
+ struct __clone_args args = {};
+ int fd = -1;
+
+ /* Rejected by kernel_clone() before copy_process(). */
+ args.flags = CLONE_PIDFD | CLONE_PARENT_SETTID;
+ args.pidfd = (unsigned long)&fd;
+ args.parent_tid = (unsigned long)&fd;
+ args.exit_signal = SIGCHLD;
+ if (sys_clone3(&args, sizeof(args)) != -1 || errno != EINVAL)
+ return 1;
+
+ /* Rejected by copy_process(). */
+ memset(&args, 0, sizeof(args));
+ args.flags = CLONE_SIGHAND;
+ args.exit_signal = SIGCHLD;
+ if (sys_clone3(&args, sizeof(args)) != -1 || errno != EINVAL)
+ return 2;
+
+ /* Rejected by clone3() itself. */
+ if (sys_clone3(&args, 1) != -1 || errno != EINVAL)
+ return 3;
+
+ return 0;
+}
+
+TEST(user_notification_fork_result_fail)
+{
+ struct seccomp_notif entry, rec;
+ struct pollfd pollfd;
+ int i, status, listener;
+ pid_t pid;
+
+ pid = fork_result_child(_metadata, fork_result_fail, &listener);
+
+ for (i = 0; i < 3; i++) {
+ continue_with_result(_metadata, listener, pid, &entry);
+ EXPECT_EQ(entry.data.nr, __NR_clone3);
+ if (i == 2)
+ break;
+ recv_result(_metadata, listener, &entry, &rec);
+ EXPECT_EQ(rec.flags, SECCOMP_NOTIF_FLAG_RESULT);
+ EXPECT_EQ(rec.result.val, -EINVAL);
+ EXPECT_EQ(rec.result.child_pid, 0);
+ EXPECT_EQ(rec.result.child_ino, 0);
+ }
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_EQ(true, WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ /* The last record outlives its task, and closing frees it unread. */
+ pollfd.fd = listener;
+ pollfd.events = POLLIN;
+ EXPECT_EQ(poll(&pollfd, 1, 0), 1);
+ EXPECT_NE(pollfd.revents & POLLIN, 0);
+ close(listener);
+}
+
+static void *fork_result_noop(void *arg)
+{
+ return arg;
+}
+
+static int fork_result_thread(void)
+{
+ pthread_t thread;
+
+ if (pthread_create(&thread, NULL, fork_result_noop, NULL))
+ return 1;
+ return pthread_join(thread, NULL) != 0;
+}
+
+TEST(user_notification_fork_result_thread)
+{
+ struct seccomp_notif entry, rec;
+ int status, listener;
+ pid_t pid;
+
+ pid = fork_result_child(_metadata, fork_result_thread, &listener);
+ continue_with_result(_metadata, listener, pid, &entry);
+
+ recv_result(_metadata, listener, &entry, &rec);
+ EXPECT_EQ(rec.flags,
+ SECCOMP_NOTIF_FLAG_RESULT | SECCOMP_NOTIF_FLAG_THREAD);
+ ASSERT_GT(rec.result.val, 0);
+ EXPECT_EQ(rec.result.child_pid, rec.result.val);
+ EXPECT_NE(rec.result.child_ino, 0);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_EQ(true, WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+ close(listener);
+}
+
+static int fork_result_einval(void)
+{
+ if (syscall(__NR_getppid) != USER_NOTIF_MAGIC)
+ return 1;
+ return fork_result_fork();
+}
+
+TEST(user_notification_fork_result_einval)
+{
+ struct seccomp_notif_resp resp = {};
+ struct seccomp_notif req = {};
+ struct pollfd pollfd;
+ int status, listener;
+ pid_t pid;
+
+ pid = fork_result_child(_metadata, fork_result_einval, &listener);
+
+ /* Not a fork. */
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+ EXPECT_EQ(req.data.nr, __NR_getppid);
+ resp.id = req.id;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE |
+ SECCOMP_USER_NOTIF_FLAG_RESULT;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), -1);
+ EXPECT_EQ(errno, EINVAL);
+ resp.flags = 0;
+ resp.val = USER_NOTIF_MAGIC;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+ /* A fork, but RESULT needs CONTINUE. */
+ memset(&req, 0, sizeof(req));
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+ EXPECT_TRUE(is_fork_nr(req.data.nr));
+ resp.id = req.id;
+ resp.val = 0;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_RESULT;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), -1);
+ EXPECT_EQ(errno, EINVAL);
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+ /* The grandchild's getppid(). */
+ memset(&req, 0, sizeof(req));
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+ EXPECT_EQ(req.flags, 0);
+ EXPECT_EQ(req.data.nr, __NR_getppid);
+ resp.id = req.id;
+ resp.flags = 0;
+ resp.val = USER_NOTIF_MAGIC;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_EQ(true, WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ /* No record without RESULT. */
+ pollfd.fd = listener;
+ pollfd.events = POLLIN;
+ EXPECT_EQ(poll(&pollfd, 1, 0), 1);
+ EXPECT_EQ(pollfd.revents & POLLIN, 0);
+ close(listener);
+}
+
struct tsync_vs_thread_leader_args {
pthread_t leader;
};
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 8/9] selftests/seccomp: test held fork results
2026-10-20 0:11 [RFC PATCH 0/9] seccomp: report the result of continued system calls Cong Wang
` (6 preceding siblings ...)
2026-10-20 0:11 ` [RFC PATCH 7/9] selftests/seccomp: test fork result records Cong Wang
@ 2026-10-20 0:11 ` Cong Wang
2026-10-20 0:11 ` [RFC PATCH 9/9] Documentation: seccomp: describe system call result records Cong Wang
8 siblings, 0 replies; 10+ messages in thread
From: Cong Wang @ 2026-10-20 0:11 UTC (permalink / raw)
To: linux-kernel; +Cc: Andy Lutomirski, Kees Cook, Will Drewry, Christian Brauner
From: Cong Wang <cwang@multikernel.io>
Cover SECCOMP_USER_NOTIF_FLAG_HOLD: a held child does not run until the
held notification is continued, the held notification refuses anything
but a plain CONTINUE as well as ADDFD, a failed fork is held too and
keeps the parent from returning, killing the parent turns the held
notification into a one-way record and releases the child, and closing
the listener releases the child too.
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
tools/testing/selftests/seccomp/seccomp_bpf.c | 190 ++++++++++++++++++
1 file changed, 190 insertions(+)
diff --git a/tools/testing/selftests/seccomp/seccomp_bpf.c b/tools/testing/selftests/seccomp/seccomp_bpf.c
index d5dd5bc22faa..80faa3a35cf4 100644
--- a/tools/testing/selftests/seccomp/seccomp_bpf.c
+++ b/tools/testing/selftests/seccomp/seccomp_bpf.c
@@ -5353,6 +5353,196 @@ TEST(user_notification_fork_result_einval)
close(listener);
}
+static int hold_pipe[2];
+
+static int fork_hold_fork(void)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ return 1;
+ if (pid == 0)
+ _exit(write(hold_pipe[1], "x", 1) != 1);
+ if (waitpid(pid, &status, 0) != pid)
+ return 2;
+ return !WIFEXITED(status) || WEXITSTATUS(status);
+}
+
+static int fork_hold_fail(void)
+{
+ struct __clone_args args = {};
+ bool failed = sys_clone3(&args, 1) == -1 && errno == EINVAL;
+
+ return write(hold_pipe[1], "x", 1) != 1 || !failed;
+}
+
+/* Start a sandboxed fork, continue it with HOLD and wait until it is held. */
+static pid_t fork_hold_start(struct __test_metadata *_metadata,
+ int (*fn)(void), int *listener,
+ struct seccomp_notif *entry)
+{
+ struct seccomp_notif_resp resp = {};
+ struct pollfd pollfd;
+ pid_t pid;
+
+ ASSERT_EQ(pipe(hold_pipe), 0);
+ pid = fork_result_child(_metadata, fn, listener);
+ close(hold_pipe[1]);
+
+ memset(entry, 0, sizeof(*entry));
+ ASSERT_EQ(ioctl(*listener, SECCOMP_IOCTL_NOTIF_RECV, entry), 0);
+ ASSERT_TRUE(is_fork_nr(entry->data.nr));
+
+ resp.id = entry->id;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_HOLD;
+ EXPECT_EQ(ioctl(*listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), -1);
+ EXPECT_EQ(errno, EINVAL);
+
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE |
+ SECCOMP_USER_NOTIF_FLAG_RESULT |
+ SECCOMP_USER_NOTIF_FLAG_HOLD;
+ ASSERT_EQ(ioctl(*listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0) {
+ if (errno == EINVAL)
+ SKIP(return pid, "Kernel does not support SECCOMP_USER_NOTIF_FLAG_HOLD");
+ }
+
+ pollfd.fd = *listener;
+ pollfd.events = POLLIN;
+ EXPECT_EQ(poll(&pollfd, 1, -1), 1);
+ return pid;
+}
+
+/* Has the held task run past the hold? */
+static bool hold_released(int timeout_ms)
+{
+ struct pollfd pollfd = { .fd = hold_pipe[0], .events = POLLIN };
+ char c;
+
+ return poll(&pollfd, 1, timeout_ms) == 1 &&
+ read(hold_pipe[0], &c, 1) == 1 && c == 'x';
+}
+
+TEST(user_notification_fork_hold)
+{
+ struct seccomp_notif_addfd addfd = {};
+ struct seccomp_notif_resp resp = {};
+ struct seccomp_notif entry, rec;
+ int status, listener;
+ pid_t pid;
+
+ pid = fork_hold_start(_metadata, fork_hold_fork, &listener, &entry);
+
+ recv_result(_metadata, listener, &entry, &rec);
+ EXPECT_EQ(rec.flags, SECCOMP_NOTIF_FLAG_RESULT | SECCOMP_NOTIF_FLAG_HELD);
+ ASSERT_GT(rec.result.val, 0);
+ EXPECT_EQ(rec.result.child_pid, rec.result.val);
+ EXPECT_NE(rec.result.child_ino, 0);
+
+ EXPECT_EQ(hold_released(200), false);
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_ID_VALID, &rec.id), 0);
+
+ /* A held fork can only be continued. */
+ resp.id = rec.id;
+ resp.error = -EPERM;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), -1);
+ EXPECT_EQ(errno, EINVAL);
+ resp.error = 0;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE |
+ SECCOMP_USER_NOTIF_FLAG_RESULT;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), -1);
+ EXPECT_EQ(errno, EINVAL);
+
+ addfd.id = rec.id;
+ addfd.srcfd = listener;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd), -1);
+ EXPECT_EQ(errno, EINVAL);
+
+ EXPECT_EQ(hold_released(0), false);
+
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+ EXPECT_EQ(hold_released(-1), true);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_EQ(true, WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+ close(hold_pipe[0]);
+ close(listener);
+}
+
+/* A held result need not be a new task: the parent does not return yet. */
+TEST(user_notification_fork_hold_fail)
+{
+ struct seccomp_notif_resp resp = {};
+ struct seccomp_notif entry, rec;
+ int status, listener;
+ pid_t pid;
+
+ pid = fork_hold_start(_metadata, fork_hold_fail, &listener, &entry);
+
+ recv_result(_metadata, listener, &entry, &rec);
+ EXPECT_EQ(rec.flags, SECCOMP_NOTIF_FLAG_RESULT | SECCOMP_NOTIF_FLAG_HELD);
+ EXPECT_EQ(rec.result.val, -EINVAL);
+ EXPECT_EQ(rec.result.child_pid, 0);
+ EXPECT_EQ(rec.result.child_ino, 0);
+
+ EXPECT_EQ(hold_released(200), false);
+ resp.id = rec.id;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+ EXPECT_EQ(hold_released(-1), true);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_EQ(true, WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+ close(hold_pipe[0]);
+ close(listener);
+}
+
+TEST(user_notification_fork_hold_killed)
+{
+ struct seccomp_notif entry, rec;
+ int status, listener;
+ pid_t pid;
+
+ pid = fork_hold_start(_metadata, fork_hold_fork, &listener, &entry);
+
+ /* Kill the parent before the held fork was received. */
+ EXPECT_EQ(kill(pid, SIGKILL), 0);
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_EQ(true, WIFSIGNALED(status));
+ EXPECT_EQ(SIGKILL, WTERMSIG(status));
+
+ /* The child is still reported, no longer held, and runs. */
+ recv_result(_metadata, listener, &entry, &rec);
+ EXPECT_EQ(rec.flags, SECCOMP_NOTIF_FLAG_RESULT);
+ EXPECT_GT(rec.result.val, 0);
+ EXPECT_NE(rec.result.child_ino, 0);
+ EXPECT_EQ(hold_released(-1), true);
+ close(hold_pipe[0]);
+ close(listener);
+}
+
+TEST(user_notification_fork_hold_closed)
+{
+ struct seccomp_notif entry;
+ int status, listener;
+ pid_t pid;
+
+ pid = fork_hold_start(_metadata, fork_hold_fork, &listener, &entry);
+
+ /* Losing the listener releases a held fork. */
+ close(listener);
+ EXPECT_EQ(hold_released(-1), true);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_EQ(true, WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+ close(hold_pipe[0]);
+}
+
struct tsync_vs_thread_leader_args {
pthread_t leader;
};
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 9/9] Documentation: seccomp: describe system call result records
2026-10-20 0:11 [RFC PATCH 0/9] seccomp: report the result of continued system calls Cong Wang
` (7 preceding siblings ...)
2026-10-20 0:11 ` [RFC PATCH 8/9] selftests/seccomp: test held fork results Cong Wang
@ 2026-10-20 0:11 ` Cong Wang
8 siblings, 0 replies; 10+ messages in thread
From: Cong Wang @ 2026-10-20 0:11 UTC (permalink / raw)
To: linux-kernel; +Cc: Andy Lutomirski, Kees Cook, Will Drewry, Christian Brauner
From: Cong Wang <cwang@multikernel.io>
Document SECCOMP_USER_NOTIF_FLAG_RESULT and SECCOMP_USER_NOTIF_FLAG_HOLD:
the record they produce, what its return value means after a signal,
what the fork family adds to it, how to open a pidfd for a new task from
it, and how a held result is reported and answered.
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
.../userspace-api/seccomp_filter.rst | 117 +++++++++++++++++-
1 file changed, 116 insertions(+), 1 deletion(-)
diff --git a/Documentation/userspace-api/seccomp_filter.rst b/Documentation/userspace-api/seccomp_filter.rst
index cff0fa7f3175..a4c9fcb4d204 100644
--- a/Documentation/userspace-api/seccomp_filter.rst
+++ b/Documentation/userspace-api/seccomp_filter.rst
@@ -224,7 +224,10 @@ The interface for a seccomp notification fd consists of two structures:
__u64 id;
__u32 pid;
__u32 flags;
- struct seccomp_data data;
+ union {
+ struct seccomp_data data;
+ struct seccomp_notif_result result;
+ };
};
struct seccomp_notif_resp {
@@ -281,6 +284,118 @@ 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.
+System call results
+-------------------
+
+A supervisor that continues a system call can ask how the call turned out by
+adding ``SECCOMP_USER_NOTIF_FLAG_RESULT`` to
+``SECCOMP_USER_NOTIF_FLAG_CONTINUE`` in its response. The set of supported
+system calls may grow; so far it is ``fork()``, ``vfork()``, ``clone()`` and
+``clone3()``. The flag is refused with ``EINVAL`` for any other system call,
+for system call conventions the kernel cannot classify, and by kernels that
+do not support it, so trying it is a reliable probe.
+
+The outcome is reported once, as a notification with
+``SECCOMP_NOTIF_FLAG_RESULT`` set in ``flags`` and the same ``id`` as the
+continued notification. It is a record, not a request: it is not answered,
+and the task does not wait for it to be read. It is queued by the time the
+task returns to user space, so it arrives before the task's next
+notification. Its ``pid`` is the task that made the call, and ``result``
+replaces ``data``:
+
+.. code-block:: c
+
+ struct seccomp_notif_result {
+ __s32 nr;
+ __u32 arch;
+ __s64 val;
+ __u32 child_pid;
+ __u32 __reserved;
+ __u64 child_ino;
+ };
+
+``nr`` and ``arch`` identify the system call. ``val`` is its return value as
+the task saw it, including any change made at a ptrace syscall exit stop. A
+negative value is an error number:
+
+- One of the kernel internal ``-ERESTART*`` values (-512 to -516) means the
+ call did not complete because of a signal. It is then either restarted,
+ and passes through the filter again as a new notification, or fails with
+ ``EINTR``.
+- If a signal interrupted the notification after the response was sent, the
+ call never ran, and the record reports the error it fails with.
+- If the task exits before the call returns, ``val`` is ``-ESRCH``.
+
+Fork results
+~~~~~~~~~~~~
+
+A successful fork also fills in ``child_pid``, the new task in the listener's
+pid namespace, and ``child_ino``, its pidfs inode number.
+``SECCOMP_NOTIF_FLAG_THREAD`` is set if the new task is a thread. Both fields
+are zero if the fork failed.
+
+The record of a successful fork is queued before the new task first runs, and
+records are received before any pending notification, so the supervisor
+always hears of a task before it hears from it. The new task inherits the
+filter, so its own forks are reported the same way, and so on down the tree.
+
+The task may have run, exited and been reaped by the time the record is read,
+so its pid may already name another task. ``child_ino`` is never reused:
+opening a pidfd by it either yields exactly the new task or fails, in which
+case the task has already exited:
+
+.. code-block:: c
+
+ struct {
+ unsigned int handle_bytes;
+ int handle_type;
+ __u64 ino;
+ } fh = { sizeof(__u64), FILEID_KERNFS /* 0xfe */, rec.result.child_ino };
+
+ pidfd = open_by_handle_at(FD_PIDFS_ROOT, (struct file_handle *)&fh, 0);
+
+This needs no privileges. Together with the pidfd's exit notification it lets
+a supervisor track every task below it, including one that is killed before
+it makes a single notified system call.
+
+Holding a result
+~~~~~~~~~~~~~~~~
+
+A record is one-way: the task does not wait for it to be read. Where the
+supervisor must act on a result before the task goes on, the response can
+carry ``SECCOMP_USER_NOTIF_FLAG_HOLD`` together with
+``SECCOMP_USER_NOTIF_FLAG_RESULT`` and ``SECCOMP_USER_NOTIF_FLAG_CONTINUE``.
+It is supported for the same system calls as ``SECCOMP_USER_NOTIF_FLAG_RESULT``
+and refused with ``EINVAL`` for the others.
+
+The result is then reported as a held notification instead of a record:
+``flags`` has both ``SECCOMP_NOTIF_FLAG_RESULT`` and
+``SECCOMP_NOTIF_FLAG_HELD`` set, ``id`` is that of the continued
+notification, and ``result`` is filled in as above. The task does not return
+to user space until the held notification is answered, whether the call
+succeeded or failed.
+
+For the fork family, a successful fork is held before the new task is woken,
+so neither the parent nor the new task runs until the answer. This is for a
+supervisor that must know a new task before it executes anything at all, for
+example when tasks share memory and one could change another's arguments
+without a system call.
+
+A held notification is answered like any other, except:
+
+- The only accepted response is ``SECCOMP_USER_NOTIF_FLAG_CONTINUE`` with
+ ``val`` and ``error`` zero. Anything else fails with ``EINVAL``; the call
+ has completed and its result cannot be changed.
+- ``SECCOMP_IOCTL_NOTIF_ADDFD`` fails with ``EINVAL``.
+- ``SECCOMP_IOCTL_NOTIF_ID_VALID`` works as usual.
+
+The task waits killable, so non-fatal signals do not end the wait. If the task
+is killed before the supervisor received the held notification, it is turned
+into an ordinary record, so the supervisor still learns the result. A result
+reported because the task exits (``-ESRCH``) is never held. If the listener is
+closed, the task continues. For a fork, the new task is woken once the wait
+ends in every case.
+
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] 10+ messages in thread
end of thread, other threads:[~2026-10-20 0:11 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-20 0:11 [RFC PATCH 0/9] seccomp: report the result of continued system calls Cong Wang
2026-10-20 0:11 ` [RFC PATCH 1/9] seccomp: add result records to the notification listener Cong Wang
2026-10-20 0:11 ` [RFC PATCH 2/9] seccomp: add __NR_seccomp_* aliases for the fork family Cong Wang
2026-10-20 0:11 ` [RFC PATCH 3/9] seccomp: report the result of a continued system call Cong Wang
2026-10-20 0:11 ` [RFC PATCH 4/9] seccomp: report the result of a continued fork Cong Wang
2026-10-20 0:11 ` [RFC PATCH 5/9] seccomp: factor out the user notification queue and wait Cong Wang
2026-10-20 0:11 ` [RFC PATCH 6/9] seccomp: add SECCOMP_USER_NOTIF_FLAG_HOLD Cong Wang
2026-10-20 0:11 ` [RFC PATCH 7/9] selftests/seccomp: test fork result records Cong Wang
2026-10-20 0:11 ` [RFC PATCH 8/9] selftests/seccomp: test held fork results Cong Wang
2026-10-20 0:11 ` [RFC PATCH 9/9] Documentation: seccomp: describe system call result records 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®