* [RFC PATCH 00/24] pidfd: add a minimal process spawn builder
@ 2026-07-16 14:31 Li Chen
2026-07-16 14:31 ` [RFC PATCH 01/24] pidfd: add spawn builder uapi Li Chen
` (23 more replies)
0 siblings, 24 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm
Hi,
This RFC follows feedback on my earlier spawn_template RFC [1]. That
proposal made caching the primary interface; this one starts with general
process construction. Christian suggested a pidfd/pidfs exec builder
modeled after fsconfig(), with enough semantics for userspace to implement
posix_spawn() [2], and Kees agreed [3].
This RFC is based on linux-next next-20260710 and depends on two pidfs
fixes that I sent separately:
* pidfs: preserve thread pidfds reopened by file handle
https://lore.kernel.org/all/20260716052726.1032092-1-me@linux.beauty/
* pidfs: handle FS_IOC32_GETVERSION in compat ioctl
https://lore.kernel.org/all/20260716052822.1034228-1-me@linux.beauty/
The initial implementation is source-based. The executable path can be
provided with the final run request:
struct pidfd_spawn_run_args run = {
.path = (unsigned long)"/usr/bin/rg",
.argv = (unsigned long)argv,
.envp = (unsigned long)envp,
};
fd = pidfd_open(0, PIDFD_EMPTY);
pidfd_spawn_run(fd, &run, sizeof(run));
Alternatively, the path can be staged before the final run step:
struct pidfd_spawn_run_args run = {
.argv = (unsigned long)argv,
.envp = (unsigned long)envp,
};
fd = pidfd_open(0, PIDFD_EMPTY);
pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
PIDFD_CONFIG_KEY_PATH, "/usr/bin/rg", 0);
pidfd_spawn_run(fd, &run, sizeof(run));
pidfd_open(0, PIDFD_EMPTY) creates a taskless future pidfd with a stable
pidfs inode, but no task, PID, or process-count charge. pidfd_spawn_run()
creates the task and PID; after publication the same fd is the child pidfd,
and a numeric pidfd resolves to the same inode. Live-task operations return
-ESRCH before publication. A terminal pre-task failure wakes poll/epoll
with POLLERR | POLLHUP.
Source-based mode follows posix_spawn-style defaults. At run time it
samples the caller's cwd, root, umask, fd table, signal dispositions and
blocked mask, and namespaces.
Non-FD_CLOEXEC descriptors remain unless an action changes them; file and
filesystem state are private child copies before actions. Configuration and
run stay bound to the creating mm_struct, exact credential object, and
child PID namespace, so SCM_RIGHTS does not delegate launch authority.
The first authorized run claims the builder before copying its payload, so
later failures are terminal. Pre-task failure leaves the fd taskless;
setup or exec failure leaves it as the child pidfd and exits the child with
status 127. PIDFD_GET_INFO with PIDFD_INFO_EXIT distinguishes them.
Success returns the positive child PID in the caller's PID namespace.
The RFC supports ordered DUP2, CLOSE_RANGE, and FCHDIR actions in
extensible UAPI records. This exercises child-private fd and cwd setup, but
is not the complete posix_spawn() action or attribute surface.
Between task publication and successful exec, the child is explicitly
embryonic and may not have a valid userspace register frame. Ptrace and
pidfd_getfd() are denied, procfs treats the PID as absent to other tasks,
and coredump information reports PIDFD_COREDUMP_SKIP. The child can use its
own proc entries during executable lookup. Setup runs as initial child task
work, and successful exec releases this state before exec events are
published.
Seccomp sees pidfd_spawn_run(), not separate file-action or exec syscalls,
and cannot inspect the path or action records behind the run pointer. An
exec-only denylist that allows unknown syscalls therefore does not block
this initial exec; policy must filter the builder syscall as a unit. Should
later expansion provide an immutable restriction profile or action mask
that seccomp can reason about, or is the coarse syscall boundary
preferable?
LSM exec checks and inherited seccomp state remain active. Child setup uses
a dedicated AUDIT_PIDFD_SPAWN transaction, not a synthetic AUDIT_SYSCALL.
Should it be selected through the source pidfd_spawn_run() exit rule? An
existing rule naming only execve() or execveat() does not select it.
For source/child correlation, could an auxiliary record carry
the source PID plus the stable pidfs inode? An already traced source is
rejected before the claim; ptrace auto-attach is not implemented.
The implementation still uses CLONE_VM | CLONE_VFORK plus exec internally.
Mateusz suggested that an initial implementation might start with vfork to
get the API off the ground [4]. That is what this RFC does. It does not yet
construct a pristine target process without first inheriting source state.
Missing posix_spawn() pieces include open and close file actions, resetids,
signal masks/defaults, process groups, sessions, scheduler attributes,
affinity, cgroup placement, PATH lookup/posix_spawnp(), and exec by fd. The
RFC also does not include pristine/no-source creation or the executable
metadata/template cache from my earlier work.
John Ericson described a real, partially initialized process that remains
unscheduled while callers install its state, and linked an exploratory
FreeBSD proc_new()/proc_setfd()/proc_start() refactoring [5]. This RFC
implements the source-based mode first; a lower-authority pristine/no-source
mode would be explicit follow-up work.
Direct process construction is not unprecedented. XNU's posix_spawn path
does not inherit the parent's address space [6], and Windows
CreateProcess() accepts explicit startup state [7]. Josh's io_uring_spawn
LPC slides list "set up process from scratch" as future work and provide
useful performance context [8]; I am not using those numbers as a claim for
this RFC.
If the direction is acceptable, I plan to continue toward:
* the complete file-action and attribute set needed by posix_spawn();
* pristine target-process construction and an explicit no-source mode;
* PATH/posix_spawnp support, if it belongs on the kernel side; and
* an optional executable/template layer for workloads such as agent tool
calling and compiler drivers.
The exposed UAPI surface is intentionally limited so the state model and
kernel/userspace boundary can be reviewed first. If maintainers would
prefer more posix_spawn semantics or backend work in this RFC, please say
so and I will adjust the split.
Codex GPT-5.5 and GPT-5.6-sol provided substantial assistance across design
conceptualization, implementation, patch splitting, code review, development
of self-test cases, and test planning and execution.
Thanks to Christian, Kees, Mateusz, Gabriel, Josh, Andy, John, and others
for the review and direction.
[1]: https://patchew.org/linux/20260528095235.2491226-1-me%40linux.beauty/
[2]: https://lore.kernel.org/all/20260528-madig-fachrichtung-fehlinformation-61117ba640da@brauner/
[3]: https://lore.kernel.org/all/202606011254.5FCBD65@keescook/
[4]: https://lore.kernel.org/all/vealb52tv5suireenkke4lul2l3wbnaul2rp3ea545ly5wa5ty@yk3aksvp7skt/
[5]: https://lore.kernel.org/all/ce71d6df-6851-4e4b-9603-1d55d8d522b8@app.fastmail.com/
[6]: https://github.com/apple-oss-distributions/xnu/blob/f6217f891ac0bb64f3d375211650a4c1ff8ca1ea/bsd/kern/kern_exec.c#L4039
[7]: https://learn.microsoft.com/en-us/windows/win32/procthread/creating-processes
[8]: https://lpc.events/event/16/contributions/1213/attachments/1012/1945/io-uring-spawn.pdf
Li Chen (24):
pidfd: add spawn builder uapi
libfs: allow custom validation of stashed inode data
pidfs: add taskless future pidfd inodes
pidfd: create taskless spawn builders
pidfd: add spawn builder path configuration
exec: expose execveat internals to process builders
fork: expose vfork completion helper
pidfs: attach pids to future pidfd files
fork: let process builders supply preallocated pids
pidfs: publish future pidfd files
pidfd: add spawn builder state tracking
fork: let kernel callers create embryonic tasks
fork: let new tasks start with task work
pidfd: create and execute spawn builder tasks
fork: keep embryonic tasks hidden until exec completes
audit: add pidfd spawn child contexts
pidfd: audit child spawn execution
pidfd: make spawn builder execution signal-safe
file: expose spawn file-action helpers
pidfd: add initial spawn file actions
pidfd: consume spawn builders on the first run attempt
pidfd: expose spawn builder system calls
selftests/pidfd: cover pidfd spawn builders
Documentation: describe pidfd spawn builders
Documentation/userspace-api/index.rst | 1 +
Documentation/userspace-api/pidfd_spawn.rst | 247 ++++
MAINTAINERS | 6 +
arch/alpha/kernel/syscalls/syscall.tbl | 2 +
arch/arm/tools/syscall.tbl | 2 +
arch/arm64/tools/syscall_32.tbl | 2 +
arch/m68k/kernel/syscalls/syscall.tbl | 2 +
arch/microblaze/kernel/syscalls/syscall.tbl | 2 +
arch/mips/kernel/syscalls/syscall_n32.tbl | 2 +
arch/mips/kernel/syscalls/syscall_n64.tbl | 2 +
arch/mips/kernel/syscalls/syscall_o32.tbl | 2 +
arch/parisc/kernel/syscalls/syscall.tbl | 2 +
arch/powerpc/kernel/syscalls/syscall.tbl | 2 +
arch/s390/kernel/syscalls/syscall.tbl | 2 +
arch/sh/kernel/syscalls/syscall.tbl | 2 +
arch/sparc/kernel/syscalls/syscall.tbl | 2 +
arch/x86/entry/syscalls/syscall_32.tbl | 2 +
arch/x86/entry/syscalls/syscall_64.tbl | 2 +
arch/xtensa/kernel/syscalls/syscall.tbl | 2 +
fs/Makefile | 2 +-
fs/coredump.c | 4 +-
fs/exec.c | 30 +-
fs/exec_internal.h | 40 +
fs/file.c | 11 +-
fs/internal.h | 3 +
fs/libfs.c | 5 +-
fs/open.c | 7 +-
fs/pidfd_spawn.c | 1095 +++++++++++++++
fs/pidfs.c | 478 ++++++-
fs/proc/base.c | 11 +-
fs/proc/internal.h | 18 +-
include/linux/audit.h | 31 +
include/linux/pid.h | 13 +
include/linux/pidfd_spawn.h | 9 +
include/linux/pidfs.h | 28 +
include/linux/sched.h | 21 +
include/linux/sched/task.h | 6 +
include/linux/syscalls.h | 7 +
include/uapi/asm-generic/unistd.h | 8 +-
include/uapi/linux/audit.h | 1 +
include/uapi/linux/pidfd.h | 1 +
include/uapi/linux/pidfd_spawn.h | 49 +
kernel/audit.h | 1 +
kernel/auditsc.c | 105 +-
kernel/fork.c | 26 +-
kernel/nsproxy.c | 11 +-
kernel/pid.c | 41 +-
kernel/ptrace.c | 4 +
kernel/signal.c | 2 +-
scripts/syscall.tbl | 2 +
tools/include/uapi/asm-generic/unistd.h | 8 +-
tools/include/uapi/linux/pidfd_spawn.h | 49 +
.../arch/alpha/entry/syscalls/syscall.tbl | 2 +
.../perf/arch/arm/entry/syscalls/syscall.tbl | 2 +
.../arch/arm64/entry/syscalls/syscall_32.tbl | 11 +
.../arch/mips/entry/syscalls/syscall_n64.tbl | 2 +
.../arch/parisc/entry/syscalls/syscall.tbl | 2 +
.../arch/powerpc/entry/syscalls/syscall.tbl | 2 +
.../perf/arch/s390/entry/syscalls/syscall.tbl | 2 +
tools/perf/arch/sh/entry/syscalls/syscall.tbl | 2 +
.../arch/sparc/entry/syscalls/syscall.tbl | 2 +
.../arch/x86/entry/syscalls/syscall_32.tbl | 2 +
.../arch/x86/entry/syscalls/syscall_64.tbl | 2 +
.../arch/xtensa/entry/syscalls/syscall.tbl | 2 +
tools/scripts/syscall.tbl | 2 +
tools/testing/selftests/landlock/audit.h | 6 +-
tools/testing/selftests/pidfd/.gitignore | 9 +
tools/testing/selftests/pidfd/Makefile | 25 +-
tools/testing/selftests/pidfd/config | 6 +
.../pidfd/pidfd_spawn_accounting_test.c | 428 ++++++
.../pidfd/pidfd_spawn_actions_test.c | 474 +++++++
.../selftests/pidfd/pidfd_spawn_audit_test.c | 521 +++++++
.../selftests/pidfd/pidfd_spawn_common.c | 512 +++++++
.../selftests/pidfd/pidfd_spawn_common.h | 59 +
.../selftests/pidfd/pidfd_spawn_compat.c | 221 +++
.../selftests/pidfd/pidfd_spawn_exec_test.c | 301 ++++
.../selftests/pidfd/pidfd_spawn_policy_test.c | 294 ++++
.../selftests/pidfd/pidfd_spawn_race_test.c | 923 ++++++++++++
.../pidfd/pidfd_spawn_security_test.c | 1242 +++++++++++++++++
.../selftests/pidfd/pidfd_spawn_test.c | 550 ++++++++
80 files changed, 7938 insertions(+), 81 deletions(-)
create mode 100644 Documentation/userspace-api/pidfd_spawn.rst
create mode 100644 fs/exec_internal.h
create mode 100644 fs/pidfd_spawn.c
create mode 100644 include/linux/pidfd_spawn.h
create mode 100644 include/uapi/linux/pidfd_spawn.h
create mode 100644 tools/include/uapi/linux/pidfd_spawn.h
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_common.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_common.h
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_compat.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_race_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_security_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_test.c
--
2.52.0
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC PATCH 01/24] pidfd: add spawn builder uapi
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 02/24] libfs: allow custom validation of stashed inode data Li Chen
` (22 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Define PIDFD_EMPTY, the fsconfig-style configuration command and path
key, and versioned run arguments for pidfd spawn builders. Add the
initial ordered file-action records.
Store userspace pointers in full-width aligned containers on every ABI.
Keep flags and reserved fields zero so later kernels can extend the
structures without changing their initial layout.
Suggested-by: Christian Brauner <brauner@kernel.org>
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
MAINTAINERS | 2 ++
include/uapi/linux/pidfd.h | 1 +
include/uapi/linux/pidfd_spawn.h | 49 ++++++++++++++++++++++++++
tools/include/uapi/linux/pidfd_spawn.h | 49 ++++++++++++++++++++++++++
4 files changed, 101 insertions(+)
create mode 100644 include/uapi/linux/pidfd_spawn.h
create mode 100644 tools/include/uapi/linux/pidfd_spawn.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 8729cea57c3dd..b63bc1ee0171f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21287,7 +21287,9 @@ M: Christian Brauner <christian@brauner.io>
L: linux-kernel@vger.kernel.org
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/brauner/linux.git
+F: include/uapi/linux/pidfd_spawn.h
F: samples/pidfd/
+F: tools/include/uapi/linux/pidfd_spawn.h
F: tools/testing/selftests/clone3/
F: tools/testing/selftests/pidfd/
K: (?i)pidfd
diff --git a/include/uapi/linux/pidfd.h b/include/uapi/linux/pidfd.h
index 0919246a1611c..cad5b722e9f0e 100644
--- a/include/uapi/linux/pidfd.h
+++ b/include/uapi/linux/pidfd.h
@@ -10,6 +10,7 @@
/* Flags for pidfd_open(). */
#define PIDFD_NONBLOCK O_NONBLOCK
#define PIDFD_THREAD O_EXCL
+#define PIDFD_EMPTY (1U << 27)
#ifdef __KERNEL__
#include <linux/sched.h>
#define PIDFD_STALE CLONE_PIDFD
diff --git a/include/uapi/linux/pidfd_spawn.h b/include/uapi/linux/pidfd_spawn.h
new file mode 100644
index 0000000000000..46d61e72435cc
--- /dev/null
+++ b/include/uapi/linux/pidfd_spawn.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+
+#ifndef _UAPI_LINUX_PIDFD_SPAWN_H
+#define _UAPI_LINUX_PIDFD_SPAWN_H
+
+#include <linux/types.h>
+
+#define PIDFD_SPAWN_RUN_SIZE_VER0 64
+#define PIDFD_SPAWN_ACTION_SIZE_VER0 32
+
+/* Commands for pidfd_config(). */
+#define PIDFD_CONFIG_SET_STRING 0
+
+/* String keys for pidfd_config(). */
+#define PIDFD_CONFIG_KEY_PATH "path"
+
+struct pidfd_spawn_run_args {
+ __u32 flags;
+ __u32 nr_actions;
+ /* The next four fields are full-width userspace virtual addresses. */
+ __aligned_u64 path;
+ __aligned_u64 argv;
+ __aligned_u64 envp;
+ __aligned_u64 actions;
+ __u32 action_size;
+ __u32 reserved0;
+ __u64 reserved[2];
+};
+
+enum pidfd_spawn_action_type {
+ PIDFD_SPAWN_ACTION_DUP2 = 0,
+ PIDFD_SPAWN_ACTION_CLOSE_RANGE = 1,
+ PIDFD_SPAWN_ACTION_FCHDIR = 2,
+};
+
+struct pidfd_spawn_action {
+ __u32 type;
+ __u32 flags;
+ /*
+ * DUP2 uses fd as the source and newfd as the destination.
+ * CLOSE_RANGE uses fd as the first and newfd as the last descriptor.
+ * FCHDIR uses fd as the directory descriptor and requires newfd to be 0.
+ */
+ __u32 fd;
+ __u32 newfd;
+ __u64 reserved[2];
+};
+
+#endif /* _UAPI_LINUX_PIDFD_SPAWN_H */
diff --git a/tools/include/uapi/linux/pidfd_spawn.h b/tools/include/uapi/linux/pidfd_spawn.h
new file mode 100644
index 0000000000000..46d61e72435cc
--- /dev/null
+++ b/tools/include/uapi/linux/pidfd_spawn.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+
+#ifndef _UAPI_LINUX_PIDFD_SPAWN_H
+#define _UAPI_LINUX_PIDFD_SPAWN_H
+
+#include <linux/types.h>
+
+#define PIDFD_SPAWN_RUN_SIZE_VER0 64
+#define PIDFD_SPAWN_ACTION_SIZE_VER0 32
+
+/* Commands for pidfd_config(). */
+#define PIDFD_CONFIG_SET_STRING 0
+
+/* String keys for pidfd_config(). */
+#define PIDFD_CONFIG_KEY_PATH "path"
+
+struct pidfd_spawn_run_args {
+ __u32 flags;
+ __u32 nr_actions;
+ /* The next four fields are full-width userspace virtual addresses. */
+ __aligned_u64 path;
+ __aligned_u64 argv;
+ __aligned_u64 envp;
+ __aligned_u64 actions;
+ __u32 action_size;
+ __u32 reserved0;
+ __u64 reserved[2];
+};
+
+enum pidfd_spawn_action_type {
+ PIDFD_SPAWN_ACTION_DUP2 = 0,
+ PIDFD_SPAWN_ACTION_CLOSE_RANGE = 1,
+ PIDFD_SPAWN_ACTION_FCHDIR = 2,
+};
+
+struct pidfd_spawn_action {
+ __u32 type;
+ __u32 flags;
+ /*
+ * DUP2 uses fd as the source and newfd as the destination.
+ * CLOSE_RANGE uses fd as the first and newfd as the last descriptor.
+ * FCHDIR uses fd as the directory descriptor and requires newfd to be 0.
+ */
+ __u32 fd;
+ __u32 newfd;
+ __u64 reserved[2];
+};
+
+#endif /* _UAPI_LINUX_PIDFD_SPAWN_H */
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 02/24] libfs: allow custom validation of stashed inode data
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
2026-07-16 14:31 ` [RFC PATCH 01/24] pidfd: add spawn builder uapi Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 03/24] pidfs: add taskless future pidfd inodes Li Chen
` (21 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
path_from_stashed() assumes that inode->i_private is identical to the
caller data used to find or create a dentry. That is too strict for an
inode whose stable identity can later resolve through another object.
Add an optional data_matches() callback to stashed_operations. Existing
users retain the pointer-identity check when no callback is provided.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/internal.h | 1 +
fs/libfs.c | 5 ++++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/fs/internal.h b/fs/internal.h
index 174f063575558..71cc43e72b33e 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -334,6 +334,7 @@ struct stashed_operations {
struct dentry *dentry);
void (*put_data)(void *data);
int (*init_inode)(struct inode *inode, void *data);
+ bool (*data_matches)(const struct inode *inode, const void *data);
};
int path_from_stashed(struct dentry **stashed, struct vfsmount *mnt, void *data,
struct path *path);
diff --git a/fs/libfs.c b/fs/libfs.c
index 5a0d276379d13..68cc1671b4699 100644
--- a/fs/libfs.c
+++ b/fs/libfs.c
@@ -2260,7 +2260,10 @@ int path_from_stashed(struct dentry **stashed, struct vfsmount *mnt, void *data,
path->dentry = res;
path->mnt = mntget(mnt);
VFS_WARN_ON_ONCE(path->dentry->d_fsdata != stashed);
- VFS_WARN_ON_ONCE(d_inode(path->dentry)->i_private != data);
+ if (sops->data_matches)
+ VFS_WARN_ON_ONCE(!sops->data_matches(d_inode(path->dentry), data));
+ else
+ VFS_WARN_ON_ONCE(d_inode(path->dentry)->i_private != data);
return 0;
}
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 03/24] pidfs: add taskless future pidfd inodes
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
2026-07-16 14:31 ` [RFC PATCH 01/24] pidfd: add spawn builder uapi Li Chen
2026-07-16 14:31 ` [RFC PATCH 02/24] libfs: allow custom validation of stashed inode data Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 04/24] pidfd: create taskless spawn builders Li Chen
` (20 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Teach pidfs inodes to represent either a struct pid or a typed future
object. Use the ordinary pidfs file operations for both kinds and
resolve a future object through its callback.
Allocate a stable inode identity without allocating a task or struct
pid. Ordinary pidfd operations return -ESRCH until the future producer
publishes a process, while inode-only GETVERSION remains available.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfs.c | 254 ++++++++++++++++++++++++++++++++++++++----
include/linux/pid.h | 10 ++
include/linux/pidfs.h | 21 ++++
3 files changed, 262 insertions(+), 23 deletions(-)
diff --git a/fs/pidfs.c b/fs/pidfs.c
index c55f46c32801d..ebd8cc463811b 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -25,6 +25,7 @@
#include <net/net_namespace.h>
#include <linux/coredump.h>
#include <linux/rhashtable.h>
+#include <linux/security.h>
#include <linux/llist.h>
#include <linux/xattr.h>
#include <linux/cookie.h>
@@ -35,6 +36,8 @@
#define PIDFS_PID_DEAD ERR_PTR(-ESRCH)
static struct kmem_cache *pidfs_attr_cachep __ro_after_init;
+static const struct file_operations pidfs_file_operations;
+static struct vfsmount *pidfs_mnt __ro_after_init;
static struct path pidfs_root_path = {};
@@ -106,6 +109,64 @@ struct pidfs_attr {
};
};
+struct pidfs_future_file {
+ struct pidfs_node node;
+ void *data;
+ const struct pidfs_future_file_ops *ops;
+ struct pidfs_attr *attr;
+ u64 ino;
+};
+
+static struct pidfs_node *pidfs_inode_node(const struct inode *inode)
+{
+ if (!pidfs_mnt || inode->i_sb != pidfs_mnt->mnt_sb ||
+ inode->i_fop != &pidfs_file_operations)
+ return NULL;
+ return inode->i_private;
+}
+
+static struct pidfs_future_file *
+pidfs_future_file(const struct inode *inode)
+{
+ struct pidfs_node *node = pidfs_inode_node(inode);
+
+ if (!node || node->type != PIDFS_NODE_FUTURE)
+ return NULL;
+ return container_of(node, struct pidfs_future_file, node);
+}
+
+void *pidfs_future_file_data(const struct file *file,
+ const struct pidfs_future_file_ops *ops)
+{
+ struct pidfs_future_file *future;
+
+ if (file->f_op != &pidfs_file_operations)
+ return NULL;
+ future = pidfs_future_file(file_inode(file));
+ return future && future->ops == ops ? future->data : NULL;
+}
+
+static struct pid *pidfs_inode_pid(const struct inode *inode)
+{
+ struct pidfs_future_file *future;
+ struct pidfs_node *node;
+ struct pid *pid;
+
+ node = pidfs_inode_node(inode);
+ if (!node)
+ return ERR_PTR(-EBADF);
+ if (node->type == PIDFS_NODE_PID)
+ return container_of(node, struct pid, pidfs_node);
+
+ future = pidfs_future_file(inode);
+ if (!future || !future->ops || !future->ops->get_pid)
+ return ERR_PTR(-EBADF);
+ pid = future->ops->get_pid(future->data);
+ if (WARN_ON_ONCE(!pid))
+ return ERR_PTR(-EBADF);
+ return pid;
+}
+
#if BITS_PER_LONG == 32
DEFINE_SPINLOCK(pidfs_ino_lock);
@@ -166,6 +227,7 @@ static u64 pidfs_alloc_ino(void)
void pidfs_prepare_pid(struct pid *pid)
{
+ pid->pidfs_node.type = PIDFS_NODE_PID;
pid->stashed = NULL;
pid->attr = NULL;
pid->ino = 0;
@@ -275,7 +337,7 @@ static void pidfd_show_fdinfo(struct seq_file *m, struct file *f)
struct pid_namespace *ns;
pid_t nr = -1;
- if (likely(pid_has_task(pid, PIDTYPE_PID))) {
+ if (!IS_ERR(pid) && likely(pid_has_task(pid, PIDTYPE_PID))) {
ns = proc_pid_ns(file_inode(m->file)->i_sb);
nr = pid_nr_ns(pid, ns);
}
@@ -305,11 +367,15 @@ static void pidfd_show_fdinfo(struct seq_file *m, struct file *f)
*/
static __poll_t pidfd_poll(struct file *file, struct poll_table_struct *pts)
{
- struct pid *pid = pidfd_pid(file);
struct task_struct *task;
__poll_t poll_flags = 0;
+ struct pid *pid = pidfd_pid(file);
+
+ if (IS_ERR(pid))
+ return PTR_ERR(pid) == -ESRCH ? 0 : EPOLLHUP;
poll_wait(file, &pid->wait_pidfd, pts);
+
/*
* Don't wake waiters if the thread-group leader exited
* prematurely. They either get notified when the last subthread
@@ -376,6 +442,8 @@ static long pidfd_info(struct file *file, unsigned int cmd, unsigned long arg)
BUILD_BUG_ON(sizeof(struct pidfd_info) != PIDFD_INFO_SIZE_VER3);
+ if (IS_ERR(pid))
+ return PTR_ERR(pid);
if (!uinfo)
return -EINVAL;
if (usize < PIDFD_INFO_SIZE_VER0)
@@ -532,6 +600,7 @@ static long pidfd_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
struct task_struct *task __free(put_task) = NULL;
struct nsproxy *nsp __free(put_nsproxy) = NULL;
struct ns_common *ns_common = NULL;
+ struct pid *pid;
if (!pidfs_ioctl_valid(cmd))
return -ENOIOCTLCMD;
@@ -544,11 +613,15 @@ static long pidfd_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
return put_user(file_inode(file)->i_generation, argp);
}
+ pid = pidfd_pid(file);
+ if (IS_ERR(pid))
+ return PTR_ERR(pid);
+
/* Extensible IOCTL that does not open namespace FDs, take a shortcut */
if (_IOC_NR(cmd) == _IOC_NR(PIDFD_GET_INFO))
return pidfd_info(file, cmd, arg);
- task = get_pid_task(pidfd_pid(file), PIDTYPE_PID);
+ task = get_pid_task(pid, PIDTYPE_PID);
if (!task)
return -ESRCH;
@@ -673,11 +746,14 @@ static long pidfd_compat_ioctl(struct file *file, unsigned int cmd,
static int pidfs_file_release(struct inode *inode, struct file *file)
{
- struct pid *pid = inode->i_private;
+ struct pid *pid;
struct task_struct *task;
if (!(file->f_flags & PIDFD_AUTOKILL))
return 0;
+ pid = pidfd_pid(file);
+ if (IS_ERR(pid))
+ return 0;
guard(rcu)();
task = pid_task(pid, PIDTYPE_TGID);
@@ -705,9 +781,9 @@ static const struct file_operations pidfs_file_operations = {
struct pid *pidfd_pid(const struct file *file)
{
- if (file->f_op != &pidfs_file_operations)
+ if (unlikely(file->f_op != &pidfs_file_operations))
return ERR_PTR(-EBADF);
- return file_inode(file)->i_private;
+ return pidfs_inode_pid(file_inode(file));
}
/*
@@ -797,8 +873,6 @@ void pidfs_coredump(const struct coredump_params *cprm)
}
#endif
-static struct vfsmount *pidfs_mnt __ro_after_init;
-
/*
* The vfs falls back to simple_setattr() if i_op->setattr() isn't
* implemented. Let's reject it completely until we have a clean
@@ -820,7 +894,10 @@ static int pidfs_getattr(struct mnt_idmap *idmap, const struct path *path,
static ssize_t pidfs_listxattr(struct dentry *dentry, char *buf, size_t size)
{
struct inode *inode = d_inode(dentry);
- struct pid *pid = inode->i_private;
+ struct pid *pid = pidfs_inode_pid(inode);
+
+ if (IS_ERR(pid))
+ return PTR_ERR(pid);
return simple_xattr_list(inode, &pid->attr->xattrs, buf, size);
}
@@ -833,10 +910,25 @@ static const struct inode_operations pidfs_inode_operations = {
static void pidfs_evict_inode(struct inode *inode)
{
- struct pid *pid = inode->i_private;
+ struct pidfs_future_file *future;
+ struct pidfs_node *node = pidfs_inode_node(inode);
clear_inode(inode);
- put_pid(pid);
+ if (!node)
+ return;
+ if (node->type == PIDFS_NODE_PID) {
+ put_pid(container_of(node, struct pid, pidfs_node));
+ return;
+ }
+ if (WARN_ON_ONCE(node->type != PIDFS_NODE_FUTURE))
+ return;
+
+ future = container_of(node, struct pidfs_future_file, node);
+ if (future->ops->release)
+ future->ops->release(future->data);
+ if (future->attr)
+ kmem_cache_free(pidfs_attr_cachep, future->attr);
+ kfree(future);
}
static const struct super_operations pidfs_sops = {
@@ -854,15 +946,24 @@ static char *pidfs_dname(struct dentry *dentry, char *buffer, int buflen)
return dynamic_dname(buffer, buflen, "anon_inode:[pidfd]");
}
+static void pidfs_dentry_prune(struct dentry *dentry)
+{
+ if (dentry->d_fsdata)
+ stashed_dentry_prune(dentry);
+}
+
const struct dentry_operations pidfs_dentry_operations = {
.d_dname = pidfs_dname,
- .d_prune = stashed_dentry_prune,
+ .d_prune = pidfs_dentry_prune,
};
static int pidfs_encode_fh(struct inode *inode, u32 *fh, int *max_len,
struct inode *parent)
{
- const struct pid *pid = inode->i_private;
+ const struct pid *pid = pidfs_inode_pid(inode);
+
+ if (IS_ERR(pid))
+ return FILEID_INVALID;
if (*max_len < 2) {
*max_len = 2;
@@ -974,22 +1075,36 @@ static const struct export_operations pidfs_export_operations = {
.permission = pidfs_export_permission,
};
-static int pidfs_init_inode(struct inode *inode, void *data)
+static void pidfs_init_common_inode(struct inode *inode,
+ struct pidfs_node *node, u64 ino)
{
- const struct pid *pid = data;
-
- inode->i_private = data;
+ inode->i_private = node;
inode->i_flags |= S_PRIVATE | S_ANON_INODE;
/* We allow to set xattrs. */
inode->i_flags &= ~S_IMMUTABLE;
- inode->i_mode |= S_IRWXU;
+ inode->i_mode = S_IFREG | 0700;
+ inode->i_uid = GLOBAL_ROOT_UID;
+ inode->i_gid = GLOBAL_ROOT_GID;
inode->i_op = &pidfs_inode_operations;
inode->i_fop = &pidfs_file_operations;
- inode->i_ino = pidfs_ino(pid->ino);
- inode->i_generation = pidfs_gen(pid->ino);
+ inode->i_ino = pidfs_ino(ino);
+ inode->i_generation = pidfs_gen(ino);
+}
+
+static int pidfs_init_inode(struct inode *inode, void *data)
+{
+ struct pid *pid = data;
+
+ pidfs_init_common_inode(inode, &pid->pidfs_node, pid->ino);
return 0;
}
+static bool pidfs_inode_data_matches(const struct inode *inode,
+ const void *data)
+{
+ return pidfs_inode_pid(inode) == data;
+}
+
static void pidfs_put_data(void *data)
{
struct pid *pid = data;
@@ -1044,9 +1159,11 @@ int pidfs_register_pid_gfp(struct pid *pid, gfp_t gfp)
static struct dentry *pidfs_stash_dentry(struct dentry **stashed,
struct dentry *dentry)
{
+ struct pid *pid = pidfs_inode_pid(d_inode(dentry));
int ret;
- struct pid *pid = d_inode(dentry)->i_private;
+ if (WARN_ON_ONCE(IS_ERR(pid)))
+ return ERR_CAST(pid);
VFS_WARN_ON_ONCE(stashed != &pid->stashed);
ret = pidfs_register_pid(pid);
@@ -1060,15 +1177,19 @@ static const struct stashed_operations pidfs_stashed_ops = {
.stash_dentry = pidfs_stash_dentry,
.init_inode = pidfs_init_inode,
.put_data = pidfs_put_data,
+ .data_matches = pidfs_inode_data_matches,
};
static int pidfs_xattr_get(const struct xattr_handler *handler,
struct dentry *unused, struct inode *inode,
const char *suffix, void *value, size_t size)
{
- struct pid *pid = inode->i_private;
+ struct pid *pid = pidfs_inode_pid(inode);
const char *name = xattr_full_name(handler, suffix);
+ if (IS_ERR(pid))
+ return PTR_ERR(pid);
+
return simple_xattr_get(&pidfs_xa_cache, &pid->attr->xattrs, name, value, size);
}
@@ -1077,10 +1198,13 @@ static int pidfs_xattr_set(const struct xattr_handler *handler,
struct inode *inode, const char *suffix,
const void *value, size_t size, int flags)
{
- struct pid *pid = inode->i_private;
+ struct pid *pid = pidfs_inode_pid(inode);
const char *name = xattr_full_name(handler, suffix);
struct simple_xattr *old_xattr;
+ if (IS_ERR(pid))
+ return PTR_ERR(pid);
+
/* Ensure we're the only one to set @attr->xattrs. */
WARN_ON_ONCE(!inode_is_locked(inode));
@@ -1158,6 +1282,90 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
return pidfd_file;
}
+/**
+ * pidfs_alloc_future_file - allocate a taskless pidfs file
+ * @name: anonymous file name used by LSM initialization
+ * @data: producer data retained for the inode lifetime
+ * @ops: callbacks that resolve and release @data
+ * @flags: file status flags
+ *
+ * Ownership of @data transfers to the returned file on success. One serialized
+ * producer owns all later association and publication transitions. It may
+ * associate one preallocated pid with
+ * pidfs_future_file_set_pid(), publish its pidfs metadata with
+ * pidfs_future_file_publish_pid(), make @ops->get_pid() resolve that pid, and
+ * finally wake waiters with pidfs_future_file_notify().
+ *
+ * A task associated with the future pid must not become runnable before
+ * publication metadata and exit-wakeup forwarding are installed.
+ *
+ * Return: A new pidfs file on success or an error pointer on failure.
+ */
+struct file *pidfs_alloc_future_file(const char *name, void *data,
+ const struct pidfs_future_file_ops *ops,
+ unsigned int flags)
+{
+ struct pidfs_future_file *future;
+ struct inode *inode;
+ struct file *file;
+ u64 ino;
+ int ret;
+
+ if (!ops || !ops->get_pid)
+ return ERR_PTR(-EINVAL);
+
+ future = kzalloc_obj(*future, GFP_KERNEL_ACCOUNT);
+ if (!future)
+ return ERR_PTR(-ENOMEM);
+ future->attr = kmem_cache_zalloc(pidfs_attr_cachep, GFP_KERNEL_ACCOUNT);
+ if (!future->attr) {
+ kfree(future);
+ return ERR_PTR(-ENOMEM);
+ }
+ INIT_LIST_HEAD_RCU(&future->attr->xattrs);
+
+ inode = new_inode_pseudo(pidfs_mnt->mnt_sb);
+ if (!inode) {
+ kmem_cache_free(pidfs_attr_cachep, future->attr);
+ kfree(future);
+ return ERR_PTR(-ENOMEM);
+ }
+ /* Preserve the anonymous-inode creation check for future pidfds. */
+ ret = security_inode_init_security_anon(inode, &QSTR(name), NULL);
+ if (ret) {
+ iput(inode);
+ kmem_cache_free(pidfs_attr_cachep, future->attr);
+ kfree(future);
+ return ERR_PTR(ret);
+ }
+ ino = pidfs_alloc_ino();
+ simple_inode_init_ts(inode);
+ pidfs_init_common_inode(inode, NULL, ino);
+
+ file = alloc_file_pseudo(inode, pidfs_mnt, name, flags,
+ &pidfs_file_operations);
+ if (IS_ERR(file)) {
+ iput(inode);
+ kmem_cache_free(pidfs_attr_cachep, future->attr);
+ kfree(future);
+ return file;
+ }
+
+ future->node.type = PIDFS_NODE_FUTURE;
+ future->data = data;
+ future->ops = ops;
+ future->ino = ino;
+ inode->i_private = &future->node;
+ return file;
+}
+
+u64 pidfs_future_file_ino(const struct file *file)
+{
+ struct pidfs_future_file *future = pidfs_future_file(file_inode(file));
+
+ return future ? future->ino : 0;
+}
+
void __init pidfs_init(void)
{
if (rhashtable_init(&pidfs_ino_ht, &pidfs_ino_ht_params))
diff --git a/include/linux/pid.h b/include/linux/pid.h
index ddaef0bbc8ba3..a29ffe2a5fa8e 100644
--- a/include/linux/pid.h
+++ b/include/linux/pid.h
@@ -50,6 +50,15 @@
struct pidfs_attr;
+enum pidfs_node_type {
+ PIDFS_NODE_PID,
+ PIDFS_NODE_FUTURE,
+};
+
+struct pidfs_node {
+ enum pidfs_node_type type;
+};
+
struct upid {
int nr;
struct pid_namespace *ns;
@@ -59,6 +68,7 @@ struct pid {
refcount_t count;
unsigned int level;
spinlock_t lock;
+ struct pidfs_node pidfs_node;
struct {
u64 ino;
struct rhash_head pidfs_hash;
diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h
index 0abf7da9ab236..6b7fbc54ab388 100644
--- a/include/linux/pidfs.h
+++ b/include/linux/pidfs.h
@@ -5,8 +5,29 @@
#include <linux/gfp_types.h>
struct coredump_params;
+struct file;
+struct pid;
+
+/**
+ * struct pidfs_future_file_ops - callbacks for a taskless pidfs file
+ * @get_pid: Return the published, borrowed, non-NULL process identity, or an
+ * error pointer while the producer is still taskless. The producer must
+ * keep the returned pid alive for the future inode lifetime.
+ * @release: Optionally release producer-owned data when the pidfs inode is
+ * evicted. If provided, this is called at most once.
+ */
+struct pidfs_future_file_ops {
+ struct pid *(*get_pid)(void *data);
+ void (*release)(void *data);
+};
struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags);
+struct file *pidfs_alloc_future_file(const char *name, void *data,
+ const struct pidfs_future_file_ops *ops,
+ unsigned int flags);
+void *pidfs_future_file_data(const struct file *file,
+ const struct pidfs_future_file_ops *ops);
+u64 pidfs_future_file_ino(const struct file *file);
void __init pidfs_init(void);
void pidfs_prepare_pid(struct pid *pid);
int pidfs_add_pid(struct pid *pid);
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 04/24] pidfd: create taskless spawn builders
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (2 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 03/24] pidfs: add taskless future pidfd inodes Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 05/24] pidfd: add spawn builder path configuration Li Chen
` (19 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Add a pidfs-backed constructor for taskless spawn-builder files. The inode
owns the builder state, so procfd reopens and bind mounts retain it without
creating per-file lifetime rules.
The constructor allocates no task, struct pid, numeric PID, or
process-count charge. Until a later run operation publishes a pid, ordinary
pidfd operations resolve the file as -ESRCH. Preserve that error through
setns() and pidfd_send_signal() instead of translating a valid future pidfd
into an unrelated descriptor error. Assert that PIDFD_EMPTY does not
overlap a valid open flag, so future collisions fail the build instead of
silently aliasing builder creation.
Keep the public pidfd_open() entry point disabled until the complete spawn
state machine is wired later in the series.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
MAINTAINERS | 2 +
fs/Makefile | 2 +-
fs/pidfd_spawn.c | 81 +++++++++++++++++++++++++++++++++++++
fs/pidfs.c | 5 ++-
include/linux/pidfd_spawn.h | 9 +++++
kernel/nsproxy.c | 11 +++--
kernel/signal.c | 2 +-
7 files changed, 106 insertions(+), 6 deletions(-)
create mode 100644 fs/pidfd_spawn.c
create mode 100644 include/linux/pidfd_spawn.h
diff --git a/MAINTAINERS b/MAINTAINERS
index b63bc1ee0171f..8d7f94f09f414 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21287,6 +21287,8 @@ M: Christian Brauner <christian@brauner.io>
L: linux-kernel@vger.kernel.org
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/brauner/linux.git
+F: fs/pidfd_spawn.c
+F: include/linux/pidfd_spawn.h
F: include/uapi/linux/pidfd_spawn.h
F: samples/pidfd/
F: tools/include/uapi/linux/pidfd_spawn.h
diff --git a/fs/Makefile b/fs/Makefile
index aa847be93bc6f..f24beb7c9b7dc 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -8,7 +8,7 @@
obj-y := open.o read_write.o file_table.o super.o \
- char_dev.o stat.o exec.o pipe.o namei.o fcntl.o \
+ char_dev.o stat.o exec.o pidfd_spawn.o pipe.o namei.o fcntl.o \
ioctl.o readdir.o select.o dcache.o inode.o \
attr.o bad_inode.o file.o filesystems.o namespace.o \
seq_file.o xattr.o libfs.o fs-writeback.o \
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
new file mode 100644
index 0000000000000..ff2b0cb6365a5
--- /dev/null
+++ b/fs/pidfd_spawn.c
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * pidfd-backed process spawn builders
+ */
+
+#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/pid.h>
+#include <linux/pidfd_spawn.h>
+#include <linux/pidfs.h>
+#include <linux/refcount.h>
+#include <linux/slab.h>
+#include <uapi/linux/pidfd.h>
+
+struct pidfd_spawn_state {
+ refcount_t count;
+ struct pid *pid;
+};
+
+static void pidfd_spawn_state_put(struct pidfd_spawn_state *state);
+
+static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
+{
+ put_pid(state->pid);
+ kfree(state);
+}
+
+static void pidfd_spawn_state_put(struct pidfd_spawn_state *state)
+{
+ if (refcount_dec_and_test(&state->count))
+ pidfd_spawn_free_state(state);
+}
+
+static void pidfd_spawn_state_release(void *data)
+{
+ pidfd_spawn_state_put(data);
+}
+
+static struct pid *pidfd_spawn_file_pid(void *data)
+{
+ struct pidfd_spawn_state *state = data;
+ struct pid *pid;
+
+ pid = READ_ONCE(state->pid);
+ return pid ? pid : ERR_PTR(-ESRCH);
+}
+
+static const struct pidfs_future_file_ops pidfd_spawn_future_ops = {
+ .get_pid = pidfd_spawn_file_pid,
+ .release = pidfd_spawn_state_release,
+};
+
+int pidfd_empty_open(unsigned int flags)
+{
+ struct pidfd_spawn_state *state;
+ struct file *pidfile;
+ int pidfd;
+
+ state = kzalloc_obj(*state, GFP_KERNEL_ACCOUNT);
+ if (!state)
+ return -ENOMEM;
+
+ refcount_set(&state->count, 1);
+
+ pidfile = pidfs_alloc_future_file("[pidfd_spawn]", state,
+ &pidfd_spawn_future_ops,
+ O_RDWR | flags);
+ if (IS_ERR(pidfile)) {
+ pidfd_spawn_state_put(state);
+ return PTR_ERR(pidfile);
+ }
+
+ pidfd = get_unused_fd_flags(O_CLOEXEC);
+ if (pidfd < 0) {
+ fput(pidfile);
+ return pidfd;
+ }
+
+ fd_install(pidfd, pidfile);
+ return pidfd;
+}
diff --git a/fs/pidfs.c b/fs/pidfs.c
index ebd8cc463811b..28464fe274c9e 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -2,6 +2,7 @@
#include <linux/anon_inodes.h>
#include <linux/compat.h>
#include <linux/exportfs.h>
+#include <linux/fcntl.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/cgroup.h>
@@ -1261,7 +1262,9 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
* other or with uapi pidfd flags.
*/
BUILD_BUG_ON(hweight32(PIDFD_THREAD | PIDFD_NONBLOCK |
- PIDFD_STALE | PIDFD_AUTOKILL) != 4);
+ PIDFD_EMPTY | PIDFD_STALE |
+ PIDFD_AUTOKILL) != 5);
+ BUILD_BUG_ON(PIDFD_EMPTY & VALID_OPEN_FLAGS);
ret = path_from_stashed(&pid->stashed, pidfs_mnt, get_pid(pid), &path);
if (ret < 0)
diff --git a/include/linux/pidfd_spawn.h b/include/linux/pidfd_spawn.h
new file mode 100644
index 0000000000000..8df168cf0c2f9
--- /dev/null
+++ b/include/linux/pidfd_spawn.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_PIDFD_SPAWN_H
+#define _LINUX_PIDFD_SPAWN_H
+
+#include <uapi/linux/pidfd_spawn.h>
+
+int pidfd_empty_open(unsigned int flags);
+
+#endif /* _LINUX_PIDFD_SPAWN_H */
diff --git a/kernel/nsproxy.c b/kernel/nsproxy.c
index d9d3d5973bf52..20befb6185e2d 100644
--- a/kernel/nsproxy.c
+++ b/kernel/nsproxy.c
@@ -581,10 +581,15 @@ SYSCALL_DEFINE2(setns, int, fd, int, flags)
if (flags && (ns->ns_type != flags))
err = -EINVAL;
flags = ns->ns_type;
- } else if (!IS_ERR(pidfd_pid(fd_file(f)))) {
- err = check_setns_flags(flags);
} else {
- err = -EINVAL;
+ struct pid *pid = pidfd_pid(fd_file(f));
+
+ if (!IS_ERR(pid))
+ err = check_setns_flags(flags);
+ else if (PTR_ERR(pid) == -ESRCH)
+ err = -ESRCH;
+ else
+ err = -EINVAL;
}
if (err)
goto out;
diff --git a/kernel/signal.c b/kernel/signal.c
index fdee0b012a117..4c7d95981263e 100644
--- a/kernel/signal.c
+++ b/kernel/signal.c
@@ -3996,7 +3996,7 @@ static struct pid *pidfd_to_pid(const struct file *file)
struct pid *pid;
pid = pidfd_pid(file);
- if (!IS_ERR(pid))
+ if (!IS_ERR(pid) || PTR_ERR(pid) != -EBADF)
return pid;
return tgid_pidfd_to_pid(file);
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 05/24] pidfd: add spawn builder path configuration
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (3 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 04/24] pidfd: create taskless spawn builders Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 06/24] exec: expose execveat internals to process builders Li Chen
` (18 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Add an fsconfig-style pidfd_config() implementation and the initial path
string key. The builder inode retains configuration, which userspace may
replace before the child is created.
Bind configuration authority to the creator mm, credential object, and
child PID namespace. Passing the fd alone therefore does not delegate
launch authority to a different process context.
Accept absolute and relative executable paths. A later run operation
resolves relative paths from the child working directory without PATH
search. Keep the syscall unreachable until the complete spawn state machine
is wired later in the series.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfd_spawn.c | 155 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 155 insertions(+)
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index ff2b0cb6365a5..46207c8e8139f 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -3,24 +3,47 @@
* pidfd-backed process spawn builders
*/
+#include <linux/cred.h>
#include <linux/file.h>
#include <linux/fs.h>
+#include <linux/mm.h>
+#include <linux/mutex.h>
+#include <linux/namei.h>
#include <linux/pid.h>
+#include <linux/pid_namespace.h>
#include <linux/pidfd_spawn.h>
#include <linux/pidfs.h>
#include <linux/refcount.h>
+#include <linux/sched/signal.h>
#include <linux/slab.h>
+#include <linux/syscalls.h>
+#include <linux/uaccess.h>
#include <uapi/linux/pidfd.h>
+#define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE 256
+
struct pidfd_spawn_state {
+ /* Serializes configuration and payload teardown. */
+ struct mutex lock;
refcount_t count;
struct pid *pid;
+ struct mm_struct *creator_mm;
+ const struct cred *creator_cred;
+ struct pid_namespace *creator_pid_ns;
+ char *staged_path;
};
static void pidfd_spawn_state_put(struct pidfd_spawn_state *state);
static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
{
+ kfree(state->staged_path);
+ if (state->creator_mm)
+ mmdrop(state->creator_mm);
+ if (state->creator_cred)
+ put_cred(state->creator_cred);
+ if (state->creator_pid_ns)
+ put_pid_ns(state->creator_pid_ns);
put_pid(state->pid);
kfree(state);
}
@@ -31,6 +54,15 @@ static void pidfd_spawn_state_put(struct pidfd_spawn_state *state)
pidfd_spawn_free_state(state);
}
+static void pidfd_spawn_state_put_cleanup(struct pidfd_spawn_state *state)
+{
+ if (!IS_ERR_OR_NULL(state))
+ pidfd_spawn_state_put(state);
+}
+
+DEFINE_FREE(pidfd_spawn_state, struct pidfd_spawn_state *,
+ pidfd_spawn_state_put_cleanup(_T))
+
static void pidfd_spawn_state_release(void *data)
{
pidfd_spawn_state_put(data);
@@ -50,17 +82,140 @@ static const struct pidfs_future_file_ops pidfd_spawn_future_ops = {
.release = pidfd_spawn_state_release,
};
+static struct pidfd_spawn_state *
+pidfd_spawn_state_get_file(struct file *file)
+{
+ struct pidfd_spawn_state *state;
+
+ state = pidfs_future_file_data(file, &pidfd_spawn_future_ops);
+ if (!state || !refcount_inc_not_zero(&state->count))
+ return ERR_PTR(state ? -ESRCH : -EINVAL);
+ return state;
+}
+
+static bool pidfd_spawn_same_creator(struct pidfd_spawn_state *state)
+{
+ return current->mm == state->creator_mm &&
+ current_cred() == state->creator_cred &&
+ current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
+}
+
+static struct filename *pidfd_spawn_get_path(const char __user *value)
+{
+ CLASS(filename, name)(value);
+
+ if (IS_ERR(name))
+ return ERR_CAST(name);
+
+ return no_free_ptr(name);
+}
+
+static char *pidfd_spawn_copy_path(const char __user *value)
+{
+ struct filename *name __free(putname) = NULL;
+ char *path;
+
+ name = pidfd_spawn_get_path(value);
+ if (IS_ERR(name))
+ return ERR_CAST(name);
+ path = kstrdup(name->name, GFP_KERNEL_ACCOUNT);
+ if (!path)
+ return ERR_PTR(-ENOMEM);
+
+ return path;
+}
+
+static int pidfd_spawn_state_set_path(struct pidfd_spawn_state *state,
+ const char __user *value)
+{
+ char *path __free(kfree) = NULL;
+ char *old __free(kfree) = NULL;
+
+ path = pidfd_spawn_copy_path(value);
+ if (IS_ERR(path))
+ return PTR_ERR(path);
+
+ scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+ if (!pidfd_spawn_same_creator(state))
+ return -EPERM;
+
+ old = state->staged_path;
+ state->staged_path = no_free_ptr(path);
+ }
+ return 0;
+}
+
+static int pidfd_spawn_state_set_string(struct pidfd_spawn_state *state,
+ const char *key,
+ const void __user *value)
+{
+ if (!strcmp(key, PIDFD_CONFIG_KEY_PATH))
+ return pidfd_spawn_state_set_path(state, value);
+
+ return -EOPNOTSUPP;
+}
+
+SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
+ const char __user *, ukey, const void __user *, uvalue,
+ int, aux)
+{
+ char *key __free(kfree) = NULL;
+ struct pidfd_spawn_state *state __free(pidfd_spawn_state) = NULL;
+ int ret;
+
+ switch (cmd) {
+ case PIDFD_CONFIG_SET_STRING:
+ if (!ukey || !uvalue || aux)
+ return -EINVAL;
+ break;
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ CLASS(fd, f)(fd);
+ if (fd_empty(f))
+ return -EBADF;
+
+ state = pidfd_spawn_state_get_file(fd_file(f));
+ if (IS_ERR(state))
+ return PTR_ERR(state);
+
+ key = strndup_user(ukey, PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE);
+ if (IS_ERR(key))
+ return PTR_ERR(key);
+
+ switch (cmd) {
+ case PIDFD_CONFIG_SET_STRING:
+ ret = pidfd_spawn_state_set_string(state, key, uvalue);
+ break;
+ default:
+ ret = -EOPNOTSUPP;
+ break;
+ }
+
+ return ret;
+}
+
int pidfd_empty_open(unsigned int flags)
{
struct pidfd_spawn_state *state;
struct file *pidfile;
int pidfd;
+ if (!current->mm)
+ return -EINVAL;
+
state = kzalloc_obj(*state, GFP_KERNEL_ACCOUNT);
if (!state)
return -ENOMEM;
+ mutex_init(&state->lock);
refcount_set(&state->count, 1);
+ mmgrab(current->mm);
+ state->creator_mm = current->mm;
+ state->creator_cred = get_current_cred();
+ state->creator_pid_ns =
+ get_pid_ns(current->nsproxy->pid_ns_for_children);
pidfile = pidfs_alloc_future_file("[pidfd_spawn]", state,
&pidfd_spawn_future_ops,
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 06/24] exec: expose execveat internals to process builders
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (4 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 05/24] pidfd: add spawn builder path configuration Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 07/24] fork: expose vfork completion helper Li Chen
` (17 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Move struct user_arg_ptr and its native and compat constructors to an
fs-private header. Expose do_execveat_common() within fs so a process
builder can enter the normal exec path without synthesizing a userspace
execveat() syscall.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
MAINTAINERS | 1 +
fs/exec.c | 28 ++++------------------------
fs/exec_internal.h | 40 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 45 insertions(+), 24 deletions(-)
create mode 100644 fs/exec_internal.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 8d7f94f09f414..85b1306cb2ff3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9757,6 +9757,7 @@ F: Documentation/userspace-api/ELF.rst
F: fs/*binfmt_*.c
F: fs/Kconfig.binfmt
F: fs/exec.c
+F: fs/exec_internal.h
F: fs/tests/binfmt_*_kunit.c
F: fs/tests/exec_kunit.c
F: include/linux/binfmts.h
diff --git a/fs/exec.c b/fs/exec.c
index d5993cedc829a..1a256ba26425e 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -76,6 +76,7 @@
#include <trace/events/task.h>
#include "internal.h"
+#include "exec_internal.h"
#include <trace/events/sched.h>
@@ -291,17 +292,6 @@ static int bprm_mm_init(struct linux_binprm *bprm)
return err;
}
-struct user_arg_ptr {
-#ifdef CONFIG_COMPAT
- bool is_compat;
-#endif
- union {
- const char __user *const __user *native;
-#ifdef CONFIG_COMPAT
- const compat_uptr_t __user *compat;
-#endif
- } ptr;
-};
static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr)
{
@@ -1805,10 +1795,9 @@ static int bprm_execve(struct linux_binprm *bprm)
return retval;
}
-static int do_execveat_common(int fd, struct filename *filename,
- struct user_arg_ptr argv,
- struct user_arg_ptr envp,
- int flags)
+int do_execveat_common(int fd, struct filename *filename,
+ struct user_arg_ptr argv,
+ struct user_arg_ptr envp, int flags)
{
int retval;
@@ -1935,10 +1924,6 @@ void set_binfmt(struct linux_binfmt *new)
}
EXPORT_SYMBOL(set_binfmt);
-static inline struct user_arg_ptr native_arg(const char __user *const __user *p)
-{
- return (struct user_arg_ptr){.ptr.native = p};
-}
SYSCALL_DEFINE3(execve,
const char __user *, filename,
@@ -1963,11 +1948,6 @@ SYSCALL_DEFINE5(execveat,
#ifdef CONFIG_COMPAT
-static inline struct user_arg_ptr compat_arg(const compat_uptr_t __user *p)
-{
- return (struct user_arg_ptr){.is_compat = true, .ptr.compat = p};
-}
-
COMPAT_SYSCALL_DEFINE3(execve, const char __user *, filename,
const compat_uptr_t __user *, argv,
const compat_uptr_t __user *, envp)
diff --git a/fs/exec_internal.h b/fs/exec_internal.h
new file mode 100644
index 0000000000000..bf6a873e98426
--- /dev/null
+++ b/fs/exec_internal.h
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _FS_EXEC_INTERNAL_H
+#define _FS_EXEC_INTERNAL_H
+
+#include <linux/compat.h>
+#include <linux/compiler_types.h>
+#include <linux/types.h>
+
+struct filename;
+
+struct user_arg_ptr {
+#ifdef CONFIG_COMPAT
+ bool is_compat;
+#endif
+ union {
+ const char __user *const __user *native;
+#ifdef CONFIG_COMPAT
+ const compat_uptr_t __user *compat;
+#endif
+ } ptr;
+};
+
+static inline struct user_arg_ptr
+native_arg(const char __user *const __user *p)
+{
+ return (struct user_arg_ptr){.ptr.native = p};
+}
+
+#ifdef CONFIG_COMPAT
+static inline struct user_arg_ptr compat_arg(const compat_uptr_t __user *p)
+{
+ return (struct user_arg_ptr){.is_compat = true, .ptr.compat = p};
+}
+#endif
+
+int do_execveat_common(int fd, struct filename *filename,
+ struct user_arg_ptr argv,
+ struct user_arg_ptr envp, int flags);
+
+#endif /* _FS_EXEC_INTERNAL_H */
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 07/24] fork: expose vfork completion helper
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (5 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 06/24] exec: expose execveat internals to process builders Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 08/24] pidfs: attach pids to future pidfd files Li Chen
` (16 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Allow process builders that call copy_process() directly to attach and
wait for the existing vfork completion without duplicating its signal
and task-reference handling.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
include/linux/sched/task.h | 3 +++
kernel/fork.c | 3 +--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h
index e0c1ca8c6a188..92b4e3bc31e66 100644
--- a/include/linux/sched/task.h
+++ b/include/linux/sched/task.h
@@ -14,6 +14,7 @@
struct task_struct;
struct rusage;
+struct completion;
union thread_union;
struct css_set;
@@ -99,6 +100,8 @@ extern void exit_itimers(struct task_struct *);
extern pid_t kernel_clone(struct kernel_clone_args *kargs);
struct task_struct *copy_process(struct pid *pid, int trace, int node,
struct kernel_clone_args *args);
+int wait_for_vfork_done(struct task_struct *child,
+ struct completion *vfork);
struct task_struct *create_io_thread(int (*fn)(void *), void *arg, int node);
struct task_struct *fork_idle(int);
extern pid_t kernel_thread(int (*fn)(void *), void *arg, const char *name,
diff --git a/kernel/fork.c b/kernel/fork.c
index f3f378a85be4c..970810a01bbf6 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -1435,8 +1435,7 @@ static void complete_vfork_done(struct task_struct *tsk)
task_unlock(tsk);
}
-static int wait_for_vfork_done(struct task_struct *child,
- struct completion *vfork)
+int wait_for_vfork_done(struct task_struct *child, struct completion *vfork)
{
unsigned int state = TASK_KILLABLE|TASK_FREEZABLE;
int killed;
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 08/24] pidfs: attach pids to future pidfd files
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (6 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 07/24] fork: expose vfork completion helper Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 09/24] fork: let process builders supply preallocated pids Li Chen
` (15 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Associate a future pidfs inode with a preallocated struct pid and reuse
the future dentry as the canonical stashed dentry. Preserve the
reserved inode identity when the pid is registered.
Keep attachment reversible until a task owns the pid so copy_process()
failure can return the future file to its taskless state. File-handle
lookup requires both the stash and an attached task before accepting an
unpublished pid, preventing failed allocations from being reopened
through stale state.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfs.c | 89 +++++++++++++++++++++++++++++++++++++++++--
include/linux/pidfs.h | 2 +
2 files changed, 87 insertions(+), 4 deletions(-)
diff --git a/fs/pidfs.c b/fs/pidfs.c
index 28464fe274c9e..36ee4f210f73f 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -115,6 +115,7 @@ struct pidfs_future_file {
void *data;
const struct pidfs_future_file_ops *ops;
struct pidfs_attr *attr;
+ struct pid *pid;
u64 ino;
};
@@ -238,7 +239,8 @@ int pidfs_add_pid(struct pid *pid)
{
int ret;
- pid->ino = pidfs_alloc_ino();
+ if (!pid->ino)
+ pid->ino = pidfs_alloc_ino();
ret = rhashtable_insert_fast(&pidfs_ino_ht, &pid->pidfs_hash,
pidfs_ino_ht_params);
if (unlikely(ret))
@@ -987,9 +989,18 @@ static struct pid *pidfs_ino_get_pid(u64 ino)
if (!pid)
return NULL;
attr = READ_ONCE(pid->attr);
- if (IS_ERR_OR_NULL(attr))
+ if (IS_ERR(attr))
+ return NULL;
+ /*
+ * A task can become visible before its future pidfd is published. Let
+ * ->open() wait across that window, but never expose a dentry for a
+ * caller-supplied pid when copy_process() has failed. That failure path
+ * clears the stash before freeing the pid allocation.
+ */
+ if (!attr && (!READ_ONCE(pid->stashed) ||
+ !pid_has_task(pid, PIDTYPE_PID)))
return NULL;
- if (test_bit(PIDFS_ATTR_BIT_EXIT, &attr->attr_mask))
+ if (attr && test_bit(PIDFS_ATTR_BIT_EXIT, &attr->attr_mask))
return NULL;
/* Within our pid namespace hierarchy? */
if (pid_vnr(pid) == 0)
@@ -1025,7 +1036,8 @@ static struct dentry *pidfs_fh_to_dentry(struct super_block *sb,
if (ret < 0)
return ERR_PTR(ret);
- VFS_WARN_ON_ONCE(!pid->attr);
+ VFS_WARN_ON_ONCE(!pid->attr &&
+ !pidfs_future_file(d_inode(path.dentry)));
mntput(path.mnt);
return path.dentry;
@@ -1103,6 +1115,10 @@ static int pidfs_init_inode(struct inode *inode, void *data)
static bool pidfs_inode_data_matches(const struct inode *inode,
const void *data)
{
+ struct pidfs_future_file *future = pidfs_future_file(inode);
+
+ if (future)
+ return READ_ONCE(future->pid) == data;
return pidfs_inode_pid(inode) == data;
}
@@ -1266,6 +1282,10 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
PIDFD_AUTOKILL) != 5);
BUILD_BUG_ON(PIDFD_EMPTY & VALID_OPEN_FLAGS);
+ ret = pidfs_register_pid(pid);
+ if (ret)
+ return ERR_PTR(ret);
+
ret = path_from_stashed(&pid->stashed, pidfs_mnt, get_pid(pid), &path);
if (ret < 0)
return ERR_PTR(ret);
@@ -1369,6 +1389,67 @@ u64 pidfs_future_file_ino(const struct file *file)
return future ? future->ino : 0;
}
+/**
+ * pidfs_future_file_set_pid - associate a preallocated pid with a future file
+ * @file: future pidfs file
+ * @pid: preallocated pid whose pidfs inode number matches @file
+ *
+ * This stashes the future dentry in @pid before copy_process() publishes the
+ * task. No pid reference is transferred. On a later creation failure the
+ * producer must call pidfs_future_file_clear_pid() before freeing @pid.
+ *
+ * Return: Zero on success or a negative error code.
+ */
+int pidfs_future_file_set_pid(struct file *file, struct pid *pid)
+{
+ struct inode *inode = file_inode(file);
+ struct pidfs_future_file *future = pidfs_future_file(inode);
+ struct dentry *dentry = file->f_path.dentry;
+ struct dentry *stashed;
+
+ if (!future || !future->ops->get_pid || future->pid ||
+ pid->ino != future->ino)
+ return -EBADF;
+
+ WRITE_ONCE(future->pid, pid);
+ dentry->d_fsdata = &pid->stashed;
+ stashed = stash_dentry(&pid->stashed, dentry);
+ if (stashed != dentry) {
+ dput(stashed);
+ pidfs_future_file_clear_pid(file, pid);
+ return -EBUSY;
+ }
+ return 0;
+}
+
+/**
+ * pidfs_future_file_clear_pid - undo an unpublished future-pid association
+ * @file: future pidfs file
+ * @pid: pid previously associated with @file
+ *
+ * This may be called only after pidfs_future_file_set_pid() and before
+ * pidfs_future_file_publish_pid(). It removes the stashed dentry so the
+ * producer can free @pid after a task-creation failure.
+ *
+ * The producer must clear the association before attaching @pid to a task.
+ * File-handle lookup treats an attached pid as a publication-in-progress
+ * guarantee, so clearing the association later would violate that contract.
+ */
+void pidfs_future_file_clear_pid(struct file *file, struct pid *pid)
+{
+ struct inode *inode = file_inode(file);
+ struct pidfs_future_file *future = pidfs_future_file(inode);
+ struct dentry *dentry = file->f_path.dentry;
+
+ if (WARN_ON_ONCE(!future || READ_ONCE(future->pid) != pid))
+ return;
+
+ WARN_ON_ONCE(cmpxchg(&pid->stashed, dentry, NULL) != dentry);
+ dentry->d_fsdata = NULL;
+ WARN_ON_ONCE(pid->attr);
+ WRITE_ONCE(future->pid, NULL);
+}
+
void __init pidfs_init(void)
{
if (rhashtable_init(&pidfs_ino_ht, &pidfs_ino_ht_params))
diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h
index 6b7fbc54ab388..828e96f769ba1 100644
--- a/include/linux/pidfs.h
+++ b/include/linux/pidfs.h
@@ -28,6 +28,8 @@ struct file *pidfs_alloc_future_file(const char *name, void *data,
void *pidfs_future_file_data(const struct file *file,
const struct pidfs_future_file_ops *ops);
u64 pidfs_future_file_ino(const struct file *file);
+int pidfs_future_file_set_pid(struct file *file, struct pid *pid);
+void pidfs_future_file_clear_pid(struct file *file, struct pid *pid);
void __init pidfs_init(void);
void pidfs_prepare_pid(struct pid *pid);
int pidfs_add_pid(struct pid *pid);
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 09/24] fork: let process builders supply preallocated pids
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (7 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 08/24] pidfs: attach pids to future pidfd files Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 10/24] pidfs: publish future pidfd files Li Chen
` (14 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Process builders can reserve a pidfs identity before copy_process()
makes a task visible. Add an alloc_pid() variant for a reserved pidfs
inode, and let copy_process() consume a caller-provided struct pid on
success.
Keep caller ownership on failure. The legacy NULL-pid path continues to
allocate and free its own pid. Validate the target PID namespace before
using a preallocated identity.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
include/linux/pid.h | 3 +++
kernel/fork.c | 16 ++++++++++++++--
kernel/pid.c | 22 ++++++++++++++++++++--
3 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/include/linux/pid.h b/include/linux/pid.h
index a29ffe2a5fa8e..fa8acae336f7c 100644
--- a/include/linux/pid.h
+++ b/include/linux/pid.h
@@ -142,6 +142,9 @@ extern struct pid *find_ge_pid(int nr, struct pid_namespace *);
extern struct pid *alloc_pid(struct pid_namespace *ns, pid_t *set_tid,
size_t set_tid_size);
+struct pid *alloc_pid_with_pidfs_ino(struct pid_namespace *ns,
+ pid_t *set_tid, size_t set_tid_size,
+ u64 pidfs_ino);
extern void free_pid(struct pid *pid);
void free_pids(struct pid **pids);
extern void disable_pid_allocation(struct pid_namespace *ns);
diff --git a/kernel/fork.c b/kernel/fork.c
index 970810a01bbf6..d16405c037c2f 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2006,6 +2006,9 @@ static bool need_futex_hash_allocate_default(u64 clone_flags)
* It copies the registers, and all the appropriate
* parts of the process environment (as per the clone
* flags). The actual kick-off is left to the caller.
+ *
+ * Except for init_struct_pid, a caller-supplied pid reference remains owned
+ * by the caller on failure and is transferred to the new task on success.
*/
__latent_entropy struct task_struct *copy_process(
struct pid *pid,
@@ -2017,6 +2020,7 @@ __latent_entropy struct task_struct *copy_process(
struct task_struct *p;
struct multiprocess_signals delayed;
struct file *pidfile = NULL;
+ bool allocated_pid = false;
const u64 clone_flags = args->flags;
struct nsproxy *nsp = current->nsproxy;
@@ -2317,13 +2321,21 @@ __latent_entropy struct task_struct *copy_process(
stackleak_task_init(p);
- if (pid != &init_struct_pid) {
+ if (!pid) {
pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,
args->set_tid_size);
if (IS_ERR(pid)) {
retval = PTR_ERR(pid);
goto bad_fork_cleanup_thread;
}
+ allocated_pid = true;
+ } else if (pid != &init_struct_pid) {
+ if (args->set_tid_size ||
+ ns_of_pid(pid) != p->nsproxy->pid_ns_for_children ||
+ WARN_ON_ONCE(pid_has_task(pid, PIDTYPE_PID))) {
+ retval = -EINVAL;
+ goto bad_fork_cleanup_thread;
+ }
}
/*
@@ -2587,7 +2599,7 @@ __latent_entropy struct task_struct *copy_process(
put_unused_fd(pidfd);
}
bad_fork_free_pid:
- if (pid != &init_struct_pid)
+ if (allocated_pid)
free_pid(pid);
bad_fork_cleanup_thread:
exit_thread(p);
diff --git a/kernel/pid.c b/kernel/pid.c
index f55189a3d07d4..010f80177cac8 100644
--- a/kernel/pid.c
+++ b/kernel/pid.c
@@ -156,8 +156,8 @@ void free_pids(struct pid **pids)
free_pid(pids[tmp]);
}
-struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
- size_t arg_set_tid_size)
+static struct pid *__alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
+ size_t arg_set_tid_size, u64 pidfs_ino)
{
int set_tid[MAX_PID_NS_LEVEL + 1] = {};
int pid_max[MAX_PID_NS_LEVEL + 1] = {};
@@ -198,6 +198,7 @@ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
init_waitqueue_head(&pid->wait_pidfd);
INIT_HLIST_HEAD(&pid->inodes);
pidfs_prepare_pid(pid);
+ pid->ino = pidfs_ino;
/*
* 2. perm check checkpoint_restore_ns_capable()
@@ -358,6 +359,23 @@ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
return ERR_PTR(retval);
}
+struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
+ size_t arg_set_tid_size)
+{
+ return __alloc_pid(ns, arg_set_tid, arg_set_tid_size, 0);
+}
+
+struct pid *alloc_pid_with_pidfs_ino(struct pid_namespace *ns,
+ pid_t *arg_set_tid,
+ size_t arg_set_tid_size,
+ u64 pidfs_ino)
+{
+ if (!pidfs_ino)
+ return ERR_PTR(-EINVAL);
+
+ return __alloc_pid(ns, arg_set_tid, arg_set_tid_size, pidfs_ino);
+}
+
void disable_pid_allocation(struct pid_namespace *ns)
{
spin_lock(&pidmap_lock);
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 10/24] pidfs: publish future pidfd files
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (8 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 09/24] fork: let process builders supply preallocated pids Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 11/24] pidfd: add spawn builder state tracking Li Chen
` (13 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Publish an attached future pidfd by transferring pidfs attributes and
forwarding process-exit wakeups to poll registrations made while the
file was taskless.
Make numeric pidfd opens and file-handle opens wait across the interval
between task visibility and producer publication. Internal PIDFD_STALE
callers remain nonblocking so task creation cannot wait on itself.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfs.c | 127 +++++++++++++++++++++++++++++++++++++++++-
include/linux/pidfs.h | 5 ++
2 files changed, 130 insertions(+), 2 deletions(-)
diff --git a/fs/pidfs.c b/fs/pidfs.c
index 36ee4f210f73f..d62235e01b351 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -116,7 +116,10 @@ struct pidfs_future_file {
const struct pidfs_future_file_ops *ops;
struct pidfs_attr *attr;
struct pid *pid;
+ wait_queue_head_t wait_pidfd;
+ wait_queue_entry_t pid_wait;
u64 ino;
+ bool pid_wait_attached;
};
static struct pidfs_node *pidfs_inode_node(const struct inode *inode)
@@ -137,6 +140,12 @@ pidfs_future_file(const struct inode *inode)
return container_of(node, struct pidfs_future_file, node);
}
+static bool pidfs_future_file_terminal(struct pidfs_future_file *future)
+{
+ return future && future->ops->is_terminal &&
+ future->ops->is_terminal(future->data);
+}
+
void *pidfs_future_file_data(const struct file *file,
const struct pidfs_future_file_ops *ops)
{
@@ -169,6 +178,21 @@ static struct pid *pidfs_inode_pid(const struct inode *inode)
return pid;
}
+static int pidfs_wait_for_published_pid(struct inode *inode)
+{
+ struct pidfs_future_file *future = pidfs_future_file(inode);
+ int status = -ESRCH;
+ int ret;
+
+ if (!future)
+ return 0;
+
+ ret = wait_event_killable(future->wait_pidfd,
+ (status = PTR_ERR_OR_ZERO(pidfs_inode_pid(inode))) !=
+ -ESRCH || pidfs_future_file_terminal(future));
+ return ret ? ret : status;
+}
+
#if BITS_PER_LONG == 32
DEFINE_SPINLOCK(pidfs_ino_lock);
@@ -370,10 +394,22 @@ static void pidfd_show_fdinfo(struct seq_file *m, struct file *f)
*/
static __poll_t pidfd_poll(struct file *file, struct poll_table_struct *pts)
{
+ struct pidfs_future_file *future;
struct task_struct *task;
__poll_t poll_flags = 0;
- struct pid *pid = pidfd_pid(file);
+ struct pid *pid;
+ future = pidfs_future_file(file_inode(file));
+ pid = pidfd_pid(file);
+ if (IS_ERR(pid) && PTR_ERR(pid) == -ESRCH && future) {
+ /* Close the publication race after registering the future wait. */
+ poll_wait(file, &future->wait_pidfd, pts);
+ pid = pidfd_pid(file);
+ }
+
+ if (IS_ERR(pid) && PTR_ERR(pid) == -ESRCH &&
+ pidfs_future_file_terminal(future))
+ return EPOLLERR | EPOLLHUP;
if (IS_ERR(pid))
return PTR_ERR(pid) == -ESRCH ? 0 : EPOLLHUP;
@@ -927,6 +963,8 @@ static void pidfs_evict_inode(struct inode *inode)
return;
future = container_of(node, struct pidfs_future_file, node);
+ if (future->pid_wait_attached)
+ remove_wait_queue(&future->pid->wait_pidfd, &future->pid_wait);
if (future->ops->release)
future->ops->release(future->data);
if (future->attr)
@@ -934,6 +972,16 @@ static void pidfs_evict_inode(struct inode *inode)
kfree(future);
}
+static int pidfs_future_pid_wake(wait_queue_entry_t *wait,
+ unsigned int mode, int sync, void *key)
+{
+ struct pidfs_future_file *future;
+
+ future = container_of(wait, struct pidfs_future_file, pid_wait);
+ wake_up_all(&future->wait_pidfd);
+ return 0;
+}
+
static const struct super_operations pidfs_sops = {
.drop_inode = inode_just_drop,
.evict_inode = pidfs_evict_inode,
@@ -1068,12 +1116,16 @@ static int pidfs_export_permission(struct handle_to_path_ctx *ctx,
static struct file *pidfs_export_open(const struct path *path, unsigned int oflags)
{
struct file *file;
+ int ret;
/*
* Clear O_LARGEFILE as open_by_handle_at() forces it and raise
* O_RDWR as pidfds always are.
*/
oflags &= ~O_LARGEFILE;
+ ret = pidfs_wait_for_published_pid(d_inode(path->dentry));
+ if (ret)
+ return ERR_PTR(ret);
file = dentry_open(path, oflags | O_RDWR, current_cred());
/* do_dentry_open() strips O_EXCL, which encodes PIDFD_THREAD. */
if (!IS_ERR(file))
@@ -1289,6 +1341,17 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
ret = path_from_stashed(&pid->stashed, pidfs_mnt, get_pid(pid), &path);
if (ret < 0)
return ERR_PTR(ret);
+ /*
+ * A future dentry can be stashed before copy_process() makes its task
+ * visible. Non-stale callers have already observed the task, so do not
+ * return that inode until its producer has published the process state.
+ * PIDFD_STALE is used by pre-task internal callers and must not wait here.
+ */
+ if (!(flags & PIDFD_STALE)) {
+ ret = pidfs_wait_for_published_pid(d_inode(path.dentry));
+ if (ret)
+ return ERR_PTR(ret);
+ }
VFS_WARN_ON_ONCE(!pid->attr);
@@ -1317,7 +1380,8 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
* associate one preallocated pid with
* pidfs_future_file_set_pid(), publish its pidfs metadata with
* pidfs_future_file_publish_pid(), make @ops->get_pid() resolve that pid, and
- * finally wake waiters with pidfs_future_file_notify().
+ * finally wake waiters with pidfs_future_file_notify(). A producer that
+ * becomes terminal without publishing must also notify its waiters.
*
* A task associated with the future pid must not become runnable before
* publication metadata and exit-wakeup forwarding are installed.
@@ -1378,6 +1442,7 @@ struct file *pidfs_alloc_future_file(const char *name, void *data,
future->data = data;
future->ops = ops;
future->ino = ino;
+ init_waitqueue_head(&future->wait_pidfd);
inode->i_private = &future->node;
return file;
}
@@ -1422,6 +1487,60 @@ int pidfs_future_file_set_pid(struct file *file, struct pid *pid)
return 0;
}
+/**
+ * pidfs_future_file_publish_pid - publish pidfs metadata for a future pid
+ * @file: future pidfs file
+ * @pid: pid previously associated with @file
+ *
+ * This transfers the future inode attributes to @pid and forwards pid exit
+ * wakeups. After it returns, the producer must publish the state used by
+ * @ops->get_pid() before calling pidfs_future_file_notify().
+ */
+void pidfs_future_file_publish_pid(struct file *file, struct pid *pid)
+{
+ struct pidfs_future_file *future = pidfs_future_file(file_inode(file));
+ struct pidfs_attr *unused = NULL;
+
+ if (WARN_ON_ONCE(!future || READ_ONCE(future->pid) != pid))
+ return;
+
+ scoped_guard(spinlock_irq, &pid->wait_pidfd.lock) {
+ if (WARN_ON_ONCE(pid->attr == PIDFS_PID_DEAD))
+ return;
+ if (!pid->attr) {
+ pid->attr = future->attr;
+ future->attr = NULL;
+ } else {
+ unused = future->attr;
+ future->attr = NULL;
+ }
+ }
+ if (unused)
+ kmem_cache_free(pidfs_attr_cachep, unused);
+
+ init_waitqueue_func_entry(&future->pid_wait, pidfs_future_pid_wake);
+ add_wait_queue(&pid->wait_pidfd, &future->pid_wait);
+ future->pid_wait_attached = true;
+}
+
+/**
+ * pidfs_future_file_notify - wake future-pidfd state waiters
+ * @file: future pidfs file
+ *
+ * For publication, the producer must call this only after @ops->get_pid() can
+ * return the live pid. The publication must use release ordering paired with
+ * the producer's acquire-side lookup. A taskless terminal producer may call
+ * this after making @ops->is_terminal() return true.
+ */
+void pidfs_future_file_notify(struct file *file)
+{
+ struct pidfs_future_file *future = pidfs_future_file(file_inode(file));
+
+ if (WARN_ON_ONCE(!future))
+ return;
+ wake_up_all(&future->wait_pidfd);
+}
+
/**
* pidfs_future_file_clear_pid - undo an unpublished future-pid association
* @file: future pidfs file
@@ -1444,6 +1563,10 @@ void pidfs_future_file_clear_pid(struct file *file, struct pid *pid)
if (WARN_ON_ONCE(!future || READ_ONCE(future->pid) != pid))
return;
+ if (future->pid_wait_attached) {
+ remove_wait_queue(&pid->wait_pidfd, &future->pid_wait);
+ future->pid_wait_attached = false;
+ }
WARN_ON_ONCE(cmpxchg(&pid->stashed, dentry, NULL) != dentry);
dentry->d_fsdata = NULL;
WARN_ON_ONCE(pid->attr);
diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h
index 828e96f769ba1..e4df7f7748487 100644
--- a/include/linux/pidfs.h
+++ b/include/linux/pidfs.h
@@ -13,11 +13,14 @@ struct pid;
* @get_pid: Return the published, borrowed, non-NULL process identity, or an
* error pointer while the producer is still taskless. The producer must
* keep the returned pid alive for the future inode lifetime.
+ * @is_terminal: Return true when a taskless producer can no longer publish a
+ * process. Optional; a producer without terminal failures may leave it NULL.
* @release: Optionally release producer-owned data when the pidfs inode is
* evicted. If provided, this is called at most once.
*/
struct pidfs_future_file_ops {
struct pid *(*get_pid)(void *data);
+ bool (*is_terminal)(void *data);
void (*release)(void *data);
};
@@ -29,6 +32,8 @@ void *pidfs_future_file_data(const struct file *file,
const struct pidfs_future_file_ops *ops);
u64 pidfs_future_file_ino(const struct file *file);
int pidfs_future_file_set_pid(struct file *file, struct pid *pid);
+void pidfs_future_file_publish_pid(struct file *file, struct pid *pid);
+void pidfs_future_file_notify(struct file *file);
void pidfs_future_file_clear_pid(struct file *file, struct pid *pid);
void __init pidfs_init(void);
void pidfs_prepare_pid(struct pid *pid);
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 11/24] pidfd: add spawn builder state tracking
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (9 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 10/24] pidfs: publish future pidfd files Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks Li Chen
` (12 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Add explicit configuring, starting, setup, started, and cancelled
states. Serialize configuration and launch transitions with the builder
mutex so configuration cannot change after task creation starts.
Publish the eventual pid with release and acquire ordering. This state
machine also provides the boundary for later one-shot claim and
cancellation semantics.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfd_spawn.c | 50 +++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 49 insertions(+), 1 deletion(-)
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 46207c8e8139f..99e3ef56ecfa3 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -22,6 +22,14 @@
#define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE 256
+enum pidfd_spawn_status {
+ PIDFD_SPAWN_CONFIGURING,
+ PIDFD_SPAWN_STARTING,
+ PIDFD_SPAWN_SETUP_DONE,
+ PIDFD_SPAWN_STARTED,
+ PIDFD_SPAWN_CANCELLED,
+};
+
struct pidfd_spawn_state {
/* Serializes configuration and payload teardown. */
struct mutex lock;
@@ -31,10 +39,45 @@ struct pidfd_spawn_state {
const struct cred *creator_cred;
struct pid_namespace *creator_pid_ns;
char *staged_path;
+ enum pidfd_spawn_status status;
};
+static struct pid *
+pidfd_spawn_load_pid(const struct pidfd_spawn_state *state)
+{
+ /* Pair with the release publication in pidfd_spawn_publish_pid(). */
+ return smp_load_acquire(&state->pid);
+}
+
+static enum pidfd_spawn_status
+pidfd_spawn_get_status(const struct pidfd_spawn_state *state)
+{
+ return READ_ONCE(state->status);
+}
+
+static void pidfd_spawn_set_status(struct pidfd_spawn_state *state,
+ enum pidfd_spawn_status status)
+{
+ WRITE_ONCE(state->status, status);
+}
+
static void pidfd_spawn_state_put(struct pidfd_spawn_state *state);
+static int
+pidfd_spawn_configuring_error(const struct pidfd_spawn_state *state)
+{
+ switch (pidfd_spawn_get_status(state)) {
+ case PIDFD_SPAWN_CONFIGURING:
+ return 0;
+ case PIDFD_SPAWN_CANCELLED:
+ if (!pidfd_spawn_load_pid(state))
+ return -ECANCELED;
+ fallthrough;
+ default:
+ return -EBUSY;
+ }
+}
+
static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
{
kfree(state->staged_path);
@@ -73,7 +116,7 @@ static struct pid *pidfd_spawn_file_pid(void *data)
struct pidfd_spawn_state *state = data;
struct pid *pid;
- pid = READ_ONCE(state->pid);
+ pid = pidfd_spawn_load_pid(state);
return pid ? pid : ERR_PTR(-ESRCH);
}
@@ -136,6 +179,10 @@ static int pidfd_spawn_state_set_path(struct pidfd_spawn_state *state,
return PTR_ERR(path);
scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+ int ret = pidfd_spawn_configuring_error(state);
+
+ if (ret)
+ return ret;
if (!pidfd_spawn_same_creator(state))
return -EPERM;
@@ -216,6 +263,7 @@ int pidfd_empty_open(unsigned int flags)
state->creator_cred = get_current_cred();
state->creator_pid_ns =
get_pid_ns(current->nsproxy->pid_ns_for_children);
+ pidfd_spawn_set_status(state, PIDFD_SPAWN_CONFIGURING);
pidfile = pidfs_alloc_future_file("[pidfd_spawn]", state,
&pidfd_spawn_future_ops,
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (10 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 11/24] pidfd: add spawn builder state tracking Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 15:57 ` Andy Lutomirski
2026-07-16 14:31 ` [RFC PATCH 13/24] fork: let new tasks start with task work Li Chen
` (11 subsequent siblings)
23 siblings, 1 reply; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
A kernel-created task can become visible before it has installed a new
executable image or a valid userspace register frame. Exposing such a task
through ptrace can disclose kernel setup state.
Add a task-local embryonic flag and an internal clone argument for callers
that need this lifecycle. Reject ptrace access until the creator clears the
flag. Clear it with release ordering and observe it with acquire ordering.
This orders visibility of the completed exec state with the transition.
Existing fork, vfork, clone, and kernel-thread callers leave the argument
unset and retain their current behavior.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
include/linux/sched.h | 21 +++++++++++++++++++++
include/linux/sched/task.h | 1 +
kernel/fork.c | 1 +
kernel/ptrace.c | 4 ++++
4 files changed, 27 insertions(+)
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 908aff695ef86..031ae92884c3c 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1863,6 +1863,7 @@ static __always_inline bool is_user_task(struct task_struct *task)
#define PFA_SPEC_IB_DISABLE 5 /* Indirect branch speculation restricted */
#define PFA_SPEC_IB_FORCE_DISABLE 6 /* Indirect branch speculation permanently restricted */
#define PFA_SPEC_SSB_NOEXEC 7 /* Speculative Store Bypass clear on execve() */
+#define PFA_EMBRYONIC_EXEC 8 /* No valid user register frame */
#define TASK_PFA_TEST(name, func) \
static inline bool task_##func(struct task_struct *p) \
@@ -1905,6 +1906,26 @@ TASK_PFA_CLEAR(SPEC_IB_DISABLE, spec_ib_disable)
TASK_PFA_TEST(SPEC_IB_FORCE_DISABLE, spec_ib_force_disable)
TASK_PFA_SET(SPEC_IB_FORCE_DISABLE, spec_ib_force_disable)
+/* Clearing this bit publishes the register and security state from exec. */
+static inline bool task_is_embryonic_exec(struct task_struct *p)
+{
+ return test_bit_acquire(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+}
+
+/* The task is private during copy_process(), so no publication barrier. */
+static inline void task_init_embryonic_exec(struct task_struct *p, bool set)
+{
+ if (set)
+ __set_bit(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+ else
+ __clear_bit(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+}
+
+static inline void task_clear_embryonic_exec(struct task_struct *p)
+{
+ clear_bit_unlock(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+}
+
static inline void
current_restore_flags(unsigned long orig_flags, unsigned long flags)
{
diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h
index 92b4e3bc31e66..5b7facf12b33b 100644
--- a/include/linux/sched/task.h
+++ b/include/linux/sched/task.h
@@ -45,6 +45,7 @@ struct kernel_clone_args {
void *fn_arg;
struct cgroup *cgrp;
struct css_set *cset;
+ bool embryonic_exec;
unsigned int kill_seq;
};
diff --git a/kernel/fork.c b/kernel/fork.c
index d16405c037c2f..e3ade83b2d4e2 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2138,6 +2138,7 @@ __latent_entropy struct task_struct *copy_process(
retval = copy_exec_state(clone_flags, p);
if (retval)
goto bad_fork_free;
+ task_init_embryonic_exec(p, args->embryonic_exec);
p->flags &= ~PF_KTHREAD;
if (args->kthread)
p->flags |= PF_KTHREAD;
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index d041645d9d17d..36eb9b154a397 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -56,6 +56,8 @@ bool ptracer_access_allowed(struct task_struct *tsk)
guard(rcu)();
if (ptrace_parent(tsk) != current)
return false;
+ if (task_is_embryonic_exec(tsk))
+ return false;
es = task_exec_state_rcu(tsk);
return READ_ONCE(es->dumpable) == TASK_DUMPABLE_OWNER ||
ptracer_capable(tsk, es->user_ns);
@@ -312,6 +314,8 @@ static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
WARN(1, "denying ptrace access check without PTRACE_MODE_*CREDS\n");
return -EPERM;
}
+ if (task_is_embryonic_exec(task))
+ return -EPERM;
/* May we inspect the given task?
* This check is used both for attaching with ptrace
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 13/24] fork: let new tasks start with task work
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (11 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 14/24] pidfd: create and execute spawn builder tasks Li Chen
` (10 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Process builders need to run setup in the child before its first return
to userspace without storing a kernel callback in architecture-specific
saved registers.
Allow an internal clone caller to supply task work. Queue it after the
last ordinary fork failure check, while the child is still TASK_NEW. A
NULL work pointer leaves existing clone callers unchanged.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
include/linux/sched/task.h | 2 ++
kernel/fork.c | 6 ++++++
2 files changed, 8 insertions(+)
diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h
index 5b7facf12b33b..830de99aabaed 100644
--- a/include/linux/sched/task.h
+++ b/include/linux/sched/task.h
@@ -43,6 +43,8 @@ struct kernel_clone_args {
int idle;
int (*fn)(void *);
void *fn_arg;
+ /* Run before the new task first returns to userspace. */
+ struct callback_head *task_work;
struct cgroup *cgrp;
struct css_set *cset;
bool embryonic_exec;
diff --git a/kernel/fork.c b/kernel/fork.c
index e3ade83b2d4e2..c0584eaa74711 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -109,6 +109,7 @@
#include <uapi/linux/pidfd.h>
#include <linux/pidfs.h>
#include <linux/tick.h>
+#include <linux/task_work.h>
#include <linux/unwind_deferred.h>
#include <linux/pgalloc.h>
#include <linux/uaccess.h>
@@ -2504,6 +2505,11 @@ __latent_entropy struct task_struct *copy_process(
retval = -EINTR;
goto bad_fork_core_free;
}
+ if (args->task_work) {
+ retval = task_work_add(p, args->task_work, TWA_RESUME);
+ if (WARN_ON_ONCE(retval))
+ goto bad_fork_core_free;
+ }
/* No more failure paths after this point. */
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 14/24] pidfd: create and execute spawn builder tasks
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (12 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 13/24] fork: let new tasks start with task work Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 15/24] fork: keep embryonic tasks hidden until exec completes Li Chen
` (9 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Add pidfd_spawn_run() for a taskless builder. Validate the versioned run
payload, including native and compat pointer-container widths. Allocate a
numeric pid with the reserved pidfs identity. Create the child through the
existing vfork backend.
Publish the same future pidfd as the child pidfd before waking the child.
Queue setup as initial task work so no kernel callback pointers are carried
through architecture register state. Keep the task embryonic until exec has
installed a valid userspace image and register frame.
A failure before task creation initially leaves the builder configurable.
Once a task exists, return setup errors after the child exits. A later
state-machine change tightens the pre-task path to a one-shot claim.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfd_spawn.c | 406 ++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 402 insertions(+), 4 deletions(-)
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 99e3ef56ecfa3..c0e95c2ddbe6e 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -3,6 +3,7 @@
* pidfd-backed process spawn builders
*/
+#include <linux/completion.h>
#include <linux/cred.h>
#include <linux/file.h>
#include <linux/fs.h>
@@ -13,14 +14,50 @@
#include <linux/pid_namespace.h>
#include <linux/pidfd_spawn.h>
#include <linux/pidfs.h>
+#include <linux/random.h>
#include <linux/refcount.h>
+#include <linux/sched/mm.h>
#include <linux/sched/signal.h>
+#include <linux/sched/task.h>
+#include <linux/security.h>
#include <linux/slab.h>
#include <linux/syscalls.h>
+#include <linux/task_work.h>
#include <linux/uaccess.h>
#include <uapi/linux/pidfd.h>
+#include <uapi/linux/wait.h>
+#include <trace/events/sched.h>
+#include "exec_internal.h"
+
+static inline void __user *pidfd_spawn_user_ptr(__u64 p)
+{
+ return u64_to_user_ptr(p);
+}
+
+static inline bool pidfd_spawn_user_ptr_fits(__u64 p)
+{
+#ifdef CONFIG_COMPAT
+ if (in_compat_syscall())
+ return p == (__u64)(compat_uptr_t)p;
+#endif
+ return p == (__u64)(unsigned long)p;
+}
+
+static inline struct user_arg_ptr pidfd_spawn_user_arg(__u64 p)
+{
+#ifdef CONFIG_COMPAT
+ if (in_compat_syscall())
+ return compat_arg((compat_uptr_t __user *)
+ u64_to_user_ptr(p));
+#endif
+ return native_arg(u64_to_user_ptr(p));
+}
+
+#define PIDFD_SPAWN_EXIT_FAILURE (127 << 8)
#define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE 256
+DEFINE_FREE(pidfd_spawn_dismiss_filename, struct delayed_filename,
+ dismiss_delayed_filename(&_T))
enum pidfd_spawn_status {
PIDFD_SPAWN_CONFIGURING,
@@ -31,14 +68,20 @@ enum pidfd_spawn_status {
};
struct pidfd_spawn_state {
- /* Serializes configuration and payload teardown. */
+ /* Serializes configuration, launch, cancellation, and payload teardown. */
struct mutex lock;
+ struct completion done;
+ struct callback_head task_work;
refcount_t count;
struct pid *pid;
struct mm_struct *creator_mm;
const struct cred *creator_cred;
struct pid_namespace *creator_pid_ns;
+ struct delayed_filename filename;
char *staged_path;
+ struct user_arg_ptr argv;
+ struct user_arg_ptr envp;
+ int result;
enum pidfd_spawn_status status;
};
@@ -80,6 +123,10 @@ pidfd_spawn_configuring_error(const struct pidfd_spawn_state *state)
static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
{
+ if (!state)
+ return;
+
+ dismiss_delayed_filename(&state->filename);
kfree(state->staged_path);
if (state->creator_mm)
mmdrop(state->creator_mm);
@@ -136,11 +183,73 @@ pidfd_spawn_state_get_file(struct file *file)
return state;
}
+static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
+ struct filename *filename, int result)
+{
+ const struct cred *creator_cred;
+ struct mm_struct *creator_mm;
+ struct pid_namespace *creator_pid_ns;
+ char *staged_path;
+
+ /*
+ * Run data is one-shot. Detach it under the lock, then drop refs
+ * after the child no longer needs the shared setup payload.
+ */
+ mutex_lock(&state->lock);
+ creator_mm = state->creator_mm;
+ creator_cred = state->creator_cred;
+ creator_pid_ns = state->creator_pid_ns;
+ staged_path = state->staged_path;
+ state->creator_mm = NULL;
+ state->creator_cred = NULL;
+ state->creator_pid_ns = NULL;
+ state->staged_path = NULL;
+ state->argv = native_arg(NULL);
+ state->envp = native_arg(NULL);
+ state->result = result;
+ pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
+ mutex_unlock(&state->lock);
+
+ putname(filename);
+ if (creator_mm)
+ mmdrop(creator_mm);
+ if (creator_cred)
+ put_cred(creator_cred);
+ if (creator_pid_ns)
+ put_pid_ns(creator_pid_ns);
+ kfree(staged_path);
+}
+
+static bool pidfd_spawn_same_pid_ns(struct pidfd_spawn_state *state)
+{
+ return current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
+}
+
static bool pidfd_spawn_same_creator(struct pidfd_spawn_state *state)
{
+ /*
+ * Holding the pidfd alone is not enough authority to configure or start
+ * this builder. A caller must also share the creator's mm, credential
+ * object, and child PID namespace.
+ *
+ * The child PID namespace is part of the builder authority even though no
+ * numeric PID is allocated until run.
+ */
return current->mm == state->creator_mm &&
current_cred() == state->creator_cred &&
- current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
+ pidfd_spawn_same_pid_ns(state);
+}
+
+static int pidfd_spawn_check_startable(struct pidfd_spawn_state *state)
+{
+ int ret;
+
+ scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+ ret = pidfd_spawn_configuring_error(state);
+ if (!ret && !pidfd_spawn_same_creator(state))
+ ret = -EPERM;
+ }
+ return ret;
}
static struct filename *pidfd_spawn_get_path(const char __user *value)
@@ -153,6 +262,31 @@ static struct filename *pidfd_spawn_get_path(const char __user *value)
return no_free_ptr(name);
}
+static int
+pidfd_spawn_get_delayed_path(struct delayed_filename *delayed,
+ const char __user *value)
+{
+ struct filename *name __free(putname) = NULL;
+
+ name = pidfd_spawn_get_path(value);
+ if (IS_ERR(name))
+ return PTR_ERR(name);
+
+ return putname_to_delayed(delayed, no_free_ptr(name));
+}
+
+static int
+pidfd_spawn_get_delayed_kernel_path(struct delayed_filename *delayed,
+ const char *value)
+{
+ struct filename *name __free(putname) = getname_kernel(value);
+
+ if (IS_ERR(name))
+ return PTR_ERR(name);
+
+ return putname_to_delayed(delayed, no_free_ptr(name));
+}
+
static char *pidfd_spawn_copy_path(const char __user *value)
{
struct filename *name __free(putname) = NULL;
@@ -173,14 +307,14 @@ static int pidfd_spawn_state_set_path(struct pidfd_spawn_state *state,
{
char *path __free(kfree) = NULL;
char *old __free(kfree) = NULL;
+ int ret;
path = pidfd_spawn_copy_path(value);
if (IS_ERR(path))
return PTR_ERR(path);
scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
- int ret = pidfd_spawn_configuring_error(state);
-
+ ret = pidfd_spawn_configuring_error(state);
if (ret)
return ret;
if (!pidfd_spawn_same_creator(state))
@@ -243,6 +377,230 @@ SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
return ret;
}
+static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
+ struct filename *filename, int result)
+{
+ pidfd_spawn_drop_run_data(state, filename, result);
+ complete_all(&state->done);
+}
+
+static void pidfd_spawn_child(struct callback_head *work)
+{
+ struct pidfd_spawn_state *state =
+ container_of(work, struct pidfd_spawn_state, task_work);
+ struct filename *filename;
+ int ret;
+
+ filename = complete_getname(&state->filename);
+ if (IS_ERR(filename))
+ ret = PTR_ERR(filename);
+ else
+ ret = 0;
+
+ if (!ret)
+ ret = do_execveat_common(AT_FDCWD, filename,
+ state->argv, state->envp, 0);
+ if (!ret)
+ task_clear_embryonic_exec(current);
+
+ pidfd_spawn_finish_child(state, filename, ret);
+ if (ret) {
+ pidfd_spawn_state_put(state);
+ do_group_exit(PIDFD_SPAWN_EXIT_FAILURE);
+ }
+ pidfd_spawn_state_put(state);
+}
+
+static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
+ struct pidfd_spawn_run_args __user *uargs,
+ size_t usize)
+{
+ int ret;
+
+ BUILD_BUG_ON(sizeof(struct pidfd_spawn_run_args) !=
+ PIDFD_SPAWN_RUN_SIZE_VER0);
+
+ if (usize < PIDFD_SPAWN_RUN_SIZE_VER0)
+ return -EINVAL;
+ if (usize > PAGE_SIZE)
+ return -E2BIG;
+ ret = copy_struct_from_user(kargs, sizeof(*kargs), uargs, usize);
+ if (ret)
+ return ret;
+ if (kargs->flags || kargs->reserved0 || kargs->reserved[0] ||
+ kargs->reserved[1])
+ return -EINVAL;
+ if (!kargs->argv)
+ return -EINVAL;
+ if (kargs->nr_actions || kargs->actions || kargs->action_size)
+ return -EINVAL;
+ if (!pidfd_spawn_user_ptr_fits(kargs->path) ||
+ !pidfd_spawn_user_ptr_fits(kargs->argv) ||
+ !pidfd_spawn_user_ptr_fits(kargs->envp) ||
+ !pidfd_spawn_user_ptr_fits(kargs->actions))
+ return -EFAULT;
+
+ return 0;
+}
+
+static struct task_struct *
+pidfd_spawn_create_task(struct file *file, struct pidfd_spawn_state *state,
+ struct kernel_clone_args *clone_args)
+{
+ struct pid *pid;
+ struct task_struct *task;
+ u64 ino;
+ int ret;
+
+ ino = pidfs_future_file_ino(file);
+ if (!ino)
+ return ERR_PTR(-EBADF);
+
+ pid = alloc_pid_with_pidfs_ino(current->nsproxy->pid_ns_for_children,
+ NULL, 0, ino);
+ if (IS_ERR(pid))
+ return ERR_CAST(pid);
+
+ ret = pidfs_future_file_set_pid(file, pid);
+ if (ret) {
+ free_pid(pid);
+ return ERR_PTR(ret);
+ }
+
+ refcount_inc(&state->count);
+ task = copy_process(pid, 0, NUMA_NO_NODE, clone_args);
+ add_latent_entropy();
+ if (IS_ERR(task)) {
+ pidfd_spawn_state_put(state);
+ pidfs_future_file_clear_pid(file, pid);
+ free_pid(pid);
+ return task;
+ }
+
+ trace_sched_process_fork(current, task);
+ get_task_struct(task);
+ return task;
+}
+
+static void pidfd_spawn_publish_pid(struct file *file,
+ struct pidfd_spawn_state *state,
+ struct task_struct *task)
+{
+ struct pid *pid = get_task_pid(task, PIDTYPE_PID);
+
+ pidfs_future_file_publish_pid(file, pid);
+ /*
+ * Publish only after the inode identity, pidfs attributes, and exit-wakeup
+ * forwarding are ready. Pair with the acquire loads in pidfd resolution
+ * and poll so readers that observe @pid can use ordinary pidfd operations.
+ */
+ smp_store_release(&state->pid, pid);
+ pidfs_future_file_notify(file);
+}
+
+static void pidfd_spawn_attach_vfork_done(struct task_struct *task,
+ struct completion *vfork)
+{
+ init_completion(vfork);
+
+ task_lock(task);
+ task->vfork_done = vfork;
+ task_unlock(task);
+}
+
+static void pidfd_spawn_wait_for_child(struct pidfd_spawn_state *state)
+{
+ wait_for_completion(&state->done);
+}
+
+static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
+ struct task_struct *task,
+ struct completion *vfork)
+{
+ int ret;
+
+ ret = wait_for_vfork_done(task, vfork);
+ if (ret)
+ return ret;
+
+ mutex_lock(&state->lock);
+ ret = state->result;
+ pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTED);
+ mutex_unlock(&state->lock);
+ return ret;
+}
+
+static int pidfd_spawn_start(struct file *file,
+ struct pidfd_spawn_state *state,
+ const struct pidfd_spawn_run_args *kargs)
+{
+ struct delayed_filename filename
+ __free(pidfd_spawn_dismiss_filename) = {};
+ struct delayed_filename drop_filename
+ __free(pidfd_spawn_dismiss_filename) = {};
+ struct kernel_clone_args clone_args = {
+ .flags = CLONE_VM | CLONE_VFORK | CLONE_UNTRACED,
+ .exit_signal = SIGCHLD,
+ .task_work = &state->task_work,
+ .embryonic_exec = true,
+ };
+ struct completion vfork;
+ struct task_struct *task;
+ int ret;
+
+ if (kargs->path) {
+ ret = pidfd_spawn_get_delayed_path(&filename,
+ pidfd_spawn_user_ptr(kargs->path));
+ if (ret)
+ return ret;
+ }
+
+ scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+ ret = pidfd_spawn_configuring_error(state);
+ if (ret)
+ return ret;
+ if (!pidfd_spawn_same_creator(state))
+ return -EPERM;
+ if (state->staged_path && kargs->path)
+ return -EINVAL;
+ if (!state->staged_path && !kargs->path)
+ return -EINVAL;
+ if (state->staged_path) {
+ ret = pidfd_spawn_get_delayed_kernel_path(&filename,
+ state->staged_path);
+ if (ret)
+ return ret;
+ }
+ state->filename = filename;
+ INIT_DELAYED_FILENAME(&filename);
+
+ state->argv = pidfd_spawn_user_arg(kargs->argv);
+ state->envp = pidfd_spawn_user_arg(kargs->envp);
+ pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
+ reinit_completion(&state->done);
+
+ task = pidfd_spawn_create_task(file, state, &clone_args);
+ if (IS_ERR(task)) {
+ ret = PTR_ERR(task);
+ drop_filename = state->filename;
+ INIT_DELAYED_FILENAME(&state->filename);
+ state->argv = native_arg(NULL);
+ state->envp = native_arg(NULL);
+ state->result = 0;
+ pidfd_spawn_set_status(state,
+ PIDFD_SPAWN_CONFIGURING);
+ return ret;
+ }
+
+ pidfd_spawn_publish_pid(file, state, task);
+ pidfd_spawn_attach_vfork_done(task, &vfork);
+ }
+
+ wake_up_new_task(task);
+ pidfd_spawn_wait_for_child(state);
+ return pidfd_spawn_finish_vfork(state, task, &vfork);
+}
+
int pidfd_empty_open(unsigned int flags)
{
struct pidfd_spawn_state *state;
@@ -257,7 +615,13 @@ int pidfd_empty_open(unsigned int flags)
return -ENOMEM;
mutex_init(&state->lock);
+ init_completion(&state->done);
+ init_task_work(&state->task_work, pidfd_spawn_child);
refcount_set(&state->count, 1);
+ /*
+ * Remember the creator so later builder operations cannot be delegated
+ * just by passing the pidfd through SCM_RIGHTS or similar fd passing.
+ */
mmgrab(current->mm);
state->creator_mm = current->mm;
state->creator_cred = get_current_cred();
@@ -282,3 +646,37 @@ int pidfd_empty_open(unsigned int flags)
fd_install(pidfd, pidfile);
return pidfd;
}
+
+SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
+ struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
+{
+ struct pidfd_spawn_run_args kargs;
+ struct pidfd_spawn_state *state __free(pidfd_spawn_state) = NULL;
+ int ret;
+
+ ret = pidfd_spawn_copy_run_args(&kargs, uargs, usize);
+ if (ret)
+ return ret;
+
+ CLASS(fd, f)(fd);
+ if (fd_empty(f))
+ return -EBADF;
+
+ state = pidfd_spawn_state_get_file(fd_file(f));
+ if (IS_ERR(state))
+ return PTR_ERR(state);
+
+ scoped_cond_guard(mutex_intr, return -ERESTARTNOINTR,
+ ¤t->signal->cred_guard_mutex) {
+ if (READ_ONCE(current->ptrace))
+ return -EPERM;
+ ret = pidfd_spawn_check_startable(state);
+ if (ret)
+ return ret;
+ ret = pidfd_spawn_start(fd_file(f), state, &kargs);
+ }
+ if (!ret)
+ ret = pid_vnr(state->pid);
+
+ return ret;
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 15/24] fork: keep embryonic tasks hidden until exec completes
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (13 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 14/24] pidfd: create and execute spawn builder tasks Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 16/24] audit: add pidfd spawn child contexts Li Chen
` (8 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
A spawn task does not gain a valid userspace register frame when
exec_mmap() installs its private mm. A binary loader can still sleep or
fail before START_THREAD(). This can expose setup registers to ptrace or a
late failed-exec core dump. During earlier setup the task also shares the
source mm, which procfs could expose through the child's PID.
Hide procfs PID lookup, iteration, and cached dentry revalidation from
other tasks, deny coredumps, and report coredump skip state through pidfs.
Allow the embryonic task to use its own proc entries because executable and
interpreter lookup can legitimately use /proc/self. This does not expose
source state to another task. Sample the flag before reading published
credentials; acquire ordering covers the state installed by exec.
Release-publish the transition in the exec core after the final binary
handler succeeds and before audit, tracepoint, ptrace, and connector exec
events. Every successful exec observer then sees a normal task, while a
failed exec leaves the task hidden.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/coredump.c | 4 +++-
fs/exec.c | 2 ++
fs/pidfd_spawn.c | 2 --
fs/pidfs.c | 7 ++++++-
fs/proc/base.c | 11 +++++++++--
fs/proc/internal.h | 18 +++++++++++++++++-
kernel/ptrace.c | 2 +-
7 files changed, 38 insertions(+), 8 deletions(-)
diff --git a/fs/coredump.c b/fs/coredump.c
index ac3cd74808c64..4b2f0b5c7d606 100644
--- a/fs/coredump.c
+++ b/fs/coredump.c
@@ -1166,7 +1166,9 @@ void vfs_coredump(const kernel_siginfo_t *siginfo)
.limit = rlimit(RLIMIT_CORE),
/* Snapshot MMF_DUMP_FILTER_* (unlocked) and dumpable for the dump. */
.mm_flags = __mm_flags_get_word(mm),
- .dumpable = task_exec_state_get_dumpable(current),
+ .dumpable = task_is_embryonic_exec(current) ?
+ TASK_DUMPABLE_OFF :
+ task_exec_state_get_dumpable(current),
.vma_meta = NULL,
.cpu = raw_smp_processor_id(),
};
diff --git a/fs/exec.c b/fs/exec.c
index 1a256ba26425e..292c30e80aecb 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1734,6 +1734,8 @@ static int exec_binprm(struct linux_binprm *bprm)
fput(exec);
}
+ if (task_is_embryonic_exec(current))
+ task_clear_embryonic_exec(current);
audit_bprm(bprm);
trace_sched_process_exec(current, old_pid, bprm);
ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index c0e95c2ddbe6e..41d5cbb49a0f7 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -400,8 +400,6 @@ static void pidfd_spawn_child(struct callback_head *work)
if (!ret)
ret = do_execveat_common(AT_FDCWD, filename,
state->argv, state->envp, 0);
- if (!ret)
- task_clear_embryonic_exec(current);
pidfd_spawn_finish_child(state, filename, ret);
if (ret) {
diff --git a/fs/pidfs.c b/fs/pidfs.c
index d62235e01b351..7a473c049dcd4 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -477,6 +477,7 @@ static long pidfd_info(struct file *file, unsigned int cmd, unsigned long arg)
struct user_namespace *user_ns;
struct pidfs_attr *attr;
const struct cred *c;
+ bool embryonic;
__u64 mask;
BUILD_BUG_ON(sizeof(struct pidfd_info) != PIDFD_INFO_SIZE_VER3);
@@ -533,12 +534,16 @@ static long pidfd_info(struct file *file, unsigned int cmd, unsigned long arg)
goto copy_out;
}
+ embryonic = task_is_embryonic_exec(task);
c = get_task_cred(task);
if (!c)
return -ESRCH;
if ((mask & PIDFD_INFO_COREDUMP) && !kinfo.coredump_mask) {
- kinfo.coredump_mask = pidfs_coredump_mask(task_exec_state_get_dumpable(task));
+ enum task_dumpable dumpable = embryonic ?
+ TASK_DUMPABLE_OFF : task_exec_state_get_dumpable(task);
+
+ kinfo.coredump_mask = pidfs_coredump_mask(dumpable);
kinfo.mask |= PIDFD_INFO_COREDUMP;
/* No coredump actually took place, so no coredump signal. */
}
diff --git a/fs/proc/base.c b/fs/proc/base.c
index 6a39de424f62a..bd059ddcce863 100644
--- a/fs/proc/base.c
+++ b/fs/proc/base.c
@@ -2053,7 +2053,7 @@ static int pid_revalidate(struct inode *dir, const struct qstr *name,
goto out;
task = pid_task(proc_pid(inode), PIDTYPE_PID);
- if (task) {
+ if (task && proc_task_visible(task)) {
pid_update_inode(task, inode);
ret = 1;
}
@@ -3492,6 +3492,8 @@ struct dentry *proc_pid_lookup(struct dentry *dentry, unsigned int flags)
rcu_read_unlock();
if (!task)
goto out;
+ if (!proc_task_visible(task))
+ goto out_put_task;
/* Limit procfs to only ptraceable tasks */
if (fs_info->hide_pid == HIDEPID_NOT_PTRACEABLE) {
@@ -3539,11 +3541,12 @@ static bool next_tgid(struct tgid_iter *it)
if (pid) {
it->tgid = pid_nr_ns(pid, it->pid_ns);
it->task = pid_task(pid, PIDTYPE_TGID);
- if (it->task) {
+ if (it->task && proc_task_visible(it->task)) {
get_task_struct(it->task);
rcu_read_unlock();
return true;
}
+ it->task = NULL;
} else {
rcu_read_unlock();
return false;
@@ -3807,6 +3810,8 @@ static struct dentry *proc_task_lookup(struct inode *dir, struct dentry * dentry
rcu_read_unlock();
if (!task)
goto out;
+ if (!proc_task_visible(task))
+ goto out_drop_task;
if (!same_thread_group(leader, task))
goto out_drop_task;
@@ -3844,6 +3849,8 @@ static struct task_struct *first_tid(struct pid *pid, int tid, loff_t f_pos,
task = pid_task(pid, PIDTYPE_PID);
if (!task)
goto fail;
+ if (!proc_task_visible(task))
+ goto fail;
/* Attempt to start with the tid of a thread */
if (tid && nr) {
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index b232e1098117b..81ad44502272e 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -147,9 +147,25 @@ static inline struct pid *proc_pid(const struct inode *inode)
return PROC_I(inode)->pid;
}
+static inline bool proc_task_visible(struct task_struct *task)
+{
+ /*
+ * An embryonic task may need its own proc entries during exec setup,
+ * but it is not a valid userspace process image for other tasks yet.
+ */
+ return task == current || !task_is_embryonic_exec(task);
+}
+
static inline struct task_struct *get_proc_task(const struct inode *inode)
{
- return get_pid_task(proc_pid(inode), PIDTYPE_PID);
+ struct task_struct *task;
+
+ task = get_pid_task(proc_pid(inode), PIDTYPE_PID);
+ if (task && !proc_task_visible(task)) {
+ put_task_struct(task);
+ return NULL;
+ }
+ return task;
}
void task_dump_owner(struct task_struct *task, umode_t mode,
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index 36eb9b154a397..59e11dae803c8 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -314,7 +314,7 @@ static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
WARN(1, "denying ptrace access check without PTRACE_MODE_*CREDS\n");
return -EPERM;
}
- if (task_is_embryonic_exec(task))
+ if (task_is_embryonic_exec(task) && task != current)
return -EPERM;
/* May we inspect the given task?
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 16/24] audit: add pidfd spawn child contexts
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (14 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 15/24] fork: keep embryonic tasks hidden until exec completes Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 17/24] pidfd: audit child spawn execution Li Chen
` (7 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Kernel-mediated spawn executes file setup and exec from initial child task
work, not from a second syscall entry. Representing that work as another
AUDIT_SYSCALL record duplicates the source pidfd_spawn_run transaction and
invents a syscall that the child never entered.
Add a dedicated audit context and AUDIT_PIDFD_SPAWN record. Carry the
originating syscall number and arguments. Existing syscall exit filters can
then select the child transaction. CWD, PATH, and EXECVE records remain
correlated with it. Let the child close the transaction with an explicit
result instead of rewriting an architecture syscall-return frame.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
include/linux/audit.h | 31 +++++++++++
include/uapi/linux/audit.h | 1 +
kernel/audit.h | 1 +
kernel/auditsc.c | 105 ++++++++++++++++++++++++++++++++++++-
4 files changed, 136 insertions(+), 2 deletions(-)
diff --git a/include/linux/audit.h b/include/linux/audit.h
index 45abb3722d304..872624a9fba08 100644
--- a/include/linux/audit.h
+++ b/include/linux/audit.h
@@ -323,6 +323,9 @@ extern int audit_alloc(struct task_struct *task);
extern void __audit_free(struct task_struct *task);
extern void __audit_uring_entry(u8 op);
extern void __audit_uring_exit(int success, long code);
+void __audit_pidfd_spawn_entry(int major, unsigned long a0, unsigned long a1,
+ unsigned long a2, unsigned long a3);
+void __audit_pidfd_spawn_exit(int success, long code);
extern void __audit_syscall_entry(int major, unsigned long a0, unsigned long a1,
unsigned long a2, unsigned long a3);
extern void __audit_syscall_exit(int ret_success, long ret_value);
@@ -373,6 +376,22 @@ static inline void audit_uring_exit(int success, long code)
if (unlikely(audit_context()))
__audit_uring_exit(success, code);
}
+
+static inline void audit_pidfd_spawn_entry(int major, unsigned long a0,
+ unsigned long a1,
+ unsigned long a2,
+ unsigned long a3)
+{
+ if (unlikely(audit_context()))
+ __audit_pidfd_spawn_entry(major, a0, a1, a2, a3);
+}
+
+static inline void audit_pidfd_spawn_exit(int success, long code)
+{
+ if (unlikely(audit_context()))
+ __audit_pidfd_spawn_exit(success, code);
+}
+
static inline void audit_syscall_entry(int major, unsigned long a0,
unsigned long a1, unsigned long a2,
unsigned long a3)
@@ -380,6 +399,7 @@ static inline void audit_syscall_entry(int major, unsigned long a0,
if (unlikely(audit_context()))
__audit_syscall_entry(major, a0, a1, a2, a3);
}
+
static inline void audit_syscall_exit(void *pt_regs)
{
if (unlikely(audit_context())) {
@@ -611,10 +631,21 @@ static inline void audit_uring_entry(u8 op)
{ }
static inline void audit_uring_exit(int success, long code)
{ }
+
+static inline void audit_pidfd_spawn_entry(int major, unsigned long a0,
+ unsigned long a1,
+ unsigned long a2,
+ unsigned long a3)
+{ }
+
+static inline void audit_pidfd_spawn_exit(int success, long code)
+{ }
+
static inline void audit_syscall_entry(int major, unsigned long a0,
unsigned long a1, unsigned long a2,
unsigned long a3)
{ }
+
static inline void audit_syscall_exit(void *pt_regs)
{ }
static inline bool audit_dummy_context(void)
diff --git a/include/uapi/linux/audit.h b/include/uapi/linux/audit.h
index e8f5ce677df73..e09fb5222293a 100644
--- a/include/uapi/linux/audit.h
+++ b/include/uapi/linux/audit.h
@@ -122,6 +122,7 @@
#define AUDIT_OPENAT2 1337 /* Record showing openat2 how args */
#define AUDIT_DM_CTRL 1338 /* Device Mapper target control */
#define AUDIT_DM_EVENT 1339 /* Device Mapper events */
+#define AUDIT_PIDFD_SPAWN 1340 /* pidfd spawn child operation */
#define AUDIT_AVC 1400 /* SE Linux avc denial or grant */
#define AUDIT_SELINUX_ERR 1401 /* Internal SE Linux Errors */
diff --git a/kernel/audit.h b/kernel/audit.h
index 92d5e723d570b..2f740df7beecf 100644
--- a/kernel/audit.h
+++ b/kernel/audit.h
@@ -112,6 +112,7 @@ struct audit_context {
AUDIT_CTX_UNUSED, /* audit_context is currently unused */
AUDIT_CTX_SYSCALL, /* in use by syscall */
AUDIT_CTX_URING, /* in use by io_uring */
+ AUDIT_CTX_PIDFD_SPAWN, /* pidfd spawn child operation */
} context;
enum audit_state state, current_state;
struct audit_stamp stamp; /* event identifier */
diff --git a/kernel/auditsc.c b/kernel/auditsc.c
index 6610e667c728a..8f6d84a902239 100644
--- a/kernel/auditsc.c
+++ b/kernel/auditsc.c
@@ -1649,6 +1649,30 @@ static void audit_log_uring(struct audit_context *ctx)
audit_log_end(ab);
}
+/**
+ * audit_log_pidfd_spawn - generate an AUDIT_PIDFD_SPAWN record
+ * @ctx: the audit context
+ */
+static void audit_log_pidfd_spawn(struct audit_context *ctx)
+{
+ struct audit_buffer *ab;
+
+ ab = audit_log_start(ctx, GFP_KERNEL, AUDIT_PIDFD_SPAWN);
+ if (!ab)
+ return;
+ audit_log_format(ab, "arch=%x syscall=%d", ctx->arch, ctx->major);
+ if (ctx->return_valid != AUDITSC_INVALID)
+ audit_log_format(ab, " success=%s exit=%ld",
+ str_yes_no(ctx->return_valid == AUDITSC_SUCCESS),
+ ctx->return_code);
+ audit_log_format(ab, " a0=%lx a1=%lx a2=%lx a3=%lx items=%d",
+ ctx->argv[0], ctx->argv[1], ctx->argv[2],
+ ctx->argv[3], ctx->name_count);
+ audit_log_task_info(ab);
+ audit_log_key(ab, ctx->filterkey);
+ audit_log_end(ab);
+}
+
static void audit_log_exit(void)
{
int i, call_panic = 0;
@@ -1687,6 +1711,9 @@ static void audit_log_exit(void)
case AUDIT_CTX_URING:
audit_log_uring(context);
break;
+ case AUDIT_CTX_PIDFD_SPAWN:
+ audit_log_pidfd_spawn(context);
+ break;
default:
BUG();
break;
@@ -1782,7 +1809,8 @@ static void audit_log_exit(void)
audit_log_name(context, n, NULL, i++, &call_panic);
}
- if (context->context == AUDIT_CTX_SYSCALL)
+ if (context->context == AUDIT_CTX_SYSCALL ||
+ context->context == AUDIT_CTX_PIDFD_SPAWN)
audit_log_proctitle();
/* Send end of event record to help user space know we are finished */
@@ -1818,7 +1846,8 @@ void __audit_free(struct task_struct *tsk)
if (tsk == current && !context->dummy) {
context->return_valid = AUDITSC_INVALID;
context->return_code = 0;
- if (context->context == AUDIT_CTX_SYSCALL) {
+ if (context->context == AUDIT_CTX_SYSCALL ||
+ context->context == AUDIT_CTX_PIDFD_SPAWN) {
audit_filter_syscall(tsk, context);
audit_filter_inodes(tsk, context);
if (context->current_state == AUDIT_STATE_RECORD)
@@ -1967,6 +1996,78 @@ void __audit_uring_exit(int success, long code)
audit_reset_context(ctx);
}
+/**
+ * __audit_pidfd_spawn_entry - start a pidfd spawn child audit context
+ * @major: originating pidfd_spawn_run syscall number
+ * @a0: originating syscall argument 0
+ * @a1: originating syscall argument 1
+ * @a2: originating syscall argument 2
+ * @a3: originating syscall argument 3
+ */
+void __audit_pidfd_spawn_entry(int major, unsigned long a0, unsigned long a1,
+ unsigned long a2, unsigned long a3)
+{
+ struct audit_context *context = audit_context();
+ enum audit_state state;
+
+ if (!audit_enabled || !context)
+ return;
+
+ WARN_ON(context->context != AUDIT_CTX_UNUSED);
+ WARN_ON(context->name_count);
+ if (context->context != AUDIT_CTX_UNUSED || context->name_count) {
+ audit_panic("unrecoverable error in audit_pidfd_spawn_entry()");
+ return;
+ }
+
+ state = context->state;
+ if (state == AUDIT_STATE_DISABLED)
+ return;
+
+ context->dummy = !audit_n_rules;
+ if (!context->dummy && state == AUDIT_STATE_BUILD) {
+ context->prio = 0;
+ if (auditd_test_task(current))
+ return;
+ }
+
+ context->arch = syscall_get_arch(current);
+ context->major = major;
+ context->argv[0] = a0;
+ context->argv[1] = a1;
+ context->argv[2] = a2;
+ context->argv[3] = a3;
+ context->context = AUDIT_CTX_PIDFD_SPAWN;
+ context->current_state = state;
+ ktime_get_coarse_real_ts64(&context->stamp.ctime);
+}
+
+/**
+ * __audit_pidfd_spawn_exit - finish a pidfd spawn child audit context
+ * @success: whether child-side setup succeeded
+ * @return_code: child-side setup result
+ */
+void __audit_pidfd_spawn_exit(int success, long return_code)
+{
+ struct audit_context *context = audit_context();
+
+ if (!context || context->dummy ||
+ context->context != AUDIT_CTX_PIDFD_SPAWN)
+ goto out;
+
+ if (!list_empty(&context->killed_trees))
+ audit_kill_trees(context);
+
+ audit_return_fixup(context, success, return_code);
+ audit_filter_syscall(current, context);
+ audit_filter_inodes(current, context);
+ if (context->current_state == AUDIT_STATE_RECORD)
+ audit_log_exit();
+
+out:
+ audit_reset_context(context);
+}
+
/**
* __audit_syscall_entry - fill in an audit record at syscall entry
* @major: major syscall type (function)
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 17/24] pidfd: audit child spawn execution
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (15 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 16/24] audit: add pidfd spawn child contexts Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 18/24] pidfd: make spawn builder execution signal-safe Li Chen
` (6 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
The child exec path runs outside the caller's audit context. A delayed
filename can leave a name-only record in the caller transaction, but inode
metadata and exec arguments are otherwise lost because the new task starts
with an unused audit context.
Start a dedicated AUDIT_PIDFD_SPAWN transaction before executable lookup so
audit rules can observe child identity, EXECVE arguments, and inode-backed
PATH records. Close it explicitly with the child setup result.
Task work does not enter through a userspace syscall frame, so do not
synthesize one.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfd_spawn.c | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 41d5cbb49a0f7..5586926988406 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -3,6 +3,8 @@
* pidfd-backed process spawn builders
*/
+#include <asm/syscall.h>
+#include <linux/audit.h>
#include <linux/completion.h>
#include <linux/cred.h>
#include <linux/file.h>
@@ -81,6 +83,8 @@ struct pidfd_spawn_state {
char *staged_path;
struct user_arg_ptr argv;
struct user_arg_ptr envp;
+ unsigned long audit_args[4];
+ int audit_syscall;
int result;
enum pidfd_spawn_status status;
};
@@ -206,6 +210,8 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
state->staged_path = NULL;
state->argv = native_arg(NULL);
state->envp = native_arg(NULL);
+ memset(state->audit_args, 0, sizeof(state->audit_args));
+ state->audit_syscall = 0;
state->result = result;
pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
mutex_unlock(&state->lock);
@@ -377,6 +383,22 @@ SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
return ret;
}
+static void pidfd_spawn_save_audit_context(struct pidfd_spawn_state *state)
+{
+ struct pt_regs *regs = current_pt_regs();
+ unsigned long args[6];
+
+ syscall_get_arguments(current, regs, args);
+ state->audit_syscall = syscall_get_nr(current, regs);
+ memcpy(state->audit_args, args, sizeof(state->audit_args));
+}
+
+static void pidfd_spawn_audit_entry(struct pidfd_spawn_state *state)
+{
+ audit_pidfd_spawn_entry(state->audit_syscall, state->audit_args[0],
+ state->audit_args[1], state->audit_args[2],
+ state->audit_args[3]);
+}
static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
struct filename *filename, int result)
{
@@ -391,6 +413,7 @@ static void pidfd_spawn_child(struct callback_head *work)
struct filename *filename;
int ret;
+ pidfd_spawn_audit_entry(state);
filename = complete_getname(&state->filename);
if (IS_ERR(filename))
ret = PTR_ERR(filename);
@@ -403,9 +426,11 @@ static void pidfd_spawn_child(struct callback_head *work)
pidfd_spawn_finish_child(state, filename, ret);
if (ret) {
+ audit_pidfd_spawn_exit(0, ret);
pidfd_spawn_state_put(state);
do_group_exit(PIDFD_SPAWN_EXIT_FAILURE);
}
+ audit_pidfd_spawn_exit(1, 0);
pidfd_spawn_state_put(state);
}
@@ -575,6 +600,7 @@ static int pidfd_spawn_start(struct file *file,
state->argv = pidfd_spawn_user_arg(kargs->argv);
state->envp = pidfd_spawn_user_arg(kargs->envp);
pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
+ pidfd_spawn_save_audit_context(state);
reinit_completion(&state->done);
task = pidfd_spawn_create_task(file, state, &clone_args);
@@ -584,6 +610,9 @@ static int pidfd_spawn_start(struct file *file,
INIT_DELAYED_FILENAME(&state->filename);
state->argv = native_arg(NULL);
state->envp = native_arg(NULL);
+ memset(state->audit_args, 0,
+ sizeof(state->audit_args));
+ state->audit_syscall = 0;
state->result = 0;
pidfd_spawn_set_status(state,
PIDFD_SPAWN_CONFIGURING);
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 18/24] pidfd: make spawn builder execution signal-safe
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (16 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 17/24] pidfd: audit child spawn execution Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 19/24] file: expose spawn file-action helpers Li Chen
` (5 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Wait for child setup in a killable and freezable state. If the caller
is interrupted after task creation, cancel a child that is still
setting up and normalize restart errors because the spawn operation is
not idempotent.
Preserve externally delivered fatal signals over setup errors. Complete
the builder and audit transaction before entering the signal core
directly, so the callback never returns through an invalid userspace
frame. The task work can run from an outer get_signal(), so fatal delivery
deliberately nests get_signal(); the inner path exits and never returns to
the outer call. The child remains embryonic and nondumpable until exec
installs a valid frame and mm.
Reject an already traced caller while holding cred_guard_mutex across
the spawn operation. This prevents CLONE_UNTRACED from becoming an
escape from an existing ptrace supervisor.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfd_spawn.c | 189 +++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 183 insertions(+), 6 deletions(-)
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 5586926988406..0e52503976c0c 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -5,6 +5,7 @@
#include <asm/syscall.h>
#include <linux/audit.h>
+#include <linux/cgroup.h>
#include <linux/completion.h>
#include <linux/cred.h>
#include <linux/file.h>
@@ -212,8 +213,10 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
state->envp = native_arg(NULL);
memset(state->audit_args, 0, sizeof(state->audit_args));
state->audit_syscall = 0;
- state->result = result;
- pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
+ if (pidfd_spawn_get_status(state) != PIDFD_SPAWN_CANCELLED) {
+ state->result = result;
+ pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
+ }
mutex_unlock(&state->lock);
putname(filename);
@@ -226,6 +229,32 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
kfree(staged_path);
}
+static bool pidfd_spawn_cancel(struct pidfd_spawn_state *state,
+ struct task_struct *task)
+{
+ bool cancelled = false;
+
+ mutex_lock(&state->lock);
+ if (pidfd_spawn_get_status(state) == PIDFD_SPAWN_STARTING) {
+ /*
+ * Establish SIGKILL group-exit state before publishing
+ * cancellation. The child also takes state->lock before committing
+ * its setup result, so it cannot return from task work before the
+ * signal is pending.
+ */
+ if (WARN_ON_ONCE(do_send_sig_info(SIGKILL, SEND_SIG_PRIV, task,
+ PIDTYPE_PID)))
+ goto out_unlock;
+ state->result = -ECANCELED;
+ pidfd_spawn_set_status(state, PIDFD_SPAWN_CANCELLED);
+ cancelled = true;
+ }
+out_unlock:
+ mutex_unlock(&state->lock);
+
+ return cancelled;
+}
+
static bool pidfd_spawn_same_pid_ns(struct pidfd_spawn_state *state)
{
return current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
@@ -383,6 +412,35 @@ SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
return ret;
}
+static bool pidfd_spawn_child_cancelled(struct pidfd_spawn_state *state)
+{
+ bool cancelled;
+
+ mutex_lock(&state->lock);
+ cancelled = pidfd_spawn_get_status(state) == PIDFD_SPAWN_CANCELLED;
+ mutex_unlock(&state->lock);
+
+ return cancelled;
+}
+
+static bool pidfd_spawn_setup_done(struct pidfd_spawn_state *state)
+{
+ bool done = false;
+
+ mutex_lock(&state->lock);
+ switch (pidfd_spawn_get_status(state)) {
+ case PIDFD_SPAWN_SETUP_DONE:
+ case PIDFD_SPAWN_STARTED:
+ done = true;
+ break;
+ default:
+ break;
+ }
+ mutex_unlock(&state->lock);
+
+ return done;
+}
+
static void pidfd_spawn_save_audit_context(struct pidfd_spawn_state *state)
{
struct pt_regs *regs = current_pt_regs();
@@ -399,6 +457,52 @@ static void pidfd_spawn_audit_entry(struct pidfd_spawn_state *state)
state->audit_args[1], state->audit_args[2],
state->audit_args[3]);
}
+
+static int pidfd_spawn_normalize_result(int result)
+{
+ if (result == -ERESTARTSYS || result == -ERESTARTNOINTR ||
+ result == -ERESTARTNOHAND || result == -ERESTART_RESTARTBLOCK)
+ return -EINTR;
+ return result;
+}
+
+static int pidfd_spawn_pending_fatal_signal(void)
+{
+ struct sighand_struct *sighand;
+ sigset_t pending;
+ unsigned long flags;
+ int fatal = 0;
+ int sig;
+
+ /*
+ * fatal_signal_pending() only recognizes pending SIGKILL. A default-fatal
+ * coredump signal remains pending as itself, but must still win over a
+ * setup error. Inspect the same pending sets and default dispositions as
+ * get_signal() without dequeuing the signal.
+ */
+ sighand = lock_task_sighand(current, &flags);
+ if (WARN_ON_ONCE(!sighand))
+ return 0;
+
+ sigorsets(&pending, ¤t->pending.signal,
+ ¤t->signal->shared_pending.signal);
+ sigandnsets(&pending, &pending, ¤t->blocked);
+
+ for (sig = 1; sig < _NSIG; sig++) {
+ if (!sigismember(&pending, sig) || sig_kernel_ignore(sig) ||
+ sig_kernel_stop(sig) ||
+ sighand->action[sig - 1].sa.sa_handler != SIG_DFL)
+ continue;
+ if ((current->signal->flags & SIGNAL_UNKILLABLE) &&
+ !sig_kernel_only(sig))
+ continue;
+ fatal = sig;
+ break;
+ }
+ spin_unlock_irqrestore(&sighand->siglock, flags);
+ return fatal;
+}
+
static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
struct filename *filename, int result)
{
@@ -406,11 +510,40 @@ static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
complete_all(&state->done);
}
+static __noreturn void
+pidfd_spawn_deliver_fatal_signal(struct pidfd_spawn_state *state,
+ struct filename *filename, int sig)
+{
+ struct ksignal ksig;
+ sigset_t blocked;
+
+ /*
+ * The callback frame is not a valid userspace signal frame on every
+ * architecture. Complete the spawn transaction, then enter the signal
+ * core directly instead of returning through the callback trampoline.
+ *
+ * The embryonic task state prevents ptrace access and coredumps until exec
+ * installs a valid userspace frame and private mm. Block other catchable
+ * signals so the selected default-fatal signal remains terminal. If the
+ * signal core unexpectedly returns, exit without exposing the frame.
+ */
+ pidfd_spawn_finish_child(state, filename, 0);
+ audit_pidfd_spawn_exit(0, -EINTR);
+ pidfd_spawn_state_put(state);
+
+ sigfillset(&blocked);
+ sigdelset(&blocked, sig);
+ set_current_blocked(&blocked);
+ get_signal(&ksig);
+ do_group_exit(PIDFD_SPAWN_EXIT_FAILURE);
+}
+
static void pidfd_spawn_child(struct callback_head *work)
{
struct pidfd_spawn_state *state =
container_of(work, struct pidfd_spawn_state, task_work);
struct filename *filename;
+ int fatal_sig;
int ret;
pidfd_spawn_audit_entry(state);
@@ -420,9 +553,27 @@ static void pidfd_spawn_child(struct callback_head *work)
else
ret = 0;
+ if (pidfd_spawn_child_cancelled(state)) {
+ ret = -ECANCELED;
+ pidfd_spawn_finish_child(state, filename, ret);
+ audit_pidfd_spawn_exit(0, ret);
+ pidfd_spawn_state_put(state);
+ do_group_exit(SIGKILL);
+ }
+ fatal_sig = pidfd_spawn_pending_fatal_signal();
+ if (fatal_sig)
+ pidfd_spawn_deliver_fatal_signal(state, filename, fatal_sig);
+
if (!ret)
ret = do_execveat_common(AT_FDCWD, filename,
state->argv, state->envp, 0);
+ ret = pidfd_spawn_normalize_result(ret);
+ if (ret) {
+ fatal_sig = pidfd_spawn_pending_fatal_signal();
+ if (fatal_sig)
+ pidfd_spawn_deliver_fatal_signal(state, filename,
+ fatal_sig);
+ }
pidfd_spawn_finish_child(state, filename, ret);
if (ret) {
@@ -531,9 +682,16 @@ static void pidfd_spawn_attach_vfork_done(struct task_struct *task,
task_unlock(task);
}
-static void pidfd_spawn_wait_for_child(struct pidfd_spawn_state *state)
+static int pidfd_spawn_wait_for_child(struct pidfd_spawn_state *state)
{
- wait_for_completion(&state->done);
+ unsigned int wait_state = TASK_KILLABLE | TASK_FREEZABLE;
+ int ret;
+
+ cgroup_enter_frozen();
+ ret = wait_for_completion_state(&state->done, wait_state);
+ cgroup_leave_frozen(false);
+
+ return pidfd_spawn_normalize_result(ret);
}
static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
@@ -544,7 +702,7 @@ static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
ret = wait_for_vfork_done(task, vfork);
if (ret)
- return ret;
+ return pidfd_spawn_normalize_result(ret);
mutex_lock(&state->lock);
ret = state->result;
@@ -624,7 +782,26 @@ static int pidfd_spawn_start(struct file *file,
}
wake_up_new_task(task);
- pidfd_spawn_wait_for_child(state);
+ ret = pidfd_spawn_wait_for_child(state);
+ if (ret) {
+ int interrupt = ret;
+
+ if (pidfd_spawn_setup_done(state)) {
+ if (pidfd_spawn_finish_vfork(state, task, &vfork))
+ return interrupt;
+ return 0;
+ }
+ if (!pidfd_spawn_cancel(state, task) &&
+ pidfd_spawn_setup_done(state)) {
+ if (pidfd_spawn_finish_vfork(state, task, &vfork))
+ return interrupt;
+ return 0;
+ }
+
+ /* Drop the stack-based completion without waiting for child setup. */
+ wait_for_vfork_done(task, &vfork);
+ return interrupt;
+ }
return pidfd_spawn_finish_vfork(state, task, &vfork);
}
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 19/24] file: expose spawn file-action helpers
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (17 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 18/24] pidfd: make spawn builder execution signal-safe Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 20/24] pidfd: add initial spawn file actions Li Chen
` (4 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Process builders need to apply close_range() and fchdir() while
preparing a child. Split the syscall bodies into internal helpers so
callers in fs can reuse existing validation and state updates. Keep the
helpers internal to fs rather than adding new global interfaces.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/file.c | 11 ++++++++---
fs/internal.h | 2 ++
fs/open.c | 7 ++++++-
3 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/fs/file.c b/fs/file.c
index 628ca07dc4b17..4f4cb1d5752c2 100644
--- a/fs/file.c
+++ b/fs/file.c
@@ -805,7 +805,7 @@ static inline void __range_close(struct files_struct *files, unsigned int fd,
}
/**
- * sys_close_range() - Close all file descriptors in a given range.
+ * do_close_range() - Close all file descriptors in a given range.
*
* @fd: starting file descriptor to close
* @max_fd: last file descriptor to close
@@ -815,8 +815,7 @@ static inline void __range_close(struct files_struct *files, unsigned int fd,
* from @fd up to and including @max_fd are closed.
* Currently, errors to close a given file descriptor are ignored.
*/
-SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd,
- unsigned int, flags)
+int do_close_range(unsigned int fd, unsigned int max_fd, unsigned int flags)
{
struct task_struct *me = current;
struct files_struct *cur_fds = me->files, *fds = NULL;
@@ -867,6 +866,12 @@ SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd,
return 0;
}
+SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd,
+ unsigned int, flags)
+{
+ return do_close_range(fd, max_fd, flags);
+}
+
/**
* file_close_fd - return file associated with fd
* @fd: file descriptor to retrieve file for
diff --git a/fs/internal.h b/fs/internal.h
index 71cc43e72b33e..25492194e2883 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -198,6 +198,8 @@ extern struct file *do_file_open_root(const struct path *,
extern struct open_how build_open_how(int flags, umode_t mode);
extern int build_open_flags(const struct open_how *how, struct open_flags *op);
struct file *file_close_fd_locked(struct files_struct *files, unsigned fd);
+int do_close_range(unsigned int fd, unsigned int max_fd, unsigned int flags);
+int do_fchdir(unsigned int fd);
int do_ftruncate(struct file *file, loff_t length, unsigned int flags);
int chmod_common(const struct path *path, umode_t mode);
diff --git a/fs/open.c b/fs/open.c
index 408925d7bd0b7..bf9d7bdcf487c 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -568,7 +568,7 @@ SYSCALL_DEFINE1(chdir, const char __user *, filename)
return error;
}
-SYSCALL_DEFINE1(fchdir, unsigned int, fd)
+int do_fchdir(unsigned int fd)
{
CLASS(fd_raw, f)(fd);
int error;
@@ -585,6 +585,11 @@ SYSCALL_DEFINE1(fchdir, unsigned int, fd)
return error;
}
+SYSCALL_DEFINE1(fchdir, unsigned int, fd)
+{
+ return do_fchdir(fd);
+}
+
SYSCALL_DEFINE1(chroot, const char __user *, filename)
{
struct path path;
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 20/24] pidfd: add initial spawn file actions
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (18 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 19/24] file: expose spawn file-action helpers Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 21/24] pidfd: consume spawn builders on the first run attempt Li Chen
` (3 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Allow pidfd_spawn_run() to apply an ordered list of DUP2, FCHDIR, and
CLOSE_RANGE actions before exec. Copy and validate the complete action
list before creating the child so malformed input cannot leave a
partially started process. Use a versioned action stride so userspace can
pass larger future records. Bound the payload by 64 KiB instead of a fixed
action count, keeping allocation bounded without imposing an arbitrary
limit on small records.
Apply actions before completing the delayed executable name. This makes an
FCHDIR action establish the cwd used by both relative executable lookup and
the child transaction's AUDIT_CWD record.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfd_spawn.c | 171 ++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 163 insertions(+), 8 deletions(-)
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 0e52503976c0c..3e46a12c0ba07 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -5,9 +5,11 @@
#include <asm/syscall.h>
#include <linux/audit.h>
+#include <linux/close_range.h>
#include <linux/cgroup.h>
#include <linux/completion.h>
#include <linux/cred.h>
+#include <linux/fdtable.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/mm.h>
@@ -23,15 +25,18 @@
#include <linux/sched/signal.h>
#include <linux/sched/task.h>
#include <linux/security.h>
+#include <linux/sizes.h>
#include <linux/slab.h>
#include <linux/syscalls.h>
#include <linux/task_work.h>
#include <linux/uaccess.h>
+#include <linux/vmalloc.h>
#include <uapi/linux/pidfd.h>
#include <uapi/linux/wait.h>
#include <trace/events/sched.h>
#include "exec_internal.h"
+#include "internal.h"
static inline void __user *pidfd_spawn_user_ptr(__u64 p)
{
@@ -59,6 +64,9 @@ static inline struct user_arg_ptr pidfd_spawn_user_arg(__u64 p)
#define PIDFD_SPAWN_EXIT_FAILURE (127 << 8)
#define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE 256
+/* Bound allocation without imposing a fixed number of action records. */
+#define PIDFD_SPAWN_MAX_ACTIONS_SIZE SZ_64K
+
DEFINE_FREE(pidfd_spawn_dismiss_filename, struct delayed_filename,
dismiss_delayed_filename(&_T))
@@ -82,9 +90,11 @@ struct pidfd_spawn_state {
struct pid_namespace *creator_pid_ns;
struct delayed_filename filename;
char *staged_path;
+ struct pidfd_spawn_action *actions;
struct user_arg_ptr argv;
struct user_arg_ptr envp;
unsigned long audit_args[4];
+ unsigned int nr_actions;
int audit_syscall;
int result;
enum pidfd_spawn_status status;
@@ -139,6 +149,7 @@ static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
put_cred(state->creator_cred);
if (state->creator_pid_ns)
put_pid_ns(state->creator_pid_ns);
+ kvfree(state->actions);
put_pid(state->pid);
kfree(state);
}
@@ -192,6 +203,7 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
struct filename *filename, int result)
{
const struct cred *creator_cred;
+ struct pidfd_spawn_action *actions;
struct mm_struct *creator_mm;
struct pid_namespace *creator_pid_ns;
char *staged_path;
@@ -201,14 +213,17 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
* after the child no longer needs the shared setup payload.
*/
mutex_lock(&state->lock);
+ actions = state->actions;
creator_mm = state->creator_mm;
creator_cred = state->creator_cred;
creator_pid_ns = state->creator_pid_ns;
staged_path = state->staged_path;
+ state->actions = NULL;
state->creator_mm = NULL;
state->creator_cred = NULL;
state->creator_pid_ns = NULL;
state->staged_path = NULL;
+ state->nr_actions = 0;
state->argv = native_arg(NULL);
state->envp = native_arg(NULL);
memset(state->audit_args, 0, sizeof(state->audit_args));
@@ -227,6 +242,7 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
if (creator_pid_ns)
put_pid_ns(creator_pid_ns);
kfree(staged_path);
+ kvfree(actions);
}
static bool pidfd_spawn_cancel(struct pidfd_spawn_state *state,
@@ -441,6 +457,114 @@ static bool pidfd_spawn_setup_done(struct pidfd_spawn_state *state)
return done;
}
+static int pidfd_spawn_validate_action(const struct pidfd_spawn_action *action)
+{
+ if (action->reserved[0] || action->reserved[1])
+ return -EINVAL;
+
+ switch (action->type) {
+ case PIDFD_SPAWN_ACTION_DUP2:
+ if (action->flags)
+ return -EINVAL;
+ return 0;
+ case PIDFD_SPAWN_ACTION_FCHDIR:
+ if (action->flags || action->newfd)
+ return -EINVAL;
+ return 0;
+ case PIDFD_SPAWN_ACTION_CLOSE_RANGE:
+ if (action->newfd < action->fd)
+ return -EINVAL;
+ if (action->flags & ~(CLOSE_RANGE_UNSHARE |
+ CLOSE_RANGE_CLOEXEC))
+ return -EINVAL;
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int pidfd_spawn_copy_actions(struct pidfd_spawn_action **actions,
+ __u64 uactions, unsigned int nr_actions,
+ unsigned int action_size)
+{
+ const char __user *uptr;
+ struct pidfd_spawn_action *kactions __free(kvfree) = NULL;
+ unsigned int i;
+ int ret;
+
+ if (!nr_actions)
+ return 0;
+
+ BUILD_BUG_ON(sizeof(struct pidfd_spawn_action) !=
+ PIDFD_SPAWN_ACTION_SIZE_VER0);
+
+ uptr = pidfd_spawn_user_ptr(uactions);
+ kactions = kvmalloc_array(nr_actions, sizeof(*kactions),
+ GFP_KERNEL_ACCOUNT);
+ if (!kactions)
+ return -ENOMEM;
+
+ for (i = 0; i < nr_actions; i++) {
+ ret = copy_struct_from_user(&kactions[i], sizeof(kactions[i]),
+ uptr + (size_t)i * action_size,
+ action_size);
+ if (ret)
+ return ret;
+
+ ret = pidfd_spawn_validate_action(&kactions[i]);
+ if (ret)
+ return ret;
+ }
+
+ *actions = no_free_ptr(kactions);
+ return 0;
+}
+
+static int pidfd_spawn_do_dup2(unsigned int oldfd, unsigned int newfd)
+{
+ struct file *file __free(fput) = NULL;
+
+ file = fget_raw(oldfd);
+ if (!file)
+ return -EBADF;
+ if (oldfd == newfd) {
+ set_close_on_exec(newfd, 0);
+ return 0;
+ }
+
+ return replace_fd(newfd, file, 0);
+}
+
+static int pidfd_spawn_apply_actions(struct pidfd_spawn_state *state)
+{
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < state->nr_actions; i++) {
+ const struct pidfd_spawn_action *action = &state->actions[i];
+
+ switch (action->type) {
+ case PIDFD_SPAWN_ACTION_DUP2:
+ ret = pidfd_spawn_do_dup2(action->fd, action->newfd);
+ break;
+ case PIDFD_SPAWN_ACTION_FCHDIR:
+ ret = do_fchdir(action->fd);
+ break;
+ case PIDFD_SPAWN_ACTION_CLOSE_RANGE:
+ ret = do_close_range(action->fd, action->newfd,
+ action->flags);
+ break;
+ default:
+ ret = -EOPNOTSUPP;
+ break;
+ }
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
static void pidfd_spawn_save_audit_context(struct pidfd_spawn_state *state)
{
struct pt_regs *regs = current_pt_regs();
@@ -543,24 +667,32 @@ static void pidfd_spawn_child(struct callback_head *work)
struct pidfd_spawn_state *state =
container_of(work, struct pidfd_spawn_state, task_work);
struct filename *filename;
+ bool cancelled;
int fatal_sig;
int ret;
pidfd_spawn_audit_entry(state);
+ cancelled = pidfd_spawn_child_cancelled(state);
+ fatal_sig = 0;
+ ret = 0;
+ if (!cancelled) {
+ fatal_sig = pidfd_spawn_pending_fatal_signal();
+ if (!fatal_sig)
+ ret = pidfd_spawn_apply_actions(state);
+ }
+
+ /* FCHDIR must establish the cwd captured by audit_getname(). */
filename = complete_getname(&state->filename);
- if (IS_ERR(filename))
+ if (!ret && !cancelled && !fatal_sig && IS_ERR(filename))
ret = PTR_ERR(filename);
- else
- ret = 0;
- if (pidfd_spawn_child_cancelled(state)) {
+ if (cancelled) {
ret = -ECANCELED;
pidfd_spawn_finish_child(state, filename, ret);
audit_pidfd_spawn_exit(0, ret);
pidfd_spawn_state_put(state);
do_group_exit(SIGKILL);
}
- fatal_sig = pidfd_spawn_pending_fatal_signal();
if (fatal_sig)
pidfd_spawn_deliver_fatal_signal(state, filename, fatal_sig);
@@ -589,6 +721,7 @@ static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
struct pidfd_spawn_run_args __user *uargs,
size_t usize)
{
+ size_t actions_size;
int ret;
BUILD_BUG_ON(sizeof(struct pidfd_spawn_run_args) !=
@@ -606,8 +739,17 @@ static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
return -EINVAL;
if (!kargs->argv)
return -EINVAL;
- if (kargs->nr_actions || kargs->actions || kargs->action_size)
+ if (kargs->nr_actions) {
+ if (!kargs->actions ||
+ kargs->action_size < PIDFD_SPAWN_ACTION_SIZE_VER0 ||
+ !IS_ALIGNED(kargs->action_size, sizeof(__u64)))
+ return -EINVAL;
+ } else if (kargs->actions || kargs->action_size) {
return -EINVAL;
+ }
+ actions_size = array_size(kargs->nr_actions, kargs->action_size);
+ if (actions_size > PIDFD_SPAWN_MAX_ACTIONS_SIZE)
+ return -E2BIG;
if (!pidfd_spawn_user_ptr_fits(kargs->path) ||
!pidfd_spawn_user_ptr_fits(kargs->argv) ||
!pidfd_spawn_user_ptr_fits(kargs->envp) ||
@@ -713,10 +855,12 @@ static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
static int pidfd_spawn_start(struct file *file,
struct pidfd_spawn_state *state,
- const struct pidfd_spawn_run_args *kargs)
+ const struct pidfd_spawn_run_args *kargs,
+ struct pidfd_spawn_action **actions)
{
struct delayed_filename filename
__free(pidfd_spawn_dismiss_filename) = {};
+ struct pidfd_spawn_action *drop_actions __free(kvfree) = NULL;
struct delayed_filename drop_filename
__free(pidfd_spawn_dismiss_filename) = {};
struct kernel_clone_args clone_args = {
@@ -757,8 +901,11 @@ static int pidfd_spawn_start(struct file *file,
state->argv = pidfd_spawn_user_arg(kargs->argv);
state->envp = pidfd_spawn_user_arg(kargs->envp);
+ state->actions = *actions;
+ state->nr_actions = kargs->nr_actions;
pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
pidfd_spawn_save_audit_context(state);
+ *actions = NULL;
reinit_completion(&state->done);
task = pidfd_spawn_create_task(file, state, &clone_args);
@@ -766,6 +913,9 @@ static int pidfd_spawn_start(struct file *file,
ret = PTR_ERR(task);
drop_filename = state->filename;
INIT_DELAYED_FILENAME(&state->filename);
+ drop_actions = state->actions;
+ state->actions = NULL;
+ state->nr_actions = 0;
state->argv = native_arg(NULL);
state->envp = native_arg(NULL);
memset(state->audit_args, 0,
@@ -854,6 +1004,7 @@ int pidfd_empty_open(unsigned int flags)
SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
{
+ struct pidfd_spawn_action *actions __free(kvfree) = NULL;
struct pidfd_spawn_run_args kargs;
struct pidfd_spawn_state *state __free(pidfd_spawn_state) = NULL;
int ret;
@@ -869,6 +1020,10 @@ SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
state = pidfd_spawn_state_get_file(fd_file(f));
if (IS_ERR(state))
return PTR_ERR(state);
+ ret = pidfd_spawn_copy_actions(&actions, kargs.actions,
+ kargs.nr_actions, kargs.action_size);
+ if (ret)
+ return ret;
scoped_cond_guard(mutex_intr, return -ERESTARTNOINTR,
¤t->signal->cred_guard_mutex) {
@@ -877,7 +1032,7 @@ SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
ret = pidfd_spawn_check_startable(state);
if (ret)
return ret;
- ret = pidfd_spawn_start(fd_file(f), state, &kargs);
+ ret = pidfd_spawn_start(fd_file(f), state, &kargs, &actions);
}
if (!ret)
ret = pid_vnr(state->pid);
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 21/24] pidfd: consume spawn builders on the first run attempt
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (19 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 20/24] pidfd: add initial spawn file actions Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 22/24] pidfd: expose spawn builder system calls Li Chen
` (2 subsequent siblings)
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
A pre-task run failure currently returns the builder to CONFIGURING. A
second thread can then publish another invocation before the first
caller probes the pidfd, making the observed process state ambiguous.
Claim the builder after type, authority, and ptrace checks but before
run argument access. Any later validation, uaccess, allocation, or
task-creation failure leaves a terminal taskless builder. Later config
and run operations return EBUSY, while process-dependent pidfd
operations return ESRCH.
Normalize every internal restart error after the claim to EINTR so the
architecture cannot restart a consumed invocation and replace its
result with EBUSY. Preserve normal restart behavior before the claim.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfd_spawn.c | 164 +++++++++++++++++++++++++++++++----------------
1 file changed, 109 insertions(+), 55 deletions(-)
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 3e46a12c0ba07..1309fe99ec091 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -73,6 +73,7 @@ DEFINE_FREE(pidfd_spawn_dismiss_filename, struct delayed_filename,
enum pidfd_spawn_status {
PIDFD_SPAWN_CONFIGURING,
PIDFD_SPAWN_STARTING,
+ PIDFD_SPAWN_FAILED_TASKLESS,
PIDFD_SPAWN_SETUP_DONE,
PIDFD_SPAWN_STARTED,
PIDFD_SPAWN_CANCELLED,
@@ -100,6 +101,14 @@ struct pidfd_spawn_state {
enum pidfd_spawn_status status;
};
+struct pidfd_spawn_resources {
+ struct pidfd_spawn_action *actions;
+ struct mm_struct *creator_mm;
+ const struct cred *creator_cred;
+ struct pid_namespace *creator_pid_ns;
+ char *staged_path;
+};
+
static struct pid *
pidfd_spawn_load_pid(const struct pidfd_spawn_state *state)
{
@@ -183,8 +192,16 @@ static struct pid *pidfd_spawn_file_pid(void *data)
return pid ? pid : ERR_PTR(-ESRCH);
}
+static bool pidfd_spawn_file_terminal(void *data)
+{
+ struct pidfd_spawn_state *state = data;
+
+ return pidfd_spawn_get_status(state) == PIDFD_SPAWN_FAILED_TASKLESS;
+}
+
static const struct pidfs_future_file_ops pidfd_spawn_future_ops = {
.get_pid = pidfd_spawn_file_pid,
+ .is_terminal = pidfd_spawn_file_terminal,
.release = pidfd_spawn_state_release,
};
@@ -199,31 +216,47 @@ pidfd_spawn_state_get_file(struct file *file)
return state;
}
+static void
+pidfd_spawn_detach_resources(struct pidfd_spawn_state *state,
+ struct pidfd_spawn_resources *resources)
+{
+ resources->actions = state->actions;
+ resources->creator_mm = state->creator_mm;
+ resources->creator_cred = state->creator_cred;
+ resources->creator_pid_ns = state->creator_pid_ns;
+ resources->staged_path = state->staged_path;
+ state->actions = NULL;
+ state->creator_mm = NULL;
+ state->creator_cred = NULL;
+ state->creator_pid_ns = NULL;
+ state->staged_path = NULL;
+ state->nr_actions = 0;
+}
+
+static void
+pidfd_spawn_put_resources(struct pidfd_spawn_resources *resources)
+{
+ if (resources->creator_mm)
+ mmdrop(resources->creator_mm);
+ if (resources->creator_cred)
+ put_cred(resources->creator_cred);
+ if (resources->creator_pid_ns)
+ put_pid_ns(resources->creator_pid_ns);
+ kfree(resources->staged_path);
+ kvfree(resources->actions);
+}
+
static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
struct filename *filename, int result)
{
- const struct cred *creator_cred;
- struct pidfd_spawn_action *actions;
- struct mm_struct *creator_mm;
- struct pid_namespace *creator_pid_ns;
- char *staged_path;
+ struct pidfd_spawn_resources resources = {};
/*
* Run data is one-shot. Detach it under the lock, then drop refs
* after the child no longer needs the shared setup payload.
*/
mutex_lock(&state->lock);
- actions = state->actions;
- creator_mm = state->creator_mm;
- creator_cred = state->creator_cred;
- creator_pid_ns = state->creator_pid_ns;
- staged_path = state->staged_path;
- state->actions = NULL;
- state->creator_mm = NULL;
- state->creator_cred = NULL;
- state->creator_pid_ns = NULL;
- state->staged_path = NULL;
- state->nr_actions = 0;
+ pidfd_spawn_detach_resources(state, &resources);
state->argv = native_arg(NULL);
state->envp = native_arg(NULL);
memset(state->audit_args, 0, sizeof(state->audit_args));
@@ -235,14 +268,7 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
mutex_unlock(&state->lock);
putname(filename);
- if (creator_mm)
- mmdrop(creator_mm);
- if (creator_cred)
- put_cred(creator_cred);
- if (creator_pid_ns)
- put_pid_ns(creator_pid_ns);
- kfree(staged_path);
- kvfree(actions);
+ pidfd_spawn_put_resources(&resources);
}
static bool pidfd_spawn_cancel(struct pidfd_spawn_state *state,
@@ -291,16 +317,44 @@ static bool pidfd_spawn_same_creator(struct pidfd_spawn_state *state)
pidfd_spawn_same_pid_ns(state);
}
-static int pidfd_spawn_check_startable(struct pidfd_spawn_state *state)
+static int pidfd_spawn_claim(struct pidfd_spawn_state *state)
{
int ret;
- scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+ scoped_guard(mutex, &state->lock) {
ret = pidfd_spawn_configuring_error(state);
- if (!ret && !pidfd_spawn_same_creator(state))
- ret = -EPERM;
+ if (ret)
+ return ret;
+ if (!pidfd_spawn_same_creator(state))
+ return -EPERM;
+
+ pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
}
- return ret;
+ return 0;
+}
+
+static void pidfd_spawn_fail_taskless(struct file *file,
+ struct pidfd_spawn_state *state, int result)
+{
+ struct pidfd_spawn_resources resources = {};
+
+ mutex_lock(&state->lock);
+ if (WARN_ON_ONCE(pidfd_spawn_get_status(state) !=
+ PIDFD_SPAWN_STARTING || pidfd_spawn_load_pid(state))) {
+ mutex_unlock(&state->lock);
+ return;
+ }
+ state->result = result;
+ pidfd_spawn_detach_resources(state, &resources);
+ state->argv = native_arg(NULL);
+ state->envp = native_arg(NULL);
+ memset(state->audit_args, 0, sizeof(state->audit_args));
+ state->audit_syscall = 0;
+ pidfd_spawn_set_status(state, PIDFD_SPAWN_FAILED_TASKLESS);
+ mutex_unlock(&state->lock);
+
+ pidfd_spawn_put_resources(&resources);
+ pidfs_future_file_notify(file);
}
static struct filename *pidfd_spawn_get_path(const char __user *value)
@@ -718,7 +772,7 @@ static void pidfd_spawn_child(struct callback_head *work)
}
static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
- struct pidfd_spawn_run_args __user *uargs,
+ const struct pidfd_spawn_run_args __user *uargs,
size_t usize)
{
size_t actions_size;
@@ -873,6 +927,13 @@ static int pidfd_spawn_start(struct file *file,
struct task_struct *task;
int ret;
+ scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+ if (pidfd_spawn_get_status(state) != PIDFD_SPAWN_STARTING)
+ return -EBUSY;
+ if (!!state->staged_path == !!kargs->path)
+ return -EINVAL;
+ }
+
if (kargs->path) {
ret = pidfd_spawn_get_delayed_path(&filename,
pidfd_spawn_user_ptr(kargs->path));
@@ -881,15 +942,8 @@ static int pidfd_spawn_start(struct file *file,
}
scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
- ret = pidfd_spawn_configuring_error(state);
- if (ret)
- return ret;
- if (!pidfd_spawn_same_creator(state))
- return -EPERM;
- if (state->staged_path && kargs->path)
- return -EINVAL;
- if (!state->staged_path && !kargs->path)
- return -EINVAL;
+ if (pidfd_spawn_get_status(state) != PIDFD_SPAWN_STARTING)
+ return -EBUSY;
if (state->staged_path) {
ret = pidfd_spawn_get_delayed_kernel_path(&filename,
state->staged_path);
@@ -903,7 +957,6 @@ static int pidfd_spawn_start(struct file *file,
state->envp = pidfd_spawn_user_arg(kargs->envp);
state->actions = *actions;
state->nr_actions = kargs->nr_actions;
- pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
pidfd_spawn_save_audit_context(state);
*actions = NULL;
reinit_completion(&state->done);
@@ -921,9 +974,6 @@ static int pidfd_spawn_start(struct file *file,
memset(state->audit_args, 0,
sizeof(state->audit_args));
state->audit_syscall = 0;
- state->result = 0;
- pidfd_spawn_set_status(state,
- PIDFD_SPAWN_CONFIGURING);
return ret;
}
@@ -1002,17 +1052,13 @@ int pidfd_empty_open(unsigned int flags)
}
SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
- struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
+ const struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
{
struct pidfd_spawn_action *actions __free(kvfree) = NULL;
struct pidfd_spawn_run_args kargs;
struct pidfd_spawn_state *state __free(pidfd_spawn_state) = NULL;
int ret;
- ret = pidfd_spawn_copy_run_args(&kargs, uargs, usize);
- if (ret)
- return ret;
-
CLASS(fd, f)(fd);
if (fd_empty(f))
return -EBADF;
@@ -1020,19 +1066,27 @@ SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
state = pidfd_spawn_state_get_file(fd_file(f));
if (IS_ERR(state))
return PTR_ERR(state);
- ret = pidfd_spawn_copy_actions(&actions, kargs.actions,
- kargs.nr_actions, kargs.action_size);
- if (ret)
- return ret;
-
scoped_cond_guard(mutex_intr, return -ERESTARTNOINTR,
¤t->signal->cred_guard_mutex) {
if (READ_ONCE(current->ptrace))
return -EPERM;
- ret = pidfd_spawn_check_startable(state);
+ ret = pidfd_spawn_claim(state);
if (ret)
return ret;
- ret = pidfd_spawn_start(fd_file(f), state, &kargs, &actions);
+
+ ret = pidfd_spawn_copy_run_args(&kargs, uargs, usize);
+ if (!ret)
+ ret = pidfd_spawn_copy_actions(&actions, kargs.actions,
+ kargs.nr_actions,
+ kargs.action_size);
+ if (!ret)
+ ret = pidfd_spawn_start(fd_file(f), state, &kargs,
+ &actions);
+ if (ret) {
+ ret = pidfd_spawn_normalize_result(ret);
+ if (!pidfd_spawn_load_pid(state))
+ pidfd_spawn_fail_taskless(fd_file(f), state, ret);
+ }
}
if (!ret)
ret = pid_vnr(state->pid);
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 22/24] pidfd: expose spawn builder system calls
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (20 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 21/24] pidfd: consume spawn builders on the first run attempt Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 23/24] selftests/pidfd: cover pidfd spawn builders Li Chen
2026-07-16 14:31 ` [RFC PATCH 24/24] Documentation: describe " Li Chen
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
The preceding commits define the future-pidfd lifetime, configuration, and
run state machine without making them reachable from userspace. Each
preparatory step therefore remains bisectable without exposing a partial
process builder.
Make pidfd_open(0, PIDFD_EMPTY) create a taskless future pidfd and expose
pidfd_config() and pidfd_spawn_run(). Add the syscall declarations and
table entries together so the complete initial interface becomes reachable
at one commit boundary. Synchronize the lagging arm64 AArch32 perf mirror
through the current syscall table while adding the builder entries.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
arch/alpha/kernel/syscalls/syscall.tbl | 2 ++
arch/arm/tools/syscall.tbl | 2 ++
arch/arm64/tools/syscall_32.tbl | 2 ++
arch/m68k/kernel/syscalls/syscall.tbl | 2 ++
arch/microblaze/kernel/syscalls/syscall.tbl | 2 ++
arch/mips/kernel/syscalls/syscall_n32.tbl | 2 ++
arch/mips/kernel/syscalls/syscall_n64.tbl | 2 ++
arch/mips/kernel/syscalls/syscall_o32.tbl | 2 ++
arch/parisc/kernel/syscalls/syscall.tbl | 2 ++
arch/powerpc/kernel/syscalls/syscall.tbl | 2 ++
arch/s390/kernel/syscalls/syscall.tbl | 2 ++
arch/sh/kernel/syscalls/syscall.tbl | 2 ++
arch/sparc/kernel/syscalls/syscall.tbl | 2 ++
arch/x86/entry/syscalls/syscall_32.tbl | 2 ++
arch/x86/entry/syscalls/syscall_64.tbl | 2 ++
arch/xtensa/kernel/syscalls/syscall.tbl | 2 ++
include/linux/syscalls.h | 7 +++++++
include/uapi/asm-generic/unistd.h | 8 +++++++-
kernel/pid.c | 19 ++++++++++++++++++-
scripts/syscall.tbl | 2 ++
tools/include/uapi/asm-generic/unistd.h | 8 +++++++-
.../arch/alpha/entry/syscalls/syscall.tbl | 2 ++
.../perf/arch/arm/entry/syscalls/syscall.tbl | 2 ++
.../arch/arm64/entry/syscalls/syscall_32.tbl | 11 +++++++++++
.../arch/mips/entry/syscalls/syscall_n64.tbl | 2 ++
.../arch/parisc/entry/syscalls/syscall.tbl | 2 ++
.../arch/powerpc/entry/syscalls/syscall.tbl | 2 ++
.../perf/arch/s390/entry/syscalls/syscall.tbl | 2 ++
tools/perf/arch/sh/entry/syscalls/syscall.tbl | 2 ++
.../arch/sparc/entry/syscalls/syscall.tbl | 2 ++
.../arch/x86/entry/syscalls/syscall_32.tbl | 2 ++
.../arch/x86/entry/syscalls/syscall_64.tbl | 2 ++
.../arch/xtensa/entry/syscalls/syscall.tbl | 2 ++
tools/scripts/syscall.tbl | 2 ++
34 files changed, 108 insertions(+), 3 deletions(-)
diff --git a/arch/alpha/kernel/syscalls/syscall.tbl b/arch/alpha/kernel/syscalls/syscall.tbl
index f31b7afffc345..8dd0b22a407c5 100644
--- a/arch/alpha/kernel/syscalls/syscall.tbl
+++ b/arch/alpha/kernel/syscalls/syscall.tbl
@@ -511,3 +511,5 @@
579 common file_setattr sys_file_setattr
580 common listns sys_listns
581 common rseq_slice_yield sys_rseq_slice_yield
+582 common pidfd_config sys_pidfd_config
+583 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/arm/tools/syscall.tbl b/arch/arm/tools/syscall.tbl
index 94351e22bfcf7..9830c4600fc78 100644
--- a/arch/arm/tools/syscall.tbl
+++ b/arch/arm/tools/syscall.tbl
@@ -486,3 +486,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/arm64/tools/syscall_32.tbl b/arch/arm64/tools/syscall_32.tbl
index 62d93d88e0fef..9c3281b3612c2 100644
--- a/arch/arm64/tools/syscall_32.tbl
+++ b/arch/arm64/tools/syscall_32.tbl
@@ -483,3 +483,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/m68k/kernel/syscalls/syscall.tbl b/arch/m68k/kernel/syscalls/syscall.tbl
index 2489342571014..2db719475e061 100644
--- a/arch/m68k/kernel/syscalls/syscall.tbl
+++ b/arch/m68k/kernel/syscalls/syscall.tbl
@@ -471,3 +471,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/microblaze/kernel/syscalls/syscall.tbl b/arch/microblaze/kernel/syscalls/syscall.tbl
index 223d263036272..41ead93dba5c4 100644
--- a/arch/microblaze/kernel/syscalls/syscall.tbl
+++ b/arch/microblaze/kernel/syscalls/syscall.tbl
@@ -477,3 +477,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/mips/kernel/syscalls/syscall_n32.tbl b/arch/mips/kernel/syscalls/syscall_n32.tbl
index 7430714e2b8f8..b6bfca9ddedc6 100644
--- a/arch/mips/kernel/syscalls/syscall_n32.tbl
+++ b/arch/mips/kernel/syscalls/syscall_n32.tbl
@@ -410,3 +410,5 @@
469 n32 file_setattr sys_file_setattr
470 n32 listns sys_listns
471 n32 rseq_slice_yield sys_rseq_slice_yield
+472 n32 pidfd_config sys_pidfd_config
+473 n32 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/mips/kernel/syscalls/syscall_n64.tbl b/arch/mips/kernel/syscalls/syscall_n64.tbl
index 630aab9e54259..229a1a5bff0e9 100644
--- a/arch/mips/kernel/syscalls/syscall_n64.tbl
+++ b/arch/mips/kernel/syscalls/syscall_n64.tbl
@@ -386,3 +386,5 @@
469 n64 file_setattr sys_file_setattr
470 n64 listns sys_listns
471 n64 rseq_slice_yield sys_rseq_slice_yield
+472 n64 pidfd_config sys_pidfd_config
+473 n64 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/mips/kernel/syscalls/syscall_o32.tbl b/arch/mips/kernel/syscalls/syscall_o32.tbl
index 128653112284b..4ee804186e399 100644
--- a/arch/mips/kernel/syscalls/syscall_o32.tbl
+++ b/arch/mips/kernel/syscalls/syscall_o32.tbl
@@ -459,3 +459,5 @@
469 o32 file_setattr sys_file_setattr
470 o32 listns sys_listns
471 o32 rseq_slice_yield sys_rseq_slice_yield
+472 o32 pidfd_config sys_pidfd_config
+473 o32 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/parisc/kernel/syscalls/syscall.tbl b/arch/parisc/kernel/syscalls/syscall.tbl
index c6331dad94613..ff6760df2dc51 100644
--- a/arch/parisc/kernel/syscalls/syscall.tbl
+++ b/arch/parisc/kernel/syscalls/syscall.tbl
@@ -470,3 +470,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/powerpc/kernel/syscalls/syscall.tbl b/arch/powerpc/kernel/syscalls/syscall.tbl
index 4fcc7c58a105d..021b334a8e1a0 100644
--- a/arch/powerpc/kernel/syscalls/syscall.tbl
+++ b/arch/powerpc/kernel/syscalls/syscall.tbl
@@ -562,3 +562,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 nospu rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/s390/kernel/syscalls/syscall.tbl b/arch/s390/kernel/syscalls/syscall.tbl
index 09a7ef04d9791..eea88f94a7624 100644
--- a/arch/s390/kernel/syscalls/syscall.tbl
+++ b/arch/s390/kernel/syscalls/syscall.tbl
@@ -398,3 +398,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/sh/kernel/syscalls/syscall.tbl b/arch/sh/kernel/syscalls/syscall.tbl
index 70b315cbe710c..37166286c4045 100644
--- a/arch/sh/kernel/syscalls/syscall.tbl
+++ b/arch/sh/kernel/syscalls/syscall.tbl
@@ -475,3 +475,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/sparc/kernel/syscalls/syscall.tbl b/arch/sparc/kernel/syscalls/syscall.tbl
index 7e71bf7fcd14f..70f2165b9cfb3 100644
--- a/arch/sparc/kernel/syscalls/syscall.tbl
+++ b/arch/sparc/kernel/syscalls/syscall.tbl
@@ -517,3 +517,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/x86/entry/syscalls/syscall_32.tbl b/arch/x86/entry/syscalls/syscall_32.tbl
index f832ebd2d79b0..b2eeddbe99be9 100644
--- a/arch/x86/entry/syscalls/syscall_32.tbl
+++ b/arch/x86/entry/syscalls/syscall_32.tbl
@@ -477,3 +477,5 @@
469 i386 file_setattr sys_file_setattr
470 i386 listns sys_listns
471 i386 rseq_slice_yield sys_rseq_slice_yield
+472 i386 pidfd_config sys_pidfd_config
+473 i386 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da1..f0952f34c10a5 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -396,6 +396,8 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/arch/xtensa/kernel/syscalls/syscall.tbl b/arch/xtensa/kernel/syscalls/syscall.tbl
index a9bca4e484dec..45dd80ab94cf9 100644
--- a/arch/xtensa/kernel/syscalls/syscall.tbl
+++ b/arch/xtensa/kernel/syscalls/syscall.tbl
@@ -442,3 +442,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h
index 874d9067a43b5..ec90454d01214 100644
--- a/include/linux/syscalls.h
+++ b/include/linux/syscalls.h
@@ -79,6 +79,7 @@ struct mnt_id_req;
struct ns_id_req;
struct xattr_args;
struct file_attr;
+struct pidfd_spawn_run_args;
#include <linux/types.h>
#include <linux/aio_abi.h>
@@ -908,6 +909,12 @@ asmlinkage long sys_clock_adjtime32(clockid_t which_clock,
asmlinkage long sys_syncfs(int fd);
asmlinkage long sys_setns(int fd, int nstype);
asmlinkage long sys_pidfd_open(pid_t pid, unsigned int flags);
+asmlinkage long sys_pidfd_config(int fd, unsigned int cmd,
+ const char __user *key,
+ const void __user *value, int aux);
+asmlinkage long sys_pidfd_spawn_run(int fd,
+ const struct pidfd_spawn_run_args __user *uargs,
+ size_t usize);
asmlinkage long sys_sendmmsg(int fd, struct mmsghdr __user *msg,
unsigned int vlen, unsigned flags);
asmlinkage long sys_process_vm_readv(pid_t pid,
diff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h
index a627acc8fb5fe..9ab8ae5100ec1 100644
--- a/include/uapi/asm-generic/unistd.h
+++ b/include/uapi/asm-generic/unistd.h
@@ -863,8 +863,14 @@ __SYSCALL(__NR_listns, sys_listns)
#define __NR_rseq_slice_yield 471
__SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
+#define __NR_pidfd_config 472
+__SYSCALL(__NR_pidfd_config, sys_pidfd_config)
+
+#define __NR_pidfd_spawn_run 473
+__SYSCALL(__NR_pidfd_spawn_run, sys_pidfd_spawn_run)
+
#undef __NR_syscalls
-#define __NR_syscalls 472
+#define __NR_syscalls 474
/*
* 32 bit systems traditionally used different
diff --git a/kernel/pid.c b/kernel/pid.c
index 010f80177cac8..a1d05ce10ac25 100644
--- a/kernel/pid.c
+++ b/kernel/pid.c
@@ -43,6 +43,7 @@
#include <linux/sched/task.h>
#include <linux/idr.h>
#include <linux/pidfs.h>
+#include <linux/pidfd_spawn.h>
#include <net/sock.h>
#include <uapi/linux/pidfd.h>
@@ -715,10 +716,26 @@ SYSCALL_DEFINE2(pidfd_open, pid_t, pid, unsigned int, flags)
int fd;
struct pid *p;
+ /*
+ * pidfd_open(0, PIDFD_EMPTY) is the spawn-builder entry point.
+ * It creates a future pidfd instead of looking up pid 0.
+ */
+ if (pid == 0) {
+ if (!(flags & PIDFD_EMPTY))
+ return -EINVAL;
+ if (flags & ~(PIDFD_EMPTY | PIDFD_NONBLOCK))
+ return -EINVAL;
+
+ return pidfd_empty_open(flags & PIDFD_NONBLOCK);
+ }
+
+ /* PIDFD_EMPTY is only meaningful for the pid == 0 builder form. */
+ if (flags & PIDFD_EMPTY)
+ return -EINVAL;
if (flags & ~(PIDFD_NONBLOCK | PIDFD_THREAD))
return -EINVAL;
- if (pid <= 0)
+ if (pid < 0)
return -EINVAL;
p = find_get_pid(pid);
diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl
index 7a42b32b65776..29248f0ac1a07 100644
--- a/scripts/syscall.tbl
+++ b/scripts/syscall.tbl
@@ -412,3 +412,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/include/uapi/asm-generic/unistd.h b/tools/include/uapi/asm-generic/unistd.h
index a627acc8fb5fe..9ab8ae5100ec1 100644
--- a/tools/include/uapi/asm-generic/unistd.h
+++ b/tools/include/uapi/asm-generic/unistd.h
@@ -863,8 +863,14 @@ __SYSCALL(__NR_listns, sys_listns)
#define __NR_rseq_slice_yield 471
__SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
+#define __NR_pidfd_config 472
+__SYSCALL(__NR_pidfd_config, sys_pidfd_config)
+
+#define __NR_pidfd_spawn_run 473
+__SYSCALL(__NR_pidfd_spawn_run, sys_pidfd_spawn_run)
+
#undef __NR_syscalls
-#define __NR_syscalls 472
+#define __NR_syscalls 474
/*
* 32 bit systems traditionally used different
diff --git a/tools/perf/arch/alpha/entry/syscalls/syscall.tbl b/tools/perf/arch/alpha/entry/syscalls/syscall.tbl
index 74720667fe091..a20f81b950349 100644
--- a/tools/perf/arch/alpha/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/alpha/entry/syscalls/syscall.tbl
@@ -502,3 +502,5 @@
570 common lsm_set_self_attr sys_lsm_set_self_attr
571 common lsm_list_modules sys_lsm_list_modules
572 common mseal sys_mseal
+582 common pidfd_config sys_pidfd_config
+583 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/arm/entry/syscalls/syscall.tbl b/tools/perf/arch/arm/entry/syscalls/syscall.tbl
index 94351e22bfcf7..9830c4600fc78 100644
--- a/tools/perf/arch/arm/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/arm/entry/syscalls/syscall.tbl
@@ -486,3 +486,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl b/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl
index 9a37930d4e26f..9c3281b3612c2 100644
--- a/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl
+++ b/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl
@@ -474,3 +474,14 @@
460 common lsm_set_self_attr sys_lsm_set_self_attr
461 common lsm_list_modules sys_lsm_list_modules
462 common mseal sys_mseal
+463 common setxattrat sys_setxattrat
+464 common getxattrat sys_getxattrat
+465 common listxattrat sys_listxattrat
+466 common removexattrat sys_removexattrat
+467 common open_tree_attr sys_open_tree_attr
+468 common file_getattr sys_file_getattr
+469 common file_setattr sys_file_setattr
+470 common listns sys_listns
+471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl b/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl
index 630aab9e54259..229a1a5bff0e9 100644
--- a/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl
+++ b/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl
@@ -386,3 +386,5 @@
469 n64 file_setattr sys_file_setattr
470 n64 listns sys_listns
471 n64 rseq_slice_yield sys_rseq_slice_yield
+472 n64 pidfd_config sys_pidfd_config
+473 n64 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/parisc/entry/syscalls/syscall.tbl b/tools/perf/arch/parisc/entry/syscalls/syscall.tbl
index 66dc406b12e44..88311785913f8 100644
--- a/tools/perf/arch/parisc/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/parisc/entry/syscalls/syscall.tbl
@@ -461,3 +461,5 @@
460 common lsm_set_self_attr sys_lsm_set_self_attr
461 common lsm_list_modules sys_lsm_list_modules
462 common mseal sys_mseal
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl b/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl
index 4fcc7c58a105d..021b334a8e1a0 100644
--- a/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl
@@ -562,3 +562,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 nospu rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/s390/entry/syscalls/syscall.tbl b/tools/perf/arch/s390/entry/syscalls/syscall.tbl
index 09a7ef04d9791..eea88f94a7624 100644
--- a/tools/perf/arch/s390/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/s390/entry/syscalls/syscall.tbl
@@ -398,3 +398,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/sh/entry/syscalls/syscall.tbl b/tools/perf/arch/sh/entry/syscalls/syscall.tbl
index 70b315cbe710c..37166286c4045 100644
--- a/tools/perf/arch/sh/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/sh/entry/syscalls/syscall.tbl
@@ -475,3 +475,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/sparc/entry/syscalls/syscall.tbl b/tools/perf/arch/sparc/entry/syscalls/syscall.tbl
index 7e71bf7fcd14f..70f2165b9cfb3 100644
--- a/tools/perf/arch/sparc/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/sparc/entry/syscalls/syscall.tbl
@@ -517,3 +517,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl b/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl
index f832ebd2d79b0..b2eeddbe99be9 100644
--- a/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl
+++ b/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl
@@ -477,3 +477,5 @@
469 i386 file_setattr sys_file_setattr
470 i386 listns sys_listns
471 i386 rseq_slice_yield sys_rseq_slice_yield
+472 i386 pidfd_config sys_pidfd_config
+473 i386 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da1..f0952f34c10a5 100644
--- a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
@@ -396,6 +396,8 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl b/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl
index a9bca4e484dec..45dd80ab94cf9 100644
--- a/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl
@@ -442,3 +442,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/scripts/syscall.tbl b/tools/scripts/syscall.tbl
index 7a42b32b65776..29248f0ac1a07 100644
--- a/tools/scripts/syscall.tbl
+++ b/tools/scripts/syscall.tbl
@@ -412,3 +412,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 23/24] selftests/pidfd: cover pidfd spawn builders
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (21 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 22/24] pidfd: expose spawn builder system calls Li Chen
@ 2026-07-16 14:31 ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 24/24] Documentation: describe " Li Chen
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Add comprehensive coverage for the pidfd spawn builder lifecycle and ABI.
Exercise taskless configuration, direct and staged executable paths,
one-shot execution, precise terminal errors, source-state inheritance,
and pidfs identity before and after task publication.
Cover ordered DUP2, CLOSE_RANGE, and FCHDIR actions, including validation,
same-fd FD_CLOEXEC semantics, child-only state, ordered effects, and
failures after publication. Verify run argument input-only behavior,
normal exec failure, status-127 children, and auto-reap metadata.
Use deterministic userfaultfd stalls and bounded monotonic waits to
exercise publication races, competing run claims, cancellation before
and after task creation, coredump suppression, and pre-uaccess claiming.
Verify ptrace, pidfd_getfd, procfs, source credential and PID namespace,
SCM_RIGHTS, RLIMIT_NPROC, and pids cgroup boundaries. Cover Landlock and
seccomp policy behavior, dedicated audit transactions, and compat pointer
containers.
The suite also checks pidfs reopen, bind-mount, xattr, and epoll behavior.
Keep feature-dependent cases skippable and request the required kselftest
configuration.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
tools/testing/selftests/landlock/audit.h | 6 +-
tools/testing/selftests/pidfd/.gitignore | 9 +
tools/testing/selftests/pidfd/Makefile | 25 +-
tools/testing/selftests/pidfd/config | 6 +
.../pidfd/pidfd_spawn_accounting_test.c | 428 ++++++
.../pidfd/pidfd_spawn_actions_test.c | 474 +++++++
.../selftests/pidfd/pidfd_spawn_audit_test.c | 521 +++++++
.../selftests/pidfd/pidfd_spawn_common.c | 512 +++++++
.../selftests/pidfd/pidfd_spawn_common.h | 59 +
.../selftests/pidfd/pidfd_spawn_compat.c | 221 +++
.../selftests/pidfd/pidfd_spawn_exec_test.c | 301 ++++
.../selftests/pidfd/pidfd_spawn_policy_test.c | 294 ++++
.../selftests/pidfd/pidfd_spawn_race_test.c | 923 ++++++++++++
.../pidfd/pidfd_spawn_security_test.c | 1242 +++++++++++++++++
.../selftests/pidfd/pidfd_spawn_test.c | 550 ++++++++
15 files changed, 5568 insertions(+), 3 deletions(-)
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_common.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_common.h
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_compat.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_race_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_security_test.c
create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_test.c
diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h
index f45fdef35681a..9530b8aca6c9f 100644
--- a/tools/testing/selftests/landlock/audit.h
+++ b/tools/testing/selftests/landlock/audit.h
@@ -550,7 +550,8 @@ static int audit_init_filter_exe(struct audit_filter *filter, const char *path)
return 0;
}
-static int audit_cleanup(int audit_fd, struct audit_filter *filter)
+static int __maybe_unused
+audit_cleanup(int audit_fd, struct audit_filter *filter)
{
struct audit_filter new_filter;
int err = 0;
@@ -581,7 +582,8 @@ static int audit_cleanup(int audit_fd, struct audit_filter *filter)
return err;
}
-static int audit_init_with_exe_filter(struct audit_filter *filter)
+static int __maybe_unused
+audit_init_with_exe_filter(struct audit_filter *filter)
{
int fd, err;
diff --git a/tools/testing/selftests/pidfd/.gitignore b/tools/testing/selftests/pidfd/.gitignore
index 4cd8ec7fd349a..72acb3371fadc 100644
--- a/tools/testing/selftests/pidfd/.gitignore
+++ b/tools/testing/selftests/pidfd/.gitignore
@@ -13,3 +13,12 @@ pidfd_exec_helper
pidfd_xattr_test
pidfd_setattr_test
pidfd_autoreap_test
+pidfd_spawn_test
+pidfd_spawn_actions_test
+pidfd_spawn_exec_test
+pidfd_spawn_race_test
+pidfd_spawn_security_test
+pidfd_spawn_accounting_test
+pidfd_spawn_policy_test
+pidfd_spawn_audit_test
+pidfd_spawn_compat
diff --git a/tools/testing/selftests/pidfd/Makefile b/tools/testing/selftests/pidfd/Makefile
index 4211f91e9af85..1d58f76261200 100644
--- a/tools/testing/selftests/pidfd/Makefile
+++ b/tools/testing/selftests/pidfd/Makefile
@@ -1,12 +1,35 @@
# SPDX-License-Identifier: GPL-2.0-only
CFLAGS += -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) -pthread -Wall
+CAN_BUILD_I386 := $(shell ../x86/check_cc.sh "$(CC)" \
+ ../x86/trivial_32bit_program.c -m32 -static)
+
+PIDFD_SPAWN_TESTS := pidfd_spawn_test pidfd_spawn_exec_test \
+ pidfd_spawn_actions_test pidfd_spawn_race_test \
+ pidfd_spawn_security_test pidfd_spawn_accounting_test \
+ pidfd_spawn_policy_test pidfd_spawn_audit_test
+
TEST_GEN_PROGS := pidfd_test pidfd_fdinfo_test pidfd_open_test \
pidfd_poll_test pidfd_wait pidfd_getfd_test pidfd_setns_test \
pidfd_file_handle_test pidfd_bind_mount pidfd_info_test \
- pidfd_xattr_test pidfd_setattr_test pidfd_autoreap_test
+ pidfd_xattr_test pidfd_setattr_test pidfd_autoreap_test \
+ $(PIDFD_SPAWN_TESTS)
+
+ifeq ($(CAN_BUILD_I386),1)
+TEST_GEN_PROGS += pidfd_spawn_compat
+endif
TEST_GEN_PROGS_EXTENDED := pidfd_exec_helper
+LOCAL_HDRS += pidfd_spawn_common.h ../landlock/audit.h
include ../lib.mk
+$(addprefix $(OUTPUT)/,$(PIDFD_SPAWN_TESTS)): \
+ pidfd_spawn_common.c pidfd_spawn_common.h
+
+ifeq ($(CAN_BUILD_I386),1)
+pidfd_spawn_compat: CFLAGS += -m32
+pidfd_spawn_compat: LDLIBS += -static
+$(OUTPUT)/pidfd_spawn_compat: CFLAGS += -m32
+$(OUTPUT)/pidfd_spawn_compat: LDLIBS += -static
+endif
diff --git a/tools/testing/selftests/pidfd/config b/tools/testing/selftests/pidfd/config
index cf7cc0ce02484..699d43aaa4093 100644
--- a/tools/testing/selftests/pidfd/config
+++ b/tools/testing/selftests/pidfd/config
@@ -5,4 +5,10 @@ CONFIG_PID_NS=y
CONFIG_NET_NS=y
CONFIG_TIME_NS=y
CONFIG_CGROUPS=y
+CONFIG_CGROUP_PIDS=y
CONFIG_CHECKPOINT_RESTORE=y
+CONFIG_USERFAULTFD=y
+CONFIG_SECURITY_LANDLOCK=y
+CONFIG_SECCOMP=y
+CONFIG_SECCOMP_FILTER=y
+CONFIG_AUDIT=y
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c
new file mode 100644
index 0000000000000..25fe493a34d44
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c
@@ -0,0 +1,428 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+static int write_string_file(const char *path, const char *value)
+{
+ size_t len = strlen(value);
+ ssize_t ret;
+ int saved_errno;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -1;
+ ret = write(fd, value, len);
+ saved_errno = errno;
+ close(fd);
+ if (ret != (ssize_t)len) {
+ errno = ret < 0 ? saved_errno : EIO;
+ return -1;
+ }
+ return 0;
+}
+
+static int path_join(char **out, const char *dir, const char *name)
+{
+ if (asprintf(out, "%s/%s", dir, name) < 0) {
+ errno = ENOMEM;
+ return -1;
+ }
+ return 0;
+}
+
+static bool controller_list_has(const char *controllers, const char *name)
+{
+ size_t name_len = strlen(name);
+ const char *next = controllers;
+
+ while (next) {
+ const char *end = strchr(next, ',');
+ size_t len = end ? end - next : strlen(next);
+
+ if (len == name_len && !strncmp(next, name, len))
+ return true;
+ next = end ? end + 1 : NULL;
+ }
+ return false;
+}
+
+static int read_current_cgroup(char *buf, size_t size, bool *unified)
+{
+ char line[PATH_MAX];
+ FILE *file;
+
+ file = fopen("/proc/self/cgroup", "re");
+ if (!file)
+ return -1;
+ while (fgets(line, sizeof(line), file)) {
+ char *controllers;
+ char *path;
+ char *separator;
+
+ separator = strchr(line, ':');
+ if (!separator)
+ continue;
+ controllers = separator + 1;
+ separator = strchr(controllers, ':');
+ if (!separator)
+ continue;
+ *separator = '\0';
+ if (*controllers &&
+ !controller_list_has(controllers, "pids"))
+ continue;
+ path = separator + 1;
+ path[strcspn(path, "\n")] = '\0';
+ if (strlen(path) >= size) {
+ fclose(file);
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+ strcpy(buf, path);
+ *unified = !*controllers;
+ fclose(file);
+ return 0;
+ }
+ fclose(file);
+ errno = ENOENT;
+ return -1;
+}
+
+static int current_cgroup_path(char **pathp)
+{
+ char current[PATH_MAX];
+ const char *root;
+ bool unified;
+ int ret;
+
+ ret = read_current_cgroup(current, sizeof(current), &unified);
+ if (ret)
+ return ret;
+ root = unified ? "/sys/fs/cgroup" : "/sys/fs/cgroup/pids";
+ if (!strcmp(current, "/"))
+ ret = asprintf(pathp, "%s", root);
+ else
+ ret = asprintf(pathp, "%s%s", root, current);
+ if (ret < 0) {
+ errno = ENOMEM;
+ return -1;
+ }
+ return 0;
+}
+
+static int enable_pids_controller(const char *cgroup)
+{
+ char *controllers = NULL;
+ char *control = NULL;
+ int ret;
+
+ ret = path_join(&controllers, cgroup, "cgroup.controllers");
+ if (ret)
+ return ret;
+ ret = access(controllers, F_OK);
+ free(controllers);
+ if (ret && errno == ENOENT)
+ return 0;
+ if (ret)
+ return ret;
+ ret = path_join(&control, cgroup, "cgroup.subtree_control");
+ if (ret)
+ return ret;
+ ret = write_string_file(control, "+pids");
+ free(control);
+ return ret;
+}
+
+static int set_cgroup_pids_max(const char *cgroup, const char *value)
+{
+ char *max = NULL;
+ int ret;
+
+ ret = path_join(&max, cgroup, "pids.max");
+ if (ret)
+ return ret;
+ ret = write_string_file(max, value);
+ free(max);
+ return ret;
+}
+
+static int enter_cgroup(const char *cgroup)
+{
+ char pid[32];
+ char *procs = NULL;
+ int ret;
+
+ ret = snprintf(pid, sizeof(pid), "%d", getpid());
+ if (ret < 0 || ret >= (int)sizeof(pid)) {
+ errno = EINVAL;
+ return -1;
+ }
+ ret = path_join(&procs, cgroup, "cgroup.procs");
+ if (ret)
+ return ret;
+ ret = write_string_file(procs, pid);
+ free(procs);
+ return ret;
+}
+
+static int make_limited_pids_cgroup_at(const char *base, char **parentp,
+ char **leafp)
+{
+ char name[64];
+ char *parent = NULL;
+ char *leaf = NULL;
+ char *max = NULL;
+ bool parent_created = false;
+ bool leaf_created = false;
+ int ret;
+
+ /* The controller may already be enabled for children. */
+ enable_pids_controller(base);
+ snprintf(name, sizeof(name), "pidfd-spawn-pids-%d", getpid());
+ ret = path_join(&parent, base, name);
+ if (ret)
+ goto out;
+ if (mkdir(parent, 0755)) {
+ ret = -1;
+ goto out;
+ }
+ parent_created = true;
+ ret = path_join(&max, parent, "pids.max");
+ if (ret)
+ goto out;
+ ret = access(max, W_OK);
+ free(max);
+ max = NULL;
+ if (ret)
+ goto out;
+ ret = enable_pids_controller(parent);
+ if (ret)
+ goto out;
+ ret = path_join(&leaf, parent, "leaf");
+ if (ret)
+ goto out;
+ if (mkdir(leaf, 0755)) {
+ ret = -1;
+ goto out;
+ }
+ leaf_created = true;
+ ret = set_cgroup_pids_max(leaf, "1");
+ if (ret)
+ goto out;
+
+ *parentp = parent;
+ *leafp = leaf;
+ parent_created = false;
+ leaf_created = false;
+ parent = NULL;
+ leaf = NULL;
+out:
+ if (leaf_created)
+ rmdir(leaf);
+ if (parent_created)
+ rmdir(parent);
+ free(max);
+ free(leaf);
+ free(parent);
+ return ret;
+}
+
+static int make_limited_pids_cgroup(char **parentp, char **leafp)
+{
+ char *base = NULL;
+ int saved_errno;
+ int ret;
+
+ ret = current_cgroup_path(&base);
+ if (ret)
+ return ret;
+ ret = make_limited_pids_cgroup_at(base, parentp, leafp);
+ saved_errno = errno;
+ free(base);
+ if (!ret)
+ return 0;
+ ret = make_limited_pids_cgroup_at("/sys/fs/cgroup", parentp, leafp);
+ if (!ret)
+ return 0;
+ errno = saved_errno;
+ return -1;
+}
+
+static int pidfd_spawn_pids_worker(const char *cgroup, const char *path)
+{
+ char * const argv[] = { "pidfd_spawn_accounting_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ int failed_builder;
+ int builder;
+
+ if (enter_cgroup(cgroup))
+ return 1;
+ failed_builder = sys_pidfd_empty_open();
+ if (failed_builder < 0)
+ return 2;
+ errno = 0;
+ if (spawn_run_path(failed_builder, path, argv, NULL, 0) != -1 ||
+ errno != EAGAIN)
+ return 3;
+ errno = 0;
+ if (spawn_run_path(failed_builder, path, argv, NULL, 0) != -1 ||
+ errno != EBUSY)
+ return 4;
+ if (set_cgroup_pids_max(cgroup, "2"))
+ return 5;
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ return 6;
+ if (spawn_run_path(builder, path, argv, NULL, 0))
+ return 7;
+ if (wait_pidfd_exit(builder, 0))
+ return 8;
+ if (close(builder) || close(failed_builder))
+ return 9;
+ return 0;
+}
+
+#define PIDFD_SPAWN_UID_FIRST 60000
+#define PIDFD_SPAWN_UID_LAST 65000
+#define PIDFD_SPAWN_UID_BUSY 78
+
+static int pidfd_spawn_rlimit_worker(const char *path, uid_t uid)
+{
+ char * const argv[] = { "pidfd_spawn_accounting_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct rlimit limit = {
+ .rlim_cur = 1,
+ .rlim_max = 2,
+ };
+ int failed_builder;
+ int builder;
+
+ if (setrlimit(RLIMIT_NPROC, &limit))
+ return 1;
+ if (setgid(uid) || setuid(uid))
+ return 77;
+
+ failed_builder = sys_pidfd_empty_open();
+ if (failed_builder < 0)
+ return 2;
+ errno = 0;
+ if (spawn_run_path(failed_builder, path, argv, NULL, 0) != -1 ||
+ errno != EAGAIN)
+ return 3;
+ errno = 0;
+ if (spawn_run_path(failed_builder, path, argv, NULL, 0) != -1 ||
+ errno != EBUSY)
+ return 4;
+
+ limit.rlim_cur = 2;
+ if (setrlimit(RLIMIT_NPROC, &limit))
+ return 5;
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ return 6;
+ if (spawn_run_path(builder, path, argv, NULL, 0))
+ return errno == EAGAIN ? PIDFD_SPAWN_UID_BUSY : 7;
+ if (wait_pidfd_exit(builder, 0))
+ return 8;
+ if (close(builder) || close(failed_builder))
+ return 9;
+ return 0;
+}
+
+TEST(pidfd_spawn_charges_rlimit_nproc_at_run)
+{
+ const char *path = self_exe_path();
+ pid_t worker;
+ pid_t waited;
+ uid_t uid;
+ int status;
+
+ if (getuid())
+ SKIP(return, "test requires root to select an unused uid");
+ ASSERT_NE(path, NULL);
+ for (uid = PIDFD_SPAWN_UID_FIRST; uid < PIDFD_SPAWN_UID_LAST; uid++) {
+ worker = fork();
+ ASSERT_GE(worker, 0);
+ if (!worker)
+ _exit(pidfd_spawn_rlimit_worker(path, uid));
+ waited = waitpid_timeout(worker, &status,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ ASSERT_EQ(waited, worker);
+ ASSERT_TRUE(WIFEXITED(status));
+ if (WEXITSTATUS(status) == 77)
+ SKIP(return, "changing uid is unavailable");
+ if (WEXITSTATUS(status) == PIDFD_SPAWN_UID_BUSY)
+ continue;
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+ return;
+ }
+ SKIP(return, "no unused uid available for RLIMIT_NPROC");
+}
+
+TEST(pidfd_spawn_charges_pids_cgroup_at_run)
+{
+ const char *path = self_exe_path();
+ char *parent = NULL;
+ char *leaf = NULL;
+ int cleanup_leaf;
+ int cleanup_parent;
+ int saved_errno;
+ pid_t worker;
+ pid_t waited;
+ int status;
+
+ ASSERT_NE(path, NULL);
+ if (make_limited_pids_cgroup(&parent, &leaf)) {
+ saved_errno = errno;
+ SKIP(return, "writable pids cgroup is unavailable: %s",
+ strerror(saved_errno));
+ }
+
+ worker = fork();
+ if (worker < 0) {
+ cleanup_leaf = rmdir(leaf);
+ cleanup_parent = rmdir(parent);
+ free(leaf);
+ free(parent);
+ ASSERT_GE(worker, 0);
+ ASSERT_EQ(cleanup_leaf, 0);
+ ASSERT_EQ(cleanup_parent, 0);
+ }
+ if (!worker)
+ _exit(pidfd_spawn_pids_worker(leaf, path));
+ waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ cleanup_leaf = rmdir(leaf);
+ cleanup_parent = rmdir(parent);
+ free(leaf);
+ free(parent);
+
+ ASSERT_EQ(cleanup_leaf, 0);
+ ASSERT_EQ(cleanup_parent, 0);
+ ASSERT_EQ(waited, worker);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+int main(int argc, char **argv)
+{
+ int ret = helper_main(argc, argv);
+
+ if (ret >= 0)
+ return ret;
+ return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c
new file mode 100644
index 0000000000000..d44873280b6d2
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c
@@ -0,0 +1,474 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/close_range.h>
+#include <linux/pidfd_spawn.h>
+#include <poll.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+static ssize_t read_exact_timeout(int fd, void *buf, size_t size)
+{
+ struct pollfd pfd = {
+ .fd = fd,
+ .events = POLLIN,
+ };
+ size_t offset = 0;
+
+ while (offset < size) {
+ ssize_t len;
+ int ret;
+
+ ret = poll(&pfd, 1, PIDFD_SPAWN_TIMEOUT_MS);
+ if (ret <= 0) {
+ if (!ret)
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ if (pfd.revents & (POLLERR | POLLNVAL)) {
+ errno = EIO;
+ return -1;
+ }
+ len = read(fd, (char *)buf + offset, size - offset);
+ if (len < 0) {
+ if (errno == EINTR)
+ continue;
+ return -1;
+ }
+ if (!len) {
+ errno = EPIPE;
+ return -1;
+ }
+ offset += len;
+ }
+ return offset;
+}
+
+static int run_args_fail_taskless(struct pidfd_spawn_run_args *args,
+ size_t size, int expected_errno)
+{
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ const char *path = self_exe_path();
+ int saved_errno;
+ int ret;
+ int fd;
+
+ if (!path)
+ return -1;
+ fd = sys_pidfd_empty_open();
+ if (fd < 0)
+ return -1;
+ errno = 0;
+ ret = sys_pidfd_spawn_run(fd, args, size);
+ saved_errno = errno;
+ if (ret != -1 || saved_errno != expected_errno)
+ goto fail;
+ errno = 0;
+ if (ioctl(fd, PIDFD_GET_INFO, &info) != -1 || errno != ESRCH)
+ goto fail;
+ errno = 0;
+ if (config_path(fd, path) != -1 || errno != EBUSY)
+ goto fail;
+ return close(fd);
+
+fail:
+ saved_errno = errno;
+ close(fd);
+ errno = saved_errno;
+ return -1;
+}
+
+TEST(pidfd_spawn_run_rejects_bad_actions)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_spawn_action action = {
+ .type = PIDFD_SPAWN_ACTION_DUP2,
+ .flags = 1,
+ };
+ struct pidfd_spawn_run_args args = {
+ .path = ptr_to_u64(path),
+ .argv = ptr_to_u64(argv),
+ .envp = ptr_to_u64(empty_envp),
+ .actions = ptr_to_u64(&action),
+ .nr_actions = 1,
+ .action_size = sizeof(action),
+ };
+
+ ASSERT_NE(path, NULL);
+ ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+ action.flags = 0;
+ action.reserved[0] = 1;
+ ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+ action.reserved[0] = 0;
+ action.reserved[1] = 1;
+ ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+ action.reserved[1] = 0;
+ action.type = UINT32_MAX;
+ ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EOPNOTSUPP), 0);
+
+ action.type = PIDFD_SPAWN_ACTION_CLOSE_RANGE;
+ action.fd = 10;
+ action.newfd = 9;
+ ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+
+ action.type = PIDFD_SPAWN_ACTION_DUP2;
+ action.fd = STDOUT_FILENO;
+ action.newfd = STDOUT_FILENO;
+ args.action_size = PIDFD_SPAWN_ACTION_SIZE_VER0 - sizeof(__u64);
+ ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+ args.action_size = PIDFD_SPAWN_ACTION_SIZE_VER0 + sizeof(__u32);
+ ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+ args.action_size = PIDFD_SPAWN_ACTION_SIZE_VER0;
+ args.nr_actions = 65536 / PIDFD_SPAWN_ACTION_SIZE_VER0 + 1;
+ ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), E2BIG), 0);
+
+ args.actions = 0;
+ args.nr_actions = 0;
+ ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+}
+
+TEST(pidfd_spawn_run_accepts_versioned_action_elements)
+{
+ struct {
+ struct pidfd_spawn_action action;
+ __u64 tail;
+ } extended_action = {
+ .action = {
+ .type = PIDFD_SPAWN_ACTION_DUP2,
+ .fd = STDOUT_FILENO,
+ .newfd = STDOUT_FILENO,
+ },
+ .tail = 1,
+ };
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_spawn_run_args args = {
+ .path = ptr_to_u64(path),
+ .argv = ptr_to_u64(argv),
+ .envp = ptr_to_u64(empty_envp),
+ .actions = ptr_to_u64(&extended_action),
+ .nr_actions = 1,
+ .action_size = sizeof(extended_action),
+ };
+ int child_pid;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), E2BIG), 0);
+
+ extended_action.tail = 0;
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ child_pid = sys_pidfd_spawn_run(fd, &args, sizeof(args));
+ ASSERT_GT(child_pid, 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_action_failure_publishes_waitable_child)
+{
+ struct pidfd_spawn_action action = {
+ .type = PIDFD_SPAWN_ACTION_DUP2,
+ .fd = UINT_MAX,
+ .newfd = STDOUT_FILENO,
+ };
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), -1);
+ ASSERT_EQ(errno, EBADF);
+ ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), 0);
+ ASSERT_GT(info.pid, 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 127), 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), -1);
+ ASSERT_EQ(errno, EBUSY);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_fchdir_rejects_regular_file)
+{
+ struct pidfd_spawn_action action = {
+ .type = PIDFD_SPAWN_ACTION_FCHDIR,
+ };
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ int regular_fd;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ regular_fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(regular_fd, 0);
+ action.fd = regular_fd;
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), -1);
+ ASSERT_EQ(errno, ENOTDIR);
+ ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), 0);
+ ASSERT_GT(info.pid, 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 127), 0);
+ ASSERT_GE(fcntl(regular_fd, F_GETFD), 0);
+ ASSERT_EQ(close(fd), 0);
+ ASSERT_EQ(close(regular_fd), 0);
+}
+
+TEST(pidfd_spawn_dup2_captures_stdout)
+{
+ struct pidfd_spawn_action action = {
+ .type = PIDFD_SPAWN_ACTION_DUP2,
+ .newfd = STDOUT_FILENO,
+ };
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "print", NULL };
+ char buf[13] = {};
+ int pipefd[2];
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ ASSERT_EQ(pipe2(pipefd, O_CLOEXEC), 0);
+ action.fd = pipefd[1];
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), 0);
+ ASSERT_EQ(close(pipefd[1]), 0);
+ ASSERT_EQ(read_exact_timeout(pipefd[0], buf, sizeof(buf) - 1),
+ sizeof(buf) - 1);
+ ASSERT_STREQ(buf, "pidfd-spawn\n");
+ ASSERT_EQ(close(pipefd[0]), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_close_range_closes_only_child_fd)
+{
+ struct pidfd_spawn_action action = {
+ .type = PIDFD_SPAWN_ACTION_CLOSE_RANGE,
+ };
+ const char *path = self_exe_path();
+ char fdarg[32];
+ char * const argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "fd-closed", fdarg,
+ NULL };
+ int devnull;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ devnull = open("/dev/null", O_RDONLY);
+ ASSERT_GE(devnull, 0);
+ ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", devnull), 0);
+ action.fd = devnull;
+ action.newfd = devnull;
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_GE(fcntl(devnull, F_GETFD), 0);
+ ASSERT_EQ(close(fd), 0);
+ ASSERT_EQ(close(devnull), 0);
+}
+
+TEST(pidfd_spawn_dup2_same_fd_clears_child_cloexec)
+{
+ struct pidfd_spawn_action action = {
+ .type = PIDFD_SPAWN_ACTION_DUP2,
+ };
+ const char *path = self_exe_path();
+ char fdarg[32];
+ char * const argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "fd-open", fdarg,
+ NULL };
+ int devnull;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ devnull = open("/dev/null", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(devnull, 0);
+ ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", devnull), 0);
+ action.fd = devnull;
+ action.newfd = devnull;
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_NE(fcntl(devnull, F_GETFD) & FD_CLOEXEC, 0);
+ ASSERT_EQ(close(fd), 0);
+ ASSERT_EQ(close(devnull), 0);
+}
+
+TEST(pidfd_spawn_close_range_cloexec_is_child_local)
+{
+ struct pidfd_spawn_action action = {
+ .type = PIDFD_SPAWN_ACTION_CLOSE_RANGE,
+ .flags = CLOSE_RANGE_CLOEXEC,
+ };
+ const char *path = self_exe_path();
+ char fdarg[32];
+ char * const argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "fd-closed", fdarg,
+ NULL };
+ int devnull;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ devnull = open("/dev/null", O_RDONLY);
+ ASSERT_GE(devnull, 0);
+ ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", devnull), 0);
+ action.fd = devnull;
+ action.newfd = devnull;
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(fcntl(devnull, F_GETFD) & FD_CLOEXEC, 0);
+ ASSERT_EQ(close(fd), 0);
+ ASSERT_EQ(close(devnull), 0);
+}
+
+TEST(pidfd_spawn_fchdir_changes_only_child_cwd)
+{
+ struct pidfd_spawn_action actions[2] = {
+ {
+ .type = PIDFD_SPAWN_ACTION_FCHDIR,
+ },
+ {
+ .type = PIDFD_SPAWN_ACTION_DUP2,
+ .newfd = STDOUT_FILENO,
+ },
+ };
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "cwd-file", NULL };
+ char parent_cwd[PATH_MAX];
+ char current_cwd[PATH_MAX];
+ char template[] = "/tmp/pidfd-spawn-cwd.XXXXXX";
+ char payload[PATH_MAX];
+ char buf[9] = {};
+ int pipefd[2];
+ int dirfd;
+ int filefd;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ ASSERT_NE(getcwd(parent_cwd, sizeof(parent_cwd)), NULL);
+ ASSERT_NE(mkdtemp(template), NULL);
+ ASSERT_LT(snprintf(payload, sizeof(payload), "%s/payload.txt", template),
+ sizeof(payload));
+ filefd = open(payload, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
+ ASSERT_GE(filefd, 0);
+ ASSERT_EQ(write(filefd, "run-cwd\n", 8), 8);
+ ASSERT_EQ(close(filefd), 0);
+ dirfd = open(template, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(dirfd, 0);
+ ASSERT_EQ(pipe2(pipefd, O_CLOEXEC), 0);
+ actions[0].fd = dirfd;
+ actions[1].fd = pipefd[1];
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, actions,
+ ARRAY_SIZE(actions)), 0);
+ ASSERT_EQ(close(pipefd[1]), 0);
+ ASSERT_EQ(read_exact_timeout(pipefd[0], buf, sizeof(buf) - 1),
+ sizeof(buf) - 1);
+ ASSERT_STREQ(buf, "run-cwd\n");
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_NE(getcwd(current_cwd, sizeof(current_cwd)), NULL);
+ ASSERT_STREQ(current_cwd, parent_cwd);
+ ASSERT_GE(fcntl(dirfd, F_GETFD), 0);
+ ASSERT_EQ(close(pipefd[0]), 0);
+ ASSERT_EQ(close(dirfd), 0);
+ ASSERT_EQ(close(fd), 0);
+ ASSERT_EQ(unlink(payload), 0);
+ ASSERT_EQ(rmdir(template), 0);
+}
+
+TEST(pidfd_spawn_file_actions_run_in_array_order)
+{
+ struct pidfd_spawn_action actions[2] = {
+ {
+ .type = PIDFD_SPAWN_ACTION_CLOSE_RANGE,
+ },
+ {
+ .type = PIDFD_SPAWN_ACTION_DUP2,
+ },
+ };
+ const char *path = self_exe_path();
+ char fdarg[32];
+ char * const open_argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "fd-open", fdarg,
+ NULL };
+ char * const closed_argv[] = { "pidfd_spawn_actions_test",
+ "--pidfd-spawn-helper", "fd-closed",
+ fdarg, NULL };
+ int target;
+ int devnull;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ devnull = open("/dev/null", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(devnull, 0);
+ target = fcntl(devnull, F_DUPFD_CLOEXEC, 100);
+ ASSERT_GE(target, 100);
+ ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", target), 0);
+ actions[0].fd = target;
+ actions[0].newfd = target;
+ actions[1].fd = devnull;
+ actions[1].newfd = target;
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, path, open_argv, actions,
+ ARRAY_SIZE(actions)), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+
+ actions[0].type = PIDFD_SPAWN_ACTION_DUP2;
+ actions[0].fd = devnull;
+ actions[0].newfd = target;
+ actions[1].type = PIDFD_SPAWN_ACTION_CLOSE_RANGE;
+ actions[1].fd = target;
+ actions[1].newfd = target;
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, path, closed_argv, actions,
+ ARRAY_SIZE(actions)), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_NE(fcntl(target, F_GETFD) & FD_CLOEXEC, 0);
+ ASSERT_EQ(close(fd), 0);
+ ASSERT_EQ(close(target), 0);
+ ASSERT_EQ(close(devnull), 0);
+}
+
+int main(int argc, char **argv)
+{
+ int ret = helper_main(argc, argv);
+
+ if (ret >= 0)
+ return ret;
+ return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c
new file mode 100644
index 0000000000000..759af7d6a68f8
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c
@@ -0,0 +1,521 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/audit.h>
+#include <linux/netlink.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+#include "../landlock/audit.h"
+
+struct pidfd_spawn_audit_event {
+ unsigned long long serial;
+ pid_t pid;
+ bool syscall;
+ bool pidfd_spawn;
+ bool execve;
+ bool cwd;
+ bool cwd_matches;
+ bool path;
+};
+
+struct pidfd_spawn_audit_observation {
+ bool parent_syscall;
+ bool child_syscall;
+ bool child_pidfd_spawn;
+ bool child_execve;
+ bool child_cwd;
+ bool child_cwd_matches;
+ bool child_path;
+};
+
+static int pidfd_spawn_audit_filter_syscall(int audit_fd, __u16 type)
+{
+ struct audit_message msg = {
+ .header = {
+ .nlmsg_len = NLMSG_SPACE(sizeof(msg.rule)),
+ .nlmsg_type = type,
+ .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
+ },
+ .rule = {
+ .flags = AUDIT_FILTER_EXIT,
+ .action = AUDIT_ALWAYS,
+ },
+ };
+
+ if (AUDIT_WORD(__NR_pidfd_spawn_run) >= AUDIT_BITMASK_SIZE)
+ return -E2BIG;
+ msg.rule.mask[AUDIT_WORD(__NR_pidfd_spawn_run)] =
+ AUDIT_BIT(__NR_pidfd_spawn_run);
+ return audit_request(audit_fd, &msg, NULL);
+}
+
+static int pidfd_spawn_audit_get_status(struct audit_status *status)
+{
+ const struct audit_message request = {
+ .header = {
+ .nlmsg_len = NLMSG_SPACE(0),
+ .nlmsg_type = AUDIT_GET,
+ .nlmsg_flags = NLM_F_REQUEST,
+ },
+ };
+ struct audit_message reply;
+ int audit_fd;
+ int ret;
+
+ audit_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
+ if (audit_fd < 0)
+ return -errno;
+ ret = audit_send(audit_fd, &request);
+ while (!ret) {
+ memset(&reply, 0, sizeof(reply));
+ ret = audit_recv(audit_fd, &reply);
+ if (ret)
+ continue;
+ if (reply.header.nlmsg_type == NLMSG_ERROR) {
+ ret = reply.err.error ? reply.err.error : -EIO;
+ break;
+ }
+ if (reply.header.nlmsg_type != AUDIT_GET)
+ continue;
+ *status = reply.status;
+ break;
+ }
+ close(audit_fd);
+ return ret;
+}
+
+static int pidfd_spawn_audit_init(int *audit_fd, bool *owner_set)
+{
+ int fd;
+ int ret;
+
+ fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
+ if (fd < 0)
+ return -errno;
+ *audit_fd = fd;
+ ret = audit_set_status(fd, AUDIT_STATUS_PID, getpid());
+ if (ret)
+ return ret;
+ *owner_set = true;
+ ret = audit_set_status(fd, AUDIT_STATUS_ENABLED, 1);
+ if (ret)
+ return ret;
+ ret = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast,
+ sizeof(audit_tv_fast));
+ if (ret)
+ return -errno;
+ while (audit_recv(fd, NULL) == 0)
+ ;
+ ret = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default,
+ sizeof(audit_tv_default));
+ return ret ? -errno : 0;
+}
+
+static void pidfd_spawn_audit_record_cleanup(int *first_error, int error)
+{
+ if (!*first_error && error)
+ *first_error = error;
+}
+
+static int pidfd_spawn_audit_release(int audit_fd, __u32 enabled)
+{
+ const struct audit_message msg = {
+ .header = {
+ .nlmsg_len = NLMSG_SPACE(sizeof(msg.status)),
+ .nlmsg_type = AUDIT_SET,
+ .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
+ },
+ .status = {
+ .mask = AUDIT_STATUS_ENABLED | AUDIT_STATUS_PID,
+ .enabled = enabled,
+ .pid = 0,
+ },
+ };
+ int ret;
+
+ ret = audit_request(audit_fd, &msg, NULL);
+ return ret > 0 ? 0 : ret;
+}
+
+static unsigned long long pidfd_spawn_audit_serial(const char *record)
+{
+ const char *audit;
+ const char *separator;
+ char *end;
+ unsigned long long serial;
+
+ audit = strstr(record, "audit(");
+ if (!audit)
+ return 0;
+ separator = strchr(audit, ':');
+ if (!separator)
+ return 0;
+ errno = 0;
+ serial = strtoull(separator + 1, &end, 10);
+ if (errno || end == separator + 1 || *end != ')')
+ return 0;
+ return serial;
+}
+
+static int pidfd_spawn_audit_field(const char *record, const char *field,
+ long *value)
+{
+ char pattern[32];
+ const char *start;
+ char *end;
+ int len;
+
+ len = snprintf(pattern, sizeof(pattern), " %s=", field);
+ if (len < 0 || len >= sizeof(pattern))
+ return -E2BIG;
+ start = strstr(record, pattern);
+ if (!start)
+ return -ENOENT;
+ errno = 0;
+ *value = strtol(start + len, &end, 10);
+ if (errno || end == start + len)
+ return -EINVAL;
+ return 0;
+}
+
+static bool pidfd_spawn_audit_cwd_matches(const char *record, const char *cwd)
+{
+ char field[PATH_MAX + sizeof("cwd=\"\"")];
+ int len;
+
+ len = snprintf(field, sizeof(field), "cwd=\"%s\"", cwd);
+ return len > 0 && len < (int)sizeof(field) && strstr(record, field);
+}
+
+static struct pidfd_spawn_audit_event *
+pidfd_spawn_audit_event(struct pidfd_spawn_audit_event *events,
+ unsigned int *nr_events, unsigned long long serial)
+{
+ unsigned int i;
+
+ for (i = 0; i < *nr_events; i++)
+ if (events[i].serial == serial)
+ return &events[i];
+ if (*nr_events == 16)
+ return NULL;
+ events[*nr_events].serial = serial;
+ return &events[(*nr_events)++];
+}
+
+static int
+pidfd_spawn_collect_audit(int audit_fd, pid_t parent, pid_t child,
+ const char *expected_cwd,
+ struct pidfd_spawn_audit_observation *obs)
+{
+ struct pidfd_spawn_audit_event events[16] = {};
+ unsigned int nr_events = 0;
+ struct audit_message msg;
+ unsigned int i;
+ int ret;
+
+ for (;;) {
+ struct pidfd_spawn_audit_event *event;
+ unsigned long long serial;
+
+ memset(&msg, 0, sizeof(msg));
+ ret = audit_recv(audit_fd, &msg);
+ if (ret == -EAGAIN)
+ break;
+ if (ret)
+ return ret;
+ serial = pidfd_spawn_audit_serial(msg.data);
+ if (!serial)
+ continue;
+ event = pidfd_spawn_audit_event(events, &nr_events, serial);
+ if (!event)
+ return -E2BIG;
+ switch (msg.header.nlmsg_type) {
+ case AUDIT_SYSCALL:
+ case AUDIT_PIDFD_SPAWN: {
+ long pid;
+ long syscall;
+
+ if (pidfd_spawn_audit_field(msg.data, "syscall", &syscall) ||
+ syscall != __NR_pidfd_spawn_run ||
+ pidfd_spawn_audit_field(msg.data, "pid", &pid))
+ break;
+ event->pid = pid;
+ if (msg.header.nlmsg_type == AUDIT_SYSCALL)
+ event->syscall = true;
+ else
+ event->pidfd_spawn = true;
+ break;
+ }
+ case AUDIT_EXECVE:
+ event->execve = true;
+ break;
+ case AUDIT_CWD:
+ event->cwd = true;
+ if (expected_cwd &&
+ pidfd_spawn_audit_cwd_matches(msg.data, expected_cwd))
+ event->cwd_matches = true;
+ break;
+ case AUDIT_PATH:
+ event->path = true;
+ break;
+ }
+ }
+
+ for (i = 0; i < nr_events; i++) {
+ if (events[i].pid == parent)
+ obs->parent_syscall |= events[i].syscall;
+ if (events[i].pid != child)
+ continue;
+ obs->child_syscall |= events[i].syscall;
+ obs->child_pidfd_spawn |= events[i].pidfd_spawn;
+ obs->child_execve |= events[i].execve;
+ obs->child_cwd |= events[i].cwd;
+ obs->child_cwd_matches |= events[i].cwd_matches;
+ obs->child_path |= events[i].path;
+ }
+ return 0;
+}
+
+static int pidfd_spawn_audit_worker(int pipe_fd, const char *path, int cwd_fd)
+{
+ struct pidfd_spawn_action action = {
+ .type = PIDFD_SPAWN_ACTION_FCHDIR,
+ .fd = cwd_fd,
+ };
+ char * const argv[] = { "pidfd_spawn_audit_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ pid_t child;
+ int builder;
+
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ return 1;
+ child = spawn_run_path_pid(builder, path, argv,
+ cwd_fd < 0 ? NULL : &action, cwd_fd < 0 ? 0 : 1);
+ if (child < 0) {
+ close(builder);
+ return 2;
+ }
+ if (write(pipe_fd, &child, sizeof(child)) != sizeof(child)) {
+ close(builder);
+ return 3;
+ }
+ if (wait_pidfd_exit(builder, 0)) {
+ close(builder);
+ return 4;
+ }
+ return close(builder) ? 5 : 0;
+}
+
+static int
+pidfd_spawn_observe_audit(struct pidfd_spawn_audit_observation *obs,
+ bool fail_after_filter, const char *path, int cwd_fd,
+ const char *expected_cwd)
+{
+ struct audit_records records;
+ struct audit_filter filter;
+ struct audit_status original = {};
+ bool audit_owner_set = false;
+ bool filter_added = false;
+ bool rule_added = false;
+ int pipe_fds[2] = { -1, -1 };
+ int audit_fd = -1;
+ int cleanup_error = 0;
+ int cleanup;
+ int status;
+ int ret;
+ pid_t child = 0;
+ pid_t source;
+ pid_t waited;
+ ssize_t len;
+
+ if (!path)
+ path = self_exe_path();
+ if (!path)
+ return -ENOENT;
+ ret = pidfd_spawn_audit_get_status(&original);
+ if (ret)
+ return ret;
+ if (original.pid)
+ return -EEXIST;
+ if (original.enabled == 2)
+ return -EPERM;
+ ret = pidfd_spawn_audit_init(&audit_fd, &audit_owner_set);
+ if (ret)
+ goto out;
+ ret = audit_init_filter_exe(&filter, NULL);
+ if (ret)
+ goto out;
+ ret = audit_filter_exe(audit_fd, &filter, AUDIT_ADD_RULE);
+ if (ret)
+ goto out;
+ filter_added = true;
+ if (fail_after_filter) {
+ ret = -ECANCELED;
+ goto out;
+ }
+ ret = pidfd_spawn_audit_filter_syscall(audit_fd, AUDIT_ADD_RULE);
+ if (ret)
+ goto out;
+ rule_added = true;
+ if (pipe2(pipe_fds, O_CLOEXEC)) {
+ ret = -errno;
+ goto out;
+ }
+ source = fork();
+ if (source < 0) {
+ ret = -errno;
+ goto out;
+ }
+ if (!source) {
+ close(pipe_fds[0]);
+ _exit(pidfd_spawn_audit_worker(pipe_fds[1], path, cwd_fd));
+ }
+ close(pipe_fds[1]);
+ pipe_fds[1] = -1;
+ do {
+ len = read(pipe_fds[0], &child, sizeof(child));
+ } while (len < 0 && errno == EINTR);
+ close(pipe_fds[0]);
+ pipe_fds[0] = -1;
+ waited = waitpid_timeout(source, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (waited != source || !WIFEXITED(status) || WEXITSTATUS(status)) {
+ ret = -ECHILD;
+ goto out;
+ }
+ if (len != sizeof(child)) {
+ ret = len < 0 ? -errno : -EIO;
+ goto out;
+ }
+ ret = pidfd_spawn_collect_audit(audit_fd, source, child, expected_cwd,
+ obs);
+ if (!ret)
+ ret = audit_count_records(audit_fd, &records);
+out:
+ if (pipe_fds[0] >= 0)
+ close(pipe_fds[0]);
+ if (pipe_fds[1] >= 0)
+ close(pipe_fds[1]);
+ if (audit_fd < 0)
+ return ret;
+ if (!audit_owner_set) {
+ close(audit_fd);
+ return ret;
+ }
+ if (rule_added) {
+ cleanup = pidfd_spawn_audit_filter_syscall(audit_fd,
+ AUDIT_DEL_RULE);
+ pidfd_spawn_audit_record_cleanup(&cleanup_error, cleanup);
+ }
+ if (filter_added) {
+ cleanup = audit_filter_exe(audit_fd, &filter, AUDIT_DEL_RULE);
+ pidfd_spawn_audit_record_cleanup(&cleanup_error, cleanup);
+ }
+ cleanup = pidfd_spawn_audit_release(audit_fd, original.enabled);
+ pidfd_spawn_audit_record_cleanup(&cleanup_error, cleanup);
+ close(audit_fd);
+ return cleanup_error ? cleanup_error : ret;
+}
+
+TEST(pidfd_spawn_audit_records_child_exec)
+{
+ struct pidfd_spawn_audit_observation observation = {};
+ int ret;
+
+ ret = pidfd_spawn_observe_audit(&observation, false, NULL, -1, NULL);
+ if (ret == -EPERM || ret == -EACCES || ret == -EEXIST ||
+ ret == -EAFNOSUPPORT || ret == -EPROTONOSUPPORT)
+ SKIP(return, "Audit control is unavailable: %s", strerror(-ret));
+ ASSERT_EQ(ret, 0);
+ ASSERT_TRUE(observation.parent_syscall);
+ ASSERT_FALSE(observation.child_syscall);
+ ASSERT_TRUE(observation.child_pidfd_spawn);
+ ASSERT_TRUE(observation.child_execve);
+ ASSERT_TRUE(observation.child_cwd);
+ ASSERT_TRUE(observation.child_path);
+}
+
+TEST(pidfd_spawn_audit_records_post_fchdir_cwd)
+{
+ struct pidfd_spawn_audit_observation observation = {};
+ const char *path = self_exe_path();
+ static const char relative_path[] = "pidfd-spawn-audit-helper";
+ char template[] = "/tmp/pidfd-spawn-audit.XXXXXX";
+ char link_path[PATH_MAX];
+ int dirfd;
+ int len;
+ int ret;
+
+ ASSERT_NE(path, NULL);
+ ASSERT_NE(mkdtemp(template), NULL);
+ len = snprintf(link_path, sizeof(link_path), "%s/%s", template,
+ relative_path);
+ ASSERT_GT(len, 0);
+ ASSERT_LT(len, (int)sizeof(link_path));
+ ASSERT_EQ(symlink(path, link_path), 0);
+ dirfd = open(template, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(dirfd, 0);
+
+ ret = pidfd_spawn_observe_audit(&observation, false, relative_path,
+ dirfd, template);
+ ASSERT_EQ(close(dirfd), 0);
+ ASSERT_EQ(unlink(link_path), 0);
+ ASSERT_EQ(rmdir(template), 0);
+ if (ret == -EPERM || ret == -EACCES || ret == -EEXIST ||
+ ret == -EAFNOSUPPORT || ret == -EPROTONOSUPPORT)
+ SKIP(return, "Audit control is unavailable: %s", strerror(-ret));
+ ASSERT_EQ(ret, 0);
+ ASSERT_TRUE(observation.parent_syscall);
+ ASSERT_TRUE(observation.child_pidfd_spawn);
+ ASSERT_TRUE(observation.child_execve);
+ ASSERT_TRUE(observation.child_cwd);
+ ASSERT_TRUE(observation.child_cwd_matches);
+ ASSERT_TRUE(observation.child_path);
+}
+
+TEST(pidfd_spawn_audit_setup_failure_restores_status)
+{
+ struct pidfd_spawn_audit_observation observation = {};
+ struct audit_status original = {};
+ struct audit_status restored = {};
+ int ret;
+
+ ret = pidfd_spawn_audit_get_status(&original);
+ if (ret == -EPERM || ret == -EACCES ||
+ ret == -EAFNOSUPPORT || ret == -EPROTONOSUPPORT)
+ SKIP(return, "Audit control is unavailable: %s", strerror(-ret));
+ ASSERT_EQ(ret, 0);
+ if (original.pid)
+ SKIP(return, "Audit is owned by pid %u", original.pid);
+ if (original.enabled == 2)
+ SKIP(return, "Audit configuration is immutable");
+ ret = pidfd_spawn_observe_audit(&observation, true, NULL, -1, NULL);
+ if (ret == -EPERM || ret == -EACCES || ret == -EEXIST ||
+ ret == -EAFNOSUPPORT || ret == -EPROTONOSUPPORT)
+ SKIP(return, "Audit control is unavailable: %s", strerror(-ret));
+ ASSERT_EQ(ret, -ECANCELED);
+ ASSERT_EQ(pidfd_spawn_audit_get_status(&restored), 0);
+ ASSERT_EQ(restored.enabled, original.enabled);
+ ASSERT_EQ(restored.pid, original.pid);
+}
+
+int main(int argc, char **argv)
+{
+ int ret = helper_main(argc, argv);
+
+ if (ret >= 0)
+ return ret;
+ return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_common.c b/tools/testing/selftests/pidfd/pidfd_spawn_common.c
new file mode 100644
index 0000000000000..aab7211f5e2c7
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_common.c
@@ -0,0 +1,512 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "pidfd.h"
+#include "pidfd_spawn_common.h"
+
+char * const empty_envp[] = { NULL };
+
+int helper_main(int argc, char **argv)
+{
+ if (argc < 3 || strcmp(argv[1], "--pidfd-spawn-helper")) {
+ int fd = sys_pidfd_empty_open();
+
+ if (fd >= 0) {
+ close(fd);
+ return -1;
+ }
+ if (errno != EINVAL && errno != ENOSYS)
+ return -1;
+ ksft_print_msg("pidfd spawn is unavailable: %s\n",
+ strerror(errno));
+ return KSFT_SKIP;
+ }
+
+ if (!strcmp(argv[2], "exit0"))
+ return 0;
+ if (!strcmp(argv[2], "empty-env"))
+ return environ[0] ? 1 : 0;
+ if (!strcmp(argv[2], "print")) {
+ if (write(STDOUT_FILENO, "pidfd-spawn\n", 12) != 12)
+ return 1;
+ return 0;
+ }
+ if (!strcmp(argv[2], "cwd-file")) {
+ char buf[32];
+ int fd;
+ ssize_t len;
+
+ fd = open("payload.txt", O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return 1;
+ len = read(fd, buf, sizeof(buf));
+ close(fd);
+ if (len < 0)
+ return 1;
+ if (write(STDOUT_FILENO, buf, len) != len)
+ return 1;
+ return 0;
+ }
+ if (!strcmp(argv[2], "fd-closed")) {
+ int fd;
+
+ if (argc != 4)
+ return 127;
+ fd = atoi(argv[3]);
+ if (fcntl(fd, F_GETFD) == -1 && errno == EBADF)
+ return 0;
+ return 1;
+ }
+ if (!strcmp(argv[2], "fd-open")) {
+ int fd;
+
+ if (argc != 4)
+ return 127;
+ fd = atoi(argv[3]);
+ return fcntl(fd, F_GETFD) >= 0 ? 0 : 1;
+ }
+ if (!strcmp(argv[2], "wait-fd")) {
+ char byte;
+ ssize_t len;
+ int fd;
+
+ if (argc != 4)
+ return 127;
+ fd = atoi(argv[3]);
+ do {
+ len = read(fd, &byte, sizeof(byte));
+ } while (len < 0 && errno == EINTR);
+ return len == sizeof(byte) ? 0 : 1;
+ }
+ if (!strcmp(argv[2], "ready-wait-fd")) {
+ char byte = 1;
+ ssize_t len;
+ int ready_fd;
+ int wait_fd;
+
+ if (argc != 5)
+ return 127;
+ ready_fd = atoi(argv[3]);
+ wait_fd = atoi(argv[4]);
+ if (write(ready_fd, &byte, sizeof(byte)) != sizeof(byte))
+ return 1;
+ do {
+ len = read(wait_fd, &byte, sizeof(byte));
+ } while (len < 0 && errno == EINTR);
+ return len == sizeof(byte) ? 0 : 1;
+ }
+ if (!strcmp(argv[2], "seccomp-actions")) {
+ char * const blocked_argv[] = { "blocked", NULL };
+ char cwd[PATH_MAX];
+ int closed_fd;
+ int cwd_fd;
+ int target;
+
+ if (argc != 7)
+ return 127;
+ target = atoi(argv[3]);
+ closed_fd = atoi(argv[4]);
+ cwd_fd = atoi(argv[5]);
+ if (fcntl(target, F_GETFD) < 0)
+ return 1;
+ if (fcntl(closed_fd, F_GETFD) != -1 || errno != EBADF)
+ return 2;
+ if (!getcwd(cwd, sizeof(cwd)) || strcmp(cwd, argv[6]))
+ return 3;
+#ifdef __NR_dup2
+ errno = 0;
+ if (dup2(target, target) != -1 || errno != EPERM)
+ return 4;
+#endif
+ errno = 0;
+ if (syscall(__NR_close_range, target, target, 0) != -1 ||
+ errno != EPERM)
+ return 5;
+ errno = 0;
+ if (fchdir(cwd_fd) != -1 || errno != EPERM)
+ return 6;
+ errno = 0;
+ if (execve("/no/such/pidfd-spawn-seccomp", blocked_argv,
+ empty_envp) != -1 || errno != EPERM)
+ return 7;
+ errno = 0;
+ if (syscall(__NR_execveat, AT_FDCWD,
+ "/no/such/pidfd-spawn-seccomp", blocked_argv,
+ empty_envp, 0) != -1 || errno != EPERM)
+ return 8;
+ return 0;
+ }
+ return 127;
+}
+
+const char *self_exe_path(void)
+{
+ static char path[PATH_MAX];
+ ssize_t len;
+
+ if (path[0])
+ return path;
+
+ len = readlink("/proc/self/exe", path, sizeof(path) - 1);
+ if (len < 0)
+ return NULL;
+ path[len] = '\0';
+ return path;
+}
+
+int enter_self_exe_directory(char **storage, const char **relative)
+{
+ const char *path = self_exe_path();
+ char *slash;
+ int cwd_fd;
+
+ if (!path) {
+ errno = ENOENT;
+ return -1;
+ }
+ *storage = strdup(path);
+ if (!*storage)
+ return -1;
+ slash = strrchr(*storage, '/');
+ if (!slash || !slash[1]) {
+ free(*storage);
+ *storage = NULL;
+ errno = EINVAL;
+ return -1;
+ }
+ *slash = '\0';
+ *relative = slash + 1;
+ cwd_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (cwd_fd < 0 || chdir(**storage ? *storage : "/")) {
+ int saved_errno = errno;
+
+ if (cwd_fd >= 0)
+ close(cwd_fd);
+ free(*storage);
+ *storage = NULL;
+ errno = saved_errno;
+ return -1;
+ }
+ return cwd_fd;
+}
+
+int leave_self_exe_directory(int cwd_fd, char *storage)
+{
+ int ret = fchdir(cwd_fd);
+ int saved_errno = errno;
+
+ if (close(cwd_fd) && !ret) {
+ ret = -1;
+ saved_errno = errno;
+ }
+ free(storage);
+ errno = saved_errno;
+ return ret;
+}
+
+int config_path(int fd, const char *path)
+{
+ return sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+ PIDFD_CONFIG_KEY_PATH, path, 0);
+}
+
+int spawn_run_path_pid(int fd, const char *path, char * const argv[],
+ struct pidfd_spawn_action *actions,
+ unsigned int nr_actions)
+{
+ struct pidfd_spawn_run_args args = {
+ .path = ptr_to_u64(path),
+ .argv = ptr_to_u64(argv),
+ .envp = ptr_to_u64(empty_envp),
+ .actions = ptr_to_u64(actions),
+ .nr_actions = nr_actions,
+ .action_size = nr_actions ? sizeof(*actions) : 0,
+ };
+
+ return sys_pidfd_spawn_run(fd, &args, sizeof(args));
+}
+
+int spawn_run_path(int fd, const char *path, char * const argv[],
+ struct pidfd_spawn_action *actions,
+ unsigned int nr_actions)
+{
+ return spawn_run_path_pid(fd, path, argv, actions, nr_actions) < 0 ?
+ -1 : 0;
+}
+
+int spawn_run_staged(int fd, char * const argv[],
+ struct pidfd_spawn_action *actions,
+ unsigned int nr_actions)
+{
+ return spawn_run_path(fd, NULL, argv, actions, nr_actions);
+}
+
+static int deadline_after_ms(struct timespec *deadline, unsigned int timeout_ms)
+{
+ if (clock_gettime(CLOCK_MONOTONIC, deadline))
+ return -1;
+ deadline->tv_sec += timeout_ms / 1000;
+ deadline->tv_nsec += (timeout_ms % 1000) * 1000000L;
+ if (deadline->tv_nsec >= 1000000000L) {
+ deadline->tv_sec++;
+ deadline->tv_nsec -= 1000000000L;
+ }
+ return 0;
+}
+
+static int deadline_remaining_ms(const struct timespec *deadline)
+{
+ struct timespec now;
+ int64_t remaining_ns;
+
+ if (clock_gettime(CLOCK_MONOTONIC, &now))
+ return -1;
+ remaining_ns = (deadline->tv_sec - now.tv_sec) * 1000000000LL +
+ deadline->tv_nsec - now.tv_nsec;
+ if (remaining_ns <= 0)
+ return 0;
+ if (remaining_ns > (int64_t)INT_MAX * 1000000)
+ return INT_MAX;
+ return (remaining_ns + 999999) / 1000000;
+}
+
+static int poll_until(int fd, short events, const struct timespec *deadline)
+{
+ struct pollfd pfd = {
+ .fd = fd,
+ .events = events,
+ };
+ int timeout;
+ int ret;
+
+ for (;;) {
+ timeout = deadline_remaining_ms(deadline);
+ if (timeout <= 0) {
+ if (!timeout)
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ ret = poll(&pfd, 1, timeout);
+ if (ret > 0) {
+ if (pfd.revents & POLLNVAL) {
+ errno = EBADF;
+ return -1;
+ }
+ if (pfd.revents & (events | POLLERR | POLLHUP))
+ return 0;
+ continue;
+ }
+ if (!ret) {
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ if (errno != EINTR)
+ return -1;
+ }
+}
+
+int wait_pidfd_exit_info(int pidfd, struct pidfd_info *info)
+{
+ struct timespec deadline;
+
+ if (deadline_after_ms(&deadline, PIDFD_SPAWN_TIMEOUT_MS))
+ return -1;
+ for (;;) {
+ memset(info, 0, sizeof(*info));
+ info->mask = PIDFD_INFO_EXIT;
+ if (!ioctl(pidfd, PIDFD_GET_INFO, info)) {
+ if (info->mask & PIDFD_INFO_EXIT)
+ return 0;
+ } else if (errno != ESRCH && errno != EINTR) {
+ return -1;
+ }
+ if (poll_until(pidfd, POLLIN, &deadline))
+ return -1;
+ }
+}
+
+static int wait_pidfd_info(int pidfd, siginfo_t *info)
+{
+ struct timespec deadline;
+
+ if (deadline_after_ms(&deadline, PIDFD_SPAWN_TIMEOUT_MS))
+ return -1;
+ for (;;) {
+ memset(info, 0, sizeof(*info));
+ if (!sys_waitid(P_PIDFD, pidfd, info, WEXITED | WNOHANG)) {
+ if (info->si_pid)
+ return 0;
+ } else if (errno != EINTR) {
+ return -1;
+ }
+ if (poll_until(pidfd, POLLIN, &deadline))
+ return -1;
+ }
+}
+
+int wait_pidfd_exit(int pidfd, int status)
+{
+ siginfo_t info = {};
+
+ if (wait_pidfd_info(pidfd, &info))
+ return -1;
+ if (info.si_code != CLD_EXITED || info.si_status != status) {
+ errno = ECHILD;
+ return -1;
+ }
+ return 0;
+}
+
+int wait_pidfd_signal(int pidfd, int signal)
+{
+ siginfo_t info = {};
+
+ if (wait_pidfd_info(pidfd, &info))
+ return -1;
+ if (info.si_code != CLD_KILLED || info.si_status != signal) {
+ errno = ECHILD;
+ return -1;
+ }
+ return 0;
+}
+
+#define PIDFD_SPAWN_MAX_SENT_FDS 2
+
+int send_fds_status(int socket, int status, const int *fds, size_t nr_fds)
+{
+ union {
+ char buf[CMSG_SPACE(sizeof(int) * PIDFD_SPAWN_MAX_SENT_FDS)];
+ struct cmsghdr align;
+ } control = {};
+ struct iovec iov = {
+ .iov_base = &status,
+ .iov_len = sizeof(status),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ };
+ struct cmsghdr *cmsg;
+
+ if (nr_fds > PIDFD_SPAWN_MAX_SENT_FDS) {
+ errno = E2BIG;
+ return -1;
+ }
+ if (nr_fds) {
+ msg.msg_control = control.buf;
+ msg.msg_controllen = CMSG_SPACE(sizeof(*fds) * nr_fds);
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(*fds) * nr_fds);
+ memcpy(CMSG_DATA(cmsg), fds, sizeof(*fds) * nr_fds);
+ }
+
+ return sendmsg(socket, &msg, 0) == sizeof(status) ? 0 : -1;
+}
+
+int recv_fds_status(int socket, int *status, int *fds, size_t nr_fds)
+{
+ union {
+ char buf[CMSG_SPACE(sizeof(int) * PIDFD_SPAWN_MAX_SENT_FDS)];
+ struct cmsghdr align;
+ } control = {};
+ struct iovec iov = {
+ .iov_base = status,
+ .iov_len = sizeof(*status),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = control.buf,
+ .msg_controllen = sizeof(control.buf),
+ };
+ struct cmsghdr *cmsg;
+
+ if (!nr_fds || nr_fds > PIDFD_SPAWN_MAX_SENT_FDS) {
+ errno = EINVAL;
+ return -1;
+ }
+ if (recvmsg(socket, &msg, MSG_CMSG_CLOEXEC) != sizeof(*status))
+ return -1;
+ if (*status)
+ return -1;
+
+ cmsg = CMSG_FIRSTHDR(&msg);
+ if (!cmsg || cmsg->cmsg_level != SOL_SOCKET ||
+ cmsg->cmsg_type != SCM_RIGHTS ||
+ cmsg->cmsg_len != CMSG_LEN(sizeof(*fds) * nr_fds) ||
+ (msg.msg_flags & (MSG_CTRUNC | MSG_TRUNC))) {
+ errno = EBADMSG;
+ return -1;
+ }
+
+ memcpy(fds, CMSG_DATA(cmsg), sizeof(*fds) * nr_fds);
+ return 0;
+}
+
+pid_t waitpid_timeout(pid_t pid, int *status, unsigned int timeout_ms)
+{
+ struct timespec deadline;
+ pid_t ret;
+ int timeout;
+
+ if (deadline_after_ms(&deadline, timeout_ms))
+ return -1;
+ for (;;) {
+ ret = waitpid(pid, status, WNOHANG);
+ if (ret > 0)
+ return ret;
+ if (ret < 0 && errno != EINTR)
+ return -1;
+ timeout = deadline_remaining_ms(&deadline);
+ if (timeout <= 0)
+ break;
+ if (timeout > 10)
+ timeout = 10;
+ if (poll(NULL, 0, timeout) < 0 && errno != EINTR)
+ return -1;
+ }
+
+ errno = ETIMEDOUT;
+ return 0;
+}
+
+int pthread_join_timeout(pthread_t thread, void **retval,
+ unsigned int timeout_ms)
+{
+ struct timespec deadline;
+ int timeout;
+ int ret;
+
+ if (deadline_after_ms(&deadline, timeout_ms))
+ return errno;
+ for (;;) {
+ ret = pthread_tryjoin_np(thread, retval);
+ if (ret != EBUSY)
+ return ret;
+ timeout = deadline_remaining_ms(&deadline);
+ if (timeout <= 0)
+ return timeout ? errno : ETIMEDOUT;
+ if (timeout > 10)
+ timeout = 10;
+ if (poll(NULL, 0, timeout) < 0 && errno != EINTR)
+ return errno;
+ }
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_common.h b/tools/testing/selftests/pidfd/pidfd_spawn_common.h
new file mode 100644
index 0000000000000..33f9b72655d67
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_common.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __PIDFD_SPAWN_COMMON_H
+#define __PIDFD_SPAWN_COMMON_H
+
+#include <linux/pidfd_spawn.h>
+#include <pthread.h>
+#include <stddef.h>
+
+#include "pidfd.h"
+
+#ifndef PIDFD_EMPTY
+#define PIDFD_EMPTY 0x08000000U
+#endif
+
+#define PIDFD_SPAWN_TIMEOUT_MS 5000
+
+extern char * const empty_envp[];
+
+int helper_main(int argc, char **argv);
+static inline int sys_pidfd_empty_open(void)
+{
+ return sys_pidfd_open(0, PIDFD_EMPTY);
+}
+
+static inline int sys_pidfd_config(int fd, unsigned int cmd,
+ const char *key, const void *value, int aux)
+{
+ return syscall(__NR_pidfd_config, fd, cmd, key, value, aux);
+}
+
+static inline int sys_pidfd_spawn_run(int fd,
+ struct pidfd_spawn_run_args *args,
+ size_t size)
+{
+ return syscall(__NR_pidfd_spawn_run, fd, args, size);
+}
+
+const char *self_exe_path(void);
+int enter_self_exe_directory(char **storage, const char **relative);
+int leave_self_exe_directory(int cwd_fd, char *storage);
+int config_path(int fd, const char *path);
+int spawn_run_path_pid(int fd, const char *path, char * const argv[],
+ struct pidfd_spawn_action *actions,
+ unsigned int nr_actions);
+int spawn_run_path(int fd, const char *path, char * const argv[],
+ struct pidfd_spawn_action *actions, unsigned int nr_actions);
+int spawn_run_staged(int fd, char * const argv[],
+ struct pidfd_spawn_action *actions,
+ unsigned int nr_actions);
+int wait_pidfd_exit_info(int pidfd, struct pidfd_info *info);
+int wait_pidfd_exit(int pidfd, int status);
+int wait_pidfd_signal(int pidfd, int signal);
+int send_fds_status(int socket, int status, const int *fds, size_t nr_fds);
+int recv_fds_status(int socket, int *status, int *fds, size_t nr_fds);
+pid_t waitpid_timeout(pid_t pid, int *status, unsigned int timeout_ms);
+int pthread_join_timeout(pthread_t thread, void **retval,
+ unsigned int timeout_ms);
+
+#endif /* __PIDFD_SPAWN_COMMON_H */
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_compat.c b/tools/testing/selftests/pidfd/pidfd_spawn_compat.c
new file mode 100644
index 0000000000000..a4e31d271a5d5
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_compat.c
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <limits.h>
+#include <linux/fs.h>
+#include <linux/pidfd.h>
+#include <linux/pidfd_spawn.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../kselftest.h"
+
+#ifndef __NR_pidfd_config
+#define __NR_pidfd_config 472
+#endif
+#ifndef __NR_pidfd_spawn_run
+#define __NR_pidfd_spawn_run 473
+#endif
+
+static __u64 compat_ptr64(const void *ptr)
+{
+ return (__u64)(uintptr_t)ptr;
+}
+
+static __u64 invalid_full_width_ptr(const void *ptr)
+{
+ return compat_ptr64(ptr) | (1ULL << 32);
+}
+
+static int read_self_exe(char *path, size_t size)
+{
+ ssize_t len;
+
+ len = readlink("/proc/self/exe", path, size - 1);
+ if (len < 0 || (size_t)len >= size - 1)
+ return -1;
+ path[len] = '\0';
+ return 0;
+}
+
+static int run_child(bool staged)
+{
+ char * const argv[] = { "pidfd_spawn_compat", "--exit0", NULL };
+ char * const envp[] = { NULL };
+ char path[PATH_MAX] = {};
+ struct pidfd_spawn_action action = {
+ .type = PIDFD_SPAWN_ACTION_DUP2,
+ .fd = STDOUT_FILENO,
+ .newfd = STDOUT_FILENO,
+ };
+ struct pidfd_spawn_run_args args = {
+ .path = staged ? 0 : compat_ptr64(path),
+ .argv = compat_ptr64(argv),
+ .envp = compat_ptr64(envp),
+ .actions = compat_ptr64(&action),
+ .nr_actions = 1,
+ .action_size = sizeof(action),
+ };
+ siginfo_t info = {};
+ __u32 published_generation;
+ __u32 future_generation;
+ int fd;
+
+ if (read_self_exe(path, sizeof(path)))
+ return 1;
+ fd = syscall(__NR_pidfd_open, 0, PIDFD_EMPTY);
+ if (fd < 0)
+ return 2;
+ if (ioctl(fd, FS_IOC_GETVERSION, &future_generation))
+ return 3;
+ if (staged && syscall(__NR_pidfd_config, fd, PIDFD_CONFIG_SET_STRING,
+ PIDFD_CONFIG_KEY_PATH, path, 0))
+ return 4;
+ if (syscall(__NR_pidfd_spawn_run, fd, &args, sizeof(args)) < 0)
+ return 5;
+ if (ioctl(fd, FS_IOC_GETVERSION, &published_generation))
+ return 6;
+ if (published_generation != future_generation)
+ return 7;
+ if (waitid(P_PIDFD, fd, &info, WEXITED))
+ return 8;
+ if (info.si_code != CLD_EXITED || info.si_status)
+ return 9;
+ return close(fd) ? 10 : 0;
+}
+
+static int run_empty_env(void)
+{
+ char * const argv[] = { "pidfd_spawn_compat", "--check-empty-env",
+ NULL };
+ char path[PATH_MAX];
+ struct pidfd_spawn_run_args args = {
+ .argv = compat_ptr64(argv),
+ };
+ siginfo_t info = {};
+ int fd;
+
+ if (read_self_exe(path, sizeof(path)))
+ return 1;
+ args.path = compat_ptr64(path);
+ fd = syscall(__NR_pidfd_open, 0, PIDFD_EMPTY);
+ if (fd < 0)
+ return 2;
+ if (syscall(__NR_pidfd_spawn_run, fd, &args, sizeof(args)) < 0)
+ return 3;
+ if (waitid(P_PIDFD, fd, &info, WEXITED))
+ return 4;
+ if (info.si_code != CLD_EXITED || info.si_status)
+ return 5;
+ return close(fd) ? 6 : 0;
+}
+
+enum bad_pointer {
+ BAD_PATH,
+ BAD_ARGV,
+ BAD_ENVP,
+ BAD_ACTIONS,
+};
+
+static int reject_bad_pointer(enum bad_pointer bad)
+{
+ char * const argv[] = { "true", NULL };
+ char * const envp[] = { NULL };
+ const char *path = "/bin/true";
+ struct pidfd_spawn_action action = {
+ .type = PIDFD_SPAWN_ACTION_DUP2,
+ .fd = STDOUT_FILENO,
+ .newfd = STDOUT_FILENO,
+ };
+ struct pidfd_spawn_run_args args = {
+ .path = compat_ptr64(path),
+ .argv = compat_ptr64(argv),
+ .envp = compat_ptr64(envp),
+ .actions = compat_ptr64(&action),
+ .nr_actions = 1,
+ .action_size = sizeof(action),
+ };
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ int fd;
+
+ switch (bad) {
+ case BAD_PATH:
+ args.path = invalid_full_width_ptr(path);
+ break;
+ case BAD_ARGV:
+ args.argv = invalid_full_width_ptr(argv);
+ break;
+ case BAD_ENVP:
+ args.envp = invalid_full_width_ptr(envp);
+ break;
+ case BAD_ACTIONS:
+ args.actions = invalid_full_width_ptr(&action);
+ break;
+ }
+ fd = syscall(__NR_pidfd_open, 0, PIDFD_EMPTY);
+ if (fd < 0)
+ return 1;
+ errno = 0;
+ if (syscall(__NR_pidfd_spawn_run, fd, &args, sizeof(args)) != -1 ||
+ errno != EFAULT)
+ return 2;
+ errno = 0;
+ if (ioctl(fd, PIDFD_GET_INFO, &info) != -1 || errno != ESRCH)
+ return 3;
+ errno = 0;
+ if (syscall(__NR_pidfd_spawn_run, fd, &args, sizeof(args)) != -1 ||
+ errno != EBUSY)
+ return 4;
+ return close(fd) ? 5 : 0;
+}
+
+int main(int argc, char **argv)
+{
+ const char *stage = "direct path";
+ enum bad_pointer bad;
+ int fd;
+ int ret;
+
+ if (argc == 2 && !strcmp(argv[1], "--exit0"))
+ return 0;
+ if (argc == 2 && !strcmp(argv[1], "--check-empty-env"))
+ return environ[0] ? 1 : 0;
+ fd = syscall(__NR_pidfd_open, 0, PIDFD_EMPTY);
+ if (fd < 0 && (errno == EINVAL || errno == ENOSYS)) {
+ ksft_print_msg("pidfd spawn is unavailable: %s\n",
+ strerror(errno));
+ return KSFT_SKIP;
+ }
+ if (fd < 0 || close(fd)) {
+ fprintf(stderr, "compat pidfd spawn feature probe failed: %s\n",
+ strerror(errno));
+ return 1;
+ }
+ ret = run_child(false);
+ if (!ret) {
+ stage = "staged path";
+ ret = run_child(true);
+ }
+ if (!ret) {
+ stage = "empty env";
+ ret = run_empty_env();
+ }
+ for (bad = BAD_PATH; bad <= BAD_ACTIONS && !ret; bad++) {
+ stage = "bad pointer";
+ ret = reject_bad_pointer(bad);
+ }
+ if (ret)
+ fprintf(stderr, "compat pidfd spawn %s failed at step %d: %s\n",
+ stage, ret, strerror(errno));
+ return ret ? KSFT_FAIL : KSFT_PASS;
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c
new file mode 100644
index 0000000000000..c03f8d2ee69c8
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/pidfd_spawn.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+TEST(pidfd_spawn_run_execs_path_without_config)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_exec_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_execs_proc_self_exe)
+{
+ char * const argv[] = { "pidfd_spawn_exec_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ int fd;
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, "/proc/self/exe", argv, NULL, 0), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_uses_current_source_cwd)
+{
+ char * const argv[] = { "pidfd_spawn_exec_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ const char *relative;
+ char *storage;
+ int cwd_fd;
+ int fd;
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ cwd_fd = enter_self_exe_directory(&storage, &relative);
+ ASSERT_GE(cwd_fd, 0);
+ ASSERT_EQ(spawn_run_path(fd, relative, argv, NULL, 0), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+ ASSERT_EQ(leave_self_exe_directory(cwd_fd, storage), 0);
+}
+
+TEST(pidfd_spawn_run_inherits_new_non_cloexec_fd)
+{
+ const char *path = self_exe_path();
+ char fdarg[32];
+ char * const argv[] = { "pidfd_spawn_exec_test",
+ "--pidfd-spawn-helper", "fd-open", fdarg,
+ NULL };
+ int inherited_fd;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ inherited_fd = open("/dev/null", O_RDONLY);
+ ASSERT_GE(inherited_fd, 0);
+ ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", inherited_fd), 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(inherited_fd), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_honors_new_cloexec_fd)
+{
+ const char *path = self_exe_path();
+ char fdarg[32];
+ char * const argv[] = { "pidfd_spawn_exec_test",
+ "--pidfd-spawn-helper", "fd-closed", fdarg,
+ NULL };
+ int inherited_fd;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ inherited_fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(inherited_fd, 0);
+ ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", inherited_fd), 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(inherited_fd), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_accepts_null_envp)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_exec_test",
+ "--pidfd-spawn-helper", "empty-env", NULL };
+ struct pidfd_spawn_run_args args = {
+ .path = ptr_to_u64(path),
+ .argv = ptr_to_u64(argv),
+ };
+ int child_pid;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ child_pid = sys_pidfd_spawn_run(fd, &args, sizeof(args));
+ ASSERT_GT(child_pid, 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_exec_failure_publishes_waitable_child)
+{
+ const char *path = self_exe_path();
+ char * const bad_argv[] = { "missing-pidfd-spawn-helper", NULL };
+ char * const good_argv[] = { "pidfd_spawn_exec_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ int ret;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ errno = 0;
+ ret = spawn_run_path(fd, "/no/such/pidfd-spawn-helper", bad_argv,
+ NULL, 0);
+ ASSERT_EQ(ret, -1);
+ ASSERT_EQ(errno, ENOENT);
+ ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), 0);
+ ASSERT_GT(info.pid, 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 127), 0);
+ ASSERT_EQ(spawn_run_path(fd, path, good_argv, NULL, 0), -1);
+ ASSERT_EQ(errno, EBUSY);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_pre_task_failure_is_terminal)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_exec_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_spawn_run_args args = {
+ .argv = ptr_to_u64(argv),
+ .envp = ptr_to_u64(empty_envp),
+ };
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ struct epoll_event interest = {
+ .events = EPOLLIN,
+ };
+ struct epoll_event event = {};
+ struct pollfd pfd = {
+ .events = POLLIN,
+ };
+ int epoll_fd;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ ASSERT_GE(epoll_fd, 0);
+ interest.data.fd = fd;
+ ASSERT_EQ(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &interest), 0);
+ ASSERT_EQ(sys_pidfd_spawn_run(fd, &args, sizeof(args)), -1);
+ ASSERT_EQ(errno, EINVAL);
+ pfd.fd = fd;
+ ASSERT_EQ(poll(&pfd, 1, 0), 1);
+ ASSERT_EQ(pfd.revents & (POLLERR | POLLHUP), POLLERR | POLLHUP);
+ ASSERT_EQ(epoll_wait(epoll_fd, &event, 1, 0), 1);
+ ASSERT_EQ(event.data.fd, fd);
+ ASSERT_EQ(event.events & (EPOLLERR | EPOLLHUP),
+ EPOLLERR | EPOLLHUP);
+ ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), -1);
+ ASSERT_EQ(errno, ESRCH);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), -1);
+ ASSERT_EQ(errno, EBUSY);
+ ASSERT_EQ(close(epoll_fd), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_staged_path_conflict_precedes_path_uaccess)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_exec_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_spawn_run_args args = {
+ .path = ptr_to_u64(MAP_FAILED),
+ .argv = ptr_to_u64(argv),
+ .envp = ptr_to_u64(empty_envp),
+ };
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(config_path(fd, path), 0);
+ ASSERT_EQ(sys_pidfd_spawn_run(fd, &args, sizeof(args)), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), -1);
+ ASSERT_EQ(errno, ESRCH);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_exec_failure_survives_auto_reap)
+{
+ char * const argv[] = { "missing-pidfd-spawn-helper", NULL };
+ struct sigaction ignored = {
+ .sa_handler = SIG_IGN,
+ };
+ struct pidfd_info info;
+ struct sigaction old;
+ siginfo_t wait_info = {};
+ int saved_errno;
+ int ret;
+ int fd;
+
+ sigemptyset(&ignored.sa_mask);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(sigaction(SIGCHLD, &ignored, &old), 0);
+ ret = spawn_run_path(fd, "/no/such/pidfd-spawn-helper", argv, NULL, 0);
+ saved_errno = errno;
+ ASSERT_EQ(wait_pidfd_exit_info(fd, &info), 0);
+ ASSERT_EQ(sigaction(SIGCHLD, &old, NULL), 0);
+ errno = saved_errno;
+ ASSERT_EQ(ret, -1);
+ ASSERT_EQ(errno, ENOENT);
+ ASSERT_NE(info.mask & PIDFD_INFO_EXIT, 0);
+ ASSERT_EQ(info.exit_code, 127 << 8);
+ ASSERT_EQ(sys_waitid(P_PIDFD, fd, &wait_info, WEXITED | WNOHANG), -1);
+ ASSERT_EQ(errno, ECHILD);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_args_are_input_only)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_exec_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ long page_size = sysconf(_SC_PAGESIZE);
+ struct pidfd_spawn_run_args *args;
+ void *page;
+ int fd;
+
+ ASSERT_GT(page_size, 0);
+ ASSERT_NE(path, NULL);
+ page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(page, MAP_FAILED);
+ args = page;
+ *args = (struct pidfd_spawn_run_args) {
+ .path = ptr_to_u64(path),
+ .argv = ptr_to_u64(argv),
+ .envp = ptr_to_u64(empty_envp),
+ };
+ ASSERT_EQ(mprotect(page, page_size, PROT_READ), 0);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_GT(sys_pidfd_spawn_run(fd, args, sizeof(*args)), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+ ASSERT_EQ(munmap(page, page_size), 0);
+}
+
+int main(int argc, char **argv)
+{
+ int ret = helper_main(argc, argv);
+
+ if (ret >= 0)
+ return ret;
+ return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c
new file mode 100644
index 0000000000000..87c03007f793b
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/filter.h>
+#include <linux/landlock.h>
+#include <linux/pidfd_spawn.h>
+#include <linux/seccomp.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+static int
+pidfd_spawn_landlock_create_ruleset(const struct landlock_ruleset_attr *attr)
+{
+ return syscall(__NR_landlock_create_ruleset, attr, sizeof(*attr), 0);
+}
+
+static int pidfd_spawn_landlock_restrict_self(int ruleset_fd)
+{
+ return syscall(__NR_landlock_restrict_self, ruleset_fd, 0);
+}
+
+static int pidfd_spawn_landlock_worker(const char *path)
+{
+ char * const argv[] = { "pidfd_spawn_policy_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
+ };
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ int ruleset_fd;
+ int builder;
+ int ret;
+
+ ruleset_fd = pidfd_spawn_landlock_create_ruleset(&ruleset_attr);
+ if (ruleset_fd < 0)
+ return errno == ENOMSG || errno == EOPNOTSUPP ? 77 : 1;
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) ||
+ pidfd_spawn_landlock_restrict_self(ruleset_fd)) {
+ close(ruleset_fd);
+ return 2;
+ }
+ close(ruleset_fd);
+
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ return 3;
+ errno = 0;
+ ret = spawn_run_path(builder, path, argv, NULL, 0);
+ if (ret != -1 || errno != EACCES) {
+ if (!ret)
+ wait_pidfd_exit(builder, 0);
+ close(builder);
+ return 4;
+ }
+ if (ioctl(builder, PIDFD_GET_INFO, &info) || !info.pid) {
+ close(builder);
+ return 5;
+ }
+ if (wait_pidfd_exit(builder, 127)) {
+ close(builder);
+ return 6;
+ }
+ return close(builder) ? 7 : 0;
+}
+
+static int pidfd_spawn_install_seccomp(struct sock_filter *filter,
+ size_t nr_filter)
+{
+ struct sock_fprog program = {
+ .len = nr_filter,
+ .filter = filter,
+ };
+
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
+ return -errno;
+ if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program))
+ return -errno;
+ return 0;
+}
+
+static int pidfd_spawn_seccomp_setup_worker(const char *path)
+{
+ struct sock_filter filter[] = {
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+ offsetof(struct seccomp_data, nr)),
+#ifdef __NR_dup2
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_dup2, 0, 1),
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
+#endif
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_close_range, 0, 1),
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fchdir, 0, 1),
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 0, 1),
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execveat, 0, 1),
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+ };
+ struct pidfd_spawn_action actions[] = {
+ {
+ .type = PIDFD_SPAWN_ACTION_DUP2,
+ },
+ {
+ .type = PIDFD_SPAWN_ACTION_CLOSE_RANGE,
+ },
+ {
+ .type = PIDFD_SPAWN_ACTION_FCHDIR,
+ },
+ };
+ char template[] = "/tmp/pidfd-spawn-seccomp.XXXXXX";
+ char target_arg[32];
+ char closed_arg[32];
+ char cwd_arg[32];
+ char * const argv[] = { "pidfd_spawn_policy_test",
+ "--pidfd-spawn-helper", "seccomp-actions",
+ target_arg, closed_arg, cwd_arg, template, NULL };
+ int target = 200;
+ int devnull = -1;
+ int builder = -1;
+ int cwd = -1;
+ int result = 0;
+ int ret;
+
+ if (!mkdtemp(template))
+ return 1;
+ cwd = open(template, O_PATH | O_DIRECTORY);
+ if (cwd < 0) {
+ result = 2;
+ goto out;
+ }
+ devnull = open("/dev/null", O_RDONLY | O_CLOEXEC);
+ if (devnull < 0) {
+ result = 3;
+ goto out;
+ }
+ builder = sys_pidfd_empty_open();
+ if (builder < 0) {
+ result = 4;
+ goto out;
+ }
+ actions[0].fd = devnull;
+ actions[0].newfd = target;
+ actions[1].fd = devnull;
+ actions[1].newfd = devnull;
+ actions[2].fd = cwd;
+ snprintf(target_arg, sizeof(target_arg), "%d", target);
+ snprintf(closed_arg, sizeof(closed_arg), "%d", devnull);
+ snprintf(cwd_arg, sizeof(cwd_arg), "%d", cwd);
+
+ ret = pidfd_spawn_install_seccomp(filter, ARRAY_SIZE(filter));
+ if (ret) {
+ result = ret == -EINVAL || ret == -ENOSYS ? 77 : 5;
+ goto out;
+ }
+ ret = spawn_run_path(builder, path, argv, actions,
+ ARRAY_SIZE(actions));
+ if (ret) {
+ result = 6;
+ goto out;
+ }
+ if (wait_pidfd_exit(builder, 0))
+ result = 7;
+out:
+ if (builder >= 0 && close(builder) && !result)
+ result = 8;
+ if (devnull >= 0 && close(devnull) && !result)
+ result = 9;
+ if (cwd >= 0 && close(cwd) && !result)
+ result = 10;
+ if (rmdir(template) && !result)
+ result = 11;
+ return result;
+}
+
+static int pidfd_spawn_seccomp_run_worker(const char *path)
+{
+ struct sock_filter filter[] = {
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+ offsetof(struct seccomp_data, nr)),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_pidfd_spawn_run,
+ 0, 1),
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EACCES),
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+ };
+ char * const argv[] = { "pidfd_spawn_policy_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ int builder;
+ int ret;
+
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ return 1;
+ ret = pidfd_spawn_install_seccomp(filter, ARRAY_SIZE(filter));
+ if (ret)
+ return ret == -EINVAL || ret == -ENOSYS ? 77 : 2;
+ errno = 0;
+ if (spawn_run_path(builder, path, argv, NULL, 0) != -1 ||
+ errno != EACCES)
+ return 3;
+ errno = 0;
+ if (ioctl(builder, PIDFD_GET_INFO, &info) != -1 || errno != ESRCH)
+ return 4;
+ if (config_path(builder, path))
+ return 5;
+ return close(builder) ? 6 : 0;
+}
+
+TEST(pidfd_spawn_landlock_exec_denial_is_reported)
+{
+ const char *path = self_exe_path();
+ pid_t worker;
+ pid_t waited;
+ int status;
+
+ ASSERT_NE(path, NULL);
+ worker = fork();
+ ASSERT_GE(worker, 0);
+ if (!worker)
+ _exit(pidfd_spawn_landlock_worker(path));
+ waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ ASSERT_EQ(waited, worker);
+ ASSERT_TRUE(WIFEXITED(status));
+ if (WEXITSTATUS(status) == 77)
+ SKIP(return, "Landlock is unavailable");
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST(pidfd_spawn_seccomp_uses_spawn_syscall_boundary)
+{
+ const char *path = self_exe_path();
+ pid_t worker;
+ pid_t waited;
+ int status;
+
+ ASSERT_NE(path, NULL);
+ worker = fork();
+ ASSERT_GE(worker, 0);
+ if (!worker)
+ _exit(pidfd_spawn_seccomp_setup_worker(path));
+ waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ ASSERT_EQ(waited, worker);
+ ASSERT_TRUE(WIFEXITED(status));
+ if (WEXITSTATUS(status) == 77)
+ SKIP(return, "seccomp filters are unavailable");
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST(pidfd_spawn_seccomp_run_filter_keeps_builder_taskless)
+{
+ const char *path = self_exe_path();
+ pid_t worker;
+ pid_t waited;
+ int status;
+
+ ASSERT_NE(path, NULL);
+ worker = fork();
+ ASSERT_GE(worker, 0);
+ if (!worker)
+ _exit(pidfd_spawn_seccomp_run_worker(path));
+ waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ ASSERT_EQ(waited, worker);
+ ASSERT_TRUE(WIFEXITED(status));
+ if (WEXITSTATUS(status) == 77)
+ SKIP(return, "seccomp filters are unavailable");
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+int main(int argc, char **argv)
+{
+ int ret = helper_main(argc, argv);
+
+ if (ret >= 0)
+ return ret;
+ return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_race_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_race_test.c
new file mode 100644
index 0000000000000..19a82c77e5bc9
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_race_test.c
@@ -0,0 +1,923 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/pidfd_spawn.h>
+#include <linux/userfaultfd.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+struct pidfd_spawn_run_result {
+ int ret;
+ int error;
+ int wait_error;
+};
+
+struct pidfd_spawn_concurrent_run {
+ pthread_barrier_t *barrier;
+ char * const *argv;
+ const char *path;
+ int fd;
+ int ret;
+ int error;
+ int barrier_error;
+};
+
+static void *pidfd_spawn_concurrent_run(void *data)
+{
+ struct pidfd_spawn_concurrent_run *run = data;
+ int ret;
+
+ ret = pthread_barrier_wait(run->barrier);
+ if (ret && ret != PTHREAD_BARRIER_SERIAL_THREAD) {
+ run->barrier_error = ret;
+ return NULL;
+ }
+
+ errno = 0;
+ run->ret = spawn_run_path_pid(run->fd, run->path, run->argv,
+ NULL, 0);
+ run->error = errno;
+ return NULL;
+}
+
+static int wait_readable(int fd, int timeout_ms)
+{
+ struct pollfd pfd = {
+ .fd = fd,
+ .events = POLLIN,
+ };
+ int ret;
+
+ do {
+ ret = poll(&pfd, 1, timeout_ms);
+ } while (ret < 0 && errno == EINTR);
+ if (ret <= 0) {
+ if (!ret)
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ if (!(pfd.revents & (POLLIN | POLLHUP))) {
+ errno = EIO;
+ return -1;
+ }
+ return 0;
+}
+
+static int read_result(int socket, struct pidfd_spawn_run_result *result)
+{
+ ssize_t len;
+
+ if (wait_readable(socket, PIDFD_SPAWN_TIMEOUT_MS))
+ return -1;
+ do {
+ len = read(socket, result, sizeof(*result));
+ } while (len < 0 && errno == EINTR);
+ if (len != sizeof(*result)) {
+ errno = len < 0 ? errno : EIO;
+ return -1;
+ }
+ return 0;
+}
+
+static bool race_check(bool condition, int *failure_errno,
+ int *failure_line, int line)
+{
+ if (condition)
+ return true;
+
+ *failure_errno = errno;
+ *failure_line = line;
+ return false;
+}
+
+static int open_missing_userfaultfd(void *fault_page, long page_size)
+{
+ struct uffdio_register registration = {
+ .range = {
+ .start = (uintptr_t)fault_page,
+ .len = page_size,
+ },
+ .mode = UFFDIO_REGISTER_MODE_MISSING,
+ };
+ struct uffdio_api api = {
+ .api = UFFD_API,
+ };
+ int saved_errno;
+ int uffd;
+
+ uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+ if (uffd < 0)
+ return -1;
+ if (!ioctl(uffd, UFFDIO_API, &api) &&
+ !ioctl(uffd, UFFDIO_REGISTER, ®istration))
+ return uffd;
+
+ saved_errno = errno;
+ close(uffd);
+ errno = saved_errno;
+ return -1;
+}
+
+static void pidfd_spawn_blocked_exec_worker(int socket, void *fault_page,
+ long page_size, pid_t parent,
+ const char *path,
+ int expected_signal)
+{
+ char * const argv[] = { fault_page, "--pidfd-spawn-helper", "exit0",
+ NULL };
+ struct pidfd_spawn_run_result result = {};
+ sigset_t unblocked;
+ int bootstrap[2];
+ int builder;
+ int uffd;
+
+ if (expected_signal) {
+ sigemptyset(&unblocked);
+ sigaddset(&unblocked, expected_signal);
+ if (signal(expected_signal, SIG_DFL) == SIG_ERR ||
+ sigprocmask(SIG_UNBLOCK, &unblocked, NULL))
+ _exit(99);
+ }
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+ _exit(100);
+ uffd = open_missing_userfaultfd(fault_page, page_size);
+ if (uffd < 0)
+ _exit(101);
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ _exit(102);
+ bootstrap[0] = builder;
+ bootstrap[1] = uffd;
+ if (send_fds_status(socket, 0, bootstrap, ARRAY_SIZE(bootstrap)))
+ _exit(103);
+ close(uffd);
+
+ errno = 0;
+ result.ret = spawn_run_path_pid(builder, path, argv, NULL, 0);
+ result.error = errno;
+ if (result.ret > 0 && expected_signal)
+ result.wait_error = wait_pidfd_signal(builder, expected_signal);
+ else if (result.ret > 0)
+ result.wait_error = wait_pidfd_exit(builder, 0);
+ else
+ result.wait_error = -1;
+ if (write(socket, &result, sizeof(result)) != sizeof(result))
+ _exit(104);
+ close(builder);
+ close(socket);
+ _exit(0);
+}
+
+static void pidfd_spawn_cancel_worker(int socket, void *fault_page,
+ long page_size, pid_t parent,
+ const char *path)
+{
+ char * const argv[] = { fault_page, "--pidfd-spawn-helper", "exit0",
+ NULL };
+ int bootstrap[2];
+ int builder;
+ int uffd;
+
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+ _exit(110);
+ uffd = open_missing_userfaultfd(fault_page, page_size);
+ if (uffd < 0)
+ _exit(111);
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ _exit(112);
+ bootstrap[0] = builder;
+ bootstrap[1] = uffd;
+ if (send_fds_status(socket, 0, bootstrap, ARRAY_SIZE(bootstrap)))
+ _exit(113);
+ close(uffd);
+
+ spawn_run_path_pid(builder, path, argv, NULL, 0);
+ _exit(114);
+}
+
+static void pidfd_spawn_claim_worker(int socket, void *fault_page,
+ long page_size, pid_t parent)
+{
+ struct pidfd_spawn_run_result result = {
+ .wait_error = -1,
+ };
+ int bootstrap[2];
+ int builder;
+ int uffd;
+
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+ _exit(120);
+ uffd = open_missing_userfaultfd(fault_page, page_size);
+ if (uffd < 0)
+ _exit(121);
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ _exit(122);
+ bootstrap[0] = builder;
+ bootstrap[1] = uffd;
+ if (send_fds_status(socket, 0, bootstrap, ARRAY_SIZE(bootstrap)))
+ _exit(123);
+ close(uffd);
+
+ errno = 0;
+ result.ret = sys_pidfd_spawn_run(builder, fault_page,
+ sizeof(struct pidfd_spawn_run_args));
+ result.error = errno;
+ if (write(socket, &result, sizeof(result)) != sizeof(result))
+ _exit(124);
+ close(builder);
+ close(socket);
+ _exit(0);
+}
+
+#define PIDFD_SPAWN_RUN_RACE_ITERATIONS 16
+
+TEST(pidfd_spawn_concurrent_runs_have_one_winner)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_race_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ unsigned int iteration;
+
+ ASSERT_NE(path, NULL);
+ for (iteration = 0; iteration < PIDFD_SPAWN_RUN_RACE_ITERATIONS;
+ iteration++) {
+ struct pidfd_spawn_concurrent_run runs[2] = {};
+ pthread_barrier_t barrier;
+ pthread_t threads[2];
+ void *thread_ret;
+ unsigned int busy = 0;
+ unsigned int success = 0;
+ unsigned int i;
+ int ret;
+ int fd;
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(pthread_barrier_init(&barrier, NULL, 3), 0);
+ for (i = 0; i < ARRAY_SIZE(runs); i++) {
+ runs[i].barrier = &barrier;
+ runs[i].argv = argv;
+ runs[i].path = path;
+ runs[i].fd = fd;
+ ret = pthread_create(&threads[i], NULL,
+ pidfd_spawn_concurrent_run, &runs[i]);
+ ASSERT_EQ(ret, 0);
+ }
+ ret = pthread_barrier_wait(&barrier);
+ ASSERT_TRUE(!ret || ret == PTHREAD_BARRIER_SERIAL_THREAD);
+ for (i = 0; i < ARRAY_SIZE(runs); i++) {
+ ret = pthread_join_timeout(threads[i], &thread_ret,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(thread_ret, NULL);
+ ASSERT_EQ(runs[i].barrier_error, 0);
+ if (runs[i].ret > 0) {
+ success++;
+ continue;
+ }
+ ASSERT_EQ(runs[i].ret, -1);
+ ASSERT_EQ(runs[i].error, EBUSY);
+ busy++;
+ }
+ ASSERT_EQ(success, 1);
+ ASSERT_EQ(busy, 1);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(pthread_barrier_destroy(&barrier), 0);
+ ASSERT_EQ(close(fd), 0);
+ }
+}
+
+TEST(pidfd_spawn_fatal_signal_cancels_child)
+{
+ struct uffdio_zeropage zeropage = {};
+ struct uffdio_api api = {
+ .api = UFFD_API,
+ };
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ struct uffd_msg message;
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_race_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ int sockets[2] = { -1, -1 };
+ int bootstrap[2] = { -1, -1 };
+ bool worker_reaped = false;
+ long page_size;
+ void *fault_page;
+ pid_t parent;
+ pid_t worker = -1;
+ pid_t waited;
+ int failure_errno = 0;
+ int failure_line = 0;
+ int builder = -1;
+ int status = -1;
+ int probe;
+ int ret;
+ int uffd = -1;
+
+#define RACE_CHECK(condition) \
+ race_check((condition), &failure_errno, &failure_line, __LINE__)
+
+ ASSERT_NE(path, NULL);
+ page_size = sysconf(_SC_PAGESIZE);
+ ASSERT_GT(page_size, 0);
+ probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+ if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+ SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+ ASSERT_GE(probe, 0);
+ ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+ ASSERT_EQ(close(probe), 0);
+
+ fault_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(fault_page, MAP_FAILED);
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+ sockets), 0);
+ parent = getpid();
+ worker = fork();
+ ASSERT_GE(worker, 0);
+ if (!worker) {
+ close(sockets[0]);
+ pidfd_spawn_cancel_worker(sockets[1], fault_page, page_size,
+ parent, path);
+ }
+ close(sockets[1]);
+ sockets[1] = -1;
+ ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+ if (!RACE_CHECK(!ret))
+ goto cleanup;
+ ret = recv_fds_status(sockets[0], &status, bootstrap,
+ ARRAY_SIZE(bootstrap));
+ if (!RACE_CHECK(!ret))
+ goto cleanup;
+ builder = bootstrap[0];
+ uffd = bootstrap[1];
+
+ if (!RACE_CHECK(!wait_readable(uffd, PIDFD_SPAWN_TIMEOUT_MS)))
+ goto cleanup;
+ if (!RACE_CHECK(read(uffd, &message, sizeof(message)) ==
+ sizeof(message)))
+ goto cleanup;
+ if (!RACE_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+ goto cleanup;
+ if (!RACE_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+ goto cleanup;
+ if (!RACE_CHECK(info.pid > 0))
+ goto cleanup;
+ if (!RACE_CHECK(!kill(worker, SIGKILL)))
+ goto cleanup;
+ waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (!RACE_CHECK(waited == worker))
+ goto cleanup;
+ worker_reaped = true;
+ if (!RACE_CHECK(WIFSIGNALED(status)))
+ goto cleanup;
+ if (!RACE_CHECK(WTERMSIG(status) == SIGKILL))
+ goto cleanup;
+ if (!RACE_CHECK(!wait_pidfd_exit_info(builder, &info)))
+ goto cleanup;
+ if (!RACE_CHECK(WIFSIGNALED(info.exit_code)))
+ goto cleanup;
+ if (!RACE_CHECK(WTERMSIG(info.exit_code) == SIGKILL))
+ goto cleanup;
+ if (!RACE_CHECK(spawn_run_path_pid(builder, path, argv, NULL, 0) == -1))
+ goto cleanup;
+ if (!RACE_CHECK(errno == EBUSY))
+ goto cleanup;
+
+cleanup:
+ if (uffd >= 0) {
+ zeropage.range.start = (uintptr_t)fault_page;
+ zeropage.range.len = page_size;
+ ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage);
+ }
+ if (!worker_reaped && worker > 0) {
+ if (builder >= 0)
+ sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+ kill(worker, SIGKILL);
+ waited = waitpid_timeout(worker, &status,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ worker_reaped = waited == worker;
+ }
+ if (builder >= 0)
+ close(builder);
+ if (uffd >= 0)
+ close(uffd);
+ if (sockets[0] >= 0)
+ close(sockets[0]);
+ if (sockets[1] >= 0)
+ close(sockets[1]);
+ munmap(fault_page, page_size);
+ if (failure_line)
+ TH_LOG("cancellation test failed at line %d: %s", failure_line,
+ strerror(failure_errno));
+ ASSERT_EQ(failure_line, 0);
+ ASSERT_TRUE(worker_reaped);
+
+#undef RACE_CHECK
+}
+
+TEST(pidfd_spawn_default_fatal_signal_wins_during_setup)
+{
+ struct uffdio_copy copy = {};
+ struct uffdio_zeropage zeropage = {};
+ struct uffdio_api api = {
+ .api = UFFD_API,
+ };
+ struct pidfd_spawn_run_result result = {};
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ struct uffd_msg message;
+ const char *path = self_exe_path();
+ int sockets[2] = { -1, -1 };
+ int bootstrap[2] = { -1, -1 };
+ bool page_resolved = false;
+ bool worker_reaped = false;
+ long page_size;
+ void *fault_page;
+ void *source_page;
+ pid_t parent;
+ pid_t worker = -1;
+ pid_t waited;
+ int failure_errno = 0;
+ int failure_line = 0;
+ int builder = -1;
+ int child_pid = -1;
+ int status = -1;
+ int probe;
+ int ret;
+ int uffd = -1;
+
+#define RACE_CHECK(condition) \
+ race_check((condition), &failure_errno, &failure_line, __LINE__)
+
+ ASSERT_NE(path, NULL);
+ page_size = sysconf(_SC_PAGESIZE);
+ ASSERT_GT(page_size, 0);
+ probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+ if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+ SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+ ASSERT_GE(probe, 0);
+ ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+ ASSERT_EQ(close(probe), 0);
+
+ fault_page = mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(fault_page, MAP_FAILED);
+ ASSERT_EQ(mprotect(fault_page + page_size, page_size, PROT_NONE), 0);
+ source_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(source_page, MAP_FAILED);
+ memset(source_page, 'x', page_size);
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+ sockets), 0);
+ parent = getpid();
+ worker = fork();
+ ASSERT_GE(worker, 0);
+ if (!worker) {
+ close(sockets[0]);
+ pidfd_spawn_blocked_exec_worker(sockets[1], fault_page,
+ page_size, parent, path,
+ SIGSEGV);
+ }
+ close(sockets[1]);
+ sockets[1] = -1;
+ ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+ if (!RACE_CHECK(!ret))
+ goto cleanup;
+ ret = recv_fds_status(sockets[0], &status, bootstrap,
+ ARRAY_SIZE(bootstrap));
+ if (!RACE_CHECK(!ret))
+ goto cleanup;
+ builder = bootstrap[0];
+ uffd = bootstrap[1];
+
+ if (!RACE_CHECK(!wait_readable(uffd, PIDFD_SPAWN_TIMEOUT_MS)))
+ goto cleanup;
+ if (!RACE_CHECK(read(uffd, &message, sizeof(message)) ==
+ sizeof(message)))
+ goto cleanup;
+ if (!RACE_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+ goto cleanup;
+ if (!RACE_CHECK((message.arg.pagefault.address & ~(page_size - 1)) ==
+ (uintptr_t)fault_page))
+ goto cleanup;
+ if (!RACE_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+ goto cleanup;
+ if (!RACE_CHECK(info.pid > 0))
+ goto cleanup;
+ child_pid = info.pid;
+ if (!RACE_CHECK(!sys_pidfd_send_signal(builder, SIGSEGV, NULL, 0)))
+ goto cleanup;
+ copy.src = (uintptr_t)source_page;
+ copy.dst = (uintptr_t)fault_page;
+ copy.len = page_size;
+ if (!RACE_CHECK(!ioctl(uffd, UFFDIO_COPY, ©)))
+ goto cleanup;
+ page_resolved = true;
+ if (!RACE_CHECK(!read_result(sockets[0], &result)))
+ goto cleanup;
+ if (!RACE_CHECK(result.ret == child_pid))
+ goto cleanup;
+ if (!RACE_CHECK(!result.error))
+ goto cleanup;
+ if (!RACE_CHECK(!result.wait_error))
+ goto cleanup;
+ if (!RACE_CHECK(!wait_pidfd_exit_info(builder, &info)))
+ goto cleanup;
+ if (!RACE_CHECK(WIFSIGNALED(info.exit_code)))
+ goto cleanup;
+ if (!RACE_CHECK(WTERMSIG(info.exit_code) == SIGSEGV))
+ goto cleanup;
+ waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (!RACE_CHECK(waited == worker))
+ goto cleanup;
+ worker_reaped = true;
+ if (!RACE_CHECK(WIFEXITED(status)))
+ goto cleanup;
+ if (!RACE_CHECK(!WEXITSTATUS(status)))
+ goto cleanup;
+
+cleanup:
+ if (!page_resolved && uffd >= 0) {
+ zeropage.range.start = (uintptr_t)fault_page;
+ zeropage.range.len = page_size;
+ ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage);
+ }
+ if (!worker_reaped && worker > 0) {
+ if (builder >= 0)
+ sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+ kill(worker, SIGKILL);
+ waited = waitpid_timeout(worker, &status,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ worker_reaped = waited == worker;
+ }
+ if (builder >= 0)
+ close(builder);
+ if (uffd >= 0)
+ close(uffd);
+ if (sockets[0] >= 0)
+ close(sockets[0]);
+ if (sockets[1] >= 0)
+ close(sockets[1]);
+ munmap(source_page, page_size);
+ munmap(fault_page, page_size * 2);
+ if (failure_line)
+ TH_LOG("default-fatal test failed at line %d: %s",
+ failure_line, strerror(failure_errno));
+ ASSERT_EQ(failure_line, 0);
+ ASSERT_TRUE(worker_reaped);
+
+#undef RACE_CHECK
+}
+
+TEST(pidfd_spawn_claim_precedes_run_uaccess)
+{
+ struct uffdio_zeropage zeropage = {};
+ struct uffdio_api api = {
+ .api = UFFD_API,
+ };
+ struct pidfd_spawn_run_result result = {};
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ struct pollfd pidfd_poll = {
+ .events = POLLIN,
+ };
+ struct uffd_msg message;
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_race_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ int sockets[2] = { -1, -1 };
+ int bootstrap[2] = { -1, -1 };
+ bool worker_reaped = false;
+ bool page_resolved = false;
+ long page_size;
+ void *fault_page;
+ pid_t parent;
+ pid_t worker = -1;
+ pid_t waited;
+ int failure_errno = 0;
+ int failure_line = 0;
+ int builder = -1;
+ int status = -1;
+ int probe;
+ int ret;
+ int uffd = -1;
+
+#define RACE_CHECK(condition) \
+ race_check((condition), &failure_errno, &failure_line, __LINE__)
+
+ ASSERT_NE(path, NULL);
+ page_size = sysconf(_SC_PAGESIZE);
+ ASSERT_GT(page_size, 0);
+ probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+ if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+ SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+ ASSERT_GE(probe, 0);
+ ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+ ASSERT_EQ(close(probe), 0);
+
+ fault_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(fault_page, MAP_FAILED);
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+ sockets), 0);
+ parent = getpid();
+ worker = fork();
+ ASSERT_GE(worker, 0);
+ if (!worker) {
+ close(sockets[0]);
+ pidfd_spawn_claim_worker(sockets[1], fault_page, page_size,
+ parent);
+ }
+ close(sockets[1]);
+ sockets[1] = -1;
+ ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+ if (!RACE_CHECK(!ret))
+ goto cleanup;
+ ret = recv_fds_status(sockets[0], &status, bootstrap,
+ ARRAY_SIZE(bootstrap));
+ if (!RACE_CHECK(!ret))
+ goto cleanup;
+ builder = bootstrap[0];
+ uffd = bootstrap[1];
+ pidfd_poll.fd = builder;
+
+ if (!RACE_CHECK(!wait_readable(uffd, PIDFD_SPAWN_TIMEOUT_MS)))
+ goto cleanup;
+ if (!RACE_CHECK(read(uffd, &message, sizeof(message)) ==
+ sizeof(message)))
+ goto cleanup;
+ if (!RACE_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+ goto cleanup;
+ if (!RACE_CHECK(config_path(builder, path) == -1))
+ goto cleanup;
+ if (!RACE_CHECK(errno == EBUSY))
+ goto cleanup;
+ if (!RACE_CHECK(spawn_run_path_pid(builder, path, argv, NULL, 0) == -1))
+ goto cleanup;
+ if (!RACE_CHECK(errno == EBUSY))
+ goto cleanup;
+ if (!RACE_CHECK(ioctl(builder, PIDFD_GET_INFO, &info) == -1))
+ goto cleanup;
+ if (!RACE_CHECK(errno == ESRCH))
+ goto cleanup;
+ if (!RACE_CHECK(sys_pidfd_send_signal(builder, 0, NULL, 0) == -1))
+ goto cleanup;
+ if (!RACE_CHECK(errno == ESRCH))
+ goto cleanup;
+ if (!RACE_CHECK(poll(&pidfd_poll, 1, 0) == 0))
+ goto cleanup;
+
+ zeropage.range.start = (uintptr_t)fault_page;
+ zeropage.range.len = page_size;
+ if (!RACE_CHECK(!ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage)))
+ goto cleanup;
+ page_resolved = true;
+ if (!RACE_CHECK(!read_result(sockets[0], &result)))
+ goto cleanup;
+ if (!RACE_CHECK(result.ret == -1))
+ goto cleanup;
+ if (!RACE_CHECK(result.error == EINVAL))
+ goto cleanup;
+ if (!RACE_CHECK(result.wait_error == -1))
+ goto cleanup;
+ if (!RACE_CHECK(spawn_run_path_pid(builder, path, argv, NULL, 0) == -1))
+ goto cleanup;
+ if (!RACE_CHECK(errno == EBUSY))
+ goto cleanup;
+ waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (!RACE_CHECK(waited == worker))
+ goto cleanup;
+ worker_reaped = true;
+ if (!RACE_CHECK(WIFEXITED(status)))
+ goto cleanup;
+ if (!RACE_CHECK(!WEXITSTATUS(status)))
+ goto cleanup;
+
+cleanup:
+ if (!page_resolved && uffd >= 0) {
+ zeropage.range.start = (uintptr_t)fault_page;
+ zeropage.range.len = page_size;
+ ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage);
+ }
+ if (!worker_reaped && worker > 0) {
+ kill(worker, SIGKILL);
+ waited = waitpid_timeout(worker, &status,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ worker_reaped = waited == worker;
+ }
+ if (builder >= 0)
+ close(builder);
+ if (uffd >= 0)
+ close(uffd);
+ if (sockets[0] >= 0)
+ close(sockets[0]);
+ if (sockets[1] >= 0)
+ close(sockets[1]);
+ munmap(fault_page, page_size);
+ if (failure_line)
+ TH_LOG("claim test failed at line %d: %s", failure_line,
+ strerror(failure_errno));
+ ASSERT_EQ(failure_line, 0);
+ ASSERT_TRUE(worker_reaped);
+
+#undef RACE_CHECK
+}
+
+TEST(pidfd_spawn_publication_precedes_exec_completion)
+{
+ struct uffdio_zeropage zeropage = {};
+ struct uffdio_api api = {
+ .api = UFFD_API,
+ };
+ struct pidfd_spawn_run_result result = {};
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID | PIDFD_INFO_COREDUMP,
+ };
+ struct pollfd result_poll = {
+ .events = POLLIN,
+ };
+ struct uffd_msg message;
+ const char *path = self_exe_path();
+ int sockets[2] = { -1, -1 };
+ int bootstrap[2] = { -1, -1 };
+ struct stat builder_stat;
+ struct stat opened_stat;
+ bool worker_reaped = false;
+ bool page_resolved = false;
+ long page_size;
+ void *fault_page;
+ pid_t parent;
+ pid_t worker = -1;
+ pid_t waited;
+ int failure_errno = 0;
+ int failure_line = 0;
+ int opened = -1;
+ int builder = -1;
+ int status = -1;
+ int probe;
+ int ret;
+ int uffd = -1;
+
+#define RACE_CHECK(condition) \
+ race_check((condition), &failure_errno, &failure_line, __LINE__)
+
+ ASSERT_NE(path, NULL);
+ page_size = sysconf(_SC_PAGESIZE);
+ ASSERT_GT(page_size, 0);
+ probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+ if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+ SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+ ASSERT_GE(probe, 0);
+ ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+ ASSERT_EQ(close(probe), 0);
+
+ fault_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(fault_page, MAP_FAILED);
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+ sockets), 0);
+ parent = getpid();
+ worker = fork();
+ ASSERT_GE(worker, 0);
+ if (!worker) {
+ close(sockets[0]);
+ pidfd_spawn_blocked_exec_worker(sockets[1], fault_page,
+ page_size, parent, path, 0);
+ }
+ close(sockets[1]);
+ sockets[1] = -1;
+ ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+ if (!RACE_CHECK(!ret))
+ goto cleanup;
+ ret = recv_fds_status(sockets[0], &status, bootstrap,
+ ARRAY_SIZE(bootstrap));
+ if (!RACE_CHECK(!ret))
+ goto cleanup;
+ builder = bootstrap[0];
+ uffd = bootstrap[1];
+
+ if (!RACE_CHECK(!wait_readable(uffd, PIDFD_SPAWN_TIMEOUT_MS)))
+ goto cleanup;
+ if (!RACE_CHECK(read(uffd, &message, sizeof(message)) ==
+ sizeof(message)))
+ goto cleanup;
+ if (!RACE_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+ goto cleanup;
+ if (!RACE_CHECK((message.arg.pagefault.address & ~(page_size - 1)) ==
+ (uintptr_t)fault_page))
+ goto cleanup;
+ if (!RACE_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+ goto cleanup;
+ if (!RACE_CHECK(info.pid > 0))
+ goto cleanup;
+ if (!RACE_CHECK(info.mask & PIDFD_INFO_COREDUMP))
+ goto cleanup;
+ if (!RACE_CHECK(info.coredump_mask == PIDFD_COREDUMP_SKIP))
+ goto cleanup;
+ if (!RACE_CHECK(!(info.mask & (PIDFD_INFO_COREDUMP_SIGNAL |
+ PIDFD_INFO_COREDUMP_CODE))))
+ goto cleanup;
+ if (!RACE_CHECK(!info.coredump_signal && !info.coredump_code))
+ goto cleanup;
+ opened = sys_pidfd_open(info.pid, 0);
+ if (!RACE_CHECK(opened >= 0))
+ goto cleanup;
+ if (!RACE_CHECK(!fstat(builder, &builder_stat)))
+ goto cleanup;
+ if (!RACE_CHECK(!fstat(opened, &opened_stat)))
+ goto cleanup;
+ if (!RACE_CHECK(builder_stat.st_dev == opened_stat.st_dev))
+ goto cleanup;
+ if (!RACE_CHECK(builder_stat.st_ino == opened_stat.st_ino))
+ goto cleanup;
+ result_poll.fd = sockets[0];
+ if (!RACE_CHECK(poll(&result_poll, 1, 0) == 0))
+ goto cleanup;
+
+ zeropage.range.start = (uintptr_t)fault_page;
+ zeropage.range.len = page_size;
+ if (!RACE_CHECK(!ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage)))
+ goto cleanup;
+ page_resolved = true;
+ if (!RACE_CHECK(!read_result(sockets[0], &result)))
+ goto cleanup;
+ if (!RACE_CHECK(result.ret == info.pid))
+ goto cleanup;
+ if (!RACE_CHECK(!result.error))
+ goto cleanup;
+ if (!RACE_CHECK(!result.wait_error))
+ goto cleanup;
+ waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (!RACE_CHECK(waited == worker))
+ goto cleanup;
+ worker_reaped = true;
+ if (!RACE_CHECK(WIFEXITED(status)))
+ goto cleanup;
+ if (!RACE_CHECK(!WEXITSTATUS(status)))
+ goto cleanup;
+
+cleanup:
+ if (!page_resolved && uffd >= 0) {
+ zeropage.range.start = (uintptr_t)fault_page;
+ zeropage.range.len = page_size;
+ ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage);
+ }
+ if (!worker_reaped && worker > 0) {
+ if (builder >= 0)
+ sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+ kill(worker, SIGKILL);
+ waited = waitpid_timeout(worker, &status,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ worker_reaped = waited == worker;
+ }
+ if (opened >= 0)
+ close(opened);
+ if (builder >= 0)
+ close(builder);
+ if (uffd >= 0)
+ close(uffd);
+ if (sockets[0] >= 0)
+ close(sockets[0]);
+ if (sockets[1] >= 0)
+ close(sockets[1]);
+ munmap(fault_page, page_size);
+ if (failure_line)
+ TH_LOG("publication test failed at line %d: %s", failure_line,
+ strerror(failure_errno));
+ ASSERT_EQ(failure_line, 0);
+ ASSERT_TRUE(worker_reaped);
+
+#undef RACE_CHECK
+}
+
+int main(int argc, char **argv)
+{
+ int ret = helper_main(argc, argv);
+
+ if (ret >= 0)
+ return ret;
+ return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_security_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_security_test.c
new file mode 100644
index 0000000000000..4da23384e8b79
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_security_test.c
@@ -0,0 +1,1242 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/pidfd_spawn.h>
+#include <linux/userfaultfd.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/fsuid.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/ptrace.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+struct pidfd_spawn_run_result {
+ int ret;
+ int error;
+ int wait_error;
+};
+
+struct pidfd_spawn_cred_test {
+ const char *path;
+ int builder;
+ int setup_error;
+ int restore_error;
+ int config_ret;
+ int config_error;
+ int run_ret;
+ int run_error;
+};
+
+struct pidfd_spawn_pidns_test {
+ const char *path;
+ int builder;
+ int setup_error;
+ int config_ret;
+ int config_error;
+ int run_ret;
+ int run_error;
+};
+
+#define PIDFD_SPAWN_UNPRIVILEGED_ID 65534
+#define PIDFD_SPAWN_SLEEP_PATH "/bin/sleep"
+
+static void *pidfd_spawn_changed_cred_thread(void *data)
+{
+ struct pidfd_spawn_cred_test *test = data;
+ char * const argv[] = { "pidfd_spawn_security_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ uid_t original;
+ uid_t target;
+
+ original = setfsuid((uid_t)-1);
+ target = original ? 0 : 1;
+ setfsuid(target);
+ if (setfsuid((uid_t)-1) != target) {
+ test->setup_error = EPERM;
+ return NULL;
+ }
+
+ errno = 0;
+ test->config_ret = config_path(test->builder, test->path);
+ test->config_error = errno;
+ errno = 0;
+ test->run_ret = spawn_run_path_pid(test->builder, test->path, argv,
+ NULL, 0);
+ test->run_error = errno;
+
+ setfsuid(original);
+ if (setfsuid((uid_t)-1) != original)
+ test->restore_error = EPERM;
+ return NULL;
+}
+
+static void *pidfd_spawn_changed_pidns_thread(void *data)
+{
+ struct pidfd_spawn_pidns_test *test = data;
+ char * const argv[] = { "pidfd_spawn_security_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+
+ if (unshare(CLONE_NEWPID)) {
+ test->setup_error = errno;
+ return NULL;
+ }
+
+ errno = 0;
+ test->config_ret = config_path(test->builder, test->path);
+ test->config_error = errno;
+ errno = 0;
+ test->run_ret = spawn_run_path_pid(test->builder, test->path, argv,
+ NULL, 0);
+ test->run_error = errno;
+ return NULL;
+}
+
+static int wait_readable(int fd, int timeout_ms)
+{
+ struct pollfd pfd = {
+ .fd = fd,
+ .events = POLLIN,
+ };
+ int ret;
+
+ do {
+ ret = poll(&pfd, 1, timeout_ms);
+ } while (ret < 0 && errno == EINTR);
+ if (ret <= 0) {
+ if (!ret)
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ if (!(pfd.revents & (POLLIN | POLLHUP))) {
+ errno = EIO;
+ return -1;
+ }
+ return 0;
+}
+
+static int read_result(int socket, struct pidfd_spawn_run_result *result)
+{
+ ssize_t len;
+
+ if (wait_readable(socket, PIDFD_SPAWN_TIMEOUT_MS))
+ return -1;
+ do {
+ len = read(socket, result, sizeof(*result));
+ } while (len < 0 && errno == EINTR);
+ if (len != sizeof(*result)) {
+ errno = len < 0 ? errno : EIO;
+ return -1;
+ }
+ return 0;
+}
+
+static bool security_check(bool condition, int *failure_errno,
+ int *failure_line, int line)
+{
+ if (condition)
+ return true;
+
+ *failure_errno = errno;
+ *failure_line = line;
+ return false;
+}
+
+static int open_missing_userfaultfd(void *fault_page, long page_size)
+{
+ struct uffdio_register registration = {
+ .range = {
+ .start = (uintptr_t)fault_page,
+ .len = page_size,
+ },
+ .mode = UFFDIO_REGISTER_MODE_MISSING,
+ };
+ struct uffdio_api api = {
+ .api = UFFD_API,
+ };
+ int saved_errno;
+ int uffd;
+
+ uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+ if (uffd < 0)
+ return -1;
+ if (!ioctl(uffd, UFFDIO_API, &api) &&
+ !ioctl(uffd, UFFDIO_REGISTER, ®istration))
+ return uffd;
+
+ saved_errno = errno;
+ close(uffd);
+ errno = saved_errno;
+ return -1;
+}
+
+static int pidfd_spawn_ptrace_preflight(const char *path)
+{
+ int ready_pipe[2] = { -1, -1 };
+ int release_pipe[2] = { -1, -1 };
+ bool attached = false;
+ char ready_arg[32];
+ char release_arg[32];
+ char byte = 1;
+ pid_t child = -1;
+ pid_t waited;
+ int saved_errno = 0;
+ int status = -1;
+ int ret = -1;
+
+ if (pipe(ready_pipe) || pipe(release_pipe))
+ goto out;
+ child = fork();
+ if (child < 0)
+ goto out;
+ if (!child) {
+ close(ready_pipe[0]);
+ close(release_pipe[1]);
+ if (snprintf(ready_arg, sizeof(ready_arg), "%d", ready_pipe[1]) < 0 ||
+ snprintf(release_arg, sizeof(release_arg), "%d",
+ release_pipe[0]) < 0)
+ _exit(120);
+ execl(path, path, "--pidfd-spawn-helper", "ready-wait-fd",
+ ready_arg, release_arg, NULL);
+ _exit(121);
+ }
+ close(ready_pipe[1]);
+ ready_pipe[1] = -1;
+ close(release_pipe[0]);
+ release_pipe[0] = -1;
+ if (wait_readable(ready_pipe[0], PIDFD_SPAWN_TIMEOUT_MS))
+ goto out;
+ if (read(ready_pipe[0], &byte, sizeof(byte)) != sizeof(byte))
+ goto out;
+ if (ptrace(PTRACE_SEIZE, child, NULL, NULL))
+ goto out;
+ attached = true;
+ if (ptrace(PTRACE_INTERRUPT, child, NULL, NULL))
+ goto out;
+ waited = waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (waited != child || !WIFSTOPPED(status)) {
+ errno = EIO;
+ goto out;
+ }
+ if (ptrace(PTRACE_DETACH, child, NULL, NULL))
+ goto out;
+ attached = false;
+ if (write(release_pipe[1], &byte, sizeof(byte)) != sizeof(byte))
+ goto out;
+ waited = waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (waited != child || !WIFEXITED(status) || WEXITSTATUS(status)) {
+ errno = EIO;
+ goto out;
+ }
+ child = -1;
+ ret = 0;
+out:
+ saved_errno = errno;
+ if (attached && child > 0) {
+ ptrace(PTRACE_INTERRUPT, child, NULL, NULL);
+ waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ ptrace(PTRACE_DETACH, child, NULL, NULL);
+ }
+ if (child > 0) {
+ kill(child, SIGKILL);
+ waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ }
+ if (ready_pipe[0] >= 0)
+ close(ready_pipe[0]);
+ if (ready_pipe[1] >= 0)
+ close(ready_pipe[1]);
+ if (release_pipe[0] >= 0)
+ close(release_pipe[0]);
+ if (release_pipe[1] >= 0)
+ close(release_pipe[1]);
+ errno = saved_errno;
+ return ret;
+}
+
+static void pidfd_spawn_ptrace_worker(int socket, void *fault_page,
+ long page_size, pid_t parent,
+ const char *path, int wait_fd,
+ int release_fd)
+{
+ char fdarg[32];
+ char * const argv[] = { fault_page, "--pidfd-spawn-helper", "wait-fd",
+ fdarg, NULL };
+ struct pidfd_spawn_run_result result = {};
+ int bootstrap[2];
+ int builder;
+ int uffd;
+ int len;
+
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+ _exit(100);
+ close(release_fd);
+ len = snprintf(fdarg, sizeof(fdarg), "%d", wait_fd);
+ if (len < 0 || len >= sizeof(fdarg))
+ _exit(101);
+ uffd = open_missing_userfaultfd(fault_page, page_size);
+ if (uffd < 0)
+ _exit(102);
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ _exit(103);
+ bootstrap[0] = builder;
+ bootstrap[1] = uffd;
+ if (send_fds_status(socket, 0, bootstrap, ARRAY_SIZE(bootstrap)))
+ _exit(104);
+ close(uffd);
+
+ errno = 0;
+ result.ret = spawn_run_path_pid(builder, path, argv, NULL, 0);
+ result.error = errno;
+ if (write(socket, &result, sizeof(result)) != sizeof(result))
+ _exit(105);
+ if (result.ret < 0)
+ _exit(106);
+ if (wait_pidfd_exit(builder, 0))
+ _exit(107);
+ close(wait_fd);
+ close(builder);
+ close(socket);
+ _exit(0);
+}
+
+static int pidfd_spawn_unprivileged_preflight(void)
+{
+ pid_t child;
+ pid_t waited;
+ int status;
+
+ child = fork();
+ if (child < 0)
+ return -1;
+ if (!child) {
+ if (setresgid(PIDFD_SPAWN_UNPRIVILEGED_ID,
+ PIDFD_SPAWN_UNPRIVILEGED_ID,
+ PIDFD_SPAWN_UNPRIVILEGED_ID) ||
+ setresuid(PIDFD_SPAWN_UNPRIVILEGED_ID,
+ PIDFD_SPAWN_UNPRIVILEGED_ID,
+ PIDFD_SPAWN_UNPRIVILEGED_ID))
+ _exit(77);
+ if (access(PIDFD_SPAWN_SLEEP_PATH, X_OK))
+ _exit(78);
+ if (prctl(PR_SET_DUMPABLE, 1))
+ _exit(79);
+ _exit(0);
+ }
+
+ waited = waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (waited != child) {
+ kill(child, SIGKILL);
+ waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ if (!WIFEXITED(status)) {
+ errno = EIO;
+ return -1;
+ }
+ switch (WEXITSTATUS(status)) {
+ case 0:
+ return 0;
+ case 77:
+ errno = EPERM;
+ break;
+ case 78:
+ errno = EACCES;
+ break;
+ default:
+ errno = EINVAL;
+ break;
+ }
+ return -1;
+}
+
+static void pidfd_spawn_proc_worker(int socket, void *fault_page,
+ long page_size, pid_t parent)
+{
+ char * const argv[] = { "sleep", fault_page, NULL };
+ struct pidfd_spawn_run_result result = {
+ .wait_error = -1,
+ };
+ char exec_path[64];
+ int bootstrap[2];
+ int builder;
+ int exec_fd;
+ int ret;
+ int uffd;
+
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+ _exit(130);
+ uffd = open_missing_userfaultfd(fault_page, page_size);
+ if (uffd < 0)
+ _exit(131);
+ if (setresgid(PIDFD_SPAWN_UNPRIVILEGED_ID,
+ PIDFD_SPAWN_UNPRIVILEGED_ID,
+ PIDFD_SPAWN_UNPRIVILEGED_ID) ||
+ setresuid(PIDFD_SPAWN_UNPRIVILEGED_ID,
+ PIDFD_SPAWN_UNPRIVILEGED_ID,
+ PIDFD_SPAWN_UNPRIVILEGED_ID) ||
+ prctl(PR_SET_DUMPABLE, 1) ||
+ prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+ _exit(132);
+ /*
+ * Force the child to instantiate its numeric proc dentry before argv
+ * copying reaches the userfaultfd stall. Keep the fd non-CLOEXEC so the
+ * proc fd path remains usable throughout executable lookup.
+ */
+ exec_fd = open(PIDFD_SPAWN_SLEEP_PATH, O_PATH);
+ if (exec_fd < 0)
+ _exit(138);
+ ret = snprintf(exec_path, sizeof(exec_path), "/proc/self/fd/%d", exec_fd);
+ if (ret < 0 || ret >= (int)sizeof(exec_path))
+ _exit(139);
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ _exit(133);
+ bootstrap[0] = builder;
+ bootstrap[1] = uffd;
+ if (send_fds_status(socket, 0, bootstrap, ARRAY_SIZE(bootstrap)))
+ _exit(134);
+ close(uffd);
+
+ errno = 0;
+ result.ret = spawn_run_path_pid(builder, exec_path, argv, NULL, 0);
+ result.error = errno;
+ close(exec_fd);
+ if (write(socket, &result, sizeof(result)) != sizeof(result))
+ _exit(135);
+ close(socket);
+ if (result.ret < 0)
+ _exit(136);
+ if (wait_pidfd_signal(builder, SIGKILL))
+ _exit(137);
+ close(builder);
+ _exit(0);
+}
+
+static void pidfd_spawn_traced_source_worker(int socket, pid_t parent,
+ const char *path)
+{
+ char * const argv[] = { "pidfd_spawn_security_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_spawn_run_result result;
+ char command;
+ ssize_t len;
+ int builder;
+ int i;
+
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+ _exit(120);
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ _exit(121);
+ if (send_fds_status(socket, 0, &builder, 1))
+ _exit(122);
+
+ for (i = 0; i < 2; i++) {
+ do {
+ len = read(socket, &command, sizeof(command));
+ } while (len < 0 && errno == EINTR);
+ if (len != sizeof(command))
+ _exit(123);
+
+ result = (struct pidfd_spawn_run_result) {};
+ errno = 0;
+ result.ret = spawn_run_path_pid(builder, path, argv, NULL, 0);
+ result.error = errno;
+ if (result.ret > 0)
+ result.wait_error = wait_pidfd_exit(builder, 0);
+ else
+ result.wait_error = -1;
+ if (write(socket, &result, sizeof(result)) != sizeof(result))
+ _exit(124);
+ }
+
+ close(builder);
+ close(socket);
+ _exit(0);
+}
+
+static void pidfd_spawn_authority_worker(int socket, pid_t parent,
+ const char *path)
+{
+ char * const argv[] = { "pidfd_spawn_security_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_spawn_run_result result = {};
+ char command;
+ ssize_t len;
+ int builder;
+
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+ _exit(130);
+ builder = sys_pidfd_empty_open();
+ if (builder < 0)
+ _exit(131);
+ if (send_fds_status(socket, 0, &builder, 1))
+ _exit(132);
+ do {
+ len = read(socket, &command, sizeof(command));
+ } while (len < 0 && errno == EINTR);
+ if (len != sizeof(command))
+ _exit(133);
+
+ errno = 0;
+ result.ret = spawn_run_path_pid(builder, path, argv, NULL, 0);
+ result.error = errno;
+ if (result.ret > 0)
+ result.wait_error = wait_pidfd_exit(builder, 0);
+ else
+ result.wait_error = -1;
+ if (write(socket, &result, sizeof(result)) != sizeof(result))
+ _exit(134);
+ close(builder);
+ close(socket);
+ _exit(0);
+}
+
+TEST(pidfd_spawn_embryonic_task_denies_ptrace)
+{
+ struct uffdio_zeropage zeropage = {};
+ struct uffdio_api api = {
+ .api = UFFD_API,
+ };
+ struct pidfd_spawn_run_result result = {};
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ struct uffd_msg message;
+ const char *path = self_exe_path();
+ int release_pipe[2] = { -1, -1 };
+ int sockets[2] = { -1, -1 };
+ int bootstrap[2] = { -1, -1 };
+ bool worker_reaped = false;
+ bool page_resolved = false;
+ bool child_released = false;
+ bool attached = false;
+ long page_size;
+ void *fault_page;
+ pid_t parent;
+ pid_t worker = -1;
+ pid_t waited;
+ int failure_errno = 0;
+ int failure_line = 0;
+ int builder = -1;
+ int child_pid = -1;
+ int child_wait_fd = -1;
+ int received_fd = -1;
+ int status = -1;
+ int probe;
+ int ret;
+ int uffd = -1;
+ char byte = 1;
+
+#define SECURITY_CHECK(condition) \
+ security_check((condition), &failure_errno, &failure_line, __LINE__)
+
+ ASSERT_NE(path, NULL);
+ ret = pidfd_spawn_ptrace_preflight(path);
+ if (ret && errno == EPERM)
+ SKIP(return, "ambient ptrace policy denies normal attach");
+ ASSERT_EQ(ret, 0);
+ page_size = sysconf(_SC_PAGESIZE);
+ ASSERT_GT(page_size, 0);
+ probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+ if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+ SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+ ASSERT_GE(probe, 0);
+ ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+ ASSERT_EQ(close(probe), 0);
+
+ fault_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(fault_page, MAP_FAILED);
+ ASSERT_EQ(pipe(release_pipe), 0);
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+ sockets), 0);
+ parent = getpid();
+ worker = fork();
+ ASSERT_GE(worker, 0);
+ if (!worker) {
+ close(sockets[0]);
+ pidfd_spawn_ptrace_worker(sockets[1], fault_page, page_size,
+ parent, path, release_pipe[0],
+ release_pipe[1]);
+ }
+ child_wait_fd = release_pipe[0];
+ close(sockets[1]);
+ sockets[1] = -1;
+ close(release_pipe[0]);
+ release_pipe[0] = -1;
+ ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+ if (!SECURITY_CHECK(!ret))
+ goto cleanup;
+ ret = recv_fds_status(sockets[0], &status, bootstrap,
+ ARRAY_SIZE(bootstrap));
+ if (!SECURITY_CHECK(!ret))
+ goto cleanup;
+ builder = bootstrap[0];
+ uffd = bootstrap[1];
+
+ if (!SECURITY_CHECK(!wait_readable(uffd,
+ PIDFD_SPAWN_TIMEOUT_MS)))
+ goto cleanup;
+ if (!SECURITY_CHECK(read(uffd, &message, sizeof(message)) ==
+ sizeof(message)))
+ goto cleanup;
+ if (!SECURITY_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+ goto cleanup;
+ if (!SECURITY_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+ goto cleanup;
+ if (!SECURITY_CHECK(info.pid > 0))
+ goto cleanup;
+ child_pid = info.pid;
+
+ errno = 0;
+ ret = ptrace(PTRACE_SEIZE, child_pid, NULL, NULL);
+ if (!ret)
+ attached = true;
+ if (!SECURITY_CHECK(ret == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(errno == EPERM))
+ goto cleanup;
+ errno = 0;
+ received_fd = sys_pidfd_getfd(builder, child_wait_fd, 0);
+ if (!SECURITY_CHECK(received_fd == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(errno == EPERM))
+ goto cleanup;
+
+ zeropage.range.start = (uintptr_t)fault_page;
+ zeropage.range.len = page_size;
+ if (!SECURITY_CHECK(!ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage)))
+ goto cleanup;
+ page_resolved = true;
+ if (!SECURITY_CHECK(!read_result(sockets[0], &result)))
+ goto cleanup;
+ if (!SECURITY_CHECK(result.ret == child_pid))
+ goto cleanup;
+ if (!SECURITY_CHECK(!result.error))
+ goto cleanup;
+ received_fd = sys_pidfd_getfd(builder, child_wait_fd, 0);
+ if (!SECURITY_CHECK(received_fd >= 0))
+ goto cleanup;
+ if (!SECURITY_CHECK(!close(received_fd)))
+ goto cleanup;
+ received_fd = -1;
+
+ if (!SECURITY_CHECK(!ptrace(PTRACE_SEIZE, child_pid, NULL, NULL)))
+ goto cleanup;
+ attached = true;
+ if (!SECURITY_CHECK(!ptrace(PTRACE_INTERRUPT, child_pid, NULL, NULL)))
+ goto cleanup;
+ waited = waitpid_timeout(child_pid, &status,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ if (!SECURITY_CHECK(waited == child_pid))
+ goto cleanup;
+ if (!SECURITY_CHECK(WIFSTOPPED(status)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!ptrace(PTRACE_DETACH, child_pid, NULL, NULL)))
+ goto cleanup;
+ attached = false;
+ if (!SECURITY_CHECK(write(release_pipe[1], &byte, sizeof(byte)) ==
+ sizeof(byte)))
+ goto cleanup;
+ child_released = true;
+ waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (!SECURITY_CHECK(waited == worker))
+ goto cleanup;
+ worker_reaped = true;
+ if (!SECURITY_CHECK(WIFEXITED(status)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!WEXITSTATUS(status)))
+ goto cleanup;
+
+cleanup:
+ if (attached && child_pid > 0) {
+ ptrace(PTRACE_INTERRUPT, child_pid, NULL, NULL);
+ waitpid_timeout(child_pid, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ ptrace(PTRACE_DETACH, child_pid, NULL, NULL);
+ }
+ if (!page_resolved && uffd >= 0) {
+ zeropage.range.start = (uintptr_t)fault_page;
+ zeropage.range.len = page_size;
+ ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage);
+ }
+ if (!child_released && release_pipe[1] >= 0)
+ write(release_pipe[1], &byte, sizeof(byte));
+ if (!worker_reaped && worker > 0) {
+ if (builder >= 0)
+ sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+ kill(worker, SIGKILL);
+ waited = waitpid_timeout(worker, &status,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ worker_reaped = waited == worker;
+ }
+ if (builder >= 0)
+ close(builder);
+ if (received_fd >= 0)
+ close(received_fd);
+ if (uffd >= 0)
+ close(uffd);
+ if (sockets[0] >= 0)
+ close(sockets[0]);
+ if (sockets[1] >= 0)
+ close(sockets[1]);
+ if (release_pipe[0] >= 0)
+ close(release_pipe[0]);
+ if (release_pipe[1] >= 0)
+ close(release_pipe[1]);
+ munmap(fault_page, page_size);
+ if (failure_line)
+ TH_LOG("ptrace test failed at line %d: %s", failure_line,
+ strerror(failure_errno));
+ ASSERT_EQ(failure_line, 0);
+ ASSERT_TRUE(worker_reaped);
+
+#undef SECURITY_CHECK
+}
+
+TEST(pidfd_spawn_embryonic_task_is_hidden_from_proc)
+{
+ struct uffdio_copy uffd_copy = {};
+ struct uffdio_api api = {
+ .api = UFFD_API,
+ };
+ struct pidfd_spawn_run_result result = {};
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ struct uffd_msg message;
+ struct stat proc_dir_stat;
+ struct stat proc_mem_stat;
+ int sockets[2] = { -1, -1 };
+ int bootstrap[2] = { -1, -1 };
+ bool child_killed = false;
+ bool worker_reaped = false;
+ bool page_resolved = false;
+ char proc_dir[64];
+ char proc_cmdline[64];
+ char proc_mem[64];
+ char cmdline[32];
+ char *fill_page;
+ long page_size;
+ void *fault_page;
+ void *mapping;
+ pid_t parent;
+ pid_t worker = -1;
+ pid_t waited;
+ int failure_errno = 0;
+ int failure_line = 0;
+ int builder = -1;
+ int cmdline_fd = -1;
+ int status = -1;
+ int probe;
+ int ret;
+ int uffd = -1;
+
+#define SECURITY_CHECK(condition) \
+ security_check((condition), &failure_errno, &failure_line, __LINE__)
+
+ if (geteuid())
+ SKIP(return, "test requires root to create a distinct task uid");
+ ret = pidfd_spawn_unprivileged_preflight();
+ if (ret && (errno == EPERM || errno == EACCES || errno == EINVAL))
+ SKIP(return, "unprivileged proc ownership setup is unavailable");
+ ASSERT_EQ(ret, 0);
+ page_size = sysconf(_SC_PAGESIZE);
+ ASSERT_GT(page_size, 0);
+ probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+ if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+ SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+ ASSERT_GE(probe, 0);
+ ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+ ASSERT_EQ(close(probe), 0);
+
+ mapping = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+ fault_page = mapping;
+ fill_page = (char *)mapping + page_size;
+ memcpy(fill_page, "30", 3);
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+ sockets), 0);
+ parent = getpid();
+ worker = fork();
+ ASSERT_GE(worker, 0);
+ if (!worker) {
+ close(sockets[0]);
+ pidfd_spawn_proc_worker(sockets[1], fault_page, page_size,
+ parent);
+ }
+ close(sockets[1]);
+ sockets[1] = -1;
+ ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+ if (!SECURITY_CHECK(!ret))
+ goto cleanup;
+ ret = recv_fds_status(sockets[0], &status, bootstrap,
+ ARRAY_SIZE(bootstrap));
+ if (!SECURITY_CHECK(!ret))
+ goto cleanup;
+ builder = bootstrap[0];
+ uffd = bootstrap[1];
+
+ if (!SECURITY_CHECK(!wait_readable(uffd,
+ PIDFD_SPAWN_TIMEOUT_MS)))
+ goto cleanup;
+ if (!SECURITY_CHECK(read(uffd, &message, sizeof(message)) ==
+ sizeof(message)))
+ goto cleanup;
+ if (!SECURITY_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+ goto cleanup;
+ if (!SECURITY_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+ goto cleanup;
+ if (!SECURITY_CHECK(info.pid > 0))
+ goto cleanup;
+ ret = snprintf(proc_dir, sizeof(proc_dir), "/proc/%u", info.pid);
+ if (!SECURITY_CHECK(ret > 0 && ret < sizeof(proc_dir)))
+ goto cleanup;
+ ret = snprintf(proc_cmdline, sizeof(proc_cmdline), "/proc/%u/cmdline",
+ info.pid);
+ if (!SECURITY_CHECK(ret > 0 && ret < sizeof(proc_cmdline)))
+ goto cleanup;
+ ret = snprintf(proc_mem, sizeof(proc_mem), "/proc/%u/mem", info.pid);
+ if (!SECURITY_CHECK(ret > 0 && ret < sizeof(proc_mem)))
+ goto cleanup;
+ errno = 0;
+ if (!SECURITY_CHECK(stat(proc_dir, &proc_dir_stat) == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(errno == ENOENT))
+ goto cleanup;
+ errno = 0;
+ cmdline_fd = open(proc_cmdline, O_RDONLY | O_CLOEXEC);
+ if (!SECURITY_CHECK(cmdline_fd == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(errno == ENOENT))
+ goto cleanup;
+
+ uffd_copy.dst = (uintptr_t)fault_page;
+ uffd_copy.src = (uintptr_t)fill_page;
+ uffd_copy.len = page_size;
+ if (!SECURITY_CHECK(!ioctl(uffd, UFFDIO_COPY, &uffd_copy)))
+ goto cleanup;
+ page_resolved = true;
+ if (!SECURITY_CHECK(!read_result(sockets[0], &result)))
+ goto cleanup;
+ if (!SECURITY_CHECK(result.ret == info.pid))
+ goto cleanup;
+ if (!SECURITY_CHECK(!result.error))
+ goto cleanup;
+ if (!SECURITY_CHECK(result.wait_error == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(!stat(proc_dir, &proc_dir_stat)))
+ goto cleanup;
+ if (!SECURITY_CHECK(proc_dir_stat.st_uid ==
+ PIDFD_SPAWN_UNPRIVILEGED_ID))
+ goto cleanup;
+ if (!SECURITY_CHECK(proc_dir_stat.st_gid ==
+ PIDFD_SPAWN_UNPRIVILEGED_ID))
+ goto cleanup;
+ if (!SECURITY_CHECK(!stat(proc_mem, &proc_mem_stat)))
+ goto cleanup;
+ if (!SECURITY_CHECK(proc_mem_stat.st_uid ==
+ PIDFD_SPAWN_UNPRIVILEGED_ID))
+ goto cleanup;
+ if (!SECURITY_CHECK(proc_mem_stat.st_gid ==
+ PIDFD_SPAWN_UNPRIVILEGED_ID))
+ goto cleanup;
+ cmdline_fd = open(proc_cmdline, O_RDONLY | O_CLOEXEC);
+ if (!SECURITY_CHECK(cmdline_fd >= 0))
+ goto cleanup;
+ ret = read(cmdline_fd, cmdline, sizeof(cmdline));
+ if (!SECURITY_CHECK(ret >= (int)sizeof("sleep")))
+ goto cleanup;
+ if (!SECURITY_CHECK(!memcmp(cmdline, "sleep", sizeof("sleep"))))
+ goto cleanup;
+ if (!SECURITY_CHECK(!close(cmdline_fd)))
+ goto cleanup;
+ cmdline_fd = -1;
+ if (!SECURITY_CHECK(!sys_pidfd_send_signal(builder, SIGKILL, NULL, 0)))
+ goto cleanup;
+ child_killed = true;
+ waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (!SECURITY_CHECK(waited == worker))
+ goto cleanup;
+ worker_reaped = true;
+ if (!SECURITY_CHECK(WIFEXITED(status)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!WEXITSTATUS(status)))
+ goto cleanup;
+
+cleanup:
+ if (!page_resolved && uffd >= 0) {
+ uffd_copy.dst = (uintptr_t)fault_page;
+ uffd_copy.src = (uintptr_t)fill_page;
+ uffd_copy.len = page_size;
+ ioctl(uffd, UFFDIO_COPY, &uffd_copy);
+ }
+ if (!child_killed && builder >= 0)
+ sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+ if (!worker_reaped && worker > 0) {
+ kill(worker, SIGKILL);
+ waited = waitpid_timeout(worker, &status,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ worker_reaped = waited == worker;
+ }
+ if (builder >= 0)
+ close(builder);
+ if (cmdline_fd >= 0)
+ close(cmdline_fd);
+ if (uffd >= 0)
+ close(uffd);
+ if (sockets[0] >= 0)
+ close(sockets[0]);
+ if (sockets[1] >= 0)
+ close(sockets[1]);
+ munmap(mapping, 2 * page_size);
+ if (failure_line)
+ TH_LOG("proc visibility test failed at line %d: %s", failure_line,
+ strerror(failure_errno));
+ ASSERT_EQ(failure_line, 0);
+ ASSERT_TRUE(worker_reaped);
+
+#undef SECURITY_CHECK
+}
+
+TEST(pidfd_spawn_traced_source_does_not_consume_builder)
+{
+ struct pidfd_spawn_run_result result = {};
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ const char *path = self_exe_path();
+ int sockets[2] = { -1, -1 };
+ bool source_reaped = false;
+ bool attached = false;
+ pid_t source = -1;
+ pid_t waited;
+ int failure_errno = 0;
+ int failure_line = 0;
+ int builder = -1;
+ int status = -1;
+ char command = 1;
+ int ret;
+
+#define SECURITY_CHECK(condition) \
+ security_check((condition), &failure_errno, &failure_line, __LINE__)
+
+ ASSERT_NE(path, NULL);
+ ret = pidfd_spawn_ptrace_preflight(path);
+ if (ret && errno == EPERM)
+ SKIP(return, "ambient ptrace policy denies normal attach");
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+ sockets), 0);
+ source = fork();
+ ASSERT_GE(source, 0);
+ if (!source) {
+ close(sockets[0]);
+ pidfd_spawn_traced_source_worker(sockets[1], getppid(), path);
+ }
+ close(sockets[1]);
+ sockets[1] = -1;
+ ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+ if (!SECURITY_CHECK(!ret))
+ goto cleanup;
+ ret = recv_fds_status(sockets[0], &status, &builder, 1);
+ if (!SECURITY_CHECK(!ret))
+ goto cleanup;
+
+ if (!SECURITY_CHECK(!ptrace(PTRACE_SEIZE, source, NULL, NULL)))
+ goto cleanup;
+ attached = true;
+ if (!SECURITY_CHECK(write(sockets[0], &command, sizeof(command)) ==
+ sizeof(command)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!read_result(sockets[0], &result)))
+ goto cleanup;
+ if (!SECURITY_CHECK(result.ret == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(result.error == EPERM))
+ goto cleanup;
+ if (!SECURITY_CHECK(result.wait_error == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(ioctl(builder, PIDFD_GET_INFO, &info) == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(errno == ESRCH))
+ goto cleanup;
+
+ if (!SECURITY_CHECK(!ptrace(PTRACE_INTERRUPT, source, NULL, NULL)))
+ goto cleanup;
+ waited = waitpid_timeout(source, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (!SECURITY_CHECK(waited == source))
+ goto cleanup;
+ if (!SECURITY_CHECK(WIFSTOPPED(status)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!ptrace(PTRACE_DETACH, source, NULL, NULL)))
+ goto cleanup;
+ attached = false;
+ if (!SECURITY_CHECK(write(sockets[0], &command, sizeof(command)) ==
+ sizeof(command)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!read_result(sockets[0], &result)))
+ goto cleanup;
+ if (!SECURITY_CHECK(result.ret > 0))
+ goto cleanup;
+ if (!SECURITY_CHECK(!result.error))
+ goto cleanup;
+ if (!SECURITY_CHECK(!result.wait_error))
+ goto cleanup;
+ info = (struct pidfd_info) {
+ .mask = PIDFD_INFO_EXIT,
+ };
+ if (!SECURITY_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+ goto cleanup;
+ if (!SECURITY_CHECK(info.mask & PIDFD_INFO_EXIT))
+ goto cleanup;
+ if (!SECURITY_CHECK(WIFEXITED(info.exit_code)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!WEXITSTATUS(info.exit_code)))
+ goto cleanup;
+ waited = waitpid_timeout(source, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (!SECURITY_CHECK(waited == source))
+ goto cleanup;
+ source_reaped = true;
+ if (!SECURITY_CHECK(WIFEXITED(status)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!WEXITSTATUS(status)))
+ goto cleanup;
+
+cleanup:
+ if (attached && source > 0) {
+ ptrace(PTRACE_INTERRUPT, source, NULL, NULL);
+ waitpid_timeout(source, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ ptrace(PTRACE_DETACH, source, NULL, NULL);
+ }
+ if (!source_reaped && source > 0) {
+ if (builder >= 0)
+ sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+ kill(source, SIGKILL);
+ waited = waitpid_timeout(source, &status,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ source_reaped = waited == source;
+ }
+ if (builder >= 0)
+ close(builder);
+ if (sockets[0] >= 0)
+ close(sockets[0]);
+ if (sockets[1] >= 0)
+ close(sockets[1]);
+ if (failure_line)
+ TH_LOG("traced-source test failed at line %d: %s", failure_line,
+ strerror(failure_errno));
+ ASSERT_EQ(failure_line, 0);
+ ASSERT_TRUE(source_reaped);
+
+#undef SECURITY_CHECK
+}
+
+TEST(pidfd_spawn_passed_builder_does_not_delegate_authority)
+{
+ char * const argv[] = { "pidfd_spawn_security_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_spawn_run_result result = {};
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ const char *path = self_exe_path();
+ int sockets[2] = { -1, -1 };
+ bool source_reaped = false;
+ pid_t parent = getpid();
+ pid_t source = -1;
+ pid_t waited;
+ int failure_errno = 0;
+ int failure_line = 0;
+ int builder = -1;
+ int status = -1;
+ char command = 1;
+ int ret;
+
+#define SECURITY_CHECK(condition) \
+ security_check((condition), &failure_errno, &failure_line, __LINE__)
+
+ ASSERT_NE(path, NULL);
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+ sockets), 0);
+ source = fork();
+ ASSERT_GE(source, 0);
+ if (!source) {
+ close(sockets[0]);
+ pidfd_spawn_authority_worker(sockets[1], parent, path);
+ }
+ close(sockets[1]);
+ sockets[1] = -1;
+ ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+ if (!SECURITY_CHECK(!ret))
+ goto cleanup;
+ ret = recv_fds_status(sockets[0], &status, &builder, 1);
+ if (!SECURITY_CHECK(!ret))
+ goto cleanup;
+
+ if (!SECURITY_CHECK(config_path(builder, path) == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(errno == EPERM))
+ goto cleanup;
+ if (!SECURITY_CHECK(spawn_run_path_pid(builder, path, argv,
+ NULL, 0) == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(errno == EPERM))
+ goto cleanup;
+ if (!SECURITY_CHECK(ioctl(builder, PIDFD_GET_INFO, &info) == -1))
+ goto cleanup;
+ if (!SECURITY_CHECK(errno == ESRCH))
+ goto cleanup;
+
+ if (!SECURITY_CHECK(write(sockets[0], &command, sizeof(command)) ==
+ sizeof(command)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!read_result(sockets[0], &result)))
+ goto cleanup;
+ if (!SECURITY_CHECK(result.ret > 0))
+ goto cleanup;
+ if (!SECURITY_CHECK(!result.error))
+ goto cleanup;
+ if (!SECURITY_CHECK(!result.wait_error))
+ goto cleanup;
+ info = (struct pidfd_info) {
+ .mask = PIDFD_INFO_EXIT,
+ };
+ if (!SECURITY_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+ goto cleanup;
+ if (!SECURITY_CHECK(info.mask & PIDFD_INFO_EXIT))
+ goto cleanup;
+ if (!SECURITY_CHECK(WIFEXITED(info.exit_code)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!WEXITSTATUS(info.exit_code)))
+ goto cleanup;
+ waited = waitpid_timeout(source, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (!SECURITY_CHECK(waited == source))
+ goto cleanup;
+ source_reaped = true;
+ if (!SECURITY_CHECK(WIFEXITED(status)))
+ goto cleanup;
+ if (!SECURITY_CHECK(!WEXITSTATUS(status)))
+ goto cleanup;
+
+cleanup:
+ if (!source_reaped && source > 0) {
+ if (builder >= 0)
+ sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+ kill(source, SIGKILL);
+ waited = waitpid_timeout(source, &status,
+ PIDFD_SPAWN_TIMEOUT_MS);
+ source_reaped = waited == source;
+ }
+ if (builder >= 0)
+ close(builder);
+ if (sockets[0] >= 0)
+ close(sockets[0]);
+ if (sockets[1] >= 0)
+ close(sockets[1]);
+ if (failure_line)
+ TH_LOG("authority test failed at line %d: %s", failure_line,
+ strerror(failure_errno));
+ ASSERT_EQ(failure_line, 0);
+ ASSERT_TRUE(source_reaped);
+
+#undef SECURITY_CHECK
+}
+
+TEST(pidfd_spawn_changed_cred_does_not_have_authority)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_security_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_spawn_cred_test test = {
+ .path = path,
+ };
+ pthread_t thread;
+ void *thread_ret;
+ int ret;
+
+ ASSERT_NE(path, NULL);
+ test.builder = sys_pidfd_empty_open();
+ ASSERT_GE(test.builder, 0);
+ ret = pthread_create(&thread, NULL, pidfd_spawn_changed_cred_thread,
+ &test);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(pthread_join_timeout(thread, &thread_ret,
+ PIDFD_SPAWN_TIMEOUT_MS), 0);
+ ASSERT_EQ(thread_ret, NULL);
+ if (test.setup_error) {
+ ASSERT_EQ(close(test.builder), 0);
+ SKIP(return, "changing fsuid is unavailable");
+ }
+ ASSERT_EQ(test.restore_error, 0);
+ ASSERT_EQ(test.config_ret, -1);
+ ASSERT_EQ(test.config_error, EPERM);
+ ASSERT_EQ(test.run_ret, -1);
+ ASSERT_EQ(test.run_error, EPERM);
+ ASSERT_GT(spawn_run_path_pid(test.builder, path, argv, NULL, 0), 0);
+ ASSERT_EQ(wait_pidfd_exit(test.builder, 0), 0);
+ ASSERT_EQ(close(test.builder), 0);
+}
+
+TEST(pidfd_spawn_changed_pidns_does_not_have_authority)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_security_test",
+ "--pidfd-spawn-helper", "exit0", NULL };
+ struct pidfd_spawn_pidns_test test = {
+ .path = path,
+ };
+ pthread_t thread;
+ void *thread_ret;
+ int ret;
+
+ ASSERT_NE(path, NULL);
+ test.builder = sys_pidfd_empty_open();
+ ASSERT_GE(test.builder, 0);
+ ret = pthread_create(&thread, NULL, pidfd_spawn_changed_pidns_thread,
+ &test);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(pthread_join_timeout(thread, &thread_ret,
+ PIDFD_SPAWN_TIMEOUT_MS), 0);
+ ASSERT_EQ(thread_ret, NULL);
+ if (test.setup_error) {
+ ASSERT_EQ(close(test.builder), 0);
+ if (test.setup_error == EPERM || test.setup_error == EINVAL)
+ SKIP(return, "PID namespace creation is unavailable");
+ ASSERT_EQ(test.setup_error, 0);
+ }
+ ASSERT_EQ(test.config_ret, -1);
+ ASSERT_EQ(test.config_error, EPERM);
+ ASSERT_EQ(test.run_ret, -1);
+ ASSERT_EQ(test.run_error, EPERM);
+ ASSERT_GT(spawn_run_path_pid(test.builder, path, argv, NULL, 0), 0);
+ ASSERT_EQ(wait_pidfd_exit(test.builder, 0), 0);
+ ASSERT_EQ(close(test.builder), 0);
+}
+
+int main(int argc, char **argv)
+{
+ int ret = helper_main(argc, argv);
+
+ if (ret >= 0)
+ return ret;
+ return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_test.c
new file mode 100644
index 0000000000000..da28ad125a345
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_test.c
@@ -0,0 +1,550 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/fs.h>
+#include <linux/pidfd_spawn.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/epoll.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+#include <sys/xattr.h>
+#include <unistd.h>
+
+#include "pidfd.h"
+#include "kselftest_harness.h"
+#include "../filesystems/wrappers.h"
+
+#include "pidfd_spawn_common.h"
+
+struct pidfd_spawn_xattr_writer {
+ const char *value;
+ int fd;
+};
+
+static void *pidfd_spawn_xattr_writer(void *data)
+{
+ struct pidfd_spawn_xattr_writer *writer = data;
+ size_t len = strlen(writer->value);
+ int i;
+
+ for (i = 0; i < 1000; i++) {
+ if (fsetxattr(writer->fd, "trusted.pidfd_spawn", writer->value,
+ len, 0))
+ return (void *)(intptr_t)errno;
+ }
+ return NULL;
+}
+
+static int read_fdinfo_pid(int fd)
+{
+ char path[64];
+ char line[128];
+ FILE *file;
+ int pid = INT_MIN;
+
+ snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", fd);
+ file = fopen(path, "re");
+ if (!file)
+ return INT_MIN;
+ while (fgets(line, sizeof(line), file)) {
+ if (sscanf(line, "Pid:\t%d", &pid) == 1)
+ break;
+ }
+ fclose(file);
+ return pid;
+}
+
+TEST(pidfd_open_empty_rejects_bad_flags)
+{
+ ASSERT_EQ(sys_pidfd_open(0, 0), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(sys_pidfd_open(0, PIDFD_EMPTY | PIDFD_THREAD), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(sys_pidfd_open(0, PIDFD_EMPTY | (1U << 26)), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(sys_pidfd_open(getpid(), PIDFD_EMPTY), -1);
+ ASSERT_EQ(errno, EINVAL);
+}
+
+TEST(pidfd_open_empty_close_discards_builder)
+{
+ int fd;
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_open_empty_accepts_nonblock)
+{
+ int fd;
+ int flags;
+
+ fd = sys_pidfd_open(0, PIDFD_EMPTY | PIDFD_NONBLOCK);
+ ASSERT_GE(fd, 0);
+
+ flags = fcntl(fd, F_GETFL);
+ ASSERT_GE(flags, 0);
+ ASSERT_NE(flags & O_NONBLOCK, 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_config_stages_absolute_path)
+{
+ const char *path = self_exe_path();
+ int fd;
+
+ ASSERT_NE(path, NULL);
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(config_path(fd, path), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_config_stages_relative_path)
+{
+ char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+ "exit0", NULL };
+ const char *relative;
+ char *storage;
+ int cwd_fd;
+ int fd;
+
+ cwd_fd = enter_self_exe_directory(&storage, &relative);
+ ASSERT_GE(cwd_fd, 0);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(config_path(fd, relative), 0);
+ ASSERT_EQ(spawn_run_staged(fd, argv, NULL, 0), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+ ASSERT_EQ(leave_self_exe_directory(cwd_fd, storage), 0);
+}
+
+TEST(pidfd_config_rejects_bad_arguments)
+{
+ char unterminated_key[256];
+ int fd;
+
+ memset(unterminated_key, 'x', sizeof(unterminated_key));
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+
+ ASSERT_EQ(sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING, NULL,
+ "/bin/true", 0),
+ -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+ PIDFD_CONFIG_KEY_PATH, NULL, 0),
+ -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+ PIDFD_CONFIG_KEY_PATH, "/bin/true", 1),
+ -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(sys_pidfd_config(fd, UINT_MAX, PIDFD_CONFIG_KEY_PATH,
+ "/bin/true", 0),
+ -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+ ASSERT_EQ(sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING, "unknown",
+ "/bin/true", 0),
+ -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+ ASSERT_EQ(sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+ unterminated_key, "/bin/true", 0),
+ -1);
+ ASSERT_EQ(errno, EINVAL);
+
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_open_empty_getversion_survives_publication)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+ "exit0", NULL };
+ __u32 published_generation;
+ __u32 future_generation;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(ioctl(fd, FS_IOC_GETVERSION, &future_generation), 0);
+ ASSERT_GT(spawn_run_path_pid(fd, path, argv, NULL, 0), 0);
+ ASSERT_EQ(ioctl(fd, FS_IOC_GETVERSION, &published_generation), 0);
+ ASSERT_EQ(published_generation, future_generation);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_reopened_file_does_not_cancel_builder)
+{
+ char proc_path[64];
+ const char *path = self_exe_path();
+ int reopened;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_GT(snprintf(proc_path, sizeof(proc_path), "/proc/self/fd/%d", fd),
+ 0);
+ reopened = open(proc_path, O_RDWR | O_CLOEXEC);
+ ASSERT_GE(reopened, 0);
+ ASSERT_EQ(close(reopened), 0);
+
+ ASSERT_EQ(config_path(fd, path), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+static int pidfd_spawn_bind_mount_child(const char *path)
+{
+ char template[] = P_tmpdir "/pidfd_spawn_mount_XXXXXX";
+ char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+ "exit0", NULL };
+ int mounted_fd = -1;
+ int mount_fd = -1;
+ int target_fd = -1;
+ bool mounted = false;
+ int ret = 1;
+ int fd = -1;
+
+ if (unshare(CLONE_NEWNS))
+ return errno == EPERM ? 77 : 1;
+ if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
+ return errno == EPERM ? 77 : 2;
+ target_fd = mkstemp(template);
+ if (target_fd < 0)
+ return 3;
+
+ fd = sys_pidfd_empty_open();
+ if (fd < 0) {
+ ret = 4;
+ goto out;
+ }
+ mount_fd = sys_open_tree(fd, "", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC |
+ AT_EMPTY_PATH);
+ if (mount_fd < 0) {
+ ret = 5;
+ goto out;
+ }
+ if (move_mount(mount_fd, "", target_fd, "",
+ MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH)) {
+ ret = 6;
+ goto out;
+ }
+ mounted = true;
+ close(mount_fd);
+ mount_fd = -1;
+ close(fd);
+ fd = -1;
+
+ mounted_fd = open(template, O_RDWR | O_CLOEXEC);
+ if (mounted_fd < 0) {
+ ret = 7;
+ goto out;
+ }
+ if (spawn_run_path(mounted_fd, path, argv, NULL, 0)) {
+ ret = 8;
+ goto out;
+ }
+ if (wait_pidfd_exit(mounted_fd, 0)) {
+ ret = 9;
+ goto out;
+ }
+ ret = 0;
+
+out:
+ if (mounted_fd >= 0)
+ close(mounted_fd);
+ if (mounted)
+ umount2(template, MNT_DETACH);
+ if (mount_fd >= 0)
+ close(mount_fd);
+ if (fd >= 0)
+ close(fd);
+ if (target_fd >= 0)
+ close(target_fd);
+ if (target_fd >= 0)
+ unlink(template);
+ return ret;
+}
+
+TEST(pidfd_spawn_bind_mount_keeps_builder_alive)
+{
+ const char *path = self_exe_path();
+ int status;
+ pid_t waited;
+ pid_t pid;
+
+ ASSERT_NE(path, NULL);
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (!pid)
+ _exit(pidfd_spawn_bind_mount_child(path));
+ waited = waitpid_timeout(pid, &status, PIDFD_SPAWN_TIMEOUT_MS);
+ if (!waited) {
+ kill(pid, SIGKILL);
+ waited = waitpid_timeout(pid, &status, 1000);
+ }
+ ASSERT_EQ(waited, pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ if (WEXITSTATUS(status) == 77)
+ SKIP(return, "mount namespaces are unavailable");
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST(pidfd_spawn_run_preserves_pidfd_identity)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+ "exit0", NULL };
+ struct stat builder_stat;
+ struct stat future_stat;
+ struct stat opened_stat;
+ struct stat ordinary_stat;
+ int opened;
+ int ordinary;
+ int child_pid;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ ordinary = sys_pidfd_open(getpid(), 0);
+ ASSERT_GE(ordinary, 0);
+ ASSERT_EQ(fstat(ordinary, &ordinary_stat), 0);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fstat(fd, &future_stat), 0);
+ ASSERT_EQ(future_stat.st_mode, ordinary_stat.st_mode);
+ ASSERT_EQ(future_stat.st_uid, ordinary_stat.st_uid);
+ ASSERT_EQ(future_stat.st_gid, ordinary_stat.st_gid);
+ ASSERT_EQ(close(ordinary), 0);
+ child_pid = spawn_run_path_pid(fd, path, argv, NULL, 0);
+ ASSERT_GT(child_pid, 0);
+ opened = sys_pidfd_open(child_pid, 0);
+ ASSERT_GE(opened, 0);
+ ASSERT_EQ(fstat(fd, &builder_stat), 0);
+ ASSERT_EQ(fstat(opened, &opened_stat), 0);
+ ASSERT_EQ(future_stat.st_dev, builder_stat.st_dev);
+ ASSERT_EQ(future_stat.st_ino, builder_stat.st_ino);
+ ASSERT_EQ(future_stat.st_mode, builder_stat.st_mode);
+ ASSERT_EQ(future_stat.st_uid, builder_stat.st_uid);
+ ASSERT_EQ(future_stat.st_gid, builder_stat.st_gid);
+ ASSERT_EQ(builder_stat.st_dev, opened_stat.st_dev);
+ ASSERT_EQ(builder_stat.st_ino, opened_stat.st_ino);
+ ASSERT_EQ(builder_stat.st_mode, opened_stat.st_mode);
+ ASSERT_EQ(builder_stat.st_uid, opened_stat.st_uid);
+ ASSERT_EQ(builder_stat.st_gid, opened_stat.st_gid);
+
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(opened), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_preserves_pidfd_xattrs)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+ "exit0", NULL };
+ struct pidfd_spawn_xattr_writer writers[2];
+ pthread_t threads[2];
+ void *thread_ret;
+ int child_pid;
+ int opened;
+ int fd;
+ int ret;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ child_pid = spawn_run_path_pid(fd, path, argv, NULL, 0);
+ ASSERT_GT(child_pid, 0);
+ opened = sys_pidfd_open(child_pid, 0);
+ ASSERT_GE(opened, 0);
+
+ errno = 0;
+ ret = fsetxattr(fd, "trusted.pidfd_spawn", "probe", 5, 0);
+ if (ret && (errno == EPERM || errno == EACCES)) {
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(opened), 0);
+ ASSERT_EQ(close(fd), 0);
+ SKIP(return, "trusted xattrs require CAP_SYS_ADMIN");
+ }
+ ASSERT_EQ(ret, 0);
+
+ writers[0].fd = fd;
+ writers[0].value = "builder";
+ writers[1].fd = opened;
+ writers[1].value = "opened";
+ ASSERT_EQ(pthread_create(&threads[0], NULL, pidfd_spawn_xattr_writer,
+ &writers[0]), 0);
+ ASSERT_EQ(pthread_create(&threads[1], NULL, pidfd_spawn_xattr_writer,
+ &writers[1]), 0);
+ ASSERT_EQ(pthread_join_timeout(threads[0], &thread_ret,
+ PIDFD_SPAWN_TIMEOUT_MS), 0);
+ ASSERT_EQ((intptr_t)thread_ret, 0);
+ ASSERT_EQ(pthread_join_timeout(threads[1], &thread_ret,
+ PIDFD_SPAWN_TIMEOUT_MS), 0);
+ ASSERT_EQ((intptr_t)thread_ret, 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(opened), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_prerun_epoll_reports_child_exit)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+ "exit0", NULL };
+ struct epoll_event event = {
+ .events = EPOLLIN,
+ };
+ struct epoll_event ready = {};
+ int epoll_fd;
+ int fd;
+
+ ASSERT_NE(path, NULL);
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ event.data.fd = fd;
+ epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ ASSERT_GE(epoll_fd, 0);
+ ASSERT_EQ(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event), 0);
+ ASSERT_GT(spawn_run_path_pid(fd, path, argv, NULL, 0), 0);
+ ASSERT_EQ(epoll_wait(epoll_fd, &ready, 1,
+ PIDFD_SPAWN_TIMEOUT_MS), 1);
+ ASSERT_EQ(ready.data.fd, fd);
+ ASSERT_NE(ready.events & EPOLLIN, 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(epoll_fd), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_is_one_shot)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+ "exit0", NULL };
+ int fd;
+
+ ASSERT_NE(path, NULL);
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_GT(spawn_run_path_pid(fd, path, argv, NULL, 0), 0);
+ ASSERT_EQ(spawn_run_path_pid(fd, path, argv, NULL, 0), -1);
+ ASSERT_EQ(errno, EBUSY);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_execs_staged_path)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+ "exit0", NULL };
+ int fd;
+
+ ASSERT_NE(path, NULL);
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(config_path(fd, path), 0);
+ ASSERT_EQ(spawn_run_staged(fd, argv, NULL, 0), 0);
+ ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_rejects_conflicting_paths)
+{
+ const char *path = self_exe_path();
+ char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+ "exit0", NULL };
+ int fd;
+
+ ASSERT_NE(path, NULL);
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(config_path(fd, path), 0);
+ ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_pending_pidfd_apis_return_esrch)
+{
+ char byte;
+ struct iovec iov = {
+ .iov_base = &byte,
+ .iov_len = sizeof(byte),
+ };
+ struct pidfd_info info = {
+ .mask = PIDFD_INFO_PID,
+ };
+ siginfo_t wait_info = {};
+ struct pollfd pfd = {};
+ int nsfd;
+ int fd;
+
+ fd = sys_pidfd_empty_open();
+ ASSERT_GE(fd, 0);
+
+ ASSERT_EQ(sys_pidfd_send_signal(fd, 0, NULL, 0), -1);
+ ASSERT_EQ(errno, ESRCH);
+ ASSERT_EQ(sys_pidfd_getfd(fd, STDIN_FILENO, 0), -1);
+ ASSERT_EQ(errno, ESRCH);
+ ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), -1);
+ ASSERT_EQ(errno, ESRCH);
+ nsfd = ioctl(fd, PIDFD_GET_MNT_NAMESPACE, 0);
+ ASSERT_EQ(nsfd, -1);
+ ASSERT_EQ(errno, ESRCH);
+ ASSERT_EQ(setns(fd, CLONE_NEWNS), -1);
+ ASSERT_EQ(errno, ESRCH);
+ ASSERT_EQ(read_fdinfo_pid(fd), -1);
+ ASSERT_EQ(sys_waitid(P_PIDFD, fd, &wait_info, WEXITED | WNOHANG), -1);
+ ASSERT_EQ(errno, ESRCH);
+ ASSERT_EQ(syscall(__NR_process_madvise, fd, &iov, 1,
+ MADV_DONTNEED, 0), -1);
+ ASSERT_EQ(errno, ESRCH);
+ ASSERT_EQ(syscall(__NR_process_mrelease, fd, 0), -1);
+ ASSERT_EQ(errno, ESRCH);
+ ASSERT_EQ(flistxattr(fd, NULL, 0), -1);
+ ASSERT_EQ(errno, ESRCH);
+
+ pfd.fd = fd;
+ pfd.events = POLLIN;
+ ASSERT_EQ(poll(&pfd, 1, 0), 0);
+
+ ASSERT_EQ(close(fd), 0);
+}
+
+int main(int argc, char **argv)
+{
+ int ret = helper_main(argc, argv);
+
+ if (ret >= 0)
+ return ret;
+ return test_harness_run(argc, argv);
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH 24/24] Documentation: describe pidfd spawn builders
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
` (22 preceding siblings ...)
2026-07-16 14:31 ` [RFC PATCH 23/24] selftests/pidfd: cover pidfd spawn builders Li Chen
@ 2026-07-16 14:31 ` Li Chen
23 siblings, 0 replies; 26+ messages in thread
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
Eric Paris, Mickaël Salaün, Günther Noack,
Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
linux-kselftest, linux-doc, audit, linux-security-module,
linux-arch, linux-mm, Li Chen
Document the taskless pidfd lifecycle, source-state and authority
model, optional path configuration, run and file-action ABI, one-shot
failure classification, and embryonic process visibility.
Describe policy and audit interactions, current accounting behavior,
and the features still missing from the initial interface. Add the
document to the userspace API index and PIDFD API maintenance entry.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
Documentation/userspace-api/index.rst | 1 +
Documentation/userspace-api/pidfd_spawn.rst | 247 ++++++++++++++++++++
MAINTAINERS | 1 +
3 files changed, 249 insertions(+)
create mode 100644 Documentation/userspace-api/pidfd_spawn.rst
diff --git a/Documentation/userspace-api/index.rst b/Documentation/userspace-api/index.rst
index a68b1bea57a85..28da6a4da7c78 100644
--- a/Documentation/userspace-api/index.rst
+++ b/Documentation/userspace-api/index.rst
@@ -21,6 +21,7 @@ System calls
ebpf/index
ioctl/index
mseal
+ pidfd_spawn
rseq
Security-related interfaces
diff --git a/Documentation/userspace-api/pidfd_spawn.rst b/Documentation/userspace-api/pidfd_spawn.rst
new file mode 100644
index 0000000000000..2aaf5db85f13b
--- /dev/null
+++ b/Documentation/userspace-api/pidfd_spawn.rst
@@ -0,0 +1,247 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+====================
+Pidfd spawn builders
+====================
+
+Pidfd spawn builders create a new process image with kernel-mediated setup.
+They do not expose a userspace child stub between process creation and exec.
+
+The interface is::
+
+ int fd = pidfd_open(0, PIDFD_EMPTY);
+
+ /* Optional executable-path staging. */
+ pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+ PIDFD_CONFIG_KEY_PATH, path, 0);
+
+ pidfd_spawn_run(fd, &args, sizeof(args));
+
+The same fd starts as a taskless pidfs object and becomes the child's pidfd
+when a task is created. The current implementation supports an executable
+path, argv, envp, and ordered ``DUP2``, ``CLOSE_RANGE``, and ``FCHDIR`` file
+actions.
+
+Opening a builder
+=================
+
+``pidfd_open(0, PIDFD_EMPTY)`` returns a close-on-exec builder fd.
+``PIDFD_NONBLOCK`` may be combined with ``PIDFD_EMPTY``. Other flags are
+rejected. ``PIDFD_EMPTY`` is rejected for a nonzero pid.
+
+Opening the builder allocates a pidfs inode and builder state. It does not
+allocate a ``struct pid`` or task, and does not charge ``RLIMIT_NPROC`` or the
+pids cgroup controller. PID allocation and process-count accounting occur when
+``pidfd_spawn_run()`` calls the normal task-creation path.
+
+The pidfs inode has a stable identity before and after task creation. A pidfd
+opened from the eventual numeric pid refers to the same pidfs inode. A poll or
+epoll registration made while the fd is taskless remains attached across
+publication and can report the child's eventual exit.
+
+Process-dependent pidfd operations return ``ESRCH`` while the builder is
+taskless. This includes signal delivery, ``pidfd_getfd()``,
+``PIDFD_GET_INFO``, namespace ioctls, ``setns()``, process memory operations,
+and process-dependent pidfs xattrs. Argument validation may fail before the
+taskless-state check. ``poll()`` reports no event while the builder may still
+publish a task. After a terminal taskless failure, ``poll()`` and ``epoll``
+report ``POLLERR | POLLHUP`` and wake registrations made before the failure.
+Inode-only operations such as ``FS_IOC_GETVERSION`` remain available.
+
+The builder state follows the lifetime of the pidfs inode. Duplicated or
+reopened descriptors and VFS references to that inode keep it alive. Releasing
+the final reference before a run discards the taskless builder without
+creating a process.
+
+Source state and authority
+==========================
+
+The current mode is source-based. The child is created from the calling
+process at run time. State changed after ``pidfd_open()`` but before
+``pidfd_spawn_run()``, including the current directory and file descriptor
+table, is therefore visible to normal process creation. Non-close-on-exec file
+descriptors remain available to the new image unless a file action changes
+them. The exec path applies ``FD_CLOEXEC`` normally.
+
+File and filesystem state are private child copies before file actions run.
+The current backend shares the address space only for its internal
+vfork-style setup and replaces it during exec. Applications must not depend on
+that backend detail.
+
+Configuration and run authority is bound to the mm, exact credential object,
+and child PID namespace recorded when the builder is opened. Passing the fd
+through ``SCM_RIGHTS`` or another fd-passing mechanism does not by itself
+delegate authority to configure or run it. A rejected authority check does not
+consume the builder.
+
+Configuring an executable path
+==============================
+
+The optional configuration command is::
+
+ pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+ PIDFD_CONFIG_KEY_PATH, path, 0);
+
+The command copies ``path`` into builder state. Calling it again before a run
+replaces the staged string. The key and value must be valid NUL-terminated
+userspace strings, and ``aux`` must be zero. Unknown commands or keys return
+``EOPNOTSUPP``.
+
+Staging is not required when the run arguments provide ``path``. Supplying a
+run-time path when a path is already staged, or supplying neither form,
+returns ``EINVAL``. This selection is validated before a run-time path pointer
+is dereferenced, so a staged-path conflict takes precedence over a fault in
+that pointer. Relative paths are resolved after ordered file actions from the
+child's resulting working directory. Without an ``FCHDIR`` action, that is
+the working directory copied at run time. The kernel does not search ``PATH``.
+
+Run arguments
+=============
+
+Version 0 of ``struct pidfd_spawn_run_args`` contains:
+
+* ``path``: an optional executable-path pointer;
+* ``argv``: a required argument-vector pointer;
+* ``envp``: an environment-vector pointer, or zero for an empty environment;
+* ``actions``, ``nr_actions``, and ``action_size``: an optional ordered
+ file-action array.
+
+``flags`` and all reserved fields must be zero. The syscall size must be at
+least ``PIDFD_SPAWN_RUN_SIZE_VER0`` and no larger than ``PAGE_SIZE``. Unknown
+trailing bytes in a larger structure must be zero.
+
+``path``, ``argv``, ``envp``, and ``actions`` are full-width userspace virtual
+addresses stored in ``__aligned_u64`` fields on every ABI. Native calls must
+fit the native pointer width. Compat calls must fit ``compat_uptr_t``; nonzero
+upper bits are rejected with ``EFAULT`` before task creation. Entries read
+from compat argv and envp arrays are compat pointers.
+
+File actions
+============
+
+When ``nr_actions`` is zero, ``actions`` and ``action_size`` must also be zero.
+Otherwise ``actions`` must be nonzero and ``action_size`` must be at least
+``PIDFD_SPAWN_ACTION_SIZE_VER0`` and aligned to eight bytes. A larger element
+is accepted only when its unknown tail is zero. The complete action payload is
+limited to 64 KiB; the interface does not impose a fixed action count below
+that byte limit.
+
+Actions execute in array order against child-private file and filesystem
+state before exec:
+
+``PIDFD_SPAWN_ACTION_DUP2``
+ Duplicate ``fd`` onto ``newfd``. If both numbers are equal, clear
+ ``FD_CLOEXEC`` on that descriptor. ``flags`` and reserved fields must be
+ zero.
+
+``PIDFD_SPAWN_ACTION_CLOSE_RANGE``
+ Apply ``close_range(fd, newfd, flags)``. ``newfd`` must not be less than
+ ``fd``. Only ``CLOSE_RANGE_UNSHARE`` and ``CLOSE_RANGE_CLOEXEC`` are
+ accepted.
+
+``PIDFD_SPAWN_ACTION_FCHDIR``
+ Change the child working directory to ``fd``. ``flags``, ``newfd``, and
+ reserved fields must be zero.
+
+An unknown action type returns ``EOPNOTSUPP``. Invalid fields or arguments
+return ``EINVAL``. Errors from applying an otherwise valid action are reported
+as run failures after task publication.
+
+One-shot execution and results
+==============================
+
+After fd type, creator authority, and source ptrace checks succeed, the first
+``pidfd_spawn_run()`` attempt atomically claims the builder before copying its
+run arguments. Argument faults, validation errors, memory-allocation failures,
+process limits, and task-creation failures after that claim are terminal.
+Later configuration and run attempts return ``EBUSY``.
+
+Checks that fail before the claim do not consume the builder. These include an
+invalid fd type, creator-authority mismatch, or an already traced source.
+
+On successful setup and exec, ``pidfd_spawn_run()`` returns the positive child
+pid as seen in the caller's PID namespace. The pidfd remains the stable process
+identity.
+
+A failure before task creation returns a negative error and leaves the fd
+terminal but taskless. Ordinary pidfd operations continue to return ``ESRCH``.
+``poll()`` and ``epoll`` report ``POLLERR | POLLHUP``. The builder cannot be
+retried; a caller must open a new builder.
+
+A setup or exec failure after publication returns a negative error, leaves the
+same fd as the child's pidfd, and makes the child exit with status 127. Normal
+``SIGCHLD``, ``SIG_IGN``, and ``SA_NOCLDWAIT`` rules determine whether it must
+be reaped. ``PIDFD_GET_INFO`` with ``PIDFD_INFO_EXIT`` distinguishes this case
+from a terminal taskless failure.
+
+Internal restart errors after the claim are returned to userspace as
+``EINTR``. The kernel does not automatically restart a consumed run and
+replace its first result with ``EBUSY``.
+
+A fatal signal delivered to the published child determines the child's exit
+status if it wins during setup. It is not reported as an exec setup error, so
+the run can return the positive child pid even when that child exits from the
+signal.
+
+Embryonic process state
+=======================
+
+After task publication and before successful exec completion, the child is in
+an embryonic exec state. It is not yet a normal userspace process image and
+does not have a valid userspace register frame on every architecture.
+
+During this interval:
+
+* direct ptrace access and ``pidfd_getfd()`` are denied;
+* procfs PID lookup and iteration treat the task as absent to other tasks,
+ preventing access to the source address space temporarily shared by the
+ implementation; the embryonic task may resolve its own procfs entries for
+ executable and interpreter lookup;
+* ``PIDFD_GET_INFO`` reports ``PIDFD_COREDUMP_SKIP`` when coredump state is
+ requested;
+* ordinary pidfd identity, signal, wait, and poll mechanisms operate on the
+ published task as their normal permission checks allow.
+
+Successful exec releases the embryonic state before exec event publication. A
+failed setup never exposes the borrowed pre-exec frame to userspace; the child
+exits instead.
+
+Security policy and audit
+=========================
+
+File actions and exec run inside the child kernel path. They do not appear to
+seccomp as separate ``dup2()``, ``close_range()``, ``fchdir()``, or
+``execve()`` syscalls. A source-side seccomp filter can reject
+``pidfd_spawn_run()`` before the builder is claimed. A policy that intends to
+prevent this operation must filter ``pidfd_spawn_run()`` itself; denying only
+the syscall numbers corresponding to its file actions or exec does not mediate
+the equivalent in-kernel work. In particular, an exec-only denylist that
+allows unknown syscalls does not block the initial spawn exec; it must add
+``pidfd_spawn_run()``. A syscall allowlist does not permit the new operation
+until it adds that syscall. Seccomp state inherited by the child remains active
+after exec. LSM checks, including Landlock executable access, still mediate the
+underlying exec.
+
+Audit records the source syscall transaction and a separate
+``AUDIT_PIDFD_SPAWN`` child transaction for executable lookup and exec
+arguments. The child does not emit a synthetic second ``AUDIT_SYSCALL``
+record. The child transaction is selected using the originating
+``pidfd_spawn_run()`` syscall number, so an existing exit rule that names only
+``execve()`` or ``execveat()`` does not select it. Such rules must also select
+``pidfd_spawn_run()`` with the current audit model. File actions are not
+emitted as decoded per-action audit records.
+
+An already traced source is rejected before the claim. The current backend
+does not implement ptrace auto-attach events for the created child.
+
+Current limitations
+===================
+
+The interface does not yet implement the full ``posix_spawn()`` surface. It
+lacks open and close file actions, resetids, signal mask and default controls,
+scheduler attributes, affinity, process groups, sessions, explicit cgroup
+placement, ``PATH`` search, and exec by fd.
+
+It also does not provide pristine or no-source process creation, executable
+metadata caching, or reusable spawn templates. The current implementation is
+source-based and uses the existing vfork/exec machinery internally.
diff --git a/MAINTAINERS b/MAINTAINERS
index 85b1306cb2ff3..0b6d466787a60 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21288,6 +21288,7 @@ M: Christian Brauner <christian@brauner.io>
L: linux-kernel@vger.kernel.org
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/brauner/linux.git
+F: Documentation/userspace-api/pidfd_spawn.rst
F: fs/pidfd_spawn.c
F: include/linux/pidfd_spawn.h
F: include/uapi/linux/pidfd_spawn.h
--
2.52.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* Re: [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks
2026-07-16 14:31 ` [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks Li Chen
@ 2026-07-16 15:57 ` Andy Lutomirski
0 siblings, 0 replies; 26+ messages in thread
From: Andy Lutomirski @ 2026-07-16 15:57 UTC (permalink / raw)
To: Li Chen
Cc: Christian Brauner, Kees Cook, Gabriel Krisman Bertazi,
Josh Triplett, Mateusz Guzik, Andy Lutomirski, John Ericson,
Jonathan Corbet, Shuah Khan, Arnd Bergmann, Oleg Nesterov,
Andrew Morton, Paul Moore, Eric Paris, Mickaël Salaün,
Günther Noack, Alexander Viro, Jan Kara, linux-api,
linux-fsdevel, linux-kernel, linux-kselftest, linux-doc, audit,
linux-security-module, linux-arch, linux-mm
On Thu, Jul 16, 2026 at 8:52 AM Li Chen <me@linux.beauty> wrote:
>
> A kernel-created task can become visible before it has installed a new
> executable image or a valid userspace register frame. Exposing such a task
> through ptrace can disclose kernel setup state.
>
> Add a task-local embryonic flag and an internal clone argument for callers
> that need this lifecycle. Reject ptrace access until the creator clears the
> flag. Clear it with release ordering and observe it with acquire ordering.
> This orders visibility of the completed exec state with the transition.
>
> Existing fork, vfork, clone, and kernel-thread callers leave the argument
> unset and retain their current behavior.
>
> --- a/kernel/ptrace.c
> +++ b/kernel/ptrace.c
> @@ -56,6 +56,8 @@ bool ptracer_access_allowed(struct task_struct *tsk)
> guard(rcu)();
> if (ptrace_parent(tsk) != current)
> return false;
> + if (task_is_embryonic_exec(tsk))
> + return false;
> es = task_exec_state_rcu(tsk);
> return READ_ONCE(es->dumpable) == TASK_DUMPABLE_OWNER ||
> ptracer_capable(tsk, es->user_ns);
> @@ -312,6 +314,8 @@ static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
> WARN(1, "denying ptrace access check without PTRACE_MODE_*CREDS\n");
> return -EPERM;
> }
> + if (task_is_embryonic_exec(task))
> + return -EPERM;
Would it be better to use a different error code? -ECONNREFUSED?
After all, this isn't a permission failure per se.
There's a not-locally-obvious gotcha here: reading other process
attributes prior to calling task_is_embryonic_exec may result in
(security-relevant!) data races. This should at least be documented
-- it's critical to check task_is_embryonic_exec *before* trying to
read credentials. Also, I think /proc and many pidfd APIs have the
same issue.
--Andy
^ permalink raw reply [flat|nested] 26+ messages in thread
end of thread, other threads:[~2026-07-16 15:58 UTC | newest]
Thread overview: 26+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
2026-07-16 14:31 ` [RFC PATCH 01/24] pidfd: add spawn builder uapi Li Chen
2026-07-16 14:31 ` [RFC PATCH 02/24] libfs: allow custom validation of stashed inode data Li Chen
2026-07-16 14:31 ` [RFC PATCH 03/24] pidfs: add taskless future pidfd inodes Li Chen
2026-07-16 14:31 ` [RFC PATCH 04/24] pidfd: create taskless spawn builders Li Chen
2026-07-16 14:31 ` [RFC PATCH 05/24] pidfd: add spawn builder path configuration Li Chen
2026-07-16 14:31 ` [RFC PATCH 06/24] exec: expose execveat internals to process builders Li Chen
2026-07-16 14:31 ` [RFC PATCH 07/24] fork: expose vfork completion helper Li Chen
2026-07-16 14:31 ` [RFC PATCH 08/24] pidfs: attach pids to future pidfd files Li Chen
2026-07-16 14:31 ` [RFC PATCH 09/24] fork: let process builders supply preallocated pids Li Chen
2026-07-16 14:31 ` [RFC PATCH 10/24] pidfs: publish future pidfd files Li Chen
2026-07-16 14:31 ` [RFC PATCH 11/24] pidfd: add spawn builder state tracking Li Chen
2026-07-16 14:31 ` [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks Li Chen
2026-07-16 15:57 ` Andy Lutomirski
2026-07-16 14:31 ` [RFC PATCH 13/24] fork: let new tasks start with task work Li Chen
2026-07-16 14:31 ` [RFC PATCH 14/24] pidfd: create and execute spawn builder tasks Li Chen
2026-07-16 14:31 ` [RFC PATCH 15/24] fork: keep embryonic tasks hidden until exec completes Li Chen
2026-07-16 14:31 ` [RFC PATCH 16/24] audit: add pidfd spawn child contexts Li Chen
2026-07-16 14:31 ` [RFC PATCH 17/24] pidfd: audit child spawn execution Li Chen
2026-07-16 14:31 ` [RFC PATCH 18/24] pidfd: make spawn builder execution signal-safe Li Chen
2026-07-16 14:31 ` [RFC PATCH 19/24] file: expose spawn file-action helpers Li Chen
2026-07-16 14:31 ` [RFC PATCH 20/24] pidfd: add initial spawn file actions Li Chen
2026-07-16 14:31 ` [RFC PATCH 21/24] pidfd: consume spawn builders on the first run attempt Li Chen
2026-07-16 14:31 ` [RFC PATCH 22/24] pidfd: expose spawn builder system calls Li Chen
2026-07-16 14:31 ` [RFC PATCH 23/24] selftests/pidfd: cover pidfd spawn builders Li Chen
2026-07-16 14:31 ` [RFC PATCH 24/24] Documentation: describe " Li Chen
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.