* [RFC PATCH v2 1/3] seccomp: add SECCOMP_IOCTL_NOTIF_INJECT for race-free unotify
2026-05-15 4:27 [RFC PATCH v2 0/3] seccomp: SECCOMP_IOCTL_NOTIF_INJECT for race-free unotify Cong Wang
@ 2026-05-15 4:27 ` Cong Wang
2026-05-15 4:27 ` [RFC PATCH v2 2/3] selftests/seccomp: add seccomp_notif_inject coverage Cong Wang
` (3 subsequent siblings)
4 siblings, 0 replies; 9+ messages in thread
From: Cong Wang @ 2026-05-15 4:27 UTC (permalink / raw)
To: Kees Cook, linux-kernel
Cc: Andy Lutomirski, Will Drewry, Christian Brauner, Cong Wang
From: Cong Wang <cwang@multikernel.io>
seccomp_unotify(2) leaves a documented TOCTOU window for unprivileged
supervisors: a sibling thread or CLONE_VM peer can mutate pointer-arg
buffers between the supervisor's process_vm_readv() and the kernel's
re-read on SECCOMP_USER_NOTIF_FLAG_CONTINUE. ptrace()/proc/pid/mem
are not available to unprivileged supervisors, so today there is no
race-free path for content-aware policy on CONTINUE.
Reshape the v1 PIN_ARGS proposal as a PTRACE_SYSCALL-style redirect.
The supervisor describes a substitute syscall via a single new ioctl
SECCOMP_IOCTL_NOTIF_INJECT. The struct mirrors ptrace_syscall_info.entry
(nr + args[6]) and adds a kernel-input buffer plus an args_in_buf_mask
bitmap; pointer-shaped args are encoded as byte offsets into that
buffer. SECCOMP_IOCTL_NOTIF_SEND with SECCOMP_USER_NOTIF_FLAG_INJECTED
consumes the redirect: the trapped task wakes, dispatches into the
matching kernel-mode syscall helper, and the helper's return value
becomes the trapped syscall's return value. The trapped task's user
mm is never re-read for the substituted syscall, so peer mutations
after attach have no effect.
The v1 injectable-syscall whitelist is openat (filp_open + fd_install),
bind (sockfd_lookup + kernel_bind) and write (kernel_write). The
substitute nr must match the trapped syscall's number, preventing a
malicious supervisor from converting "task tried to bind()" into
"kernel does an openat() on the task's behalf". Total cumulative
buffer size is capped at a hardcoded 1 MiB; allocations are
GFP_KERNEL_ACCOUNT so the trapped task's memcg pays the cost.
This is intentionally a strict subset of what ptrace can already do
via PTRACE_POKEDATA + PTRACE_SETREGSET. It does not add a kernel
capability. It provides a listener-fd-gated, syscall-whitelisted,
narrower interface to that capability suitable for unprivileged
seccomp_unotify supervisors, where ptrace's privilege model
(CAP_SYS_PTRACE) and per-syscall overhead (signal-stop cycle plus
O(N) peer-thread coordination per syscall) are not viable.
SECCOMP_IOCTL_NOTIF_ADDFD set the precedent for this kind of narrow
listener-fd interface to a ptrace-overlapping capability.
Lifecycle: the inject record attaches to the knotif on the INJECT
ioctl and is consumed at NOTIF_SEND with FLAG_INJECTED. It is freed
on listener close, task exit, supervisor changing its mind (CONTINUE
without INJECTED, or plain deny), or SIGKILL of the trapped task.
The whole feature lives in a new kernel/seccomp_inject.c plus a small
dispatcher in kernel/seccomp.c. fs/, mm/, net/ and lib/ are
unmodified.
Assisted-by: Claude:claude-opus-4.6
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
MAINTAINERS | 2 +
include/uapi/linux/seccomp.h | 65 ++++++++
kernel/Makefile | 1 +
kernel/seccomp.c | 121 ++++++++++++++-
kernel/seccomp_inject.c | 281 +++++++++++++++++++++++++++++++++++
kernel/seccomp_inject.h | 65 ++++++++
6 files changed, 533 insertions(+), 2 deletions(-)
create mode 100644 kernel/seccomp_inject.c
create mode 100644 kernel/seccomp_inject.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 6aa3fe2ee1bb..120f913b58b1 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -24097,6 +24097,8 @@ F: Documentation/userspace-api/seccomp_filter.rst
F: include/linux/seccomp.h
F: include/uapi/linux/seccomp.h
F: kernel/seccomp.c
+F: kernel/seccomp_inject.c
+F: kernel/seccomp_inject.h
F: tools/testing/selftests/kselftest_harness.h
F: tools/testing/selftests/kselftest_harness/
F: tools/testing/selftests/seccomp/*
diff --git a/include/uapi/linux/seccomp.h b/include/uapi/linux/seccomp.h
index dbfc9b37fcae..fa51790f51cf 100644
--- a/include/uapi/linux/seccomp.h
+++ b/include/uapi/linux/seccomp.h
@@ -108,6 +108,18 @@ struct seccomp_notif {
*/
#define SECCOMP_USER_NOTIF_FLAG_CONTINUE (1UL << 0)
+/*
+ * SECCOMP_USER_NOTIF_FLAG_INJECTED — consume a syscall-redirect
+ * description previously attached via SECCOMP_IOCTL_NOTIF_INJECT for
+ * this notification. The trapped task wakes, dispatches into the
+ * matching kernel-mode syscall helper using the supervisor-provided
+ * (in-kernel) buffer for any pointer-shaped argument, and the helper's
+ * return value becomes the trapped syscall's return value.
+ *
+ * Mutually exclusive with SECCOMP_USER_NOTIF_FLAG_CONTINUE.
+ */
+#define SECCOMP_USER_NOTIF_FLAG_INJECTED (1UL << 1)
+
struct seccomp_notif_resp {
__u64 id;
__s64 val;
@@ -137,6 +149,52 @@ struct seccomp_notif_addfd {
__u32 newfd_flags;
};
+/**
+ * struct seccomp_notif_inject — describe a kernel-validated syscall
+ * to perform on behalf of a trapped task.
+ *
+ * The supervisor attaches one of these to a pending notification via
+ * SECCOMP_IOCTL_NOTIF_INJECT, then commits with SECCOMP_IOCTL_NOTIF_SEND
+ * setting SECCOMP_USER_NOTIF_FLAG_INJECTED. The kernel substitutes the
+ * trapped task's syscall with @nr/@args[6] and dispatches into a
+ * kernel-mode helper for the syscall, with any pointer argument backed
+ * by an offset into @buf (a kernel-side copy of the supervisor's
+ * bytes), so the trapped task's user mm is not re-read.
+ *
+ * @id: notification id from SECCOMP_IOCTL_NOTIF_RECV.
+ * @nr: substitute syscall number (matches ptrace_syscall_info.entry.nr).
+ * @args: substitute syscall arguments. For each i with the i'th bit
+ * set in @args_in_buf_mask, args[i] is interpreted as a byte
+ * offset into @buf rather than as the raw syscall argument
+ * value; the kernel materializes the corresponding pointer
+ * from its in-kernel copy of @buf before dispatch. Other args
+ * pass through as scalars (matches
+ * ptrace_syscall_info.entry.args).
+ * @buf: __user pointer to kernel-input bytes. The kernel copies
+ * @buf_size bytes from this buffer at SECCOMP_IOCTL_NOTIF_INJECT
+ * time and acts on its kernel-side copy thereafter.
+ * @buf_size: bytes available at @buf, capped at
+ * SECCOMP_NOTIF_INJECT_MAX_BYTES.
+ * @args_in_buf_mask: bitmask. Bit i set means args[i] is a byte
+ * offset into @buf rather than a raw argument value.
+ */
+struct seccomp_notif_inject {
+ __u64 id;
+ __u64 nr;
+ __u64 args[6];
+ __u64 buf;
+ __u32 buf_size;
+ __u32 args_in_buf_mask;
+};
+
+/*
+ * Hard cap on the cumulative bytes a single SECCOMP_IOCTL_NOTIF_INJECT
+ * request may copy. Defensive bound only — typical injects are a few
+ * KiB (one PATH_MAX path, an argv block, etc.). Hardcoded rather than
+ * a sysctl: there is no legitimate reason to tune this at runtime.
+ */
+#define SECCOMP_NOTIF_INJECT_MAX_BYTES (1UL << 20) /* 1 MiB */
+
#define SECCOMP_IOC_MAGIC '!'
#define SECCOMP_IO(nr) _IO(SECCOMP_IOC_MAGIC, nr)
#define SECCOMP_IOR(nr, type) _IOR(SECCOMP_IOC_MAGIC, nr, type)
@@ -154,4 +212,11 @@ struct seccomp_notif_addfd {
#define SECCOMP_IOCTL_NOTIF_SET_FLAGS SECCOMP_IOW(4, __u64)
+/* Attach a kernel-validated syscall redirect to a pending notification.
+ * Consumed by SECCOMP_IOCTL_NOTIF_SEND with
+ * SECCOMP_USER_NOTIF_FLAG_INJECTED.
+ */
+#define SECCOMP_IOCTL_NOTIF_INJECT SECCOMP_IOW(5, \
+ struct seccomp_notif_inject)
+
#endif /* _UAPI_LINUX_SECCOMP_H */
diff --git a/kernel/Makefile b/kernel/Makefile
index 6785982013dc..fa4c129384c7 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -106,6 +106,7 @@ obj-$(CONFIG_LOCKUP_DETECTOR) += watchdog.o
obj-$(CONFIG_HARDLOCKUP_DETECTOR_BUDDY) += watchdog_buddy.o
obj-$(CONFIG_HARDLOCKUP_DETECTOR_PERF) += watchdog_perf.o
obj-$(CONFIG_SECCOMP) += seccomp.o
+obj-$(CONFIG_SECCOMP_FILTER) += seccomp_inject.o
obj-$(CONFIG_RELAY) += relay.o
obj-$(CONFIG_SYSCTL) += utsname_sysctl.o
obj-$(CONFIG_TASK_DELAY_ACCT) += delayacct.o
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 066909393c38..a891e3e13eef 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -44,6 +44,8 @@
#include <linux/anon_inodes.h>
#include <linux/lockdep.h>
+#include "seccomp_inject.h"
+
/*
* When SECCOMP_IOCTL_NOTIF_ID_VALID was first introduced, it had the
* wrong direction flag in the ioctl number. This is the broken one,
@@ -97,6 +99,15 @@ struct seccomp_knotif {
/* outstanding addfd requests */
struct list_head addfd;
+
+ /*
+ * Outstanding SECCOMP_IOCTL_NOTIF_INJECT record, attached by
+ * the supervisor under filter->notify_lock. Consumed by the
+ * trapped task on SECCOMP_USER_NOTIF_FLAG_INJECTED, freed in
+ * the same path. Also freed if the knotif is dropped without
+ * being injected.
+ */
+ struct seccomp_inject_record *inject;
};
/**
@@ -1248,8 +1259,41 @@ static int seccomp_do_user_notification(int this_syscall,
mutex_unlock(&match->notify_lock);
/* Userspace requests to continue the syscall. */
- if (flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE)
+ if (flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE) {
+ /*
+ * Discard any inject the supervisor attached then changed
+ * its mind about.
+ */
+ seccomp_inject_record_free(n.inject);
return 0;
+ }
+
+ /*
+ * Userspace requested kernel-side syscall injection. Run the
+ * helper in the trapped task's context, free the record, and
+ * deliver the helper's result as the syscall return value.
+ */
+ if (flags & SECCOMP_USER_NOTIF_FLAG_INJECTED) {
+ long inject_ret = -EINVAL;
+
+ if (n.inject) {
+ inject_ret = seccomp_inject_dispatch(n.inject);
+ seccomp_inject_record_free(n.inject);
+ }
+ if (inject_ret < 0)
+ syscall_set_return_value(current, current_pt_regs(),
+ (int)inject_ret, 0);
+ else
+ syscall_set_return_value(current, current_pt_regs(),
+ 0, inject_ret);
+ return -1;
+ }
+
+ /*
+ * Interrupted, listener gone, or normal deny/allow: free any
+ * inject the supervisor attached but never consumed.
+ */
+ seccomp_inject_record_free(n.inject);
syscall_set_return_value(current, current_pt_regs(),
err, ret);
@@ -1632,13 +1676,22 @@ static long seccomp_notify_send(struct seccomp_filter *filter,
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_INJECTED))
+ return -EINVAL;
+
+ if ((resp.flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE) &&
+ (resp.flags & SECCOMP_USER_NOTIF_FLAG_INJECTED))
return -EINVAL;
if ((resp.flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE) &&
(resp.error || resp.val))
return -EINVAL;
+ if ((resp.flags & SECCOMP_USER_NOTIF_FLAG_INJECTED) &&
+ (resp.error || resp.val))
+ return -EINVAL;
+
ret = mutex_lock_interruptible(&filter->notify_lock);
if (ret < 0)
return ret;
@@ -1655,6 +1708,12 @@ static long seccomp_notify_send(struct seccomp_filter *filter,
goto out;
}
+ /* INJECTED requires a prior SECCOMP_IOCTL_NOTIF_INJECT for this id. */
+ if ((resp.flags & SECCOMP_USER_NOTIF_FLAG_INJECTED) && !knotif->inject) {
+ ret = -EINVAL;
+ goto out;
+ }
+
ret = 0;
knotif->state = SECCOMP_NOTIFY_REPLIED;
knotif->error = resp.error;
@@ -1823,6 +1882,62 @@ static long seccomp_notify_addfd(struct seccomp_filter *filter,
return ret;
}
+static long seccomp_notify_inject(struct seccomp_filter *filter,
+ struct seccomp_notif_inject __user *uinj)
+{
+ struct seccomp_notif_inject inj;
+ struct seccomp_inject_record *rec = NULL;
+ struct seccomp_knotif *knotif;
+ long ret;
+
+ if (copy_from_user(&inj, uinj, sizeof(inj)))
+ return -EFAULT;
+
+ ret = seccomp_inject_record_build(&inj, &rec);
+ if (ret)
+ return ret;
+
+ ret = mutex_lock_interruptible(&filter->notify_lock);
+ if (ret < 0)
+ goto err_free;
+
+ knotif = find_notification(filter, inj.id);
+ if (!knotif) {
+ ret = -ENOENT;
+ goto err_unlock;
+ }
+
+ if (knotif->state != SECCOMP_NOTIFY_SENT) {
+ ret = -EINPROGRESS;
+ goto err_unlock;
+ }
+
+ /*
+ * The supervisor cannot redirect a trapped syscall into an
+ * unrelated syscall (e.g. inject openat into a task trapped on
+ * bind), which would be a confused-deputy.
+ */
+ if (inj.nr != (u64)knotif->data->nr) {
+ ret = -ESRCH;
+ goto err_unlock;
+ }
+
+ if (knotif->inject) {
+ ret = -EEXIST;
+ goto err_unlock;
+ }
+
+ knotif->inject = rec;
+ mutex_unlock(&filter->notify_lock);
+ return 0;
+
+err_unlock:
+ mutex_unlock(&filter->notify_lock);
+err_free:
+ seccomp_inject_record_free(rec);
+ return ret;
+}
+
static long seccomp_notify_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
@@ -1840,6 +1955,8 @@ static long seccomp_notify_ioctl(struct file *file, unsigned int cmd,
return seccomp_notify_id_valid(filter, buf);
case SECCOMP_IOCTL_NOTIF_SET_FLAGS:
return seccomp_notify_set_flags(filter, arg);
+ case SECCOMP_IOCTL_NOTIF_INJECT:
+ return seccomp_notify_inject(filter, buf);
}
/* Extensible Argument ioctls */
diff --git a/kernel/seccomp_inject.c b/kernel/seccomp_inject.c
new file mode 100644
index 000000000000..2c2be3232f15
--- /dev/null
+++ b/kernel/seccomp_inject.c
@@ -0,0 +1,281 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SECCOMP_IOCTL_NOTIF_INJECT: kernel-mode syscall execution on
+ * behalf of an unprivileged seccomp_unotify supervisor.
+ *
+ * The supervisor describes a syscall (nr + args[6]) and a kernel-input
+ * buffer; pointer-shaped args are encoded as byte offsets into that
+ * buffer. On SECCOMP_USER_NOTIF_FLAG_INJECTED, the trapped task wakes
+ * inside seccomp_do_user_notification(), dispatches into the matching
+ * kernel-mode syscall helper, and the helper's return value becomes
+ * the trapped syscall's return value. The trapped task's user mm is
+ * never re-read for the substituted syscall, closing the documented
+ * TOCTOU race against CLONE_VM peers.
+ */
+#include <linux/fdtable.h>
+#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/kernel.h>
+#include <linux/net.h>
+#include <linux/seccomp.h>
+#include <linux/slab.h>
+#include <linux/syscalls.h>
+#include <linux/uaccess.h>
+#include <linux/vmalloc.h>
+#include <net/sock.h>
+#include <uapi/linux/seccomp.h>
+#include <asm/unistd.h>
+
+#include "seccomp_inject.h"
+
+/* Per-syscall injector. Runs in the trapped task's context. */
+typedef long (*seccomp_injector_fn)(const struct seccomp_inject_record *rec);
+
+static long inject_openat(const struct seccomp_inject_record *rec);
+static long inject_bind(const struct seccomp_inject_record *rec);
+static long inject_write(const struct seccomp_inject_record *rec);
+
+/*
+ * The injectable-syscall whitelist. Keep this small and explicit:
+ * each entry pins a kernel-mode helper to a specific syscall number.
+ * A NULL injector for an unlisted nr means "supervisor cannot inject
+ * this syscall" and seccomp_inject_record_build() rejects it with
+ * -EOPNOTSUPP.
+ */
+static seccomp_injector_fn seccomp_injector_for(u64 nr)
+{
+ switch (nr) {
+#ifdef __NR_openat
+ case __NR_openat:
+ return inject_openat;
+#endif
+#ifdef __NR_bind
+ case __NR_bind:
+ return inject_bind;
+#endif
+#ifdef __NR_write
+ case __NR_write:
+ return inject_write;
+#endif
+ default:
+ return NULL;
+ }
+}
+
+/*
+ * Treat args[i] as a byte offset into rec->buf and return a kernel
+ * pointer at that offset, with @needed bytes required to be accessible
+ * (i.e. offset + needed <= buf_size). NULL on out-of-bounds.
+ */
+static const void *inject_buf_at(const struct seccomp_inject_record *rec,
+ unsigned int i, size_t needed)
+{
+ u64 off;
+
+ if (i >= 6)
+ return NULL;
+ if (!(rec->args_in_buf_mask & (1U << i)))
+ return NULL;
+ off = rec->args[i];
+ if (off > rec->buf_size)
+ return NULL;
+ if (rec->buf_size - off < needed)
+ return NULL;
+ return (const u8 *)rec->buf + off;
+}
+
+/* NUL-terminated string at args[i] within rec->buf, or NULL on bound miss. */
+static const char *inject_buf_cstr(const struct seccomp_inject_record *rec,
+ unsigned int i)
+{
+ const char *s;
+ u64 off;
+ size_t avail;
+
+ if (i >= 6)
+ return NULL;
+ if (!(rec->args_in_buf_mask & (1U << i)))
+ return NULL;
+ off = rec->args[i];
+ if (off >= rec->buf_size)
+ return NULL;
+ s = (const char *)rec->buf + off;
+ avail = rec->buf_size - off;
+ if (memchr(s, '\0', avail) == NULL)
+ return NULL;
+ return s;
+}
+
+static long inject_openat(const struct seccomp_inject_record *rec)
+{
+ int dfd = (int)rec->args[0];
+ int flags = (int)rec->args[2];
+ umode_t mode = (umode_t)rec->args[3];
+ const char *path;
+ struct file *f;
+ int fd;
+
+ /* v1: dfd must be AT_FDCWD. Other dfd values land in v2. */
+ if (dfd != AT_FDCWD)
+ return -EOPNOTSUPP;
+
+ path = inject_buf_cstr(rec, 1);
+ if (!path)
+ return -EINVAL;
+
+ f = filp_open(path, flags, mode);
+ if (IS_ERR(f))
+ return PTR_ERR(f);
+
+ fd = get_unused_fd_flags(flags);
+ if (fd < 0) {
+ filp_close(f, NULL);
+ return fd;
+ }
+ fd_install(fd, f);
+ return fd;
+}
+
+static long inject_bind(const struct seccomp_inject_record *rec)
+{
+ int sockfd = (int)rec->args[0];
+ int addrlen = (int)rec->args[2];
+ struct sockaddr_storage addr;
+ const void *src;
+ struct socket *sock;
+ long ret;
+ int err;
+
+ if (addrlen < 0 || addrlen > sizeof(addr))
+ return -EINVAL;
+
+ src = inject_buf_at(rec, 1, addrlen);
+ if (!src)
+ return -EINVAL;
+ memcpy(&addr, src, addrlen);
+
+ sock = sockfd_lookup(sockfd, &err);
+ if (!sock)
+ return err;
+
+ ret = kernel_bind(sock, (struct sockaddr_unsized *)&addr, addrlen);
+ sockfd_put(sock);
+ return ret;
+}
+
+static long inject_write(const struct seccomp_inject_record *rec)
+{
+ int fd = (int)rec->args[0];
+ size_t count = (size_t)rec->args[2];
+ const void *src;
+ ssize_t ret;
+ struct fd f;
+ loff_t pos;
+
+ if (count > MAX_RW_COUNT)
+ count = MAX_RW_COUNT;
+
+ src = inject_buf_at(rec, 1, count);
+ if (!src)
+ return -EINVAL;
+
+ f = fdget_pos(fd);
+ if (!fd_file(f))
+ return -EBADF;
+
+ pos = fd_file(f)->f_pos;
+ ret = kernel_write(fd_file(f), src, count, &pos);
+ if (ret > 0)
+ fd_file(f)->f_pos = pos;
+ fdput_pos(f);
+ return ret;
+}
+
+void seccomp_inject_record_free(struct seccomp_inject_record *rec)
+{
+ if (!rec)
+ return;
+ kvfree(rec->buf);
+ kfree(rec);
+}
+
+/*
+ * Validate @uinj fields (syscall_nr in whitelist, args_in_buf_mask
+ * within bounds, offsets in bounds, buf_size capped), copy in the
+ * supervisor's buf, and return a record the caller owns. Caller is
+ * responsible for seccomp_inject_record_free() on success path if
+ * the record is not subsequently attached to a knotif.
+ */
+long seccomp_inject_record_build(const struct seccomp_notif_inject *uinj,
+ struct seccomp_inject_record **out)
+{
+ struct seccomp_inject_record *rec;
+ void __user *user_buf;
+ long ret;
+ unsigned int i;
+
+ *out = NULL;
+
+ if (!seccomp_injector_for(uinj->nr))
+ return -EOPNOTSUPP;
+
+ /* args_in_buf_mask must reference args[0..5] only. */
+ if (uinj->args_in_buf_mask & ~((1U << 6) - 1))
+ return -EINVAL;
+
+ if (uinj->buf_size > SECCOMP_NOTIF_INJECT_MAX_BYTES)
+ return -E2BIG;
+
+ user_buf = (void __user *)(uintptr_t)uinj->buf;
+ if (uinj->buf_size && !user_buf)
+ return -EINVAL;
+
+ /* Bounds-check each in-buf arg before any allocation. */
+ for (i = 0; i < 6; i++) {
+ if (!(uinj->args_in_buf_mask & (1U << i)))
+ continue;
+ if (uinj->args[i] > uinj->buf_size)
+ return -EINVAL;
+ }
+
+ rec = kzalloc(sizeof(*rec), GFP_KERNEL_ACCOUNT);
+ if (!rec)
+ return -ENOMEM;
+
+ rec->nr = uinj->nr;
+ memcpy(rec->args, uinj->args, sizeof(rec->args));
+ rec->args_in_buf_mask = uinj->args_in_buf_mask;
+ rec->buf_size = uinj->buf_size;
+
+ if (uinj->buf_size) {
+ rec->buf = kvmalloc(uinj->buf_size, GFP_KERNEL_ACCOUNT);
+ if (!rec->buf) {
+ ret = -ENOMEM;
+ goto err;
+ }
+ if (copy_from_user(rec->buf, user_buf, uinj->buf_size)) {
+ ret = -EFAULT;
+ goto err;
+ }
+ }
+
+ *out = rec;
+ return 0;
+
+err:
+ seccomp_inject_record_free(rec);
+ return ret;
+}
+
+/*
+ * Top-level dispatch. Runs in the trapped task's context (current is
+ * the trapped task). Returns the kernel helper's result.
+ */
+long seccomp_inject_dispatch(const struct seccomp_inject_record *rec)
+{
+ seccomp_injector_fn injector = seccomp_injector_for(rec->nr);
+
+ if (!injector)
+ return -EOPNOTSUPP;
+ return injector(rec);
+}
diff --git a/kernel/seccomp_inject.h b/kernel/seccomp_inject.h
new file mode 100644
index 000000000000..b85e4399381c
--- /dev/null
+++ b/kernel/seccomp_inject.h
@@ -0,0 +1,65 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Internal interfaces for SECCOMP_IOCTL_NOTIF_INJECT.
+ *
+ * The inject record allocation, validation, per-syscall injectors and
+ * the dispatch entrypoint live in kernel/seccomp_inject.c to keep
+ * kernel/seccomp.c focused on the notify state machine.
+ */
+#ifndef _KERNEL_SECCOMP_INJECT_H
+#define _KERNEL_SECCOMP_INJECT_H
+
+#include <linux/types.h>
+#include <uapi/linux/seccomp.h>
+
+struct seccomp_knotif;
+struct seccomp_notif_inject;
+
+/**
+ * struct seccomp_inject_record - kernel-side per-knotif inject state.
+ * @nr: substitute syscall number, validated against the injectable
+ * whitelist.
+ * @args: substitute syscall arguments. For each i with the i'th bit
+ * set in @args_in_buf_mask, args[i] is a byte offset into
+ * @buf rather than the raw argument value.
+ * @args_in_buf_mask: bitmask of pointer-shaped args backed by @buf.
+ * @buf_size: bytes valid in @buf.
+ * @buf: kernel-owned copy of the supervisor-supplied bytes; allocated
+ * at attach time, freed at consumption (or knotif teardown).
+ *
+ * Allocated by SECCOMP_IOCTL_NOTIF_INJECT, attached to the knotif under
+ * filter->notify_lock, consumed by the trapped task on
+ * SECCOMP_USER_NOTIF_FLAG_INJECTED, freed in the same path. Also freed
+ * if the knotif is dropped without being injected (listener close,
+ * task exit, supervisor changes its mind).
+ */
+struct seccomp_inject_record {
+ u64 nr;
+ u64 args[6];
+ u32 args_in_buf_mask;
+ u32 buf_size;
+ void *buf;
+};
+
+#ifdef CONFIG_SECCOMP_FILTER
+
+/* Allocate, validate, and copy in @uinj. Caller takes ownership of *out. */
+long seccomp_inject_record_build(const struct seccomp_notif_inject *uinj,
+ struct seccomp_inject_record **out);
+
+/* Free a record built by seccomp_inject_record_build(). */
+void seccomp_inject_record_free(struct seccomp_inject_record *rec);
+
+/* Dispatch a built record into the matching kernel-mode syscall helper.
+ * Runs in the trapped task's context (current is the trapped task).
+ * Returns the helper's result, which becomes the syscall return value.
+ */
+long seccomp_inject_dispatch(const struct seccomp_inject_record *rec);
+
+#else /* !CONFIG_SECCOMP_FILTER */
+
+static inline void seccomp_inject_record_free(struct seccomp_inject_record *rec) { }
+
+#endif /* CONFIG_SECCOMP_FILTER */
+
+#endif /* _KERNEL_SECCOMP_INJECT_H */
--
2.43.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [RFC PATCH v2 2/3] selftests/seccomp: add seccomp_notif_inject coverage
2026-05-15 4:27 [RFC PATCH v2 0/3] seccomp: SECCOMP_IOCTL_NOTIF_INJECT for race-free unotify Cong Wang
2026-05-15 4:27 ` [RFC PATCH v2 1/3] seccomp: add " Cong Wang
@ 2026-05-15 4:27 ` Cong Wang
2026-05-15 4:27 ` [RFC PATCH v2 3/3] Documentation: seccomp: document SECCOMP_IOCTL_NOTIF_INJECT Cong Wang
` (2 subsequent siblings)
4 siblings, 0 replies; 9+ messages in thread
From: Cong Wang @ 2026-05-15 4:27 UTC (permalink / raw)
To: Kees Cook, linux-kernel
Cc: Andy Lutomirski, Will Drewry, Christian Brauner, Cong Wang
From: Cong Wang <cwang@multikernel.io>
Add a standalone selftest binary for SECCOMP_IOCTL_NOTIF_INJECT
covering positive end-to-end injection on each of the v1 syscalls
(openat, bind, write) and the negative paths (unsupported syscall,
trapped/inject syscall mismatch, double inject, CONTINUE+INJECTED
conflict, INJECTED without prior attach).
The end-to-end tests fork a child that issues a filtered syscall
with one set of arguments and verify that, after the supervisor
attaches a substitute via SECCOMP_IOCTL_NOTIF_INJECT and replies
with SECCOMP_USER_NOTIF_FLAG_INJECTED, the kernel acts on the
supervisor-supplied bytes rather than the child's user mm.
Lives in its own file rather than seccomp_bpf.c since the feature
is unrelated to the BPF filter machinery.
Assisted-by: Claude:claude-opus-4.6
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
tools/testing/selftests/seccomp/.gitignore | 1 +
tools/testing/selftests/seccomp/Makefile | 2 +-
.../selftests/seccomp/seccomp_notif_inject.c | 434 ++++++++++++++++++
3 files changed, 436 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/seccomp/seccomp_notif_inject.c
diff --git a/tools/testing/selftests/seccomp/.gitignore b/tools/testing/selftests/seccomp/.gitignore
index dec678577f9c..096f77a0b136 100644
--- a/tools/testing/selftests/seccomp/.gitignore
+++ b/tools/testing/selftests/seccomp/.gitignore
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
seccomp_bpf
seccomp_benchmark
+seccomp_notif_inject
diff --git a/tools/testing/selftests/seccomp/Makefile b/tools/testing/selftests/seccomp/Makefile
index 584fba487037..8a4ca92f1af3 100644
--- a/tools/testing/selftests/seccomp/Makefile
+++ b/tools/testing/selftests/seccomp/Makefile
@@ -3,5 +3,5 @@ CFLAGS += -Wl,-no-as-needed -Wall $(KHDR_INCLUDES)
LDFLAGS += -lpthread
LDLIBS += -lcap
-TEST_GEN_PROGS := seccomp_bpf seccomp_benchmark
+TEST_GEN_PROGS := seccomp_bpf seccomp_benchmark seccomp_notif_inject
include ../lib.mk
diff --git a/tools/testing/selftests/seccomp/seccomp_notif_inject.c b/tools/testing/selftests/seccomp/seccomp_notif_inject.c
new file mode 100644
index 000000000000..3d5bb97b8df9
--- /dev/null
+++ b/tools/testing/selftests/seccomp/seccomp_notif_inject.c
@@ -0,0 +1,434 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Selftest for SECCOMP_IOCTL_NOTIF_INJECT.
+ *
+ * Exercises end-to-end syscall injection via the listener-fd
+ * supervisor pattern: the child issues a filtered syscall, the
+ * supervisor describes a substitute syscall in a kernel buffer, the
+ * kernel runs the substitute on the child's behalf using kernel-mode
+ * helpers (filp_open/kernel_bind/kernel_write), and the result lands
+ * as the child's syscall return value. The trapped task's user mm is
+ * never re-read for the substituted syscall.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/filter.h>
+#include <linux/seccomp.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../kselftest_harness.h"
+
+#ifndef __NR_seccomp
+#define __NR_seccomp 317
+#endif
+
+static int seccomp_install(int nr)
+{
+ 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, 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 syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER,
+ SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog);
+}
+
+/* ----------------------------------------------------------------
+ * openat injection.
+ * ----------------------------------------------------------------
+ */
+TEST(notif_inject_openat)
+{
+ char tmp_real[] = "/tmp/seccomp-inject-XXXXXX";
+ int real_fd, listener, status;
+ pid_t pid;
+
+ real_fd = mkstemp(tmp_real);
+ ASSERT_GE(real_fd, 0);
+ ASSERT_EQ(write(real_fd, "real-data", 9), 9);
+ ASSERT_EQ(close(real_fd), 0);
+
+ ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+ listener = seccomp_install(__NR_openat);
+ ASSERT_GE(listener, 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ char readback[16] = {0};
+ int fd;
+
+ fd = openat(AT_FDCWD, "/this/path/does/not/exist", O_RDONLY);
+ if (fd < 0)
+ _exit(10);
+ if (read(fd, readback, sizeof(readback) - 1) <= 0)
+ _exit(11);
+ _exit(memcmp(readback, "real-data", 9) == 0 ? 0 : 12);
+ }
+
+ struct seccomp_notif req = {0};
+ struct seccomp_notif_resp resp = {0};
+ struct seccomp_notif_inject inj = {0};
+
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+ EXPECT_EQ(req.data.nr, __NR_openat);
+
+ inj.id = req.id;
+ inj.nr = __NR_openat;
+ inj.args[0] = AT_FDCWD;
+ inj.args[1] = 0;
+ inj.args[2] = O_RDONLY;
+ inj.args[3] = 0;
+ inj.buf = (uintptr_t)tmp_real;
+ inj.buf_size = strlen(tmp_real) + 1;
+ inj.args_in_buf_mask = 1U << 1;
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), 0) {
+ TH_LOG("INJECT failed: %s", strerror(errno));
+ }
+
+ resp.id = req.id;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_INJECTED;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(WEXITSTATUS(status), 0) {
+ TH_LOG("child exit status: %d", WEXITSTATUS(status));
+ }
+
+ unlink(tmp_real);
+ close(listener);
+}
+
+/* ----------------------------------------------------------------
+ * write injection.
+ * Child issues write(fd, "agent-data", 10); supervisor injects
+ * write(fd, "kernel-bytes", 12); verify file content matches the
+ * kernel-injected bytes.
+ * ----------------------------------------------------------------
+ */
+TEST(notif_inject_write)
+{
+ char path[] = "/tmp/seccomp-inject-write-XXXXXX";
+ static const char inject_bytes[] = "kernel-bytes";
+ int file_fd, listener, status;
+ char file_content[32];
+ pid_t pid;
+
+ file_fd = mkstemp(path);
+ ASSERT_GE(file_fd, 0);
+
+ ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+ listener = seccomp_install(__NR_write);
+ ASSERT_GE(listener, 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ ssize_t n;
+
+ n = write(file_fd, "agent-data", 10);
+ _exit(n > 0 ? 0 : 10);
+ }
+
+ struct seccomp_notif req = {0};
+ struct seccomp_notif_resp resp = {0};
+ struct seccomp_notif_inject inj = {0};
+
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+ EXPECT_EQ(req.data.nr, __NR_write);
+
+ inj.id = req.id;
+ inj.nr = __NR_write;
+ inj.args[0] = req.data.args[0]; /* fd, pass-through */
+ inj.args[1] = 0;
+ inj.args[2] = strlen(inject_bytes);
+ inj.buf = (uintptr_t)inject_bytes;
+ inj.buf_size = strlen(inject_bytes);
+ inj.args_in_buf_mask = 1U << 1;
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), 0);
+
+ resp.id = req.id;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_INJECTED;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(WEXITSTATUS(status), 0);
+
+ memset(file_content, 0, sizeof(file_content));
+ ASSERT_EQ(lseek(file_fd, 0, SEEK_SET), 0);
+ ASSERT_EQ(read(file_fd, file_content, sizeof(file_content) - 1),
+ (ssize_t)strlen(inject_bytes));
+ EXPECT_EQ(memcmp(file_content, inject_bytes, strlen(inject_bytes)), 0) {
+ TH_LOG("file content: '%s'", file_content);
+ }
+
+ close(file_fd);
+ unlink(path);
+ close(listener);
+}
+
+/* ----------------------------------------------------------------
+ * bind injection.
+ * ----------------------------------------------------------------
+ */
+TEST(notif_inject_bind)
+{
+ struct sockaddr_un real_addr = { .sun_family = AF_UNIX };
+ char real_path[] = "/tmp/seccomp-inject-bind-XXXXXX";
+ int listener, status;
+ pid_t pid;
+
+ mktemp(real_path);
+ strcpy(real_addr.sun_path, real_path);
+ unlink(real_addr.sun_path);
+
+ ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+ listener = seccomp_install(__NR_bind);
+ ASSERT_GE(listener, 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ struct sockaddr_un fake = { .sun_family = AF_UNIX };
+ int s;
+
+ strcpy(fake.sun_path, "/tmp/seccomp-inject-bind-fake");
+ s = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (s < 0)
+ _exit(10);
+ if (bind(s, (struct sockaddr *)&fake, sizeof(fake)) < 0)
+ _exit(11);
+ _exit(0);
+ }
+
+ struct seccomp_notif req = {0};
+ struct seccomp_notif_resp resp = {0};
+ struct seccomp_notif_inject inj = {0};
+ struct stat st;
+
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+ EXPECT_EQ(req.data.nr, __NR_bind);
+
+ inj.id = req.id;
+ inj.nr = __NR_bind;
+ inj.args[0] = req.data.args[0]; /* sockfd, pass-through */
+ inj.args[1] = 0;
+ inj.args[2] = sizeof(real_addr);
+ inj.buf = (uintptr_t)&real_addr;
+ inj.buf_size = sizeof(real_addr);
+ inj.args_in_buf_mask = 1U << 1;
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), 0);
+
+ resp.id = req.id;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_INJECTED;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ EXPECT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(WEXITSTATUS(status), 0);
+
+ /*
+ * The kernel-injected path should exist; the agent's intended
+ * path should not.
+ */
+ EXPECT_EQ(stat(real_path, &st), 0);
+ EXPECT_EQ(stat("/tmp/seccomp-inject-bind-fake", &st), -1);
+
+ unlink(real_path);
+ close(listener);
+}
+
+/* ----------------------------------------------------------------
+ * Negative paths.
+ * ----------------------------------------------------------------
+ */
+TEST(notif_inject_unsupported_syscall)
+{
+ int listener, status;
+ pid_t pid;
+
+ ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+ listener = seccomp_install(__NR_close);
+ ASSERT_GE(listener, 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ close(99);
+ _exit(0);
+ }
+
+ struct seccomp_notif req = {0};
+ struct seccomp_notif_resp resp = {0};
+ struct seccomp_notif_inject inj = {0};
+
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+
+ /* close() is not in the injectable whitelist. */
+ inj.id = req.id;
+ inj.nr = __NR_close;
+ inj.args[0] = 99;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), -1);
+ EXPECT_EQ(errno, EOPNOTSUPP);
+
+ /* Cleanly deny so the child can exit. */
+ resp.id = req.id;
+ resp.error = -EBADF;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ close(listener);
+}
+
+TEST(notif_inject_syscall_mismatch)
+{
+ int listener, status;
+ pid_t pid;
+
+ ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+ listener = seccomp_install(__NR_openat);
+ ASSERT_GE(listener, 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ openat(AT_FDCWD, "/nonexistent", O_RDONLY);
+ _exit(0);
+ }
+
+ struct seccomp_notif req = {0};
+ struct seccomp_notif_resp resp = {0};
+ struct seccomp_notif_inject inj = {0};
+
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+
+ /* Trapped on openat, but supervisor tries to inject bind. */
+ inj.id = req.id;
+ inj.nr = __NR_bind;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), -1);
+ EXPECT_EQ(errno, ESRCH);
+
+ resp.id = req.id;
+ resp.error = -EACCES;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ close(listener);
+}
+
+TEST(notif_inject_double)
+{
+ char tmp_real[] = "/tmp/seccomp-inject-d-XXXXXX";
+ int real_fd, listener, status;
+ pid_t pid;
+
+ real_fd = mkstemp(tmp_real);
+ ASSERT_GE(real_fd, 0);
+ close(real_fd);
+
+ ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+ listener = seccomp_install(__NR_openat);
+ ASSERT_GE(listener, 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ openat(AT_FDCWD, "/nonexistent", O_RDONLY);
+ _exit(0);
+ }
+
+ struct seccomp_notif req = {0};
+ struct seccomp_notif_resp resp = {0};
+ struct seccomp_notif_inject inj = {0};
+
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+
+ inj.id = req.id;
+ inj.nr = __NR_openat;
+ inj.args[0] = AT_FDCWD;
+ inj.args[1] = 0;
+ inj.args[2] = O_RDONLY;
+ inj.buf = (uintptr_t)tmp_real;
+ inj.buf_size = strlen(tmp_real) + 1;
+ inj.args_in_buf_mask = 1U << 1;
+ ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), 0);
+
+ /* Second INJECT for the same id is rejected. */
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), -1);
+ EXPECT_EQ(errno, EEXIST);
+
+ resp.id = req.id;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_INJECTED;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+
+ unlink(tmp_real);
+ close(listener);
+}
+
+TEST(notif_inject_continue_pinned_conflict)
+{
+ int listener, status;
+ pid_t pid;
+
+ ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+ listener = seccomp_install(__NR_openat);
+ ASSERT_GE(listener, 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ openat(AT_FDCWD, "/nonexistent", O_RDONLY);
+ _exit(0);
+ }
+
+ struct seccomp_notif req = {0};
+ struct seccomp_notif_resp resp = {0};
+
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+
+ /* CONTINUE | INJECTED is rejected. */
+ resp.id = req.id;
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE |
+ SECCOMP_USER_NOTIF_FLAG_INJECTED;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), -1);
+ EXPECT_EQ(errno, EINVAL);
+
+ /* INJECTED without prior INJECT ioctl is rejected too. */
+ resp.flags = SECCOMP_USER_NOTIF_FLAG_INJECTED;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), -1);
+ EXPECT_EQ(errno, EINVAL);
+
+ /* Cleanly deny so the child exits. */
+ resp.flags = 0;
+ resp.error = -ENOENT;
+ EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+ EXPECT_EQ(waitpid(pid, &status, 0), pid);
+ close(listener);
+}
+
+TEST_HARNESS_MAIN
--
2.43.0
^ permalink raw reply [flat|nested] 9+ messages in thread