From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-dl1-f51.google.com (mail-dl1-f51.google.com [74.125.82.51]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3F7153D3007 for ; Fri, 15 May 2026 04:27:43 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.51 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778819265; cv=none; b=CE0SbetRQhFtDipB6OUJKmeBHGQB9CH1bd/MEK1mc+cPvFITIW+TlVfsIZu8SVALwq+7NL+5v891TPy9bJMIw9E4M0DQKUo7FjodjMgII5GNf2NSLf3zVA+TTJ9TweVvXCdFF+tXrjnqhdcOy2QzHvsw+VseA6yYR985enyw1Jg= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778819265; c=relaxed/simple; bh=wDkEy7xF5tGyakmkT9NhCy3rzIv97GXO6mT5IxMONe8=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=sECAMlSa+fS/sWm1k5OZiQPUyI9BlNZPUxzGFoOLAkgeUui/6HTU0IhXkOakX5ngIjbcK37TBedPdEMJLA3SR19YASJGmCmrR6Ui8rUQ62bkBlVVg7jVKX+n0E9mFX4OPIo0OhJHyk/QVfIazy4NFFqqGtwATtU0RSJXJXA/KWQ= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=r0fGJyD+; arc=none smtp.client-ip=74.125.82.51 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="r0fGJyD+" Received: by mail-dl1-f51.google.com with SMTP id a92af1059eb24-1329fc4bf77so1186677c88.1 for ; Thu, 14 May 2026 21:27:43 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778819262; x=1779424062; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=GOwSMJfIG9RiyaIscIKi1b0UXzBsMU87RObuJZ2Udfk=; b=r0fGJyD+f+tgMgQ41k90Je8xDZgjDAsEDx8CRKQtVeduWUKtidQgy9prWPU1Zyitx4 VmrZZRd6Hv4L616klrvjwDMAAut29eVTIA50fWDEAYx8Ssqof6bxqP3KLvcHKY379ua8 ET4jq7v4OWh1iIG1NowfBav6CcHnnKzSp3CWYPvzMpvk2WGHe9wsDKfDBFelQvlm+0Q3 sz3uS9u0pZ+D7Rud9xRLEzjin3fJQLODM8O8SLoHLHpDnr9IHCwXjU1YdL0+dN784KUL U+dasFEWCNXlA6RGk8kGqToD/QH8HBhiE9/u9gLfrWQ8EGHmLJrTVrV0UkeANAaijmAb h9BQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778819262; x=1779424062; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=GOwSMJfIG9RiyaIscIKi1b0UXzBsMU87RObuJZ2Udfk=; b=YQgM0wluSnE1DCxOHFsjd3NHfjNYj4hA3uVmSz/0EEqgBfoSXaUbvs6MXwKkOFoPxA 3C4S4AuP+M3/eRTe0jlUDMhl2dRIdqkxKNoPZ2ProCvHj5K4BpHaYitM841mVzS5rS9Y O+ISUPT57wpDl3nvuB2uupjyuY7ntuvaSBVFN7TGJ0uN3e/WvIlbLhCXVb/h43KJZSU8 +cBcxiAWqTfn1bT/IbeRudicDTmSyT/cEjP3ztZLdEEFwRaid/8ccCt2AsVYbDcfency JbtOSOL6wIMo6P2q3Jormy+WvFmlb9+b549SJorFXqG2jCkVZIaEJK72wqn3zp5CdWqO k9WA== X-Forwarded-Encrypted: i=1; AFNElJ8z1MsWx/7rMU4qg43h7QllUYRtHKeW5d1XGtrtM0rLckRElRcz4/4aKzIRlC6eJfBoR1gx6C4kEklNagk=@vger.kernel.org X-Gm-Message-State: AOJu0YwVpgWy7NdH5d6HJJMLikNhRe0ExxwRd1Fn05Didcw8bcwuCXFV OWnn334KWi8u67AVl+RnXpt5JpLNg593aHPDtTQD9SVDyAdNeV1YrchZ X-Gm-Gg: Acq92OGsohQZmn+hh4W8swZjisrO+bIzeoKQuxzmqloHsJ3pu1Z2PwcjkKbzr1JNJj9 qGCe/dcQG8Ok9lX8sYU32A2I5xJd+FRfRAkzeYl4YAznLG/we2xWJmx21tKtZSg2Zk78BDIcEXM qLjw5a5kErpoXzH+XjasxXeonU8VI7qopgNOYazj11FlbarpUknoVODGOssYx9ziISBCfhhcIK0 k7QKhmMSzvu64xORC3DRaFwOA1X7cQcUHZ5T2fUGfKidpOAsLGpWbjEzGsJCTLRbdA8m5vHJF24 lkwuR1OKwxWlHPVCHDz+YicEYreI4OEHWdjQya3GtrhjhdaWTyjnZZutKMHxFFODnVG7oBseTd9 TinUq+FM2P5x5DX4LS3oYeE76/vbNTUZyXRp0AVqYmBPOy8HzGUd9WykjDZrxTeFagJmcAqjsCT PYyvM1NxDH7UQMWbD3i2DohNxLgnDnEI1fl8JJnoKIYAsAiVjAJy3/frsuD/JC X-Received: by 2002:a05:7300:ed85:b0:2ff:c5b1:2d6b with SMTP id 5a478bee46e88-303986bdc57mr1199354eec.32.1778819261980; Thu, 14 May 2026 21:27:41 -0700 (PDT) Received: from pop-os.. ([2601:647:6802:dbc0:5cba:6b6f:e327:bade]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-302944ffb85sm5665521eec.7.2026.05.14.21.27.40 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 14 May 2026 21:27:41 -0700 (PDT) From: Cong Wang To: Kees Cook , linux-kernel@vger.kernel.org Cc: Andy Lutomirski , Will Drewry , Christian Brauner , Cong Wang 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 Message-ID: <20260515042738.1723296-2-xiyou.wangcong@gmail.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260515042738.1723296-1-xiyou.wangcong@gmail.com> References: <20260515042738.1723296-1-xiyou.wangcong@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From: Cong Wang 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 --- 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 #include +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include + +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