pidfd: add a minimal process spawn builder
Subsystems (31)
cover pidfd: add a minimal process spawn builder
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/[email protected]/ * pidfs: handle FS_IOC32_GETVERSION in compat ioctl https://lore.kernel.org/all/[email protected]/
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/[email protected]/
[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
Signature · 1 line
2.52.0
RFC PATCH 1/24 pidfd: add spawn builder uapi
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.
--- 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
MAINTAINERS +2 -0
@@ -21287,7 +21287,9 @@ M: Christian Brauner <[email protected]> L: [email protected] 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
include/uapi/linux/pidfd.h +1 -0
@@ -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
include/uapi/linux/pidfd_spawn.h new file +49 -0
49 lines not on this page · raw message
tools/include/uapi/linux/pidfd_spawn.h new file +49 -0
49 lines not on this page · raw message
Signature · 1 line
2.52.0
RFC PATCH 2/24 libfs: allow custom validation of stashed inode data
In reply to Li Chenpath_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.
--- fs/internal.h | 1 + fs/libfs.c | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-)
fs/internal.h +1 -0
@@ -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);
fs/libfs.c +4 -1
@@ -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 ; }
Signature · 1 line
2.52.0
RFC PATCH 3/24 pidfs: add taskless future pidfd inodes
In reply to Li ChenTeach 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.
--- fs/pidfs.c | 254 ++++++++++++++++++++++++++++++++++++++---- include/linux/pid.h | 10 ++ include/linux/pidfs.h | 21 ++++ 3 files changed, 262 insertions(+), 23 deletions(-)
fs/pidfs.c +231 -23
@@ -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))
include/linux/pid.h +10 -0
@@ -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;
include/linux/pidfs.h +21 -0
@@ -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);
Signature · 1 line
2.52.0
RFC PATCH 4/24 pidfd: create taskless spawn builders
In reply to Li ChenAdd 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.
--- 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
MAINTAINERS +2 -0
@@ -21287,6 +21287,8 @@ M: Christian Brauner <[email protected]> L: [email protected] 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
fs/Makefile +1 -1
@@ -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 \
fs/pidfd_spawn.c new file +81 -0
81 lines not on this page · raw message
fs/pidfs.c +4 -1
@@ -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 )
include/linux/pidfd_spawn.h new file +9 -0
@@ -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 */
kernel/nsproxy.c +8 -3
@@ -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;
kernel/signal.c +1 -1
@@ -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);
Signature · 1 line
2.52.0
RFC PATCH 5/24 pidfd: add spawn builder path configuration
In reply to Li ChenAdd 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.
--- fs/pidfd_spawn.c | 155 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+)
fs/pidfd_spawn.c +155 -0
@@ -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,
Signature · 1 line
2.52.0
RFC PATCH 6/24 exec: expose execveat internals to process builders
In reply to Li ChenMove 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.
--- 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
MAINTAINERS +1 -0
@@ -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
fs/exec.c +4 -24
@@ -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)
fs/exec_internal.h new file +40 -0
@@ -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 */
Signature · 1 line
2.52.0
RFC PATCH 7/24 fork: expose vfork completion helper
In reply to Li ChenAllow process builders that call copy_process() directly to attach and wait for the existing vfork completion without duplicating its signal and task-reference handling.
--- include/linux/sched/task.h | 3 +++ kernel/fork.c | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-)
include/linux/sched/task.h +3 -0
@@ -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,
kernel/fork.c +1 -2
@@ -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;
Signature · 1 line
2.52.0
RFC PATCH 8/24 pidfs: attach pids to future pidfd files
In reply to Li ChenAssociate 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.
--- fs/pidfs.c | 89 +++++++++++++++++++++++++++++++++++++++++-- include/linux/pidfs.h | 2 + 2 files changed, 87 insertions(+), 4 deletions(-)
fs/pidfs.c +85 -4
@@ -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))
include/linux/pidfs.h +2 -0
@@ -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);
Signature · 1 line
2.52.0
RFC PATCH 9/24 fork: let process builders supply preallocated pids
In reply to Li ChenProcess 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.
--- include/linux/pid.h | 3 +++ kernel/fork.c | 16 ++++++++++++++-- kernel/pid.c | 22 ++++++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-)
include/linux/pid.h +3 -0
@@ -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);
kernel/fork.c +14 -2
@@ -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);
kernel/pid.c +20 -2
@@ -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);
Signature · 1 line
2.52.0
RFC PATCH 10/24 pidfs: publish future pidfd files
In reply to Li ChenPublish 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.
--- fs/pidfs.c | 127 +++++++++++++++++++++++++++++++++++++++++- include/linux/pidfs.h | 5 ++ 2 files changed, 130 insertions(+), 2 deletions(-)
fs/pidfs.c +125 -2
@@ -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);
include/linux/pidfs.h +5 -0
@@ -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);
Signature · 1 line
2.52.0
RFC PATCH 11/24 pidfd: add spawn builder state tracking
In reply to Li ChenAdd 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.
--- fs/pidfd_spawn.c | 50 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-)
fs/pidfd_spawn.c +49 -1
@@ -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,
Signature · 1 line
2.52.0
RFC PATCH 12/24 fork: let kernel callers create embryonic tasks
In reply to Li ChenA 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.
--- include/linux/sched.h | 21 +++++++++++++++++++++ include/linux/sched/task.h | 1 + kernel/fork.c | 1 + kernel/ptrace.c | 4 ++++ 4 files changed, 27 insertions(+)
include/linux/sched.h +21 -0
@@ -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) {
include/linux/sched/task.h +1 -0
@@ -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; };
kernel/fork.c +1 -0
@@ -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 ;
kernel/ptrace.c +4 -0
@@ -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
Signature · 1 line
2.52.0
Andy Lutomirski
maintainerRe: [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks
Quoted 26 lines · 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.
kernel/ptrace.c +2
-0@@ -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);
kernel/ptrace.c static int __ptrace_may_access(struct task_struct *task, unsigned int mode) · line 318
Quoted 5 lines
kernel/ptrace.c +2
-0@@ -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
Re: [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks
Hi Andy,
---- On Thu, 16 Jul 2026 23:57:58 +0800 Andy Lutomirski <[email protected]> wrote ---
Quoted 27 lines · Li Chen
Quoted 26 lines
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.
kernel/ptrace.c +2
-0@@ -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)
Quoted 8 lines
kernel/ptrace.c static int __ptrace_may_access(struct task_struct *task, unsigned int mode) · line 318
Quoted 5 lines
kernel/ptrace.c +2
-0WARN (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.
Good point. I'm not sure ECONNREFUSED fits outside connection setup, though. ESRCH may make more sense if the task is meant to appear hidden. I'll think about this some more.
Quoted 6 lines
There's a not-locally-obvious gotcha here: reading other process attributes prior to calling
task_is_embryonic_execmay result in (security-relevant!) data races. This should at least be documented -- it's critical to checktask_is_embryonic_exec*before* trying to read credentials. Also, I think /proc and many pidfd APIs have the same issue.
Agreed. Patch 15 later in the series already applies this ordering to procfs, including cached dentry revalidation, and to PIDFD_GET_INFO.
The ordering requirement still needs to be made more explicit. I'll document it, move the check ahead of ptrace_parent() in ptracer_access_allowed(), and audit the remaining pidfd users before the next version.
Regards,
Li
RFC PATCH 13/24 fork: let new tasks start with task work
In reply to Li ChenProcess 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.
--- include/linux/sched/task.h | 2 ++ kernel/fork.c | 6 ++++++ 2 files changed, 8 insertions(+)
include/linux/sched/task.h +2 -0
@@ -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;
kernel/fork.c +6 -0
@@ -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. */
Signature · 1 line
2.52.0
RFC PATCH 14/24 pidfd: create and execute spawn builder tasks
In reply to Li ChenAdd 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.
--- fs/pidfd_spawn.c | 406 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 402 insertions(+), 4 deletions(-)
fs/pidfd_spawn.c +402 -4
@@ -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 &¤t_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; +}
Signature · 1 line
2.52.0
RFC PATCH 15/24 fork: keep embryonic tasks hidden until exec completes
In reply to Li ChenA 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.
--- 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(-)
fs/coredump.c +3 -1
@@ -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(), };
fs/exec.c +2 -0
@@ -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);
fs/pidfd_spawn.c +0 -2
@@ -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) {
fs/pidfs.c +6 -1
@@ -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. */ }
fs/proc/base.c +9 -2
@@ -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 * dentryrcu_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) {
fs/proc/internal.h +17 -1
@@ -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,
kernel/ptrace.c +1 -1
@@ -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?
Signature · 1 line
2.52.0
RFC PATCH 16/24 audit: add pidfd spawn child contexts
In reply to Li ChenKernel-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.
--- 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(-)
include/linux/audit.h +31 -0
@@ -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 )
include/uapi/linux/audit.h +1 -0
@@ -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 */
kernel/audit.h +1 -0
@@ -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 */
kernel/auditsc.c +103 -2
135 lines not on this page · raw message
Signature · 1 line
2.52.0
RFC PATCH 17/24 pidfd: audit child spawn execution
In reply to Li ChenThe 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.
--- fs/pidfd_spawn.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+)
fs/pidfd_spawn.c +29 -0
@@ -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 );
Signature · 1 line
2.52.0
RFC PATCH 18/24 pidfd: make spawn builder execution signal-safe
In reply to Li ChenWait 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.
--- fs/pidfd_spawn.c | 189 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 183 insertions(+), 6 deletions(-)
fs/pidfd_spawn.c +183 -6
@@ -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); }
Signature · 1 line
2.52.0
RFC PATCH 19/24 file: expose spawn file-action helpers
In reply to Li ChenProcess 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.
--- fs/file.c | 11 ++++++++--- fs/internal.h | 2 ++ fs/open.c | 7 ++++++- 3 files changed, 16 insertions(+), 4 deletions(-)
fs/file.c +8 -3
@@ -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
fs/internal.h +2 -0
@@ -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);
fs/open.c +6 -1
@@ -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;
Signature · 1 line
2.52.0
RFC PATCH 20/24 pidfd: add initial spawn file actions
In reply to Li ChenAllow 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.
--- fs/pidfd_spawn.c | 171 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 163 insertions(+), 8 deletions(-)
fs/pidfd_spawn.c +163 -8
@@ -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);
Signature · 1 line
2.52.0
RFC PATCH 21/24 pidfd: consume spawn builders on the first run attempt
In reply to Li ChenA 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.
--- fs/pidfd_spawn.c | 164 +++++++++++++++++++++++++++++++---------------- 1 file changed, 109 insertions(+), 55 deletions(-)
fs/pidfd_spawn.c +109 -55
@@ -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);
Signature · 1 line
2.52.0
RFC PATCH 22/24 pidfd: expose spawn builder system calls
In reply to Li ChenThe 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.
--- 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(-)
arch/alpha/kernel/syscalls/syscall.tbl +2 -0
@@ -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
arch/arm/tools/syscall.tbl +2 -0
@@ -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
arch/arm64/tools/syscall_32.tbl +2 -0
@@ -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
arch/m68k/kernel/syscalls/syscall.tbl +2 -0
@@ -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
arch/microblaze/kernel/syscalls/syscall.tbl +2 -0
@@ -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
arch/mips/kernel/syscalls/syscall_n32.tbl +2 -0
@@ -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
arch/mips/kernel/syscalls/syscall_n64.tbl +2 -0
@@ -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
arch/mips/kernel/syscalls/syscall_o32.tbl +2 -0
@@ -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
arch/parisc/kernel/syscalls/syscall.tbl +2 -0
@@ -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
arch/powerpc/kernel/syscalls/syscall.tbl +2 -0
@@ -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
arch/s390/kernel/syscalls/syscall.tbl +2 -0
@@ -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
arch/sh/kernel/syscalls/syscall.tbl +2 -0
@@ -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
arch/sparc/kernel/syscalls/syscall.tbl +2 -0
@@ -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
arch/x86/entry/syscalls/syscall_32.tbl +2 -0
@@ -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
arch/x86/entry/syscalls/syscall_64.tbl +2 -0
@@ -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
arch/xtensa/kernel/syscalls/syscall.tbl +2 -0
@@ -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
include/linux/syscalls.h +7 -0
@@ -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,
include/uapi/asm-generic/unistd.h +7 -1
@@ -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
kernel/pid.c +18 -1
@@ -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);
scripts/syscall.tbl +2 -0
@@ -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
tools/include/uapi/asm-generic/unistd.h +7 -1
@@ -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
tools/perf/arch/alpha/entry/syscalls/syscall.tbl +2 -0
@@ -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
tools/perf/arch/arm/entry/syscalls/syscall.tbl +2 -0
@@ -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
tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl +11 -0
@@ -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
tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl +2 -0
@@ -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
tools/perf/arch/parisc/entry/syscalls/syscall.tbl +2 -0
@@ -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
tools/perf/arch/powerpc/entry/syscalls/syscall.tbl +2 -0
@@ -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
tools/perf/arch/s390/entry/syscalls/syscall.tbl +2 -0
@@ -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
tools/perf/arch/sh/entry/syscalls/syscall.tbl +2 -0
@@ -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
tools/perf/arch/sparc/entry/syscalls/syscall.tbl +2 -0
@@ -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
tools/perf/arch/x86/entry/syscalls/syscall_32.tbl +2 -0
@@ -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
tools/perf/arch/x86/entry/syscalls/syscall_64.tbl +2 -0
@@ -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
tools/perf/arch/xtensa/entry/syscalls/syscall.tbl +2 -0
@@ -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
tools/scripts/syscall.tbl +2 -0
@@ -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
Signature · 1 line
2.52.0
RFC PATCH 23/24 selftests/pidfd: cover pidfd spawn builders
In reply to Li ChenAdd 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.
--- 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
tools/testing/selftests/landlock/audit.h +4 -2
@@ -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;
tools/testing/selftests/pidfd/.gitignore +9 -0
@@ -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
tools/testing/selftests/pidfd/Makefile +24 -1
@@ -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
tools/testing/selftests/pidfd/config +6 -0
@@ -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
tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c new file +428 -0
428 lines not on this page · raw message
tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c new file +474 -0
474 lines not on this page · raw message
tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c new file +521 -0
521 lines not on this page · raw message
tools/testing/selftests/pidfd/pidfd_spawn_common.c new file +512 -0
512 lines not on this page · raw message
tools/testing/selftests/pidfd/pidfd_spawn_common.h new file +59 -0
59 lines not on this page · raw message
tools/testing/selftests/pidfd/pidfd_spawn_compat.c new file +221 -0
221 lines not on this page · raw message
tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c new file +301 -0
301 lines not on this page · raw message
tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c new file +294 -0
294 lines not on this page · raw message
tools/testing/selftests/pidfd/pidfd_spawn_race_test.c new file +923 -0
114 lines not on this page · raw message
tools/testing/selftests/pidfd/pidfd_spawn_security_test.c new file +1242 -0
1,242 lines not on this page · raw message
tools/testing/selftests/pidfd/pidfd_spawn_test.c new file +550 -0
550 lines not on this page · raw message
The rest of this diff is not shown. View the raw message.
Signature · 1 line
2.52.0
Part of the diff is not shown here. View the raw message.
RFC PATCH 24/24 Documentation: describe pidfd spawn builders
In reply to Li ChenDocument 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.
--- 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
Documentation/userspace-api/index.rst +1 -0
@@ -21,6 +21,7 @@ System calls ebpf/index ioctl/index mseal + pidfd_spawn rseq Security-related interfaces
Documentation/userspace-api/pidfd_spawn.rst new file +247 -0
247 lines not on this page · raw message
MAINTAINERS +1 -0
@@ -21288,6 +21288,7 @@ M: Christian Brauner <[email protected]> L: [email protected] 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
Signature · 1 line
2.52.0
Quoted 16 lines · Li Chen
Hi,
This RFC follows feedback on my earlier
spawn_templateRFC [1]. That proposal made caching the primary interface; this one starts with general process construction. Christian suggested a pidfd/pidfs exec builder modeled afterfsconfig(), with enough semantics for userspace to implementposix_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/[email protected]/ * pidfs: handle FS_IOC32_GETVERSION in compat ioctl https://lore.kernel.org/all/[email protected]/
Quoted 5 lines
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",
This should probably be an FD for the path.
This way it prevents race conditions over multiple configuration steps.
Quoted 9 lines
.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:
Quoted 8 lines
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);Same here. Should probably be an FD to the binary instead.
Quoted 1 line
pidfd_spawn_run(fd, &run, sizeof(run));
I'm worried this pidfd_spawn_run just adds another varient to the existing myriad of exec* syscalls we already have. Would it be better to just have this work through execveat(fd, "", argv, envp, AT_EMPTY_PATH) instead?
(i.e have execveat take a pidfd directly).
Then you can get rid of pidfd_spawn_run which looks almost structurally identical to execveat (with the argv and envp collapsed).
Justin
Quoted 232 lines
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-ESRCHbefore 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 creatingmm_struct, exact credential object, and child PID namespace, soSCM_RIGHTSdoes 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_INFOwithPIDFD_INFO_EXITdistinguishes 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 completeposix_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 reportsPIDFD_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_SPAWNtransaction, not a syntheticAUDIT_SYSCALL. Should it be selected through the sourcepidfd_spawn_run()exit rule? An existing rule naming onlyexecve()orexecveat()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_VFORKplus 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_spawnpath does not inherit the parent's address space [6], and WindowsCreateProcess()accepts explicit startup state [7]. Josh'sio_uring_spawnLPC 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_spawnsemantics 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/[email protected]/
[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.pdfLi 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 buildersDocumentation/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.cSignature · 1 line
2.52.0
Hi Justin,
---- On Wed, 05 Aug 2026 04:25:25 +0800 Justin Suess <[email protected]> wrote ---
Quoted 17 lines · Li Chen
Quoted 16 lines
Hi,
This RFC follows feedback on my earlier
spawn_templateRFC [1]. That proposal made caching the primary interface; this one starts with general process construction. Christian suggested a pidfd/pidfs exec builder modeled afterfsconfig(), with enough semantics for userspace to implementposix_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/[email protected]/ * pidfs: handle FS_IOC32_GETVERSION in compat ioctl https://lore.kernel.org/all/[email protected]/
Quoted 8 lines
Quoted 5 lines
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",This should probably be an FD for the path.
This way it prevents race conditions over multiple configuration steps.
Thanks, that makes sense. Using an fd avoids the pathname race and pins the executable we actually want to run.
Quoted 20 lines
Quoted 9 lines
.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:
Quoted 8 lines
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);Same here. Should probably be an FD to the binary instead.
Quoted 1 line
pidfd_spawn_run(fd, &run, sizeof(run));
Quoted 8 lines
I'm worried this
pidfd_spawn_runjust adds another varient to the existing myriad of exec* syscalls we already have. Would it be better to just have this work throughexecveat(fd, "", argv, envp, AT_EMPTY_PATH)instead?(i.e have execveat take a pidfd directly).
Then you can get rid of
pidfd_spawn_runwhich looks almost structurally identical to execveat (with the argv and envp collapsed).
Thanks, but I'm less sure about using execveat() as the run operation, since it normally replaces the caller while the builder creates a new child and returns. A separate run operation still seems clearer to me.
Regards,
Li
Whoa, this is older than I thought.
First and a foremost apologies for radio silence, I got really busy with other stuff.
I did not have the time yet to go through the patchset, but I have some time to respond to this bit.
Quoted 24 lines · Li Chen
Hi Justin,
---- On Wed, 05 Aug 2026 04:25:25 +0800 Justin Suess <[email protected]> wrote ---
Quoted 20 lines · Li Chen
Quoted 19 lines
Hi,
This RFC follows feedback on my earlier
spawn_templateRFC [1]. That proposal made caching the primary interface; this one starts with general process construction. Christian suggested a pidfd/pidfs exec builder modeled afterfsconfig(), with enough semantics for userspace to implementposix_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/[email protected]/ * pidfs: handle FS_IOC32_GETVERSION in compat ioctl https://lore.kernel.org/all/[email protected]/The initial implementation is source-based. The executable path can be provided with the final run request:
Quoted 9 lines
Quoted 5 lines
Quoted 2 lines
struct pidfd_spawn_run_args run = { .path = (unsigned long)"/usr/bin/rg",This should probably be an FD for the path.
This way it prevents race conditions over multiple configuration steps.
Thanks, that makes sense. Using an fd avoids the pathname race and pins the executable we actually want to run.
I don't understand what race is supposed to be addressed either as none of the setup depends on the content binary.
Regardless I don't think this is going to fly as it directly shafts NUMA setups. What's needed is a path to the binary + the PATH envifornment variable or a collection of paths to try out.
Here is how it harms NUMA: In the most extreme case suppose there is a container bound to a specific node and you want to exec something into it. Further suppose none of the relevant data is in RAM yet. And finally the process doing all the work is running a different node.
Say you are going to spawn ${container}/usr/bin/coolprog.
At minimum this will allocate a dentry and an inode for usr, bin and coolprog itself.
Afterwards any path lookup going through usr in that that container will have to fetch memory from a non-local node to traverse it, same thing for the bin subdirectory.
This is a self-induced problem when using a fd.
In the real world userspace notoriously performs failed execs as it makes its way through dirs in provided in PATH, and while you can patch select programs all you want, the mechanism will have to account for it.
This is where the next NUMA issue lies its ugly head. Suppose you have a box with multiple nodes, but don't use anything to maintain affinity (as in, processes run where the scheduler sees fit).
A well-known exec-heavy workload where this is fine is compilation. Any big project will do, to give you one trivial example, make -j $BIGNUM modules in the Linux kernel directory.
Say you have a box with 4 domains and 32 threads per each. make -j 128 will end up spilling on the entirety of the machine and there will be some loss of locality when spawning gcc and whatnot. Even then, a sensibly behaving scheduler will not migrate processes between nodes without needing to do it. Meaning after the process gets to run, you can assume it stays on the given node. The stock kernel with the fork
+ exec machinery is doing a shitty job here, but the new mechanism can assure process-specific allocation to come form the target node as long as the scheduler does not mess up.
To that end, the final form of the machinery needs to be in position to do the following:
1. copy in whatever data it needs from userspace
2. if no affinity is specified, ask the scheduler: assuming something forks now, where is it going to run? the answer would be speculative ofc
3. to the minimum allocations needed (task_struct etc.) while passing the node number from above
4. have the newly spawned task handle everything else
This in particular means that a failed exec would be very expensive, as it would go through task creation and teardown. Meaning the child needs to be able to figure out where to binary to execute is. And user-visible part has to account for it from the get-go.
I'm going to write more after I go through the patchset, should be next week.
Quoted 37 lines
Quoted 20 lines
Quoted 9 lines
.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:
Quoted 8 lines
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);Same here. Should probably be an FD to the binary instead.
Quoted 1 line
pidfd_spawn_run(fd, &run, sizeof(run));Quoted 8 lines
I'm worried this
pidfd_spawn_runjust adds another varient to the existing myriad of exec* syscalls we already have. Would it be better to just have this work throughexecveat(fd, "", argv, envp, AT_EMPTY_PATH)instead?(i.e have execveat take a pidfd directly).
Then you can get rid of
pidfd_spawn_runwhich looks almost structurally identical to execveat (with the argv and envp collapsed).Thanks, but I'm less sure about using
execveat()as the run operation, since it normally replaces the caller while the builder creates a new child and returns. A separate run operation still seems clearer to me.Regards,
Li
Hi Mateusz,
Quoted 2 lines
First and a foremost apologies for radio silence, I got really busy with other stuff.
Thanks for making time to write such a detailed reply despite being busy with other work. Sorry for taking so long to get back to you.
Quoted 2 lines
I don't understand what race is supposed to be addressed either as none of the setup depends on the content binary.
You're right that none of the setup depends on an earlier check of the executable. My earlier reply mixed up two different requirements:
executing whatever a pathname resolves to at exec time, and executing a particular file selected earlier.
The posted RFC does the former. It stores the pathname without looking up or inspecting the executable during configuration.
The caller-side race I had in mind was this:
caller another process child
| | |
| inspect file A via path | |
| configure builder | |
| | replace path |
| | with file B |
| spawn_run() | |
| | lookup -> B |
| | exec B |This matters if the caller expects the file it inspected to be the one executed. Opening A first, inspecting it through that fd, and using the same fd for execution avoids the pathname substitution. The fd pins the file's identity, not immutable contents.
Quoted 3 lines
Regardless I don't think this is going to fly as it directly shafts NUMA setups. What's needed is a path to the binary + the PATH envifornment variable or a collection of paths to try out.
I went back to the spawn_template discussion and realized that you had already raised these NUMA concerns there, including child-side lookup, PATH search, and target-node allocation. Sorry, I should have connected your current comment with that earlier explanation.
Quoted 8 lines
At minimum this will allocate a dentry and an inode for usr, bin and coolprog itself.
Afterwards any path lookup going through usr in that that container will have to fetch memory from a non-local node to traverse it, same thing for the bin subdirectory.
This is a self-induced problem when using a fd.
I see how opening the executable in the parent can populate cold filesystem metadata on the parent's node, even though the child will run on a different node. Making an executable fd mandatory would force that early lookup on callers who could otherwise leave it to the child.
Quoted 10 lines
A well-known exec-heavy workload where this is fine is compilation. Any big project will do, to give you one trivial example, make -j $BIGNUM modules in the Linux kernel directory.
[...]
The stock kernel with the fork
+ exec machinery is doing a shitty job here, but the new mechanism can assure process-specific allocation to come form the target node as long as the scheduler does not mess up.
The compilation example helps too, and this matters even without explicit affinity. Moving lookup into the child does not by itself address the initial per-task allocations.
Quoted 7 lines
1. copy in whatever data it needs from userspace
2. if no affinity is specified, ask the scheduler: assuming something forks now, where is it going to run? the answer would be speculative ofc
3. to the minimum allocations needed (task_structetc.) while passing the node number from above
4. have the newly spawned task handle everything else
I'll look into using the scheduler's speculative placement for those initial allocations, then leaving the remaining work to the child where it actually runs.
Quoted 11 lines
In the real world userspace notoriously performs failed execs as it makes its way through dirs in provided in PATH, and while you can patch select programs all you want, the mechanism will have to account for it.
[...]
This in particular means that a failed exec would be very expensive, as it would go through task creation and teardown. Meaning the child needs to be able to figure out where to binary to execute is. And user-visible part has to account for it from the get-go.
Your point about failed execs during PATH search makes sense too. Creating and tearing down a task for each candidate would be wasteful. I plan to have libc build a list of candidate paths without doing any filesystem lookup, and pass that list to the kernel. The child would apply its setup once, then try the candidates in order, continuing after recoverable exec failures in the same task.
That keeps PATH parsing in libc and lookup in the child. Relative paths would stay relative, so they resolve against the child's cwd after file actions. A single pathname would just be a one-entry list. This needs interface support, and I'll need to work through the exec cleanup and error handling to preserve the expected posix_spawnp() behavior.
I'm still unsure whether supporting both modes is a good interface design. An fd option seems useful for callers that need to execute the same file they opened earlier, while child-side pathname lookup would avoid forcing that early lookup on everyone. I'm leaning towards keeping both, but haven't settled on that yet.
Quoted 1 line
I'm going to write more after I go through the patchset, should be next week.
Thanks again for taking the time to explain this. I look forward to your further comments.
Regards,
Li