From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-dy1-f170.google.com (mail-dy1-f170.google.com [74.125.82.170]) (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 901833CF976 for ; Fri, 15 May 2026 04:27:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.170 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778819263; cv=none; b=q5JnRSA96g1lQK3tcp8iN5DNfQxyKEYHTuNKTEIOfqib72MgUtMAYaAWKnvEN0ecFTMLetMm7eI4Oo4z5cEdaBbvw/TXYzB8T+cuF/IrQtqvu0ilPxPF7liL2WtJnJcxDhJ5Yf7Q3gloEUo1kz7JvrTuXoUs/pYCT/KKJSuILBE= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778819263; c=relaxed/simple; bh=tU/32MohNjB1tZ7mH/S+fShi59zWaS2cuTzsRDqWZjk=; h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; b=g8455KTcihmVgshmpH+pSRgsyIdIjNd1DnP05bQs1U9gIBVviuj9vPR1+vveT07ui3L6rZhhXzPIi4CvP1f9qvWTtG2Z59H3RlTeu6buUkC6bQjm8Kd+g17AO1bia31LrNsrkgKofaSpr0GbfZd/s3WPZAvnOxA8FHUa5uhbVuc= 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=ZWSfwYnE; arc=none smtp.client-ip=74.125.82.170 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="ZWSfwYnE" Received: by mail-dy1-f170.google.com with SMTP id 5a478bee46e88-2f0ad52830cso12982560eec.1 for ; Thu, 14 May 2026 21:27:41 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778819261; x=1779424061; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=exx4xMEWD6rWdjZrkxTyvFSJ5nRM7ZAQ7uG0JgjUgKE=; b=ZWSfwYnEOHLXa1ZQlKXsC4YW2w1flBipPVezMFShTL2IvMWASoC/ZWIV0gKKfxECyi 16OGbp679ZZPw3BRpRC5JQesotWYrWtB73XvlQtS+RFpOxui1pyd/ob/Y2Ti9+VAe0Jr jxtJhwOPMFwknbe94Ntk1VDemNTfwINN4HYHn9BBXOekE2jmnc74ZHMJrFD8u45g3EQh cBhQCyFhx+EWIIXM+q6QIwt3m7ZaDuBxqWGe9DQ7ub0DSFVZTRUMlcVDZ2VKD9Ht+lZl G+gibMej0FIGyDmf6PQjYQk8NBiT5HSXCwliZHGKmM+nFnAug6bHBCeMSe91018T5GUP WhYg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778819261; x=1779424061; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=exx4xMEWD6rWdjZrkxTyvFSJ5nRM7ZAQ7uG0JgjUgKE=; b=OE/boQitd68dpETWBwKfH9FZ7cja/3OkbVCkCGYP4gX9SoZY2iKIYLNpXron5ij+y4 VDDNy5Q12SelizEtBs0EpH8bgDnUEE2rzRaSmcoWkinTYRFN4Prm3p6dJ4zYDAHyzFwl Yea1KlY4NE798UXxLwHpUwe6uhFx5JoMxjQ5t2aGh08TAQoPxxTXncHK+TMTysZSUUqa 9ghEP7juZDa5TyP7349DQonzFIwtqLWBXv0NU+WrilhGmHKzyPEUAh3TUL/C8m/YdKSj Yyk32KQb3ZM928uYGLE48JdeEV+MZDtpVkuanql0LDafRhHcpN4v7eLmptnYQB7ricAL Bavw== X-Forwarded-Encrypted: i=1; AFNElJ8BaW7Q08cpLmMkihuSlzwAt4lg2dyBVXV7Ly2ABw727eMTLV1fn4v1TEleLTSyMMYuvJxVaxbxIso8ZTI=@vger.kernel.org X-Gm-Message-State: AOJu0Ywhk2FixyRs55AFmO8Fn5UGzsbgbVNqGg/nFVaEq5P7jd1mVkIL h7nip1CN9aqnbT4cZRcKLCbDBeP5Jvp9VIKU9NYkTyuEMyo3M/CrygaCE7dHIw== X-Gm-Gg: Acq92OGZ/WeaT2ZH3pmT+hZsMaRs+/8rWieMOifa/B2UthUaT2NInFsDLyh0jB/6OSD trQCvLDt80I+g2xs3uV12KbP3H/0dQhjPIM2cCwURPCo9jnvNn+AhYFv+HklzTGPOsfw2pcri4u Ohz0MdEVt3rSsvpl67UvxPCjxdpghUBo8ggfvxQI5vqX1iReCKrE0oZZd3FPkrExtlZ4JLJ8Bdt yR/GZELSaMJn+wqKHetnYAsJnwVx3XGKZ+OnDCICl+6fWsia9zVt+mrvjMryuT5I1qCqUerfKJT hzlRnnlYveJrLytZkTJVPchqjTez1Y0J/n3gk2XlvidqBMT3RuYAgdQv/7M1lH+Yf9LCvwe/14b FfuIpu4pJ4X2fVNm/jK2lUIndzTJqSV5apGCd/CakYytK4dljUmYpvsSJaDckm7/5eqsQD1D65n 4LXIcoZgs8i0fJ5Jyqp9N6DWSv4CQUCJopFY3DBwrkN6B5OVp/bt22/lihGBNyQVkbPNHlg/0= X-Received: by 2002:a05:7300:df06:b0:2f3:c3d4:382f with SMTP id 5a478bee46e88-30398185de3mr1289214eec.8.1778819260452; Thu, 14 May 2026 21:27:40 -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.39 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 14 May 2026 21:27:39 -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 0/3] seccomp: SECCOMP_IOCTL_NOTIF_INJECT for race-free unotify Date: Thu, 14 May 2026 21:27:35 -0700 Message-ID: <20260515042738.1723296-1-xiyou.wangcong@gmail.com> X-Mailer: git-send-email 2.43.0 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit From: Cong Wang 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