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 0/3] seccomp: SECCOMP_IOCTL_NOTIF_INJECT for race-free unotify
Date: Thu, 14 May 2026 21:27:35 -0700	[thread overview]
Message-ID: <20260515042738.1723296-1-xiyou.wangcong@gmail.com> (raw)

From: Cong Wang <cwang@multikernel.io>

This is a complete rework of v1 (PIN_ARGS), reshaped to address the
review feedback that having every syscall-arg fetch site consult a
per-task pin pointer is cross-cutting awareness that does not scale.

v1 thread:
https://lore.kernel.org/lkml/20260504011207.539408-1-xiyou.wangcong@gmail.com

## Changes since v1

The previous proposal (SECCOMP_IOCTL_NOTIF_PIN_ARGS) snapshotted
pointer-arg payloads into kernel buffers and modified four syscall
fetch sites (getname_flags in fs/namei.c, copy_strings in fs/exec.c,
move_addr_to_kernel in net/socket.c, import_ubuf in lib/iov_iter.c
plus new_sync_read/new_sync_write in fs/read_write.c) so the resumed
syscall body would consume from the snapshot instead of re-reading
user memory. The reviewer correctly pointed out that this spreads
"continue-from-snapshotted-state" awareness across the VFS and the
kernel in general, and that the right shape for this kind of feature
is one where the syscall layer does not have to care.

v2 inverts the model. The supervisor no longer pins args for a
resumed syscall body to consume; it describes a substitute syscall
(nr + args[6]) whose pointer-shaped args are encoded as byte offsets
into a kernel-side buffer. On SECCOMP_USER_NOTIF_FLAG_INJECTED, the
trapped task wakes inside seccomp_do_user_notification(), dispatches
into a kernel-mode syscall helper (filp_open / kernel_bind /
kernel_write for v1), 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.

Footprint comparison:

  v1 (PIN_ARGS):  ~1000 LOC across kernel/, fs/, mm/, net/, lib/
  v2 (INJECT):    ~540  LOC, all in kernel/seccomp_inject.{c,h} plus
                  a small dispatcher in kernel/seccomp.c. fs/, mm/,
                  net/, lib/ are unmodified.

## Motivation (short version)

seccomp_unotify(2) leaves a TOCTOU window: 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. The race is documented in the man
page. ptrace and /proc/pid/mem are unavailable to unprivileged
supervisors, so today there is no race-free path for content-aware
allow policy on CONTINUE.

The full motivation, including the threat model (adversarial AI
agents in the same address space) and the concrete user (Sandlock,
https://github.com/multikernel/sandlock), is in the v1 cover letter
above.

## Approach

The UAPI is one new ioctl and one new NOTIF_SEND flag:

    struct seccomp_notif_inject {
        __u64 id;
        __u64 nr;
        __u64 args[6];
        __u64 buf;               /* __user, kernel-input bytes */
        __u32 buf_size;
        __u32 args_in_buf_mask;  /* bit i: args[i] is offset into buf */
    };

    #define SECCOMP_IOCTL_NOTIF_INJECT \
        SECCOMP_IOW(5, struct seccomp_notif_inject)
    #define SECCOMP_USER_NOTIF_FLAG_INJECTED (1U << 1)

The struct mirrors ptrace_syscall_info.entry (nr + args[6]); pointer
args are encoded as byte offsets into buf via args_in_buf_mask. The
kernel copies buf at INJECT time and acts on its kernel-side copy
thereafter. The supervisor commits by sending NOTIF_SEND with
FLAG_INJECTED.

v1 injectable-syscall whitelist:

  - openat (filp_open + fd_install)
  - bind   (sockfd_lookup + kernel_bind)
  - write  (kernel_write)

These cover the bulk of Sandlock-shape policies. execveat is deferred
to v2 follow-up (kernel_execveat in the trapped task's context needs
careful framing, and the argv/envp packed format deserves its own
review round). sendmsg/recvmsg are deferred because msghdr's nested
pointers (msg_name + msg_iov array + msg_control with possible
SCM_RIGHTS) do not fit the flat-buffer model without a richer
packed-format design.

Per-syscall plumbing is small: each injector is ~20 LOC. Adding a
new injectable syscall is a self-contained patch (one new entry in
seccomp_injector_for() plus a new injector function). The framework
does not need to know about new syscalls; only the per-syscall
injector parses buf for its own layout.

## Relation to ptrace

ptrace can already inject syscalls via PTRACE_SETREGSET +
PTRACE_POKEDATA. CRIU, gdb and most container tooling use this
pattern. INJECT does not add a new kernel capability. It provides a
listener-fd-gated, syscall-whitelisted, narrower interface to that
capability for unprivileged supervisors, where ptrace's privilege
model (CAP_SYS_PTRACE / Yama PR_SET_PTRACER) and per-syscall overhead
(signal-stop cycle plus O(N) peer-thread coordination required for
race-free reads) are not viable.

SECCOMP_IOCTL_NOTIF_ADDFD set the precedent for this kind of narrow
listener-fd interface to a ptrace-overlapping capability. INJECT is
ADDFD generalized along one more axis: not just fd substitution, but
substitute-with-kernel-validated-args for a handful of syscalls
where Sandlock-shape policies need it.

## Why this closes TOCTOU end-to-end

For pointer args placed in buf, the kernel reads from kernel memory
that the supervisor populated at INJECT time. The agent's mm and
buf are decoupled after that ioctl returns: peers can mutate user
memory all they want, but the kernel never re-reads it for the
substituted syscall. The supervisor commits bytes; the kernel
performs the operation; the trapped task gets the result.

The remaining race (peer mutates agent mm before the supervisor's
process_vm_readv) is a userspace consistency concern, not a kernel
bypass: the supervisor decides on bytes it observed, and the kernel
performs the operation the supervisor authorized. The peer cannot
cause the kernel to act on bytes the supervisor never inspected.

## Lifecycle, bounds, accounting

  - One-shot: the inject record attaches to the knotif on INJECT and
    is consumed by NOTIF_SEND with FLAG_INJECTED. It is freed on
    listener close, task exit, supervisor changing its mind (CONTINUE
    or plain deny), or SIGKILL of the trapped task.
  - 1 MiB hardcoded cap on buf_size (defensive ceiling, not a tunable).
  - GFP_KERNEL_ACCOUNT so the trapped task's memcg pays.
  - The substitute nr must match the trapped syscall's number (no
    confused-deputy conversion from one syscall family to another).

## Testing

The selftest binary covers openat, bind and write end-to-end (child
issues syscall with one set of args; supervisor injects substitute;
verify kernel acted on supervisor-supplied bytes), plus negative
paths: unsupported syscall (-EOPNOTSUPP), trapped/inject syscall
mismatch (-ESRCH), double INJECT (-EEXIST), CONTINUE+INJECTED
conflict (-EINVAL), INJECTED without prior attach (-EINVAL). All
seven cases pass on x86_64.

---
Cong Wang (3):
  seccomp: add SECCOMP_IOCTL_NOTIF_INJECT for race-free unotify
  selftests/seccomp: add seccomp_notif_inject coverage
  Documentation: seccomp: document SECCOMP_IOCTL_NOTIF_INJECT

 .../userspace-api/seccomp_filter.rst          |  42 ++
 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 +++
 tools/testing/selftests/seccomp/.gitignore    |   1 +
 tools/testing/selftests/seccomp/Makefile      |   2 +-
 .../selftests/seccomp/seccomp_notif_inject.c  | 434 ++++++++++++++++++
 10 files changed, 1011 insertions(+), 3 deletions(-)
 create mode 100644 kernel/seccomp_inject.c
 create mode 100644 kernel/seccomp_inject.h
 create mode 100644 tools/testing/selftests/seccomp/seccomp_notif_inject.c

--
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 Cong Wang [this message]
2026-05-15  4:27 ` [RFC PATCH v2 1/3] seccomp: add " Cong Wang
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-1-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®