mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
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


  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®