From: Cong Wang <xiyou.wangcong@gmail.com>
To: Kees Cook <kees@kernel.org>, linux-kernel@vger.kernel.org
Cc: Andy Lutomirski <luto@amacapital.net>,
Will Drewry <wad@chromium.org>,
Christian Brauner <brauner@kernel.org>,
Cong Wang <cwang@multikernel.io>
Subject: [RFC PATCH v2 1/3] seccomp: add SECCOMP_IOCTL_NOTIF_INJECT for race-free unotify
Date: Thu, 14 May 2026 21:27:36 -0700 [thread overview]
Message-ID: <20260515042738.1723296-2-xiyou.wangcong@gmail.com> (raw)
In-Reply-To: <20260515042738.1723296-1-xiyou.wangcong@gmail.com>
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
next prev parent reply other threads:[~2026-05-15 4:27 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-15 4:27 [RFC PATCH v2 0/3] seccomp: " Cong Wang
2026-05-15 4:27 ` Cong Wang [this message]
2026-05-15 4:27 ` [RFC PATCH v2 2/3] selftests/seccomp: add seccomp_notif_inject coverage Cong Wang
2026-05-15 4:27 ` [RFC PATCH v2 3/3] Documentation: seccomp: document SECCOMP_IOCTL_NOTIF_INJECT Cong Wang
2026-05-26 18:06 ` [RFC PATCH v2 0/3] seccomp: SECCOMP_IOCTL_NOTIF_INJECT for race-free unotify Cong Wang
2026-05-26 19:03 ` Andy Lutomirski
2026-05-28 17:41 ` Cong Wang
2026-05-28 18:15 ` Andy Lutomirski
2026-05-29 5:07 ` Cong Wang
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260515042738.1723296-2-xiyou.wangcong@gmail.com \
--to=xiyou.wangcong@gmail.com \
--cc=brauner@kernel.org \
--cc=cwang@multikernel.io \
--cc=kees@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=luto@amacapital.net \
--cc=wad@chromium.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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®