Linux Security Modules development
 help / color / mirror / Atom feed
* [RFC PATCH 07/24] fork: expose vfork completion helper
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Allow process builders that call copy_process() directly to attach and
wait for the existing vfork completion without duplicating its signal
and task-reference handling.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 include/linux/sched/task.h | 3 +++
 kernel/fork.c              | 3 +--
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h
index e0c1ca8c6a188..92b4e3bc31e66 100644
--- a/include/linux/sched/task.h
+++ b/include/linux/sched/task.h
@@ -14,6 +14,7 @@
 
 struct task_struct;
 struct rusage;
+struct completion;
 union thread_union;
 struct css_set;
 
@@ -99,6 +100,8 @@ extern void exit_itimers(struct task_struct *);
 extern pid_t kernel_clone(struct kernel_clone_args *kargs);
 struct task_struct *copy_process(struct pid *pid, int trace, int node,
 				 struct kernel_clone_args *args);
+int wait_for_vfork_done(struct task_struct *child,
+			struct completion *vfork);
 struct task_struct *create_io_thread(int (*fn)(void *), void *arg, int node);
 struct task_struct *fork_idle(int);
 extern pid_t kernel_thread(int (*fn)(void *), void *arg, const char *name,
diff --git a/kernel/fork.c b/kernel/fork.c
index f3f378a85be4c..970810a01bbf6 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -1435,8 +1435,7 @@ static void complete_vfork_done(struct task_struct *tsk)
 	task_unlock(tsk);
 }
 
-static int wait_for_vfork_done(struct task_struct *child,
-				struct completion *vfork)
+int wait_for_vfork_done(struct task_struct *child, struct completion *vfork)
 {
 	unsigned int state = TASK_KILLABLE|TASK_FREEZABLE;
 	int killed;
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 08/24] pidfs: attach pids to future pidfd files
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Associate a future pidfs inode with a preallocated struct pid and reuse
the future dentry as the canonical stashed dentry. Preserve the
reserved inode identity when the pid is registered.

Keep attachment reversible until a task owns the pid so copy_process()
failure can return the future file to its taskless state. File-handle
lookup requires both the stash and an attached task before accepting an
unpublished pid, preventing failed allocations from being reopened
through stale state.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfs.c            | 89 +++++++++++++++++++++++++++++++++++++++++--
 include/linux/pidfs.h |  2 +
 2 files changed, 87 insertions(+), 4 deletions(-)

diff --git a/fs/pidfs.c b/fs/pidfs.c
index 28464fe274c9e..36ee4f210f73f 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -115,6 +115,7 @@ struct pidfs_future_file {
 	void *data;
 	const struct pidfs_future_file_ops *ops;
 	struct pidfs_attr *attr;
+	struct pid *pid;
 	u64 ino;
 };
 
@@ -238,7 +239,8 @@ int pidfs_add_pid(struct pid *pid)
 {
 	int ret;
 
-	pid->ino = pidfs_alloc_ino();
+	if (!pid->ino)
+		pid->ino = pidfs_alloc_ino();
 	ret = rhashtable_insert_fast(&pidfs_ino_ht, &pid->pidfs_hash,
 				     pidfs_ino_ht_params);
 	if (unlikely(ret))
@@ -987,9 +989,18 @@ static struct pid *pidfs_ino_get_pid(u64 ino)
 	if (!pid)
 		return NULL;
 	attr = READ_ONCE(pid->attr);
-	if (IS_ERR_OR_NULL(attr))
+	if (IS_ERR(attr))
+		return NULL;
+	/*
+	 * A task can become visible before its future pidfd is published. Let
+	 * ->open() wait across that window, but never expose a dentry for a
+	 * caller-supplied pid when copy_process() has failed. That failure path
+	 * clears the stash before freeing the pid allocation.
+	 */
+	if (!attr && (!READ_ONCE(pid->stashed) ||
+		      !pid_has_task(pid, PIDTYPE_PID)))
 		return NULL;
-	if (test_bit(PIDFS_ATTR_BIT_EXIT, &attr->attr_mask))
+	if (attr && test_bit(PIDFS_ATTR_BIT_EXIT, &attr->attr_mask))
 		return NULL;
 	/* Within our pid namespace hierarchy? */
 	if (pid_vnr(pid) == 0)
@@ -1025,7 +1036,8 @@ static struct dentry *pidfs_fh_to_dentry(struct super_block *sb,
 	if (ret < 0)
 		return ERR_PTR(ret);
 
-	VFS_WARN_ON_ONCE(!pid->attr);
+	VFS_WARN_ON_ONCE(!pid->attr &&
+			 !pidfs_future_file(d_inode(path.dentry)));
 
 	mntput(path.mnt);
 	return path.dentry;
@@ -1103,6 +1115,10 @@ static int pidfs_init_inode(struct inode *inode, void *data)
 static bool pidfs_inode_data_matches(const struct inode *inode,
 				     const void *data)
 {
+	struct pidfs_future_file *future = pidfs_future_file(inode);
+
+	if (future)
+		return READ_ONCE(future->pid) == data;
 	return pidfs_inode_pid(inode) == data;
 }
 
@@ -1266,6 +1282,10 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
 				PIDFD_AUTOKILL) != 5);
 	BUILD_BUG_ON(PIDFD_EMPTY & VALID_OPEN_FLAGS);
 
+	ret = pidfs_register_pid(pid);
+	if (ret)
+		return ERR_PTR(ret);
+
 	ret = path_from_stashed(&pid->stashed, pidfs_mnt, get_pid(pid), &path);
 	if (ret < 0)
 		return ERR_PTR(ret);
@@ -1369,6 +1389,67 @@ u64 pidfs_future_file_ino(const struct file *file)
 	return future ? future->ino : 0;
 }
 
+/**
+ * pidfs_future_file_set_pid - associate a preallocated pid with a future file
+ * @file: future pidfs file
+ * @pid: preallocated pid whose pidfs inode number matches @file
+ *
+ * This stashes the future dentry in @pid before copy_process() publishes the
+ * task. No pid reference is transferred. On a later creation failure the
+ * producer must call pidfs_future_file_clear_pid() before freeing @pid.
+ *
+ * Return: Zero on success or a negative error code.
+ */
+int pidfs_future_file_set_pid(struct file *file, struct pid *pid)
+{
+	struct inode *inode = file_inode(file);
+	struct pidfs_future_file *future = pidfs_future_file(inode);
+	struct dentry *dentry = file->f_path.dentry;
+	struct dentry *stashed;
+
+	if (!future || !future->ops->get_pid || future->pid ||
+	    pid->ino != future->ino)
+		return -EBADF;
+
+	WRITE_ONCE(future->pid, pid);
+	dentry->d_fsdata = &pid->stashed;
+	stashed = stash_dentry(&pid->stashed, dentry);
+	if (stashed != dentry) {
+		dput(stashed);
+		pidfs_future_file_clear_pid(file, pid);
+		return -EBUSY;
+	}
+	return 0;
+}
+
+/**
+ * pidfs_future_file_clear_pid - undo an unpublished future-pid association
+ * @file: future pidfs file
+ * @pid: pid previously associated with @file
+ *
+ * This may be called only after pidfs_future_file_set_pid() and before
+ * pidfs_future_file_publish_pid(). It removes the stashed dentry so the
+ * producer can free @pid after a task-creation failure.
+ *
+ * The producer must clear the association before attaching @pid to a task.
+ * File-handle lookup treats an attached pid as a publication-in-progress
+ * guarantee, so clearing the association later would violate that contract.
+ */
+void pidfs_future_file_clear_pid(struct file *file, struct pid *pid)
+{
+	struct inode *inode = file_inode(file);
+	struct pidfs_future_file *future = pidfs_future_file(inode);
+	struct dentry *dentry = file->f_path.dentry;
+
+	if (WARN_ON_ONCE(!future || READ_ONCE(future->pid) != pid))
+		return;
+
+	WARN_ON_ONCE(cmpxchg(&pid->stashed, dentry, NULL) != dentry);
+	dentry->d_fsdata = NULL;
+	WARN_ON_ONCE(pid->attr);
+	WRITE_ONCE(future->pid, NULL);
+}
+
 void __init pidfs_init(void)
 {
 	if (rhashtable_init(&pidfs_ino_ht, &pidfs_ino_ht_params))
diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h
index 6b7fbc54ab388..828e96f769ba1 100644
--- a/include/linux/pidfs.h
+++ b/include/linux/pidfs.h
@@ -28,6 +28,8 @@ struct file *pidfs_alloc_future_file(const char *name, void *data,
 void *pidfs_future_file_data(const struct file *file,
 			     const struct pidfs_future_file_ops *ops);
 u64 pidfs_future_file_ino(const struct file *file);
+int pidfs_future_file_set_pid(struct file *file, struct pid *pid);
+void pidfs_future_file_clear_pid(struct file *file, struct pid *pid);
 void __init pidfs_init(void);
 void pidfs_prepare_pid(struct pid *pid);
 int pidfs_add_pid(struct pid *pid);
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 09/24] fork: let process builders supply preallocated pids
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Process builders can reserve a pidfs identity before copy_process()
makes a task visible. Add an alloc_pid() variant for a reserved pidfs
inode, and let copy_process() consume a caller-provided struct pid on
success.

Keep caller ownership on failure. The legacy NULL-pid path continues to
allocate and free its own pid. Validate the target PID namespace before
using a preallocated identity.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 include/linux/pid.h |  3 +++
 kernel/fork.c       | 16 ++++++++++++++--
 kernel/pid.c        | 22 ++++++++++++++++++++--
 3 files changed, 37 insertions(+), 4 deletions(-)

diff --git a/include/linux/pid.h b/include/linux/pid.h
index a29ffe2a5fa8e..fa8acae336f7c 100644
--- a/include/linux/pid.h
+++ b/include/linux/pid.h
@@ -142,6 +142,9 @@ extern struct pid *find_ge_pid(int nr, struct pid_namespace *);
 
 extern struct pid *alloc_pid(struct pid_namespace *ns, pid_t *set_tid,
 			     size_t set_tid_size);
+struct pid *alloc_pid_with_pidfs_ino(struct pid_namespace *ns,
+				     pid_t *set_tid, size_t set_tid_size,
+				     u64 pidfs_ino);
 extern void free_pid(struct pid *pid);
 void free_pids(struct pid **pids);
 extern void disable_pid_allocation(struct pid_namespace *ns);
diff --git a/kernel/fork.c b/kernel/fork.c
index 970810a01bbf6..d16405c037c2f 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2006,6 +2006,9 @@ static bool need_futex_hash_allocate_default(u64 clone_flags)
  * It copies the registers, and all the appropriate
  * parts of the process environment (as per the clone
  * flags). The actual kick-off is left to the caller.
+ *
+ * Except for init_struct_pid, a caller-supplied pid reference remains owned
+ * by the caller on failure and is transferred to the new task on success.
  */
 __latent_entropy struct task_struct *copy_process(
 					struct pid *pid,
@@ -2017,6 +2020,7 @@ __latent_entropy struct task_struct *copy_process(
 	struct task_struct *p;
 	struct multiprocess_signals delayed;
 	struct file *pidfile = NULL;
+	bool allocated_pid = false;
 	const u64 clone_flags = args->flags;
 	struct nsproxy *nsp = current->nsproxy;
 
@@ -2317,13 +2321,21 @@ __latent_entropy struct task_struct *copy_process(
 
 	stackleak_task_init(p);
 
-	if (pid != &init_struct_pid) {
+	if (!pid) {
 		pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,
 				args->set_tid_size);
 		if (IS_ERR(pid)) {
 			retval = PTR_ERR(pid);
 			goto bad_fork_cleanup_thread;
 		}
+		allocated_pid = true;
+	} else if (pid != &init_struct_pid) {
+		if (args->set_tid_size ||
+		    ns_of_pid(pid) != p->nsproxy->pid_ns_for_children ||
+		    WARN_ON_ONCE(pid_has_task(pid, PIDTYPE_PID))) {
+			retval = -EINVAL;
+			goto bad_fork_cleanup_thread;
+		}
 	}
 
 	/*
@@ -2587,7 +2599,7 @@ __latent_entropy struct task_struct *copy_process(
 		put_unused_fd(pidfd);
 	}
 bad_fork_free_pid:
-	if (pid != &init_struct_pid)
+	if (allocated_pid)
 		free_pid(pid);
 bad_fork_cleanup_thread:
 	exit_thread(p);
diff --git a/kernel/pid.c b/kernel/pid.c
index f55189a3d07d4..010f80177cac8 100644
--- a/kernel/pid.c
+++ b/kernel/pid.c
@@ -156,8 +156,8 @@ void free_pids(struct pid **pids)
 			free_pid(pids[tmp]);
 }
 
-struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
-		      size_t arg_set_tid_size)
+static struct pid *__alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
+			       size_t arg_set_tid_size, u64 pidfs_ino)
 {
 	int set_tid[MAX_PID_NS_LEVEL + 1] = {};
 	int pid_max[MAX_PID_NS_LEVEL + 1] = {};
@@ -198,6 +198,7 @@ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
 	init_waitqueue_head(&pid->wait_pidfd);
 	INIT_HLIST_HEAD(&pid->inodes);
 	pidfs_prepare_pid(pid);
+	pid->ino = pidfs_ino;
 
 	/*
 	 * 2. perm check checkpoint_restore_ns_capable()
@@ -358,6 +359,23 @@ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
 	return ERR_PTR(retval);
 }
 
+struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
+		      size_t arg_set_tid_size)
+{
+	return __alloc_pid(ns, arg_set_tid, arg_set_tid_size, 0);
+}
+
+struct pid *alloc_pid_with_pidfs_ino(struct pid_namespace *ns,
+				     pid_t *arg_set_tid,
+				     size_t arg_set_tid_size,
+				     u64 pidfs_ino)
+{
+	if (!pidfs_ino)
+		return ERR_PTR(-EINVAL);
+
+	return __alloc_pid(ns, arg_set_tid, arg_set_tid_size, pidfs_ino);
+}
+
 void disable_pid_allocation(struct pid_namespace *ns)
 {
 	spin_lock(&pidmap_lock);
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 10/24] pidfs: publish future pidfd files
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Publish an attached future pidfd by transferring pidfs attributes and
forwarding process-exit wakeups to poll registrations made while the
file was taskless.

Make numeric pidfd opens and file-handle opens wait across the interval
between task visibility and producer publication. Internal PIDFD_STALE
callers remain nonblocking so task creation cannot wait on itself.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfs.c            | 127 +++++++++++++++++++++++++++++++++++++++++-
 include/linux/pidfs.h |   5 ++
 2 files changed, 130 insertions(+), 2 deletions(-)

diff --git a/fs/pidfs.c b/fs/pidfs.c
index 36ee4f210f73f..d62235e01b351 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -116,7 +116,10 @@ struct pidfs_future_file {
 	const struct pidfs_future_file_ops *ops;
 	struct pidfs_attr *attr;
 	struct pid *pid;
+	wait_queue_head_t wait_pidfd;
+	wait_queue_entry_t pid_wait;
 	u64 ino;
+	bool pid_wait_attached;
 };
 
 static struct pidfs_node *pidfs_inode_node(const struct inode *inode)
@@ -137,6 +140,12 @@ pidfs_future_file(const struct inode *inode)
 	return container_of(node, struct pidfs_future_file, node);
 }
 
+static bool pidfs_future_file_terminal(struct pidfs_future_file *future)
+{
+	return future && future->ops->is_terminal &&
+	       future->ops->is_terminal(future->data);
+}
+
 void *pidfs_future_file_data(const struct file *file,
 			     const struct pidfs_future_file_ops *ops)
 {
@@ -169,6 +178,21 @@ static struct pid *pidfs_inode_pid(const struct inode *inode)
 	return pid;
 }
 
+static int pidfs_wait_for_published_pid(struct inode *inode)
+{
+	struct pidfs_future_file *future = pidfs_future_file(inode);
+	int status = -ESRCH;
+	int ret;
+
+	if (!future)
+		return 0;
+
+	ret = wait_event_killable(future->wait_pidfd,
+				  (status = PTR_ERR_OR_ZERO(pidfs_inode_pid(inode))) !=
+				  -ESRCH || pidfs_future_file_terminal(future));
+	return ret ? ret : status;
+}
+
 #if BITS_PER_LONG == 32
 
 DEFINE_SPINLOCK(pidfs_ino_lock);
@@ -370,10 +394,22 @@ static void pidfd_show_fdinfo(struct seq_file *m, struct file *f)
  */
 static __poll_t pidfd_poll(struct file *file, struct poll_table_struct *pts)
 {
+	struct pidfs_future_file *future;
 	struct task_struct *task;
 	__poll_t poll_flags = 0;
-	struct pid *pid = pidfd_pid(file);
+	struct pid *pid;
 
+	future = pidfs_future_file(file_inode(file));
+	pid = pidfd_pid(file);
+	if (IS_ERR(pid) && PTR_ERR(pid) == -ESRCH && future) {
+		/* Close the publication race after registering the future wait. */
+		poll_wait(file, &future->wait_pidfd, pts);
+		pid = pidfd_pid(file);
+	}
+
+	if (IS_ERR(pid) && PTR_ERR(pid) == -ESRCH &&
+	    pidfs_future_file_terminal(future))
+		return EPOLLERR | EPOLLHUP;
 	if (IS_ERR(pid))
 		return PTR_ERR(pid) == -ESRCH ? 0 : EPOLLHUP;
 
@@ -927,6 +963,8 @@ static void pidfs_evict_inode(struct inode *inode)
 		return;
 
 	future = container_of(node, struct pidfs_future_file, node);
+	if (future->pid_wait_attached)
+		remove_wait_queue(&future->pid->wait_pidfd, &future->pid_wait);
 	if (future->ops->release)
 		future->ops->release(future->data);
 	if (future->attr)
@@ -934,6 +972,16 @@ static void pidfs_evict_inode(struct inode *inode)
 	kfree(future);
 }
 
+static int pidfs_future_pid_wake(wait_queue_entry_t *wait,
+				 unsigned int mode, int sync, void *key)
+{
+	struct pidfs_future_file *future;
+
+	future = container_of(wait, struct pidfs_future_file, pid_wait);
+	wake_up_all(&future->wait_pidfd);
+	return 0;
+}
+
 static const struct super_operations pidfs_sops = {
 	.drop_inode	= inode_just_drop,
 	.evict_inode	= pidfs_evict_inode,
@@ -1068,12 +1116,16 @@ static int pidfs_export_permission(struct handle_to_path_ctx *ctx,
 static struct file *pidfs_export_open(const struct path *path, unsigned int oflags)
 {
 	struct file *file;
+	int ret;
 
 	/*
 	 * Clear O_LARGEFILE as open_by_handle_at() forces it and raise
 	 * O_RDWR as pidfds always are.
 	 */
 	oflags &= ~O_LARGEFILE;
+	ret = pidfs_wait_for_published_pid(d_inode(path->dentry));
+	if (ret)
+		return ERR_PTR(ret);
 	file = dentry_open(path, oflags | O_RDWR, current_cred());
 	/* do_dentry_open() strips O_EXCL, which encodes PIDFD_THREAD. */
 	if (!IS_ERR(file))
@@ -1289,6 +1341,17 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
 	ret = path_from_stashed(&pid->stashed, pidfs_mnt, get_pid(pid), &path);
 	if (ret < 0)
 		return ERR_PTR(ret);
+	/*
+	 * A future dentry can be stashed before copy_process() makes its task
+	 * visible. Non-stale callers have already observed the task, so do not
+	 * return that inode until its producer has published the process state.
+	 * PIDFD_STALE is used by pre-task internal callers and must not wait here.
+	 */
+	if (!(flags & PIDFD_STALE)) {
+		ret = pidfs_wait_for_published_pid(d_inode(path.dentry));
+		if (ret)
+			return ERR_PTR(ret);
+	}
 
 	VFS_WARN_ON_ONCE(!pid->attr);
 
@@ -1317,7 +1380,8 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
  * associate one preallocated pid with
  * pidfs_future_file_set_pid(), publish its pidfs metadata with
  * pidfs_future_file_publish_pid(), make @ops->get_pid() resolve that pid, and
- * finally wake waiters with pidfs_future_file_notify().
+ * finally wake waiters with pidfs_future_file_notify(). A producer that
+ * becomes terminal without publishing must also notify its waiters.
  *
  * A task associated with the future pid must not become runnable before
  * publication metadata and exit-wakeup forwarding are installed.
@@ -1378,6 +1442,7 @@ struct file *pidfs_alloc_future_file(const char *name, void *data,
 	future->data = data;
 	future->ops = ops;
 	future->ino = ino;
+	init_waitqueue_head(&future->wait_pidfd);
 	inode->i_private = &future->node;
 	return file;
 }
@@ -1422,6 +1487,60 @@ int pidfs_future_file_set_pid(struct file *file, struct pid *pid)
 	return 0;
 }
 
+/**
+ * pidfs_future_file_publish_pid - publish pidfs metadata for a future pid
+ * @file: future pidfs file
+ * @pid: pid previously associated with @file
+ *
+ * This transfers the future inode attributes to @pid and forwards pid exit
+ * wakeups. After it returns, the producer must publish the state used by
+ * @ops->get_pid() before calling pidfs_future_file_notify().
+ */
+void pidfs_future_file_publish_pid(struct file *file, struct pid *pid)
+{
+	struct pidfs_future_file *future = pidfs_future_file(file_inode(file));
+	struct pidfs_attr *unused = NULL;
+
+	if (WARN_ON_ONCE(!future || READ_ONCE(future->pid) != pid))
+		return;
+
+	scoped_guard(spinlock_irq, &pid->wait_pidfd.lock) {
+		if (WARN_ON_ONCE(pid->attr == PIDFS_PID_DEAD))
+			return;
+		if (!pid->attr) {
+			pid->attr = future->attr;
+			future->attr = NULL;
+		} else {
+			unused = future->attr;
+			future->attr = NULL;
+		}
+	}
+	if (unused)
+		kmem_cache_free(pidfs_attr_cachep, unused);
+
+	init_waitqueue_func_entry(&future->pid_wait, pidfs_future_pid_wake);
+	add_wait_queue(&pid->wait_pidfd, &future->pid_wait);
+	future->pid_wait_attached = true;
+}
+
+/**
+ * pidfs_future_file_notify - wake future-pidfd state waiters
+ * @file: future pidfs file
+ *
+ * For publication, the producer must call this only after @ops->get_pid() can
+ * return the live pid. The publication must use release ordering paired with
+ * the producer's acquire-side lookup. A taskless terminal producer may call
+ * this after making @ops->is_terminal() return true.
+ */
+void pidfs_future_file_notify(struct file *file)
+{
+	struct pidfs_future_file *future = pidfs_future_file(file_inode(file));
+
+	if (WARN_ON_ONCE(!future))
+		return;
+	wake_up_all(&future->wait_pidfd);
+}
+
 /**
  * pidfs_future_file_clear_pid - undo an unpublished future-pid association
  * @file: future pidfs file
@@ -1444,6 +1563,10 @@ void pidfs_future_file_clear_pid(struct file *file, struct pid *pid)
 	if (WARN_ON_ONCE(!future || READ_ONCE(future->pid) != pid))
 		return;
 
+	if (future->pid_wait_attached) {
+		remove_wait_queue(&pid->wait_pidfd, &future->pid_wait);
+		future->pid_wait_attached = false;
+	}
 	WARN_ON_ONCE(cmpxchg(&pid->stashed, dentry, NULL) != dentry);
 	dentry->d_fsdata = NULL;
 	WARN_ON_ONCE(pid->attr);
diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h
index 828e96f769ba1..e4df7f7748487 100644
--- a/include/linux/pidfs.h
+++ b/include/linux/pidfs.h
@@ -13,11 +13,14 @@ struct pid;
  * @get_pid: Return the published, borrowed, non-NULL process identity, or an
  *	error pointer while the producer is still taskless. The producer must
  *	keep the returned pid alive for the future inode lifetime.
+ * @is_terminal: Return true when a taskless producer can no longer publish a
+ *	process. Optional; a producer without terminal failures may leave it NULL.
  * @release: Optionally release producer-owned data when the pidfs inode is
  *	evicted. If provided, this is called at most once.
  */
 struct pidfs_future_file_ops {
 	struct pid *(*get_pid)(void *data);
+	bool (*is_terminal)(void *data);
 	void (*release)(void *data);
 };
 
@@ -29,6 +32,8 @@ void *pidfs_future_file_data(const struct file *file,
 			     const struct pidfs_future_file_ops *ops);
 u64 pidfs_future_file_ino(const struct file *file);
 int pidfs_future_file_set_pid(struct file *file, struct pid *pid);
+void pidfs_future_file_publish_pid(struct file *file, struct pid *pid);
+void pidfs_future_file_notify(struct file *file);
 void pidfs_future_file_clear_pid(struct file *file, struct pid *pid);
 void __init pidfs_init(void);
 void pidfs_prepare_pid(struct pid *pid);
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 11/24] pidfd: add spawn builder state tracking
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Add explicit configuring, starting, setup, started, and cancelled
states. Serialize configuration and launch transitions with the builder
mutex so configuration cannot change after task creation starts.

Publish the eventual pid with release and acquire ordering. This state
machine also provides the boundary for later one-shot claim and
cancellation semantics.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfd_spawn.c | 50 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 49 insertions(+), 1 deletion(-)

diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 46207c8e8139f..99e3ef56ecfa3 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -22,6 +22,14 @@
 
 #define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE	256
 
+enum pidfd_spawn_status {
+	PIDFD_SPAWN_CONFIGURING,
+	PIDFD_SPAWN_STARTING,
+	PIDFD_SPAWN_SETUP_DONE,
+	PIDFD_SPAWN_STARTED,
+	PIDFD_SPAWN_CANCELLED,
+};
+
 struct pidfd_spawn_state {
 	/* Serializes configuration and payload teardown. */
 	struct mutex lock;
@@ -31,10 +39,45 @@ struct pidfd_spawn_state {
 	const struct cred *creator_cred;
 	struct pid_namespace *creator_pid_ns;
 	char *staged_path;
+	enum pidfd_spawn_status status;
 };
 
+static struct pid *
+pidfd_spawn_load_pid(const struct pidfd_spawn_state *state)
+{
+	/* Pair with the release publication in pidfd_spawn_publish_pid(). */
+	return smp_load_acquire(&state->pid);
+}
+
+static enum pidfd_spawn_status
+pidfd_spawn_get_status(const struct pidfd_spawn_state *state)
+{
+	return READ_ONCE(state->status);
+}
+
+static void pidfd_spawn_set_status(struct pidfd_spawn_state *state,
+				   enum pidfd_spawn_status status)
+{
+	WRITE_ONCE(state->status, status);
+}
+
 static void pidfd_spawn_state_put(struct pidfd_spawn_state *state);
 
+static int
+pidfd_spawn_configuring_error(const struct pidfd_spawn_state *state)
+{
+	switch (pidfd_spawn_get_status(state)) {
+	case PIDFD_SPAWN_CONFIGURING:
+		return 0;
+	case PIDFD_SPAWN_CANCELLED:
+		if (!pidfd_spawn_load_pid(state))
+			return -ECANCELED;
+		fallthrough;
+	default:
+		return -EBUSY;
+	}
+}
+
 static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
 {
 	kfree(state->staged_path);
@@ -73,7 +116,7 @@ static struct pid *pidfd_spawn_file_pid(void *data)
 	struct pidfd_spawn_state *state = data;
 	struct pid *pid;
 
-	pid = READ_ONCE(state->pid);
+	pid = pidfd_spawn_load_pid(state);
 	return pid ? pid : ERR_PTR(-ESRCH);
 }
 
@@ -136,6 +179,10 @@ static int pidfd_spawn_state_set_path(struct pidfd_spawn_state *state,
 		return PTR_ERR(path);
 
 	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+		int ret = pidfd_spawn_configuring_error(state);
+
+		if (ret)
+			return ret;
 		if (!pidfd_spawn_same_creator(state))
 			return -EPERM;
 
@@ -216,6 +263,7 @@ int pidfd_empty_open(unsigned int flags)
 	state->creator_cred = get_current_cred();
 	state->creator_pid_ns =
 		get_pid_ns(current->nsproxy->pid_ns_for_children);
+	pidfd_spawn_set_status(state, PIDFD_SPAWN_CONFIGURING);
 
 	pidfile = pidfs_alloc_future_file("[pidfd_spawn]", state,
 					  &pidfd_spawn_future_ops,
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

A kernel-created task can become visible before it has installed a new
executable image or a valid userspace register frame. Exposing such a task
through ptrace can disclose kernel setup state.

Add a task-local embryonic flag and an internal clone argument for callers
that need this lifecycle. Reject ptrace access until the creator clears the
flag. Clear it with release ordering and observe it with acquire ordering.
This orders visibility of the completed exec state with the transition.

Existing fork, vfork, clone, and kernel-thread callers leave the argument
unset and retain their current behavior.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 include/linux/sched.h      | 21 +++++++++++++++++++++
 include/linux/sched/task.h |  1 +
 kernel/fork.c              |  1 +
 kernel/ptrace.c            |  4 ++++
 4 files changed, 27 insertions(+)

diff --git a/include/linux/sched.h b/include/linux/sched.h
index 908aff695ef86..031ae92884c3c 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1863,6 +1863,7 @@ static __always_inline bool is_user_task(struct task_struct *task)
 #define PFA_SPEC_IB_DISABLE		5	/* Indirect branch speculation restricted */
 #define PFA_SPEC_IB_FORCE_DISABLE	6	/* Indirect branch speculation permanently restricted */
 #define PFA_SPEC_SSB_NOEXEC		7	/* Speculative Store Bypass clear on execve() */
+#define PFA_EMBRYONIC_EXEC		8	/* No valid user register frame */
 
 #define TASK_PFA_TEST(name, func)					\
 	static inline bool task_##func(struct task_struct *p)		\
@@ -1905,6 +1906,26 @@ TASK_PFA_CLEAR(SPEC_IB_DISABLE, spec_ib_disable)
 TASK_PFA_TEST(SPEC_IB_FORCE_DISABLE, spec_ib_force_disable)
 TASK_PFA_SET(SPEC_IB_FORCE_DISABLE, spec_ib_force_disable)
 
+/* Clearing this bit publishes the register and security state from exec. */
+static inline bool task_is_embryonic_exec(struct task_struct *p)
+{
+	return test_bit_acquire(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+}
+
+/* The task is private during copy_process(), so no publication barrier. */
+static inline void task_init_embryonic_exec(struct task_struct *p, bool set)
+{
+	if (set)
+		__set_bit(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+	else
+		__clear_bit(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+}
+
+static inline void task_clear_embryonic_exec(struct task_struct *p)
+{
+	clear_bit_unlock(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+}
+
 static inline void
 current_restore_flags(unsigned long orig_flags, unsigned long flags)
 {
diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h
index 92b4e3bc31e66..5b7facf12b33b 100644
--- a/include/linux/sched/task.h
+++ b/include/linux/sched/task.h
@@ -45,6 +45,7 @@ struct kernel_clone_args {
 	void *fn_arg;
 	struct cgroup *cgrp;
 	struct css_set *cset;
+	bool embryonic_exec;
 	unsigned int kill_seq;
 };
 
diff --git a/kernel/fork.c b/kernel/fork.c
index d16405c037c2f..e3ade83b2d4e2 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2138,6 +2138,7 @@ __latent_entropy struct task_struct *copy_process(
 	retval = copy_exec_state(clone_flags, p);
 	if (retval)
 		goto bad_fork_free;
+	task_init_embryonic_exec(p, args->embryonic_exec);
 	p->flags &= ~PF_KTHREAD;
 	if (args->kthread)
 		p->flags |= PF_KTHREAD;
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index d041645d9d17d..36eb9b154a397 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -56,6 +56,8 @@ bool ptracer_access_allowed(struct task_struct *tsk)
 	guard(rcu)();
 	if (ptrace_parent(tsk) != current)
 		return false;
+	if (task_is_embryonic_exec(tsk))
+		return false;
 	es = task_exec_state_rcu(tsk);
 	return READ_ONCE(es->dumpable) == TASK_DUMPABLE_OWNER ||
 	       ptracer_capable(tsk, es->user_ns);
@@ -312,6 +314,8 @@ static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
 		WARN(1, "denying ptrace access check without PTRACE_MODE_*CREDS\n");
 		return -EPERM;
 	}
+	if (task_is_embryonic_exec(task))
+		return -EPERM;
 
 	/* May we inspect the given task?
 	 * This check is used both for attaching with ptrace
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 13/24] fork: let new tasks start with task work
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Process builders need to run setup in the child before its first return
to userspace without storing a kernel callback in architecture-specific
saved registers.

Allow an internal clone caller to supply task work. Queue it after the
last ordinary fork failure check, while the child is still TASK_NEW. A
NULL work pointer leaves existing clone callers unchanged.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 include/linux/sched/task.h | 2 ++
 kernel/fork.c              | 6 ++++++
 2 files changed, 8 insertions(+)

diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h
index 5b7facf12b33b..830de99aabaed 100644
--- a/include/linux/sched/task.h
+++ b/include/linux/sched/task.h
@@ -43,6 +43,8 @@ struct kernel_clone_args {
 	int idle;
 	int (*fn)(void *);
 	void *fn_arg;
+	/* Run before the new task first returns to userspace. */
+	struct callback_head *task_work;
 	struct cgroup *cgrp;
 	struct css_set *cset;
 	bool embryonic_exec;
diff --git a/kernel/fork.c b/kernel/fork.c
index e3ade83b2d4e2..c0584eaa74711 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -109,6 +109,7 @@
 #include <uapi/linux/pidfd.h>
 #include <linux/pidfs.h>
 #include <linux/tick.h>
+#include <linux/task_work.h>
 #include <linux/unwind_deferred.h>
 #include <linux/pgalloc.h>
 #include <linux/uaccess.h>
@@ -2504,6 +2505,11 @@ __latent_entropy struct task_struct *copy_process(
 		retval = -EINTR;
 		goto bad_fork_core_free;
 	}
+	if (args->task_work) {
+		retval = task_work_add(p, args->task_work, TWA_RESUME);
+		if (WARN_ON_ONCE(retval))
+			goto bad_fork_core_free;
+	}
 
 	/* No more failure paths after this point. */
 
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 14/24] pidfd: create and execute spawn builder tasks
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Add pidfd_spawn_run() for a taskless builder. Validate the versioned run
payload, including native and compat pointer-container widths. Allocate a
numeric pid with the reserved pidfs identity. Create the child through the
existing vfork backend.

Publish the same future pidfd as the child pidfd before waking the child.
Queue setup as initial task work so no kernel callback pointers are carried
through architecture register state. Keep the task embryonic until exec has
installed a valid userspace image and register frame.

A failure before task creation initially leaves the builder configurable.
Once a task exists, return setup errors after the child exits. A later
state-machine change tightens the pre-task path to a one-shot claim.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfd_spawn.c | 406 ++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 402 insertions(+), 4 deletions(-)

diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 99e3ef56ecfa3..c0e95c2ddbe6e 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -3,6 +3,7 @@
  * pidfd-backed process spawn builders
  */
 
+#include <linux/completion.h>
 #include <linux/cred.h>
 #include <linux/file.h>
 #include <linux/fs.h>
@@ -13,14 +14,50 @@
 #include <linux/pid_namespace.h>
 #include <linux/pidfd_spawn.h>
 #include <linux/pidfs.h>
+#include <linux/random.h>
 #include <linux/refcount.h>
+#include <linux/sched/mm.h>
 #include <linux/sched/signal.h>
+#include <linux/sched/task.h>
+#include <linux/security.h>
 #include <linux/slab.h>
 #include <linux/syscalls.h>
+#include <linux/task_work.h>
 #include <linux/uaccess.h>
 #include <uapi/linux/pidfd.h>
+#include <uapi/linux/wait.h>
+#include <trace/events/sched.h>
 
+#include "exec_internal.h"
+
+static inline void __user *pidfd_spawn_user_ptr(__u64 p)
+{
+	return u64_to_user_ptr(p);
+}
+
+static inline bool pidfd_spawn_user_ptr_fits(__u64 p)
+{
+#ifdef CONFIG_COMPAT
+	if (in_compat_syscall())
+		return p == (__u64)(compat_uptr_t)p;
+#endif
+	return p == (__u64)(unsigned long)p;
+}
+
+static inline struct user_arg_ptr pidfd_spawn_user_arg(__u64 p)
+{
+#ifdef CONFIG_COMPAT
+	if (in_compat_syscall())
+		return compat_arg((compat_uptr_t __user *)
+				  u64_to_user_ptr(p));
+#endif
+	return native_arg(u64_to_user_ptr(p));
+}
+
+#define PIDFD_SPAWN_EXIT_FAILURE	(127 << 8)
 #define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE	256
+DEFINE_FREE(pidfd_spawn_dismiss_filename, struct delayed_filename,
+	    dismiss_delayed_filename(&_T))
 
 enum pidfd_spawn_status {
 	PIDFD_SPAWN_CONFIGURING,
@@ -31,14 +68,20 @@ enum pidfd_spawn_status {
 };
 
 struct pidfd_spawn_state {
-	/* Serializes configuration and payload teardown. */
+	/* Serializes configuration, launch, cancellation, and payload teardown. */
 	struct mutex lock;
+	struct completion done;
+	struct callback_head task_work;
 	refcount_t count;
 	struct pid *pid;
 	struct mm_struct *creator_mm;
 	const struct cred *creator_cred;
 	struct pid_namespace *creator_pid_ns;
+	struct delayed_filename filename;
 	char *staged_path;
+	struct user_arg_ptr argv;
+	struct user_arg_ptr envp;
+	int result;
 	enum pidfd_spawn_status status;
 };
 
@@ -80,6 +123,10 @@ pidfd_spawn_configuring_error(const struct pidfd_spawn_state *state)
 
 static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
 {
+	if (!state)
+		return;
+
+	dismiss_delayed_filename(&state->filename);
 	kfree(state->staged_path);
 	if (state->creator_mm)
 		mmdrop(state->creator_mm);
@@ -136,11 +183,73 @@ pidfd_spawn_state_get_file(struct file *file)
 	return state;
 }
 
+static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
+				      struct filename *filename, int result)
+{
+	const struct cred *creator_cred;
+	struct mm_struct *creator_mm;
+	struct pid_namespace *creator_pid_ns;
+	char *staged_path;
+
+	/*
+	 * Run data is one-shot. Detach it under the lock, then drop refs
+	 * after the child no longer needs the shared setup payload.
+	 */
+	mutex_lock(&state->lock);
+	creator_mm = state->creator_mm;
+	creator_cred = state->creator_cred;
+	creator_pid_ns = state->creator_pid_ns;
+	staged_path = state->staged_path;
+	state->creator_mm = NULL;
+	state->creator_cred = NULL;
+	state->creator_pid_ns = NULL;
+	state->staged_path = NULL;
+	state->argv = native_arg(NULL);
+	state->envp = native_arg(NULL);
+	state->result = result;
+	pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
+	mutex_unlock(&state->lock);
+
+	putname(filename);
+	if (creator_mm)
+		mmdrop(creator_mm);
+	if (creator_cred)
+		put_cred(creator_cred);
+	if (creator_pid_ns)
+		put_pid_ns(creator_pid_ns);
+	kfree(staged_path);
+}
+
+static bool pidfd_spawn_same_pid_ns(struct pidfd_spawn_state *state)
+{
+	return current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
+}
+
 static bool pidfd_spawn_same_creator(struct pidfd_spawn_state *state)
 {
+	/*
+	 * Holding the pidfd alone is not enough authority to configure or start
+	 * this builder. A caller must also share the creator's mm, credential
+	 * object, and child PID namespace.
+	 *
+	 * The child PID namespace is part of the builder authority even though no
+	 * numeric PID is allocated until run.
+	 */
 	return current->mm == state->creator_mm &&
 	       current_cred() == state->creator_cred &&
-	       current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
+	       pidfd_spawn_same_pid_ns(state);
+}
+
+static int pidfd_spawn_check_startable(struct pidfd_spawn_state *state)
+{
+	int ret;
+
+	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+		ret = pidfd_spawn_configuring_error(state);
+		if (!ret && !pidfd_spawn_same_creator(state))
+			ret = -EPERM;
+	}
+	return ret;
 }
 
 static struct filename *pidfd_spawn_get_path(const char __user *value)
@@ -153,6 +262,31 @@ static struct filename *pidfd_spawn_get_path(const char __user *value)
 	return no_free_ptr(name);
 }
 
+static int
+pidfd_spawn_get_delayed_path(struct delayed_filename *delayed,
+			     const char __user *value)
+{
+	struct filename *name __free(putname) = NULL;
+
+	name = pidfd_spawn_get_path(value);
+	if (IS_ERR(name))
+		return PTR_ERR(name);
+
+	return putname_to_delayed(delayed, no_free_ptr(name));
+}
+
+static int
+pidfd_spawn_get_delayed_kernel_path(struct delayed_filename *delayed,
+				    const char *value)
+{
+	struct filename *name __free(putname) = getname_kernel(value);
+
+	if (IS_ERR(name))
+		return PTR_ERR(name);
+
+	return putname_to_delayed(delayed, no_free_ptr(name));
+}
+
 static char *pidfd_spawn_copy_path(const char __user *value)
 {
 	struct filename *name __free(putname) = NULL;
@@ -173,14 +307,14 @@ static int pidfd_spawn_state_set_path(struct pidfd_spawn_state *state,
 {
 	char *path __free(kfree) = NULL;
 	char *old __free(kfree) = NULL;
+	int ret;
 
 	path = pidfd_spawn_copy_path(value);
 	if (IS_ERR(path))
 		return PTR_ERR(path);
 
 	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
-		int ret = pidfd_spawn_configuring_error(state);
-
+		ret = pidfd_spawn_configuring_error(state);
 		if (ret)
 			return ret;
 		if (!pidfd_spawn_same_creator(state))
@@ -243,6 +377,230 @@ SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
 	return ret;
 }
 
+static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
+				     struct filename *filename, int result)
+{
+	pidfd_spawn_drop_run_data(state, filename, result);
+	complete_all(&state->done);
+}
+
+static void pidfd_spawn_child(struct callback_head *work)
+{
+	struct pidfd_spawn_state *state =
+		container_of(work, struct pidfd_spawn_state, task_work);
+	struct filename *filename;
+	int ret;
+
+	filename = complete_getname(&state->filename);
+	if (IS_ERR(filename))
+		ret = PTR_ERR(filename);
+	else
+		ret = 0;
+
+	if (!ret)
+		ret = do_execveat_common(AT_FDCWD, filename,
+					 state->argv, state->envp, 0);
+	if (!ret)
+		task_clear_embryonic_exec(current);
+
+	pidfd_spawn_finish_child(state, filename, ret);
+	if (ret) {
+		pidfd_spawn_state_put(state);
+		do_group_exit(PIDFD_SPAWN_EXIT_FAILURE);
+	}
+	pidfd_spawn_state_put(state);
+}
+
+static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
+				     struct pidfd_spawn_run_args __user *uargs,
+				     size_t usize)
+{
+	int ret;
+
+	BUILD_BUG_ON(sizeof(struct pidfd_spawn_run_args) !=
+		     PIDFD_SPAWN_RUN_SIZE_VER0);
+
+	if (usize < PIDFD_SPAWN_RUN_SIZE_VER0)
+		return -EINVAL;
+	if (usize > PAGE_SIZE)
+		return -E2BIG;
+	ret = copy_struct_from_user(kargs, sizeof(*kargs), uargs, usize);
+	if (ret)
+		return ret;
+	if (kargs->flags || kargs->reserved0 || kargs->reserved[0] ||
+	    kargs->reserved[1])
+		return -EINVAL;
+	if (!kargs->argv)
+		return -EINVAL;
+	if (kargs->nr_actions || kargs->actions || kargs->action_size)
+		return -EINVAL;
+	if (!pidfd_spawn_user_ptr_fits(kargs->path) ||
+	    !pidfd_spawn_user_ptr_fits(kargs->argv) ||
+	    !pidfd_spawn_user_ptr_fits(kargs->envp) ||
+	    !pidfd_spawn_user_ptr_fits(kargs->actions))
+		return -EFAULT;
+
+	return 0;
+}
+
+static struct task_struct *
+pidfd_spawn_create_task(struct file *file, struct pidfd_spawn_state *state,
+			struct kernel_clone_args *clone_args)
+{
+	struct pid *pid;
+	struct task_struct *task;
+	u64 ino;
+	int ret;
+
+	ino = pidfs_future_file_ino(file);
+	if (!ino)
+		return ERR_PTR(-EBADF);
+
+	pid = alloc_pid_with_pidfs_ino(current->nsproxy->pid_ns_for_children,
+				       NULL, 0, ino);
+	if (IS_ERR(pid))
+		return ERR_CAST(pid);
+
+	ret = pidfs_future_file_set_pid(file, pid);
+	if (ret) {
+		free_pid(pid);
+		return ERR_PTR(ret);
+	}
+
+	refcount_inc(&state->count);
+	task = copy_process(pid, 0, NUMA_NO_NODE, clone_args);
+	add_latent_entropy();
+	if (IS_ERR(task)) {
+		pidfd_spawn_state_put(state);
+		pidfs_future_file_clear_pid(file, pid);
+		free_pid(pid);
+		return task;
+	}
+
+	trace_sched_process_fork(current, task);
+	get_task_struct(task);
+	return task;
+}
+
+static void pidfd_spawn_publish_pid(struct file *file,
+				    struct pidfd_spawn_state *state,
+				    struct task_struct *task)
+{
+	struct pid *pid = get_task_pid(task, PIDTYPE_PID);
+
+	pidfs_future_file_publish_pid(file, pid);
+	/*
+	 * Publish only after the inode identity, pidfs attributes, and exit-wakeup
+	 * forwarding are ready. Pair with the acquire loads in pidfd resolution
+	 * and poll so readers that observe @pid can use ordinary pidfd operations.
+	 */
+	smp_store_release(&state->pid, pid);
+	pidfs_future_file_notify(file);
+}
+
+static void pidfd_spawn_attach_vfork_done(struct task_struct *task,
+					  struct completion *vfork)
+{
+	init_completion(vfork);
+
+	task_lock(task);
+	task->vfork_done = vfork;
+	task_unlock(task);
+}
+
+static void pidfd_spawn_wait_for_child(struct pidfd_spawn_state *state)
+{
+	wait_for_completion(&state->done);
+}
+
+static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
+				    struct task_struct *task,
+				    struct completion *vfork)
+{
+	int ret;
+
+	ret = wait_for_vfork_done(task, vfork);
+	if (ret)
+		return ret;
+
+	mutex_lock(&state->lock);
+	ret = state->result;
+	pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTED);
+	mutex_unlock(&state->lock);
+	return ret;
+}
+
+static int pidfd_spawn_start(struct file *file,
+			     struct pidfd_spawn_state *state,
+			     const struct pidfd_spawn_run_args *kargs)
+{
+	struct delayed_filename filename
+		__free(pidfd_spawn_dismiss_filename) = {};
+	struct delayed_filename drop_filename
+		__free(pidfd_spawn_dismiss_filename) = {};
+	struct kernel_clone_args clone_args = {
+		.flags		= CLONE_VM | CLONE_VFORK | CLONE_UNTRACED,
+		.exit_signal	= SIGCHLD,
+		.task_work	= &state->task_work,
+		.embryonic_exec = true,
+	};
+	struct completion vfork;
+	struct task_struct *task;
+	int ret;
+
+	if (kargs->path) {
+		ret = pidfd_spawn_get_delayed_path(&filename,
+						   pidfd_spawn_user_ptr(kargs->path));
+		if (ret)
+			return ret;
+	}
+
+	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+		ret = pidfd_spawn_configuring_error(state);
+		if (ret)
+			return ret;
+		if (!pidfd_spawn_same_creator(state))
+			return -EPERM;
+		if (state->staged_path && kargs->path)
+			return -EINVAL;
+		if (!state->staged_path && !kargs->path)
+			return -EINVAL;
+		if (state->staged_path) {
+			ret = pidfd_spawn_get_delayed_kernel_path(&filename,
+								  state->staged_path);
+			if (ret)
+				return ret;
+		}
+		state->filename = filename;
+		INIT_DELAYED_FILENAME(&filename);
+
+		state->argv = pidfd_spawn_user_arg(kargs->argv);
+		state->envp = pidfd_spawn_user_arg(kargs->envp);
+		pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
+		reinit_completion(&state->done);
+
+		task = pidfd_spawn_create_task(file, state, &clone_args);
+		if (IS_ERR(task)) {
+			ret = PTR_ERR(task);
+			drop_filename = state->filename;
+			INIT_DELAYED_FILENAME(&state->filename);
+			state->argv = native_arg(NULL);
+			state->envp = native_arg(NULL);
+			state->result = 0;
+			pidfd_spawn_set_status(state,
+					       PIDFD_SPAWN_CONFIGURING);
+			return ret;
+		}
+
+		pidfd_spawn_publish_pid(file, state, task);
+		pidfd_spawn_attach_vfork_done(task, &vfork);
+	}
+
+	wake_up_new_task(task);
+	pidfd_spawn_wait_for_child(state);
+	return pidfd_spawn_finish_vfork(state, task, &vfork);
+}
+
 int pidfd_empty_open(unsigned int flags)
 {
 	struct pidfd_spawn_state *state;
@@ -257,7 +615,13 @@ int pidfd_empty_open(unsigned int flags)
 		return -ENOMEM;
 
 	mutex_init(&state->lock);
+	init_completion(&state->done);
+	init_task_work(&state->task_work, pidfd_spawn_child);
 	refcount_set(&state->count, 1);
+	/*
+	 * Remember the creator so later builder operations cannot be delegated
+	 * just by passing the pidfd through SCM_RIGHTS or similar fd passing.
+	 */
 	mmgrab(current->mm);
 	state->creator_mm = current->mm;
 	state->creator_cred = get_current_cred();
@@ -282,3 +646,37 @@ int pidfd_empty_open(unsigned int flags)
 	fd_install(pidfd, pidfile);
 	return pidfd;
 }
+
+SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
+		struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
+{
+	struct pidfd_spawn_run_args kargs;
+	struct pidfd_spawn_state *state __free(pidfd_spawn_state) = NULL;
+	int ret;
+
+	ret = pidfd_spawn_copy_run_args(&kargs, uargs, usize);
+	if (ret)
+		return ret;
+
+	CLASS(fd, f)(fd);
+	if (fd_empty(f))
+		return -EBADF;
+
+	state = pidfd_spawn_state_get_file(fd_file(f));
+	if (IS_ERR(state))
+		return PTR_ERR(state);
+
+	scoped_cond_guard(mutex_intr, return -ERESTARTNOINTR,
+			  &current->signal->cred_guard_mutex) {
+		if (READ_ONCE(current->ptrace))
+			return -EPERM;
+		ret = pidfd_spawn_check_startable(state);
+		if (ret)
+			return ret;
+		ret = pidfd_spawn_start(fd_file(f), state, &kargs);
+	}
+	if (!ret)
+		ret = pid_vnr(state->pid);
+
+	return ret;
+}
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 15/24] fork: keep embryonic tasks hidden until exec completes
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

A spawn task does not gain a valid userspace register frame when
exec_mmap() installs its private mm. A binary loader can still sleep or
fail before START_THREAD(). This can expose setup registers to ptrace or a
late failed-exec core dump. During earlier setup the task also shares the
source mm, which procfs could expose through the child's PID.

Hide procfs PID lookup, iteration, and cached dentry revalidation from
other tasks, deny coredumps, and report coredump skip state through pidfs.
Allow the embryonic task to use its own proc entries because executable and
interpreter lookup can legitimately use /proc/self. This does not expose
source state to another task. Sample the flag before reading published
credentials; acquire ordering covers the state installed by exec.

Release-publish the transition in the exec core after the final binary
handler succeeds and before audit, tracepoint, ptrace, and connector exec
events. Every successful exec observer then sees a normal task, while a
failed exec leaves the task hidden.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/coredump.c      |  4 +++-
 fs/exec.c          |  2 ++
 fs/pidfd_spawn.c   |  2 --
 fs/pidfs.c         |  7 ++++++-
 fs/proc/base.c     | 11 +++++++++--
 fs/proc/internal.h | 18 +++++++++++++++++-
 kernel/ptrace.c    |  2 +-
 7 files changed, 38 insertions(+), 8 deletions(-)

diff --git a/fs/coredump.c b/fs/coredump.c
index ac3cd74808c64..4b2f0b5c7d606 100644
--- a/fs/coredump.c
+++ b/fs/coredump.c
@@ -1166,7 +1166,9 @@ void vfs_coredump(const kernel_siginfo_t *siginfo)
 		.limit = rlimit(RLIMIT_CORE),
 		/* Snapshot MMF_DUMP_FILTER_* (unlocked) and dumpable for the dump. */
 		.mm_flags = __mm_flags_get_word(mm),
-		.dumpable = task_exec_state_get_dumpable(current),
+		.dumpable = task_is_embryonic_exec(current) ?
+			TASK_DUMPABLE_OFF :
+			task_exec_state_get_dumpable(current),
 		.vma_meta = NULL,
 		.cpu = raw_smp_processor_id(),
 	};
diff --git a/fs/exec.c b/fs/exec.c
index 1a256ba26425e..292c30e80aecb 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1734,6 +1734,8 @@ static int exec_binprm(struct linux_binprm *bprm)
 			fput(exec);
 	}
 
+	if (task_is_embryonic_exec(current))
+		task_clear_embryonic_exec(current);
 	audit_bprm(bprm);
 	trace_sched_process_exec(current, old_pid, bprm);
 	ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index c0e95c2ddbe6e..41d5cbb49a0f7 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -400,8 +400,6 @@ static void pidfd_spawn_child(struct callback_head *work)
 	if (!ret)
 		ret = do_execveat_common(AT_FDCWD, filename,
 					 state->argv, state->envp, 0);
-	if (!ret)
-		task_clear_embryonic_exec(current);
 
 	pidfd_spawn_finish_child(state, filename, ret);
 	if (ret) {
diff --git a/fs/pidfs.c b/fs/pidfs.c
index d62235e01b351..7a473c049dcd4 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -477,6 +477,7 @@ static long pidfd_info(struct file *file, unsigned int cmd, unsigned long arg)
 	struct user_namespace *user_ns;
 	struct pidfs_attr *attr;
 	const struct cred *c;
+	bool embryonic;
 	__u64 mask;
 
 	BUILD_BUG_ON(sizeof(struct pidfd_info) != PIDFD_INFO_SIZE_VER3);
@@ -533,12 +534,16 @@ static long pidfd_info(struct file *file, unsigned int cmd, unsigned long arg)
 		goto copy_out;
 	}
 
+	embryonic = task_is_embryonic_exec(task);
 	c = get_task_cred(task);
 	if (!c)
 		return -ESRCH;
 
 	if ((mask & PIDFD_INFO_COREDUMP) && !kinfo.coredump_mask) {
-		kinfo.coredump_mask = pidfs_coredump_mask(task_exec_state_get_dumpable(task));
+		enum task_dumpable dumpable = embryonic ?
+			TASK_DUMPABLE_OFF : task_exec_state_get_dumpable(task);
+
+		kinfo.coredump_mask = pidfs_coredump_mask(dumpable);
 		kinfo.mask |= PIDFD_INFO_COREDUMP;
 		/* No coredump actually took place, so no coredump signal. */
 	}
diff --git a/fs/proc/base.c b/fs/proc/base.c
index 6a39de424f62a..bd059ddcce863 100644
--- a/fs/proc/base.c
+++ b/fs/proc/base.c
@@ -2053,7 +2053,7 @@ static int pid_revalidate(struct inode *dir, const struct qstr *name,
 		goto out;
 	task = pid_task(proc_pid(inode), PIDTYPE_PID);
 
-	if (task) {
+	if (task && proc_task_visible(task)) {
 		pid_update_inode(task, inode);
 		ret = 1;
 	}
@@ -3492,6 +3492,8 @@ struct dentry *proc_pid_lookup(struct dentry *dentry, unsigned int flags)
 	rcu_read_unlock();
 	if (!task)
 		goto out;
+	if (!proc_task_visible(task))
+		goto out_put_task;
 
 	/* Limit procfs to only ptraceable tasks */
 	if (fs_info->hide_pid == HIDEPID_NOT_PTRACEABLE) {
@@ -3539,11 +3541,12 @@ static bool next_tgid(struct tgid_iter *it)
 		if (pid) {
 			it->tgid = pid_nr_ns(pid, it->pid_ns);
 			it->task = pid_task(pid, PIDTYPE_TGID);
-			if (it->task) {
+			if (it->task && proc_task_visible(it->task)) {
 				get_task_struct(it->task);
 				rcu_read_unlock();
 				return true;
 			}
+			it->task = NULL;
 		} else {
 			rcu_read_unlock();
 			return false;
@@ -3807,6 +3810,8 @@ static struct dentry *proc_task_lookup(struct inode *dir, struct dentry * dentry
 	rcu_read_unlock();
 	if (!task)
 		goto out;
+	if (!proc_task_visible(task))
+		goto out_drop_task;
 	if (!same_thread_group(leader, task))
 		goto out_drop_task;
 
@@ -3844,6 +3849,8 @@ static struct task_struct *first_tid(struct pid *pid, int tid, loff_t f_pos,
 	task = pid_task(pid, PIDTYPE_PID);
 	if (!task)
 		goto fail;
+	if (!proc_task_visible(task))
+		goto fail;
 
 	/* Attempt to start with the tid of a thread */
 	if (tid && nr) {
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index b232e1098117b..81ad44502272e 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -147,9 +147,25 @@ static inline struct pid *proc_pid(const struct inode *inode)
 	return PROC_I(inode)->pid;
 }
 
+static inline bool proc_task_visible(struct task_struct *task)
+{
+	/*
+	 * An embryonic task may need its own proc entries during exec setup,
+	 * but it is not a valid userspace process image for other tasks yet.
+	 */
+	return task == current || !task_is_embryonic_exec(task);
+}
+
 static inline struct task_struct *get_proc_task(const struct inode *inode)
 {
-	return get_pid_task(proc_pid(inode), PIDTYPE_PID);
+	struct task_struct *task;
+
+	task = get_pid_task(proc_pid(inode), PIDTYPE_PID);
+	if (task && !proc_task_visible(task)) {
+		put_task_struct(task);
+		return NULL;
+	}
+	return task;
 }
 
 void task_dump_owner(struct task_struct *task, umode_t mode,
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index 36eb9b154a397..59e11dae803c8 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -314,7 +314,7 @@ static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
 		WARN(1, "denying ptrace access check without PTRACE_MODE_*CREDS\n");
 		return -EPERM;
 	}
-	if (task_is_embryonic_exec(task))
+	if (task_is_embryonic_exec(task) && task != current)
 		return -EPERM;
 
 	/* May we inspect the given task?
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 16/24] audit: add pidfd spawn child contexts
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Kernel-mediated spawn executes file setup and exec from initial child task
work, not from a second syscall entry. Representing that work as another
AUDIT_SYSCALL record duplicates the source pidfd_spawn_run transaction and
invents a syscall that the child never entered.

Add a dedicated audit context and AUDIT_PIDFD_SPAWN record. Carry the
originating syscall number and arguments. Existing syscall exit filters can
then select the child transaction. CWD, PATH, and EXECVE records remain
correlated with it. Let the child close the transaction with an explicit
result instead of rewriting an architecture syscall-return frame.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 include/linux/audit.h      |  31 +++++++++++
 include/uapi/linux/audit.h |   1 +
 kernel/audit.h             |   1 +
 kernel/auditsc.c           | 105 ++++++++++++++++++++++++++++++++++++-
 4 files changed, 136 insertions(+), 2 deletions(-)

diff --git a/include/linux/audit.h b/include/linux/audit.h
index 45abb3722d304..872624a9fba08 100644
--- a/include/linux/audit.h
+++ b/include/linux/audit.h
@@ -323,6 +323,9 @@ extern int  audit_alloc(struct task_struct *task);
 extern void __audit_free(struct task_struct *task);
 extern void __audit_uring_entry(u8 op);
 extern void __audit_uring_exit(int success, long code);
+void __audit_pidfd_spawn_entry(int major, unsigned long a0, unsigned long a1,
+			       unsigned long a2, unsigned long a3);
+void __audit_pidfd_spawn_exit(int success, long code);
 extern void __audit_syscall_entry(int major, unsigned long a0, unsigned long a1,
 				  unsigned long a2, unsigned long a3);
 extern void __audit_syscall_exit(int ret_success, long ret_value);
@@ -373,6 +376,22 @@ static inline void audit_uring_exit(int success, long code)
 	if (unlikely(audit_context()))
 		__audit_uring_exit(success, code);
 }
+
+static inline void audit_pidfd_spawn_entry(int major, unsigned long a0,
+					   unsigned long a1,
+					   unsigned long a2,
+					   unsigned long a3)
+{
+	if (unlikely(audit_context()))
+		__audit_pidfd_spawn_entry(major, a0, a1, a2, a3);
+}
+
+static inline void audit_pidfd_spawn_exit(int success, long code)
+{
+	if (unlikely(audit_context()))
+		__audit_pidfd_spawn_exit(success, code);
+}
+
 static inline void audit_syscall_entry(int major, unsigned long a0,
 				       unsigned long a1, unsigned long a2,
 				       unsigned long a3)
@@ -380,6 +399,7 @@ static inline void audit_syscall_entry(int major, unsigned long a0,
 	if (unlikely(audit_context()))
 		__audit_syscall_entry(major, a0, a1, a2, a3);
 }
+
 static inline void audit_syscall_exit(void *pt_regs)
 {
 	if (unlikely(audit_context())) {
@@ -611,10 +631,21 @@ static inline void audit_uring_entry(u8 op)
 { }
 static inline void audit_uring_exit(int success, long code)
 { }
+
+static inline void audit_pidfd_spawn_entry(int major, unsigned long a0,
+					   unsigned long a1,
+					   unsigned long a2,
+					   unsigned long a3)
+{ }
+
+static inline void audit_pidfd_spawn_exit(int success, long code)
+{ }
+
 static inline void audit_syscall_entry(int major, unsigned long a0,
 				       unsigned long a1, unsigned long a2,
 				       unsigned long a3)
 { }
+
 static inline void audit_syscall_exit(void *pt_regs)
 { }
 static inline bool audit_dummy_context(void)
diff --git a/include/uapi/linux/audit.h b/include/uapi/linux/audit.h
index e8f5ce677df73..e09fb5222293a 100644
--- a/include/uapi/linux/audit.h
+++ b/include/uapi/linux/audit.h
@@ -122,6 +122,7 @@
 #define AUDIT_OPENAT2		1337	/* Record showing openat2 how args */
 #define AUDIT_DM_CTRL		1338	/* Device Mapper target control */
 #define AUDIT_DM_EVENT		1339	/* Device Mapper events */
+#define AUDIT_PIDFD_SPAWN	1340	/* pidfd spawn child operation */
 
 #define AUDIT_AVC		1400	/* SE Linux avc denial or grant */
 #define AUDIT_SELINUX_ERR	1401	/* Internal SE Linux Errors */
diff --git a/kernel/audit.h b/kernel/audit.h
index 92d5e723d570b..2f740df7beecf 100644
--- a/kernel/audit.h
+++ b/kernel/audit.h
@@ -112,6 +112,7 @@ struct audit_context {
 		AUDIT_CTX_UNUSED,	/* audit_context is currently unused */
 		AUDIT_CTX_SYSCALL,	/* in use by syscall */
 		AUDIT_CTX_URING,	/* in use by io_uring */
+		AUDIT_CTX_PIDFD_SPAWN,	/* pidfd spawn child operation */
 	} context;
 	enum audit_state    state, current_state;
 	struct audit_stamp  stamp;	/* event identifier */
diff --git a/kernel/auditsc.c b/kernel/auditsc.c
index 6610e667c728a..8f6d84a902239 100644
--- a/kernel/auditsc.c
+++ b/kernel/auditsc.c
@@ -1649,6 +1649,30 @@ static void audit_log_uring(struct audit_context *ctx)
 	audit_log_end(ab);
 }
 
+/**
+ * audit_log_pidfd_spawn - generate an AUDIT_PIDFD_SPAWN record
+ * @ctx: the audit context
+ */
+static void audit_log_pidfd_spawn(struct audit_context *ctx)
+{
+	struct audit_buffer *ab;
+
+	ab = audit_log_start(ctx, GFP_KERNEL, AUDIT_PIDFD_SPAWN);
+	if (!ab)
+		return;
+	audit_log_format(ab, "arch=%x syscall=%d", ctx->arch, ctx->major);
+	if (ctx->return_valid != AUDITSC_INVALID)
+		audit_log_format(ab, " success=%s exit=%ld",
+				 str_yes_no(ctx->return_valid == AUDITSC_SUCCESS),
+				 ctx->return_code);
+	audit_log_format(ab, " a0=%lx a1=%lx a2=%lx a3=%lx items=%d",
+			 ctx->argv[0], ctx->argv[1], ctx->argv[2],
+			 ctx->argv[3], ctx->name_count);
+	audit_log_task_info(ab);
+	audit_log_key(ab, ctx->filterkey);
+	audit_log_end(ab);
+}
+
 static void audit_log_exit(void)
 {
 	int i, call_panic = 0;
@@ -1687,6 +1711,9 @@ static void audit_log_exit(void)
 	case AUDIT_CTX_URING:
 		audit_log_uring(context);
 		break;
+	case AUDIT_CTX_PIDFD_SPAWN:
+		audit_log_pidfd_spawn(context);
+		break;
 	default:
 		BUG();
 		break;
@@ -1782,7 +1809,8 @@ static void audit_log_exit(void)
 		audit_log_name(context, n, NULL, i++, &call_panic);
 	}
 
-	if (context->context == AUDIT_CTX_SYSCALL)
+	if (context->context == AUDIT_CTX_SYSCALL ||
+	    context->context == AUDIT_CTX_PIDFD_SPAWN)
 		audit_log_proctitle();
 
 	/* Send end of event record to help user space know we are finished */
@@ -1818,7 +1846,8 @@ void __audit_free(struct task_struct *tsk)
 	if (tsk == current && !context->dummy) {
 		context->return_valid = AUDITSC_INVALID;
 		context->return_code = 0;
-		if (context->context == AUDIT_CTX_SYSCALL) {
+		if (context->context == AUDIT_CTX_SYSCALL ||
+		    context->context == AUDIT_CTX_PIDFD_SPAWN) {
 			audit_filter_syscall(tsk, context);
 			audit_filter_inodes(tsk, context);
 			if (context->current_state == AUDIT_STATE_RECORD)
@@ -1967,6 +1996,78 @@ void __audit_uring_exit(int success, long code)
 	audit_reset_context(ctx);
 }
 
+/**
+ * __audit_pidfd_spawn_entry - start a pidfd spawn child audit context
+ * @major: originating pidfd_spawn_run syscall number
+ * @a0: originating syscall argument 0
+ * @a1: originating syscall argument 1
+ * @a2: originating syscall argument 2
+ * @a3: originating syscall argument 3
+ */
+void __audit_pidfd_spawn_entry(int major, unsigned long a0, unsigned long a1,
+			       unsigned long a2, unsigned long a3)
+{
+	struct audit_context *context = audit_context();
+	enum audit_state state;
+
+	if (!audit_enabled || !context)
+		return;
+
+	WARN_ON(context->context != AUDIT_CTX_UNUSED);
+	WARN_ON(context->name_count);
+	if (context->context != AUDIT_CTX_UNUSED || context->name_count) {
+		audit_panic("unrecoverable error in audit_pidfd_spawn_entry()");
+		return;
+	}
+
+	state = context->state;
+	if (state == AUDIT_STATE_DISABLED)
+		return;
+
+	context->dummy = !audit_n_rules;
+	if (!context->dummy && state == AUDIT_STATE_BUILD) {
+		context->prio = 0;
+		if (auditd_test_task(current))
+			return;
+	}
+
+	context->arch = syscall_get_arch(current);
+	context->major = major;
+	context->argv[0] = a0;
+	context->argv[1] = a1;
+	context->argv[2] = a2;
+	context->argv[3] = a3;
+	context->context = AUDIT_CTX_PIDFD_SPAWN;
+	context->current_state = state;
+	ktime_get_coarse_real_ts64(&context->stamp.ctime);
+}
+
+/**
+ * __audit_pidfd_spawn_exit - finish a pidfd spawn child audit context
+ * @success: whether child-side setup succeeded
+ * @return_code: child-side setup result
+ */
+void __audit_pidfd_spawn_exit(int success, long return_code)
+{
+	struct audit_context *context = audit_context();
+
+	if (!context || context->dummy ||
+	    context->context != AUDIT_CTX_PIDFD_SPAWN)
+		goto out;
+
+	if (!list_empty(&context->killed_trees))
+		audit_kill_trees(context);
+
+	audit_return_fixup(context, success, return_code);
+	audit_filter_syscall(current, context);
+	audit_filter_inodes(current, context);
+	if (context->current_state == AUDIT_STATE_RECORD)
+		audit_log_exit();
+
+out:
+	audit_reset_context(context);
+}
+
 /**
  * __audit_syscall_entry - fill in an audit record at syscall entry
  * @major: major syscall type (function)
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 17/24] pidfd: audit child spawn execution
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

The child exec path runs outside the caller's audit context. A delayed
filename can leave a name-only record in the caller transaction, but inode
metadata and exec arguments are otherwise lost because the new task starts
with an unused audit context.

Start a dedicated AUDIT_PIDFD_SPAWN transaction before executable lookup so
audit rules can observe child identity, EXECVE arguments, and inode-backed
PATH records. Close it explicitly with the child setup result.
Task work does not enter through a userspace syscall frame, so do not
synthesize one.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfd_spawn.c | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 41d5cbb49a0f7..5586926988406 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -3,6 +3,8 @@
  * pidfd-backed process spawn builders
  */
 
+#include <asm/syscall.h>
+#include <linux/audit.h>
 #include <linux/completion.h>
 #include <linux/cred.h>
 #include <linux/file.h>
@@ -81,6 +83,8 @@ struct pidfd_spawn_state {
 	char *staged_path;
 	struct user_arg_ptr argv;
 	struct user_arg_ptr envp;
+	unsigned long audit_args[4];
+	int audit_syscall;
 	int result;
 	enum pidfd_spawn_status status;
 };
@@ -206,6 +210,8 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 	state->staged_path = NULL;
 	state->argv = native_arg(NULL);
 	state->envp = native_arg(NULL);
+	memset(state->audit_args, 0, sizeof(state->audit_args));
+	state->audit_syscall = 0;
 	state->result = result;
 	pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
 	mutex_unlock(&state->lock);
@@ -377,6 +383,22 @@ SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
 	return ret;
 }
 
+static void pidfd_spawn_save_audit_context(struct pidfd_spawn_state *state)
+{
+	struct pt_regs *regs = current_pt_regs();
+	unsigned long args[6];
+
+	syscall_get_arguments(current, regs, args);
+	state->audit_syscall = syscall_get_nr(current, regs);
+	memcpy(state->audit_args, args, sizeof(state->audit_args));
+}
+
+static void pidfd_spawn_audit_entry(struct pidfd_spawn_state *state)
+{
+	audit_pidfd_spawn_entry(state->audit_syscall, state->audit_args[0],
+				state->audit_args[1], state->audit_args[2],
+				state->audit_args[3]);
+}
 static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
 				     struct filename *filename, int result)
 {
@@ -391,6 +413,7 @@ static void pidfd_spawn_child(struct callback_head *work)
 	struct filename *filename;
 	int ret;
 
+	pidfd_spawn_audit_entry(state);
 	filename = complete_getname(&state->filename);
 	if (IS_ERR(filename))
 		ret = PTR_ERR(filename);
@@ -403,9 +426,11 @@ static void pidfd_spawn_child(struct callback_head *work)
 
 	pidfd_spawn_finish_child(state, filename, ret);
 	if (ret) {
+		audit_pidfd_spawn_exit(0, ret);
 		pidfd_spawn_state_put(state);
 		do_group_exit(PIDFD_SPAWN_EXIT_FAILURE);
 	}
+	audit_pidfd_spawn_exit(1, 0);
 	pidfd_spawn_state_put(state);
 }
 
@@ -575,6 +600,7 @@ static int pidfd_spawn_start(struct file *file,
 		state->argv = pidfd_spawn_user_arg(kargs->argv);
 		state->envp = pidfd_spawn_user_arg(kargs->envp);
 		pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
+		pidfd_spawn_save_audit_context(state);
 		reinit_completion(&state->done);
 
 		task = pidfd_spawn_create_task(file, state, &clone_args);
@@ -584,6 +610,9 @@ static int pidfd_spawn_start(struct file *file,
 			INIT_DELAYED_FILENAME(&state->filename);
 			state->argv = native_arg(NULL);
 			state->envp = native_arg(NULL);
+			memset(state->audit_args, 0,
+			       sizeof(state->audit_args));
+			state->audit_syscall = 0;
 			state->result = 0;
 			pidfd_spawn_set_status(state,
 					       PIDFD_SPAWN_CONFIGURING);
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 18/24] pidfd: make spawn builder execution signal-safe
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Wait for child setup in a killable and freezable state. If the caller
is interrupted after task creation, cancel a child that is still
setting up and normalize restart errors because the spawn operation is
not idempotent.

Preserve externally delivered fatal signals over setup errors. Complete
the builder and audit transaction before entering the signal core
directly, so the callback never returns through an invalid userspace
frame. The task work can run from an outer get_signal(), so fatal delivery
deliberately nests get_signal(); the inner path exits and never returns to
the outer call. The child remains embryonic and nondumpable until exec
installs a valid frame and mm.

Reject an already traced caller while holding cred_guard_mutex across
the spawn operation. This prevents CLONE_UNTRACED from becoming an
escape from an existing ptrace supervisor.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfd_spawn.c | 189 +++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 183 insertions(+), 6 deletions(-)

diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 5586926988406..0e52503976c0c 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -5,6 +5,7 @@
 
 #include <asm/syscall.h>
 #include <linux/audit.h>
+#include <linux/cgroup.h>
 #include <linux/completion.h>
 #include <linux/cred.h>
 #include <linux/file.h>
@@ -212,8 +213,10 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 	state->envp = native_arg(NULL);
 	memset(state->audit_args, 0, sizeof(state->audit_args));
 	state->audit_syscall = 0;
-	state->result = result;
-	pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
+	if (pidfd_spawn_get_status(state) != PIDFD_SPAWN_CANCELLED) {
+		state->result = result;
+		pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
+	}
 	mutex_unlock(&state->lock);
 
 	putname(filename);
@@ -226,6 +229,32 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 	kfree(staged_path);
 }
 
+static bool pidfd_spawn_cancel(struct pidfd_spawn_state *state,
+			       struct task_struct *task)
+{
+	bool cancelled = false;
+
+	mutex_lock(&state->lock);
+	if (pidfd_spawn_get_status(state) == PIDFD_SPAWN_STARTING) {
+		/*
+		 * Establish SIGKILL group-exit state before publishing
+		 * cancellation. The child also takes state->lock before committing
+		 * its setup result, so it cannot return from task work before the
+		 * signal is pending.
+		 */
+		if (WARN_ON_ONCE(do_send_sig_info(SIGKILL, SEND_SIG_PRIV, task,
+						  PIDTYPE_PID)))
+			goto out_unlock;
+		state->result = -ECANCELED;
+		pidfd_spawn_set_status(state, PIDFD_SPAWN_CANCELLED);
+		cancelled = true;
+	}
+out_unlock:
+	mutex_unlock(&state->lock);
+
+	return cancelled;
+}
+
 static bool pidfd_spawn_same_pid_ns(struct pidfd_spawn_state *state)
 {
 	return current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
@@ -383,6 +412,35 @@ SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
 	return ret;
 }
 
+static bool pidfd_spawn_child_cancelled(struct pidfd_spawn_state *state)
+{
+	bool cancelled;
+
+	mutex_lock(&state->lock);
+	cancelled = pidfd_spawn_get_status(state) == PIDFD_SPAWN_CANCELLED;
+	mutex_unlock(&state->lock);
+
+	return cancelled;
+}
+
+static bool pidfd_spawn_setup_done(struct pidfd_spawn_state *state)
+{
+	bool done = false;
+
+	mutex_lock(&state->lock);
+	switch (pidfd_spawn_get_status(state)) {
+	case PIDFD_SPAWN_SETUP_DONE:
+	case PIDFD_SPAWN_STARTED:
+		done = true;
+		break;
+	default:
+		break;
+	}
+	mutex_unlock(&state->lock);
+
+	return done;
+}
+
 static void pidfd_spawn_save_audit_context(struct pidfd_spawn_state *state)
 {
 	struct pt_regs *regs = current_pt_regs();
@@ -399,6 +457,52 @@ static void pidfd_spawn_audit_entry(struct pidfd_spawn_state *state)
 				state->audit_args[1], state->audit_args[2],
 				state->audit_args[3]);
 }
+
+static int pidfd_spawn_normalize_result(int result)
+{
+	if (result == -ERESTARTSYS || result == -ERESTARTNOINTR ||
+	    result == -ERESTARTNOHAND || result == -ERESTART_RESTARTBLOCK)
+		return -EINTR;
+	return result;
+}
+
+static int pidfd_spawn_pending_fatal_signal(void)
+{
+	struct sighand_struct *sighand;
+	sigset_t pending;
+	unsigned long flags;
+	int fatal = 0;
+	int sig;
+
+	/*
+	 * fatal_signal_pending() only recognizes pending SIGKILL. A default-fatal
+	 * coredump signal remains pending as itself, but must still win over a
+	 * setup error. Inspect the same pending sets and default dispositions as
+	 * get_signal() without dequeuing the signal.
+	 */
+	sighand = lock_task_sighand(current, &flags);
+	if (WARN_ON_ONCE(!sighand))
+		return 0;
+
+	sigorsets(&pending, &current->pending.signal,
+		  &current->signal->shared_pending.signal);
+	sigandnsets(&pending, &pending, &current->blocked);
+
+	for (sig = 1; sig < _NSIG; sig++) {
+		if (!sigismember(&pending, sig) || sig_kernel_ignore(sig) ||
+		    sig_kernel_stop(sig) ||
+		    sighand->action[sig - 1].sa.sa_handler != SIG_DFL)
+			continue;
+		if ((current->signal->flags & SIGNAL_UNKILLABLE) &&
+		    !sig_kernel_only(sig))
+			continue;
+		fatal = sig;
+		break;
+	}
+	spin_unlock_irqrestore(&sighand->siglock, flags);
+	return fatal;
+}
+
 static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
 				     struct filename *filename, int result)
 {
@@ -406,11 +510,40 @@ static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
 	complete_all(&state->done);
 }
 
+static __noreturn void
+pidfd_spawn_deliver_fatal_signal(struct pidfd_spawn_state *state,
+				 struct filename *filename, int sig)
+{
+	struct ksignal ksig;
+	sigset_t blocked;
+
+	/*
+	 * The callback frame is not a valid userspace signal frame on every
+	 * architecture. Complete the spawn transaction, then enter the signal
+	 * core directly instead of returning through the callback trampoline.
+	 *
+	 * The embryonic task state prevents ptrace access and coredumps until exec
+	 * installs a valid userspace frame and private mm. Block other catchable
+	 * signals so the selected default-fatal signal remains terminal. If the
+	 * signal core unexpectedly returns, exit without exposing the frame.
+	 */
+	pidfd_spawn_finish_child(state, filename, 0);
+	audit_pidfd_spawn_exit(0, -EINTR);
+	pidfd_spawn_state_put(state);
+
+	sigfillset(&blocked);
+	sigdelset(&blocked, sig);
+	set_current_blocked(&blocked);
+	get_signal(&ksig);
+	do_group_exit(PIDFD_SPAWN_EXIT_FAILURE);
+}
+
 static void pidfd_spawn_child(struct callback_head *work)
 {
 	struct pidfd_spawn_state *state =
 		container_of(work, struct pidfd_spawn_state, task_work);
 	struct filename *filename;
+	int fatal_sig;
 	int ret;
 
 	pidfd_spawn_audit_entry(state);
@@ -420,9 +553,27 @@ static void pidfd_spawn_child(struct callback_head *work)
 	else
 		ret = 0;
 
+	if (pidfd_spawn_child_cancelled(state)) {
+		ret = -ECANCELED;
+		pidfd_spawn_finish_child(state, filename, ret);
+		audit_pidfd_spawn_exit(0, ret);
+		pidfd_spawn_state_put(state);
+		do_group_exit(SIGKILL);
+	}
+	fatal_sig = pidfd_spawn_pending_fatal_signal();
+	if (fatal_sig)
+		pidfd_spawn_deliver_fatal_signal(state, filename, fatal_sig);
+
 	if (!ret)
 		ret = do_execveat_common(AT_FDCWD, filename,
 					 state->argv, state->envp, 0);
+	ret = pidfd_spawn_normalize_result(ret);
+	if (ret) {
+		fatal_sig = pidfd_spawn_pending_fatal_signal();
+		if (fatal_sig)
+			pidfd_spawn_deliver_fatal_signal(state, filename,
+							 fatal_sig);
+	}
 
 	pidfd_spawn_finish_child(state, filename, ret);
 	if (ret) {
@@ -531,9 +682,16 @@ static void pidfd_spawn_attach_vfork_done(struct task_struct *task,
 	task_unlock(task);
 }
 
-static void pidfd_spawn_wait_for_child(struct pidfd_spawn_state *state)
+static int pidfd_spawn_wait_for_child(struct pidfd_spawn_state *state)
 {
-	wait_for_completion(&state->done);
+	unsigned int wait_state = TASK_KILLABLE | TASK_FREEZABLE;
+	int ret;
+
+	cgroup_enter_frozen();
+	ret = wait_for_completion_state(&state->done, wait_state);
+	cgroup_leave_frozen(false);
+
+	return pidfd_spawn_normalize_result(ret);
 }
 
 static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
@@ -544,7 +702,7 @@ static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
 
 	ret = wait_for_vfork_done(task, vfork);
 	if (ret)
-		return ret;
+		return pidfd_spawn_normalize_result(ret);
 
 	mutex_lock(&state->lock);
 	ret = state->result;
@@ -624,7 +782,26 @@ static int pidfd_spawn_start(struct file *file,
 	}
 
 	wake_up_new_task(task);
-	pidfd_spawn_wait_for_child(state);
+	ret = pidfd_spawn_wait_for_child(state);
+	if (ret) {
+		int interrupt = ret;
+
+		if (pidfd_spawn_setup_done(state)) {
+			if (pidfd_spawn_finish_vfork(state, task, &vfork))
+				return interrupt;
+			return 0;
+		}
+		if (!pidfd_spawn_cancel(state, task) &&
+		    pidfd_spawn_setup_done(state)) {
+			if (pidfd_spawn_finish_vfork(state, task, &vfork))
+				return interrupt;
+			return 0;
+		}
+
+		/* Drop the stack-based completion without waiting for child setup. */
+		wait_for_vfork_done(task, &vfork);
+		return interrupt;
+	}
 	return pidfd_spawn_finish_vfork(state, task, &vfork);
 }
 
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 19/24] file: expose spawn file-action helpers
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Process builders need to apply close_range() and fchdir() while
preparing a child. Split the syscall bodies into internal helpers so
callers in fs can reuse existing validation and state updates. Keep the
helpers internal to fs rather than adding new global interfaces.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/file.c     | 11 ++++++++---
 fs/internal.h |  2 ++
 fs/open.c     |  7 ++++++-
 3 files changed, 16 insertions(+), 4 deletions(-)

diff --git a/fs/file.c b/fs/file.c
index 628ca07dc4b17..4f4cb1d5752c2 100644
--- a/fs/file.c
+++ b/fs/file.c
@@ -805,7 +805,7 @@ static inline void __range_close(struct files_struct *files, unsigned int fd,
 }
 
 /**
- * sys_close_range() - Close all file descriptors in a given range.
+ * do_close_range() - Close all file descriptors in a given range.
  *
  * @fd:     starting file descriptor to close
  * @max_fd: last file descriptor to close
@@ -815,8 +815,7 @@ static inline void __range_close(struct files_struct *files, unsigned int fd,
  * from @fd up to and including @max_fd are closed.
  * Currently, errors to close a given file descriptor are ignored.
  */
-SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd,
-		unsigned int, flags)
+int do_close_range(unsigned int fd, unsigned int max_fd, unsigned int flags)
 {
 	struct task_struct *me = current;
 	struct files_struct *cur_fds = me->files, *fds = NULL;
@@ -867,6 +866,12 @@ SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd,
 	return 0;
 }
 
+SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd,
+		unsigned int, flags)
+{
+	return do_close_range(fd, max_fd, flags);
+}
+
 /**
  * file_close_fd - return file associated with fd
  * @fd: file descriptor to retrieve file for
diff --git a/fs/internal.h b/fs/internal.h
index 71cc43e72b33e..25492194e2883 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -198,6 +198,8 @@ extern struct file *do_file_open_root(const struct path *,
 extern struct open_how build_open_how(int flags, umode_t mode);
 extern int build_open_flags(const struct open_how *how, struct open_flags *op);
 struct file *file_close_fd_locked(struct files_struct *files, unsigned fd);
+int do_close_range(unsigned int fd, unsigned int max_fd, unsigned int flags);
+int do_fchdir(unsigned int fd);
 
 int do_ftruncate(struct file *file, loff_t length, unsigned int flags);
 int chmod_common(const struct path *path, umode_t mode);
diff --git a/fs/open.c b/fs/open.c
index 408925d7bd0b7..bf9d7bdcf487c 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -568,7 +568,7 @@ SYSCALL_DEFINE1(chdir, const char __user *, filename)
 	return error;
 }
 
-SYSCALL_DEFINE1(fchdir, unsigned int, fd)
+int do_fchdir(unsigned int fd)
 {
 	CLASS(fd_raw, f)(fd);
 	int error;
@@ -585,6 +585,11 @@ SYSCALL_DEFINE1(fchdir, unsigned int, fd)
 	return error;
 }
 
+SYSCALL_DEFINE1(fchdir, unsigned int, fd)
+{
+	return do_fchdir(fd);
+}
+
 SYSCALL_DEFINE1(chroot, const char __user *, filename)
 {
 	struct path path;
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 20/24] pidfd: add initial spawn file actions
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Allow pidfd_spawn_run() to apply an ordered list of DUP2, FCHDIR, and
CLOSE_RANGE actions before exec. Copy and validate the complete action
list before creating the child so malformed input cannot leave a
partially started process. Use a versioned action stride so userspace can
pass larger future records. Bound the payload by 64 KiB instead of a fixed
action count, keeping allocation bounded without imposing an arbitrary
limit on small records.

Apply actions before completing the delayed executable name. This makes an
FCHDIR action establish the cwd used by both relative executable lookup and
the child transaction's AUDIT_CWD record.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfd_spawn.c | 171 ++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 163 insertions(+), 8 deletions(-)

diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 0e52503976c0c..3e46a12c0ba07 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -5,9 +5,11 @@
 
 #include <asm/syscall.h>
 #include <linux/audit.h>
+#include <linux/close_range.h>
 #include <linux/cgroup.h>
 #include <linux/completion.h>
 #include <linux/cred.h>
+#include <linux/fdtable.h>
 #include <linux/file.h>
 #include <linux/fs.h>
 #include <linux/mm.h>
@@ -23,15 +25,18 @@
 #include <linux/sched/signal.h>
 #include <linux/sched/task.h>
 #include <linux/security.h>
+#include <linux/sizes.h>
 #include <linux/slab.h>
 #include <linux/syscalls.h>
 #include <linux/task_work.h>
 #include <linux/uaccess.h>
+#include <linux/vmalloc.h>
 #include <uapi/linux/pidfd.h>
 #include <uapi/linux/wait.h>
 #include <trace/events/sched.h>
 
 #include "exec_internal.h"
+#include "internal.h"
 
 static inline void __user *pidfd_spawn_user_ptr(__u64 p)
 {
@@ -59,6 +64,9 @@ static inline struct user_arg_ptr pidfd_spawn_user_arg(__u64 p)
 
 #define PIDFD_SPAWN_EXIT_FAILURE	(127 << 8)
 #define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE	256
+/* Bound allocation without imposing a fixed number of action records. */
+#define PIDFD_SPAWN_MAX_ACTIONS_SIZE	SZ_64K
+
 DEFINE_FREE(pidfd_spawn_dismiss_filename, struct delayed_filename,
 	    dismiss_delayed_filename(&_T))
 
@@ -82,9 +90,11 @@ struct pidfd_spawn_state {
 	struct pid_namespace *creator_pid_ns;
 	struct delayed_filename filename;
 	char *staged_path;
+	struct pidfd_spawn_action *actions;
 	struct user_arg_ptr argv;
 	struct user_arg_ptr envp;
 	unsigned long audit_args[4];
+	unsigned int nr_actions;
 	int audit_syscall;
 	int result;
 	enum pidfd_spawn_status status;
@@ -139,6 +149,7 @@ static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
 		put_cred(state->creator_cred);
 	if (state->creator_pid_ns)
 		put_pid_ns(state->creator_pid_ns);
+	kvfree(state->actions);
 	put_pid(state->pid);
 	kfree(state);
 }
@@ -192,6 +203,7 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 				      struct filename *filename, int result)
 {
 	const struct cred *creator_cred;
+	struct pidfd_spawn_action *actions;
 	struct mm_struct *creator_mm;
 	struct pid_namespace *creator_pid_ns;
 	char *staged_path;
@@ -201,14 +213,17 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 	 * after the child no longer needs the shared setup payload.
 	 */
 	mutex_lock(&state->lock);
+	actions = state->actions;
 	creator_mm = state->creator_mm;
 	creator_cred = state->creator_cred;
 	creator_pid_ns = state->creator_pid_ns;
 	staged_path = state->staged_path;
+	state->actions = NULL;
 	state->creator_mm = NULL;
 	state->creator_cred = NULL;
 	state->creator_pid_ns = NULL;
 	state->staged_path = NULL;
+	state->nr_actions = 0;
 	state->argv = native_arg(NULL);
 	state->envp = native_arg(NULL);
 	memset(state->audit_args, 0, sizeof(state->audit_args));
@@ -227,6 +242,7 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 	if (creator_pid_ns)
 		put_pid_ns(creator_pid_ns);
 	kfree(staged_path);
+	kvfree(actions);
 }
 
 static bool pidfd_spawn_cancel(struct pidfd_spawn_state *state,
@@ -441,6 +457,114 @@ static bool pidfd_spawn_setup_done(struct pidfd_spawn_state *state)
 	return done;
 }
 
+static int pidfd_spawn_validate_action(const struct pidfd_spawn_action *action)
+{
+	if (action->reserved[0] || action->reserved[1])
+		return -EINVAL;
+
+	switch (action->type) {
+	case PIDFD_SPAWN_ACTION_DUP2:
+		if (action->flags)
+			return -EINVAL;
+		return 0;
+	case PIDFD_SPAWN_ACTION_FCHDIR:
+		if (action->flags || action->newfd)
+			return -EINVAL;
+		return 0;
+	case PIDFD_SPAWN_ACTION_CLOSE_RANGE:
+		if (action->newfd < action->fd)
+			return -EINVAL;
+		if (action->flags & ~(CLOSE_RANGE_UNSHARE |
+				      CLOSE_RANGE_CLOEXEC))
+			return -EINVAL;
+		return 0;
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int pidfd_spawn_copy_actions(struct pidfd_spawn_action **actions,
+				    __u64 uactions, unsigned int nr_actions,
+				    unsigned int action_size)
+{
+	const char __user *uptr;
+	struct pidfd_spawn_action *kactions __free(kvfree) = NULL;
+	unsigned int i;
+	int ret;
+
+	if (!nr_actions)
+		return 0;
+
+	BUILD_BUG_ON(sizeof(struct pidfd_spawn_action) !=
+		     PIDFD_SPAWN_ACTION_SIZE_VER0);
+
+	uptr = pidfd_spawn_user_ptr(uactions);
+	kactions = kvmalloc_array(nr_actions, sizeof(*kactions),
+				  GFP_KERNEL_ACCOUNT);
+	if (!kactions)
+		return -ENOMEM;
+
+	for (i = 0; i < nr_actions; i++) {
+		ret = copy_struct_from_user(&kactions[i], sizeof(kactions[i]),
+					    uptr + (size_t)i * action_size,
+					    action_size);
+		if (ret)
+			return ret;
+
+		ret = pidfd_spawn_validate_action(&kactions[i]);
+		if (ret)
+			return ret;
+	}
+
+	*actions = no_free_ptr(kactions);
+	return 0;
+}
+
+static int pidfd_spawn_do_dup2(unsigned int oldfd, unsigned int newfd)
+{
+	struct file *file __free(fput) = NULL;
+
+	file = fget_raw(oldfd);
+	if (!file)
+		return -EBADF;
+	if (oldfd == newfd) {
+		set_close_on_exec(newfd, 0);
+		return 0;
+	}
+
+	return replace_fd(newfd, file, 0);
+}
+
+static int pidfd_spawn_apply_actions(struct pidfd_spawn_state *state)
+{
+	unsigned int i;
+	int ret;
+
+	for (i = 0; i < state->nr_actions; i++) {
+		const struct pidfd_spawn_action *action = &state->actions[i];
+
+		switch (action->type) {
+		case PIDFD_SPAWN_ACTION_DUP2:
+			ret = pidfd_spawn_do_dup2(action->fd, action->newfd);
+			break;
+		case PIDFD_SPAWN_ACTION_FCHDIR:
+			ret = do_fchdir(action->fd);
+			break;
+		case PIDFD_SPAWN_ACTION_CLOSE_RANGE:
+			ret = do_close_range(action->fd, action->newfd,
+					     action->flags);
+			break;
+		default:
+			ret = -EOPNOTSUPP;
+			break;
+		}
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
 static void pidfd_spawn_save_audit_context(struct pidfd_spawn_state *state)
 {
 	struct pt_regs *regs = current_pt_regs();
@@ -543,24 +667,32 @@ static void pidfd_spawn_child(struct callback_head *work)
 	struct pidfd_spawn_state *state =
 		container_of(work, struct pidfd_spawn_state, task_work);
 	struct filename *filename;
+	bool cancelled;
 	int fatal_sig;
 	int ret;
 
 	pidfd_spawn_audit_entry(state);
+	cancelled = pidfd_spawn_child_cancelled(state);
+	fatal_sig = 0;
+	ret = 0;
+	if (!cancelled) {
+		fatal_sig = pidfd_spawn_pending_fatal_signal();
+		if (!fatal_sig)
+			ret = pidfd_spawn_apply_actions(state);
+	}
+
+	/* FCHDIR must establish the cwd captured by audit_getname(). */
 	filename = complete_getname(&state->filename);
-	if (IS_ERR(filename))
+	if (!ret && !cancelled && !fatal_sig && IS_ERR(filename))
 		ret = PTR_ERR(filename);
-	else
-		ret = 0;
 
-	if (pidfd_spawn_child_cancelled(state)) {
+	if (cancelled) {
 		ret = -ECANCELED;
 		pidfd_spawn_finish_child(state, filename, ret);
 		audit_pidfd_spawn_exit(0, ret);
 		pidfd_spawn_state_put(state);
 		do_group_exit(SIGKILL);
 	}
-	fatal_sig = pidfd_spawn_pending_fatal_signal();
 	if (fatal_sig)
 		pidfd_spawn_deliver_fatal_signal(state, filename, fatal_sig);
 
@@ -589,6 +721,7 @@ static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
 				     struct pidfd_spawn_run_args __user *uargs,
 				     size_t usize)
 {
+	size_t actions_size;
 	int ret;
 
 	BUILD_BUG_ON(sizeof(struct pidfd_spawn_run_args) !=
@@ -606,8 +739,17 @@ static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
 		return -EINVAL;
 	if (!kargs->argv)
 		return -EINVAL;
-	if (kargs->nr_actions || kargs->actions || kargs->action_size)
+	if (kargs->nr_actions) {
+		if (!kargs->actions ||
+		    kargs->action_size < PIDFD_SPAWN_ACTION_SIZE_VER0 ||
+		    !IS_ALIGNED(kargs->action_size, sizeof(__u64)))
+			return -EINVAL;
+	} else if (kargs->actions || kargs->action_size) {
 		return -EINVAL;
+	}
+	actions_size = array_size(kargs->nr_actions, kargs->action_size);
+	if (actions_size > PIDFD_SPAWN_MAX_ACTIONS_SIZE)
+		return -E2BIG;
 	if (!pidfd_spawn_user_ptr_fits(kargs->path) ||
 	    !pidfd_spawn_user_ptr_fits(kargs->argv) ||
 	    !pidfd_spawn_user_ptr_fits(kargs->envp) ||
@@ -713,10 +855,12 @@ static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
 
 static int pidfd_spawn_start(struct file *file,
 			     struct pidfd_spawn_state *state,
-			     const struct pidfd_spawn_run_args *kargs)
+			     const struct pidfd_spawn_run_args *kargs,
+			     struct pidfd_spawn_action **actions)
 {
 	struct delayed_filename filename
 		__free(pidfd_spawn_dismiss_filename) = {};
+	struct pidfd_spawn_action *drop_actions __free(kvfree) = NULL;
 	struct delayed_filename drop_filename
 		__free(pidfd_spawn_dismiss_filename) = {};
 	struct kernel_clone_args clone_args = {
@@ -757,8 +901,11 @@ static int pidfd_spawn_start(struct file *file,
 
 		state->argv = pidfd_spawn_user_arg(kargs->argv);
 		state->envp = pidfd_spawn_user_arg(kargs->envp);
+		state->actions = *actions;
+		state->nr_actions = kargs->nr_actions;
 		pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
 		pidfd_spawn_save_audit_context(state);
+		*actions = NULL;
 		reinit_completion(&state->done);
 
 		task = pidfd_spawn_create_task(file, state, &clone_args);
@@ -766,6 +913,9 @@ static int pidfd_spawn_start(struct file *file,
 			ret = PTR_ERR(task);
 			drop_filename = state->filename;
 			INIT_DELAYED_FILENAME(&state->filename);
+			drop_actions = state->actions;
+			state->actions = NULL;
+			state->nr_actions = 0;
 			state->argv = native_arg(NULL);
 			state->envp = native_arg(NULL);
 			memset(state->audit_args, 0,
@@ -854,6 +1004,7 @@ int pidfd_empty_open(unsigned int flags)
 SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
 		struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
 {
+	struct pidfd_spawn_action *actions __free(kvfree) = NULL;
 	struct pidfd_spawn_run_args kargs;
 	struct pidfd_spawn_state *state __free(pidfd_spawn_state) = NULL;
 	int ret;
@@ -869,6 +1020,10 @@ SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
 	state = pidfd_spawn_state_get_file(fd_file(f));
 	if (IS_ERR(state))
 		return PTR_ERR(state);
+	ret = pidfd_spawn_copy_actions(&actions, kargs.actions,
+				       kargs.nr_actions, kargs.action_size);
+	if (ret)
+		return ret;
 
 	scoped_cond_guard(mutex_intr, return -ERESTARTNOINTR,
 			  &current->signal->cred_guard_mutex) {
@@ -877,7 +1032,7 @@ SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
 		ret = pidfd_spawn_check_startable(state);
 		if (ret)
 			return ret;
-		ret = pidfd_spawn_start(fd_file(f), state, &kargs);
+		ret = pidfd_spawn_start(fd_file(f), state, &kargs, &actions);
 	}
 	if (!ret)
 		ret = pid_vnr(state->pid);
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 21/24] pidfd: consume spawn builders on the first run attempt
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

A pre-task run failure currently returns the builder to CONFIGURING. A
second thread can then publish another invocation before the first
caller probes the pidfd, making the observed process state ambiguous.

Claim the builder after type, authority, and ptrace checks but before
run argument access. Any later validation, uaccess, allocation, or
task-creation failure leaves a terminal taskless builder. Later config
and run operations return EBUSY, while process-dependent pidfd
operations return ESRCH.

Normalize every internal restart error after the claim to EINTR so the
architecture cannot restart a consumed invocation and replace its
result with EBUSY. Preserve normal restart behavior before the claim.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfd_spawn.c | 164 +++++++++++++++++++++++++++++++----------------
 1 file changed, 109 insertions(+), 55 deletions(-)

diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 3e46a12c0ba07..1309fe99ec091 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -73,6 +73,7 @@ DEFINE_FREE(pidfd_spawn_dismiss_filename, struct delayed_filename,
 enum pidfd_spawn_status {
 	PIDFD_SPAWN_CONFIGURING,
 	PIDFD_SPAWN_STARTING,
+	PIDFD_SPAWN_FAILED_TASKLESS,
 	PIDFD_SPAWN_SETUP_DONE,
 	PIDFD_SPAWN_STARTED,
 	PIDFD_SPAWN_CANCELLED,
@@ -100,6 +101,14 @@ struct pidfd_spawn_state {
 	enum pidfd_spawn_status status;
 };
 
+struct pidfd_spawn_resources {
+	struct pidfd_spawn_action *actions;
+	struct mm_struct *creator_mm;
+	const struct cred *creator_cred;
+	struct pid_namespace *creator_pid_ns;
+	char *staged_path;
+};
+
 static struct pid *
 pidfd_spawn_load_pid(const struct pidfd_spawn_state *state)
 {
@@ -183,8 +192,16 @@ static struct pid *pidfd_spawn_file_pid(void *data)
 	return pid ? pid : ERR_PTR(-ESRCH);
 }
 
+static bool pidfd_spawn_file_terminal(void *data)
+{
+	struct pidfd_spawn_state *state = data;
+
+	return pidfd_spawn_get_status(state) == PIDFD_SPAWN_FAILED_TASKLESS;
+}
+
 static const struct pidfs_future_file_ops pidfd_spawn_future_ops = {
 	.get_pid = pidfd_spawn_file_pid,
+	.is_terminal = pidfd_spawn_file_terminal,
 	.release = pidfd_spawn_state_release,
 };
 
@@ -199,31 +216,47 @@ pidfd_spawn_state_get_file(struct file *file)
 	return state;
 }
 
+static void
+pidfd_spawn_detach_resources(struct pidfd_spawn_state *state,
+			     struct pidfd_spawn_resources *resources)
+{
+	resources->actions = state->actions;
+	resources->creator_mm = state->creator_mm;
+	resources->creator_cred = state->creator_cred;
+	resources->creator_pid_ns = state->creator_pid_ns;
+	resources->staged_path = state->staged_path;
+	state->actions = NULL;
+	state->creator_mm = NULL;
+	state->creator_cred = NULL;
+	state->creator_pid_ns = NULL;
+	state->staged_path = NULL;
+	state->nr_actions = 0;
+}
+
+static void
+pidfd_spawn_put_resources(struct pidfd_spawn_resources *resources)
+{
+	if (resources->creator_mm)
+		mmdrop(resources->creator_mm);
+	if (resources->creator_cred)
+		put_cred(resources->creator_cred);
+	if (resources->creator_pid_ns)
+		put_pid_ns(resources->creator_pid_ns);
+	kfree(resources->staged_path);
+	kvfree(resources->actions);
+}
+
 static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 				      struct filename *filename, int result)
 {
-	const struct cred *creator_cred;
-	struct pidfd_spawn_action *actions;
-	struct mm_struct *creator_mm;
-	struct pid_namespace *creator_pid_ns;
-	char *staged_path;
+	struct pidfd_spawn_resources resources = {};
 
 	/*
 	 * Run data is one-shot. Detach it under the lock, then drop refs
 	 * after the child no longer needs the shared setup payload.
 	 */
 	mutex_lock(&state->lock);
-	actions = state->actions;
-	creator_mm = state->creator_mm;
-	creator_cred = state->creator_cred;
-	creator_pid_ns = state->creator_pid_ns;
-	staged_path = state->staged_path;
-	state->actions = NULL;
-	state->creator_mm = NULL;
-	state->creator_cred = NULL;
-	state->creator_pid_ns = NULL;
-	state->staged_path = NULL;
-	state->nr_actions = 0;
+	pidfd_spawn_detach_resources(state, &resources);
 	state->argv = native_arg(NULL);
 	state->envp = native_arg(NULL);
 	memset(state->audit_args, 0, sizeof(state->audit_args));
@@ -235,14 +268,7 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 	mutex_unlock(&state->lock);
 
 	putname(filename);
-	if (creator_mm)
-		mmdrop(creator_mm);
-	if (creator_cred)
-		put_cred(creator_cred);
-	if (creator_pid_ns)
-		put_pid_ns(creator_pid_ns);
-	kfree(staged_path);
-	kvfree(actions);
+	pidfd_spawn_put_resources(&resources);
 }
 
 static bool pidfd_spawn_cancel(struct pidfd_spawn_state *state,
@@ -291,16 +317,44 @@ static bool pidfd_spawn_same_creator(struct pidfd_spawn_state *state)
 	       pidfd_spawn_same_pid_ns(state);
 }
 
-static int pidfd_spawn_check_startable(struct pidfd_spawn_state *state)
+static int pidfd_spawn_claim(struct pidfd_spawn_state *state)
 {
 	int ret;
 
-	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+	scoped_guard(mutex, &state->lock) {
 		ret = pidfd_spawn_configuring_error(state);
-		if (!ret && !pidfd_spawn_same_creator(state))
-			ret = -EPERM;
+		if (ret)
+			return ret;
+		if (!pidfd_spawn_same_creator(state))
+			return -EPERM;
+
+		pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
 	}
-	return ret;
+	return 0;
+}
+
+static void pidfd_spawn_fail_taskless(struct file *file,
+				      struct pidfd_spawn_state *state, int result)
+{
+	struct pidfd_spawn_resources resources = {};
+
+	mutex_lock(&state->lock);
+	if (WARN_ON_ONCE(pidfd_spawn_get_status(state) !=
+			 PIDFD_SPAWN_STARTING || pidfd_spawn_load_pid(state))) {
+		mutex_unlock(&state->lock);
+		return;
+	}
+	state->result = result;
+	pidfd_spawn_detach_resources(state, &resources);
+	state->argv = native_arg(NULL);
+	state->envp = native_arg(NULL);
+	memset(state->audit_args, 0, sizeof(state->audit_args));
+	state->audit_syscall = 0;
+	pidfd_spawn_set_status(state, PIDFD_SPAWN_FAILED_TASKLESS);
+	mutex_unlock(&state->lock);
+
+	pidfd_spawn_put_resources(&resources);
+	pidfs_future_file_notify(file);
 }
 
 static struct filename *pidfd_spawn_get_path(const char __user *value)
@@ -718,7 +772,7 @@ static void pidfd_spawn_child(struct callback_head *work)
 }
 
 static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
-				     struct pidfd_spawn_run_args __user *uargs,
+				     const struct pidfd_spawn_run_args __user *uargs,
 				     size_t usize)
 {
 	size_t actions_size;
@@ -873,6 +927,13 @@ static int pidfd_spawn_start(struct file *file,
 	struct task_struct *task;
 	int ret;
 
+	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+		if (pidfd_spawn_get_status(state) != PIDFD_SPAWN_STARTING)
+			return -EBUSY;
+		if (!!state->staged_path == !!kargs->path)
+			return -EINVAL;
+	}
+
 	if (kargs->path) {
 		ret = pidfd_spawn_get_delayed_path(&filename,
 						   pidfd_spawn_user_ptr(kargs->path));
@@ -881,15 +942,8 @@ static int pidfd_spawn_start(struct file *file,
 	}
 
 	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
-		ret = pidfd_spawn_configuring_error(state);
-		if (ret)
-			return ret;
-		if (!pidfd_spawn_same_creator(state))
-			return -EPERM;
-		if (state->staged_path && kargs->path)
-			return -EINVAL;
-		if (!state->staged_path && !kargs->path)
-			return -EINVAL;
+		if (pidfd_spawn_get_status(state) != PIDFD_SPAWN_STARTING)
+			return -EBUSY;
 		if (state->staged_path) {
 			ret = pidfd_spawn_get_delayed_kernel_path(&filename,
 								  state->staged_path);
@@ -903,7 +957,6 @@ static int pidfd_spawn_start(struct file *file,
 		state->envp = pidfd_spawn_user_arg(kargs->envp);
 		state->actions = *actions;
 		state->nr_actions = kargs->nr_actions;
-		pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
 		pidfd_spawn_save_audit_context(state);
 		*actions = NULL;
 		reinit_completion(&state->done);
@@ -921,9 +974,6 @@ static int pidfd_spawn_start(struct file *file,
 			memset(state->audit_args, 0,
 			       sizeof(state->audit_args));
 			state->audit_syscall = 0;
-			state->result = 0;
-			pidfd_spawn_set_status(state,
-					       PIDFD_SPAWN_CONFIGURING);
 			return ret;
 		}
 
@@ -1002,17 +1052,13 @@ int pidfd_empty_open(unsigned int flags)
 }
 
 SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
-		struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
+		const struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
 {
 	struct pidfd_spawn_action *actions __free(kvfree) = NULL;
 	struct pidfd_spawn_run_args kargs;
 	struct pidfd_spawn_state *state __free(pidfd_spawn_state) = NULL;
 	int ret;
 
-	ret = pidfd_spawn_copy_run_args(&kargs, uargs, usize);
-	if (ret)
-		return ret;
-
 	CLASS(fd, f)(fd);
 	if (fd_empty(f))
 		return -EBADF;
@@ -1020,19 +1066,27 @@ SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
 	state = pidfd_spawn_state_get_file(fd_file(f));
 	if (IS_ERR(state))
 		return PTR_ERR(state);
-	ret = pidfd_spawn_copy_actions(&actions, kargs.actions,
-				       kargs.nr_actions, kargs.action_size);
-	if (ret)
-		return ret;
-
 	scoped_cond_guard(mutex_intr, return -ERESTARTNOINTR,
 			  &current->signal->cred_guard_mutex) {
 		if (READ_ONCE(current->ptrace))
 			return -EPERM;
-		ret = pidfd_spawn_check_startable(state);
+		ret = pidfd_spawn_claim(state);
 		if (ret)
 			return ret;
-		ret = pidfd_spawn_start(fd_file(f), state, &kargs, &actions);
+
+		ret = pidfd_spawn_copy_run_args(&kargs, uargs, usize);
+		if (!ret)
+			ret = pidfd_spawn_copy_actions(&actions, kargs.actions,
+						       kargs.nr_actions,
+						       kargs.action_size);
+		if (!ret)
+			ret = pidfd_spawn_start(fd_file(f), state, &kargs,
+						&actions);
+		if (ret) {
+			ret = pidfd_spawn_normalize_result(ret);
+			if (!pidfd_spawn_load_pid(state))
+				pidfd_spawn_fail_taskless(fd_file(f), state, ret);
+		}
 	}
 	if (!ret)
 		ret = pid_vnr(state->pid);
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 22/24] pidfd: expose spawn builder system calls
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

The preceding commits define the future-pidfd lifetime, configuration, and
run state machine without making them reachable from userspace. Each
preparatory step therefore remains bisectable without exposing a partial
process builder.

Make pidfd_open(0, PIDFD_EMPTY) create a taskless future pidfd and expose
pidfd_config() and pidfd_spawn_run(). Add the syscall declarations and
table entries together so the complete initial interface becomes reachable
at one commit boundary. Synchronize the lagging arm64 AArch32 perf mirror
through the current syscall table while adding the builder entries.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 arch/alpha/kernel/syscalls/syscall.tbl        |  2 ++
 arch/arm/tools/syscall.tbl                    |  2 ++
 arch/arm64/tools/syscall_32.tbl               |  2 ++
 arch/m68k/kernel/syscalls/syscall.tbl         |  2 ++
 arch/microblaze/kernel/syscalls/syscall.tbl   |  2 ++
 arch/mips/kernel/syscalls/syscall_n32.tbl     |  2 ++
 arch/mips/kernel/syscalls/syscall_n64.tbl     |  2 ++
 arch/mips/kernel/syscalls/syscall_o32.tbl     |  2 ++
 arch/parisc/kernel/syscalls/syscall.tbl       |  2 ++
 arch/powerpc/kernel/syscalls/syscall.tbl      |  2 ++
 arch/s390/kernel/syscalls/syscall.tbl         |  2 ++
 arch/sh/kernel/syscalls/syscall.tbl           |  2 ++
 arch/sparc/kernel/syscalls/syscall.tbl        |  2 ++
 arch/x86/entry/syscalls/syscall_32.tbl        |  2 ++
 arch/x86/entry/syscalls/syscall_64.tbl        |  2 ++
 arch/xtensa/kernel/syscalls/syscall.tbl       |  2 ++
 include/linux/syscalls.h                      |  7 +++++++
 include/uapi/asm-generic/unistd.h             |  8 +++++++-
 kernel/pid.c                                  | 19 ++++++++++++++++++-
 scripts/syscall.tbl                           |  2 ++
 tools/include/uapi/asm-generic/unistd.h       |  8 +++++++-
 .../arch/alpha/entry/syscalls/syscall.tbl     |  2 ++
 .../perf/arch/arm/entry/syscalls/syscall.tbl  |  2 ++
 .../arch/arm64/entry/syscalls/syscall_32.tbl  | 11 +++++++++++
 .../arch/mips/entry/syscalls/syscall_n64.tbl  |  2 ++
 .../arch/parisc/entry/syscalls/syscall.tbl    |  2 ++
 .../arch/powerpc/entry/syscalls/syscall.tbl   |  2 ++
 .../perf/arch/s390/entry/syscalls/syscall.tbl |  2 ++
 tools/perf/arch/sh/entry/syscalls/syscall.tbl |  2 ++
 .../arch/sparc/entry/syscalls/syscall.tbl     |  2 ++
 .../arch/x86/entry/syscalls/syscall_32.tbl    |  2 ++
 .../arch/x86/entry/syscalls/syscall_64.tbl    |  2 ++
 .../arch/xtensa/entry/syscalls/syscall.tbl    |  2 ++
 tools/scripts/syscall.tbl                     |  2 ++
 34 files changed, 108 insertions(+), 3 deletions(-)

diff --git a/arch/alpha/kernel/syscalls/syscall.tbl b/arch/alpha/kernel/syscalls/syscall.tbl
index f31b7afffc345..8dd0b22a407c5 100644
--- a/arch/alpha/kernel/syscalls/syscall.tbl
+++ b/arch/alpha/kernel/syscalls/syscall.tbl
@@ -511,3 +511,5 @@
 579	common	file_setattr			sys_file_setattr
 580	common	listns				sys_listns
 581	common	rseq_slice_yield		sys_rseq_slice_yield
+582	common	pidfd_config			sys_pidfd_config
+583	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/arm/tools/syscall.tbl b/arch/arm/tools/syscall.tbl
index 94351e22bfcf7..9830c4600fc78 100644
--- a/arch/arm/tools/syscall.tbl
+++ b/arch/arm/tools/syscall.tbl
@@ -486,3 +486,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/arm64/tools/syscall_32.tbl b/arch/arm64/tools/syscall_32.tbl
index 62d93d88e0fef..9c3281b3612c2 100644
--- a/arch/arm64/tools/syscall_32.tbl
+++ b/arch/arm64/tools/syscall_32.tbl
@@ -483,3 +483,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/m68k/kernel/syscalls/syscall.tbl b/arch/m68k/kernel/syscalls/syscall.tbl
index 2489342571014..2db719475e061 100644
--- a/arch/m68k/kernel/syscalls/syscall.tbl
+++ b/arch/m68k/kernel/syscalls/syscall.tbl
@@ -471,3 +471,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/microblaze/kernel/syscalls/syscall.tbl b/arch/microblaze/kernel/syscalls/syscall.tbl
index 223d263036272..41ead93dba5c4 100644
--- a/arch/microblaze/kernel/syscalls/syscall.tbl
+++ b/arch/microblaze/kernel/syscalls/syscall.tbl
@@ -477,3 +477,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/mips/kernel/syscalls/syscall_n32.tbl b/arch/mips/kernel/syscalls/syscall_n32.tbl
index 7430714e2b8f8..b6bfca9ddedc6 100644
--- a/arch/mips/kernel/syscalls/syscall_n32.tbl
+++ b/arch/mips/kernel/syscalls/syscall_n32.tbl
@@ -410,3 +410,5 @@
 469	n32	file_setattr			sys_file_setattr
 470	n32	listns				sys_listns
 471	n32	rseq_slice_yield		sys_rseq_slice_yield
+472	n32	pidfd_config			sys_pidfd_config
+473	n32	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/mips/kernel/syscalls/syscall_n64.tbl b/arch/mips/kernel/syscalls/syscall_n64.tbl
index 630aab9e54259..229a1a5bff0e9 100644
--- a/arch/mips/kernel/syscalls/syscall_n64.tbl
+++ b/arch/mips/kernel/syscalls/syscall_n64.tbl
@@ -386,3 +386,5 @@
 469	n64	file_setattr			sys_file_setattr
 470	n64	listns				sys_listns
 471	n64	rseq_slice_yield		sys_rseq_slice_yield
+472	n64	pidfd_config			sys_pidfd_config
+473	n64	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/mips/kernel/syscalls/syscall_o32.tbl b/arch/mips/kernel/syscalls/syscall_o32.tbl
index 128653112284b..4ee804186e399 100644
--- a/arch/mips/kernel/syscalls/syscall_o32.tbl
+++ b/arch/mips/kernel/syscalls/syscall_o32.tbl
@@ -459,3 +459,5 @@
 469	o32	file_setattr			sys_file_setattr
 470	o32	listns				sys_listns
 471	o32	rseq_slice_yield		sys_rseq_slice_yield
+472	o32	pidfd_config			sys_pidfd_config
+473	o32	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/parisc/kernel/syscalls/syscall.tbl b/arch/parisc/kernel/syscalls/syscall.tbl
index c6331dad94613..ff6760df2dc51 100644
--- a/arch/parisc/kernel/syscalls/syscall.tbl
+++ b/arch/parisc/kernel/syscalls/syscall.tbl
@@ -470,3 +470,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/powerpc/kernel/syscalls/syscall.tbl b/arch/powerpc/kernel/syscalls/syscall.tbl
index 4fcc7c58a105d..021b334a8e1a0 100644
--- a/arch/powerpc/kernel/syscalls/syscall.tbl
+++ b/arch/powerpc/kernel/syscalls/syscall.tbl
@@ -562,3 +562,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	nospu	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/s390/kernel/syscalls/syscall.tbl b/arch/s390/kernel/syscalls/syscall.tbl
index 09a7ef04d9791..eea88f94a7624 100644
--- a/arch/s390/kernel/syscalls/syscall.tbl
+++ b/arch/s390/kernel/syscalls/syscall.tbl
@@ -398,3 +398,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/sh/kernel/syscalls/syscall.tbl b/arch/sh/kernel/syscalls/syscall.tbl
index 70b315cbe710c..37166286c4045 100644
--- a/arch/sh/kernel/syscalls/syscall.tbl
+++ b/arch/sh/kernel/syscalls/syscall.tbl
@@ -475,3 +475,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/sparc/kernel/syscalls/syscall.tbl b/arch/sparc/kernel/syscalls/syscall.tbl
index 7e71bf7fcd14f..70f2165b9cfb3 100644
--- a/arch/sparc/kernel/syscalls/syscall.tbl
+++ b/arch/sparc/kernel/syscalls/syscall.tbl
@@ -517,3 +517,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/arch/x86/entry/syscalls/syscall_32.tbl b/arch/x86/entry/syscalls/syscall_32.tbl
index f832ebd2d79b0..b2eeddbe99be9 100644
--- a/arch/x86/entry/syscalls/syscall_32.tbl
+++ b/arch/x86/entry/syscalls/syscall_32.tbl
@@ -477,3 +477,5 @@
 469	i386	file_setattr		sys_file_setattr
 470	i386	listns			sys_listns
 471	i386	rseq_slice_yield	sys_rseq_slice_yield
+472	i386	pidfd_config		sys_pidfd_config
+473	i386	pidfd_spawn_run		sys_pidfd_spawn_run
diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da1..f0952f34c10a5 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -396,6 +396,8 @@
 469	common	file_setattr		sys_file_setattr
 470	common	listns			sys_listns
 471	common	rseq_slice_yield	sys_rseq_slice_yield
+472	common	pidfd_config		sys_pidfd_config
+473	common	pidfd_spawn_run		sys_pidfd_spawn_run
 
 #
 # Due to a historical design error, certain syscalls are numbered differently
diff --git a/arch/xtensa/kernel/syscalls/syscall.tbl b/arch/xtensa/kernel/syscalls/syscall.tbl
index a9bca4e484dec..45dd80ab94cf9 100644
--- a/arch/xtensa/kernel/syscalls/syscall.tbl
+++ b/arch/xtensa/kernel/syscalls/syscall.tbl
@@ -442,3 +442,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h
index 874d9067a43b5..ec90454d01214 100644
--- a/include/linux/syscalls.h
+++ b/include/linux/syscalls.h
@@ -79,6 +79,7 @@ struct mnt_id_req;
 struct ns_id_req;
 struct xattr_args;
 struct file_attr;
+struct pidfd_spawn_run_args;
 
 #include <linux/types.h>
 #include <linux/aio_abi.h>
@@ -908,6 +909,12 @@ asmlinkage long sys_clock_adjtime32(clockid_t which_clock,
 asmlinkage long sys_syncfs(int fd);
 asmlinkage long sys_setns(int fd, int nstype);
 asmlinkage long sys_pidfd_open(pid_t pid, unsigned int flags);
+asmlinkage long sys_pidfd_config(int fd, unsigned int cmd,
+				 const char __user *key,
+				 const void __user *value, int aux);
+asmlinkage long sys_pidfd_spawn_run(int fd,
+				    const struct pidfd_spawn_run_args __user *uargs,
+				    size_t usize);
 asmlinkage long sys_sendmmsg(int fd, struct mmsghdr __user *msg,
 			     unsigned int vlen, unsigned flags);
 asmlinkage long sys_process_vm_readv(pid_t pid,
diff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h
index a627acc8fb5fe..9ab8ae5100ec1 100644
--- a/include/uapi/asm-generic/unistd.h
+++ b/include/uapi/asm-generic/unistd.h
@@ -863,8 +863,14 @@ __SYSCALL(__NR_listns, sys_listns)
 #define __NR_rseq_slice_yield 471
 __SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
 
+#define __NR_pidfd_config 472
+__SYSCALL(__NR_pidfd_config, sys_pidfd_config)
+
+#define __NR_pidfd_spawn_run 473
+__SYSCALL(__NR_pidfd_spawn_run, sys_pidfd_spawn_run)
+
 #undef __NR_syscalls
-#define __NR_syscalls 472
+#define __NR_syscalls 474
 
 /*
  * 32 bit systems traditionally used different
diff --git a/kernel/pid.c b/kernel/pid.c
index 010f80177cac8..a1d05ce10ac25 100644
--- a/kernel/pid.c
+++ b/kernel/pid.c
@@ -43,6 +43,7 @@
 #include <linux/sched/task.h>
 #include <linux/idr.h>
 #include <linux/pidfs.h>
+#include <linux/pidfd_spawn.h>
 #include <net/sock.h>
 #include <uapi/linux/pidfd.h>
 
@@ -715,10 +716,26 @@ SYSCALL_DEFINE2(pidfd_open, pid_t, pid, unsigned int, flags)
 	int fd;
 	struct pid *p;
 
+	/*
+	 * pidfd_open(0, PIDFD_EMPTY) is the spawn-builder entry point.
+	 * It creates a future pidfd instead of looking up pid 0.
+	 */
+	if (pid == 0) {
+		if (!(flags & PIDFD_EMPTY))
+			return -EINVAL;
+		if (flags & ~(PIDFD_EMPTY | PIDFD_NONBLOCK))
+			return -EINVAL;
+
+		return pidfd_empty_open(flags & PIDFD_NONBLOCK);
+	}
+
+	/* PIDFD_EMPTY is only meaningful for the pid == 0 builder form. */
+	if (flags & PIDFD_EMPTY)
+		return -EINVAL;
 	if (flags & ~(PIDFD_NONBLOCK | PIDFD_THREAD))
 		return -EINVAL;
 
-	if (pid <= 0)
+	if (pid < 0)
 		return -EINVAL;
 
 	p = find_get_pid(pid);
diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl
index 7a42b32b65776..29248f0ac1a07 100644
--- a/scripts/syscall.tbl
+++ b/scripts/syscall.tbl
@@ -412,3 +412,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/include/uapi/asm-generic/unistd.h b/tools/include/uapi/asm-generic/unistd.h
index a627acc8fb5fe..9ab8ae5100ec1 100644
--- a/tools/include/uapi/asm-generic/unistd.h
+++ b/tools/include/uapi/asm-generic/unistd.h
@@ -863,8 +863,14 @@ __SYSCALL(__NR_listns, sys_listns)
 #define __NR_rseq_slice_yield 471
 __SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
 
+#define __NR_pidfd_config 472
+__SYSCALL(__NR_pidfd_config, sys_pidfd_config)
+
+#define __NR_pidfd_spawn_run 473
+__SYSCALL(__NR_pidfd_spawn_run, sys_pidfd_spawn_run)
+
 #undef __NR_syscalls
-#define __NR_syscalls 472
+#define __NR_syscalls 474
 
 /*
  * 32 bit systems traditionally used different
diff --git a/tools/perf/arch/alpha/entry/syscalls/syscall.tbl b/tools/perf/arch/alpha/entry/syscalls/syscall.tbl
index 74720667fe091..a20f81b950349 100644
--- a/tools/perf/arch/alpha/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/alpha/entry/syscalls/syscall.tbl
@@ -502,3 +502,5 @@
 570	common	lsm_set_self_attr		sys_lsm_set_self_attr
 571	common	lsm_list_modules		sys_lsm_list_modules
 572	common  mseal				sys_mseal
+582	common	pidfd_config			sys_pidfd_config
+583	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/perf/arch/arm/entry/syscalls/syscall.tbl b/tools/perf/arch/arm/entry/syscalls/syscall.tbl
index 94351e22bfcf7..9830c4600fc78 100644
--- a/tools/perf/arch/arm/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/arm/entry/syscalls/syscall.tbl
@@ -486,3 +486,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl b/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl
index 9a37930d4e26f..9c3281b3612c2 100644
--- a/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl
+++ b/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl
@@ -474,3 +474,14 @@
 460	common	lsm_set_self_attr		sys_lsm_set_self_attr
 461	common	lsm_list_modules		sys_lsm_list_modules
 462	common	mseal				sys_mseal
+463	common	setxattrat			sys_setxattrat
+464	common	getxattrat			sys_getxattrat
+465	common	listxattrat			sys_listxattrat
+466	common	removexattrat			sys_removexattrat
+467	common	open_tree_attr			sys_open_tree_attr
+468	common	file_getattr			sys_file_getattr
+469	common	file_setattr			sys_file_setattr
+470	common	listns				sys_listns
+471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl b/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl
index 630aab9e54259..229a1a5bff0e9 100644
--- a/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl
+++ b/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl
@@ -386,3 +386,5 @@
 469	n64	file_setattr			sys_file_setattr
 470	n64	listns				sys_listns
 471	n64	rseq_slice_yield		sys_rseq_slice_yield
+472	n64	pidfd_config			sys_pidfd_config
+473	n64	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/perf/arch/parisc/entry/syscalls/syscall.tbl b/tools/perf/arch/parisc/entry/syscalls/syscall.tbl
index 66dc406b12e44..88311785913f8 100644
--- a/tools/perf/arch/parisc/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/parisc/entry/syscalls/syscall.tbl
@@ -461,3 +461,5 @@
 460	common	lsm_set_self_attr		sys_lsm_set_self_attr
 461	common	lsm_list_modules		sys_lsm_list_modules
 462	common	mseal				sys_mseal
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl b/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl
index 4fcc7c58a105d..021b334a8e1a0 100644
--- a/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl
@@ -562,3 +562,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	nospu	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/perf/arch/s390/entry/syscalls/syscall.tbl b/tools/perf/arch/s390/entry/syscalls/syscall.tbl
index 09a7ef04d9791..eea88f94a7624 100644
--- a/tools/perf/arch/s390/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/s390/entry/syscalls/syscall.tbl
@@ -398,3 +398,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/perf/arch/sh/entry/syscalls/syscall.tbl b/tools/perf/arch/sh/entry/syscalls/syscall.tbl
index 70b315cbe710c..37166286c4045 100644
--- a/tools/perf/arch/sh/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/sh/entry/syscalls/syscall.tbl
@@ -475,3 +475,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/perf/arch/sparc/entry/syscalls/syscall.tbl b/tools/perf/arch/sparc/entry/syscalls/syscall.tbl
index 7e71bf7fcd14f..70f2165b9cfb3 100644
--- a/tools/perf/arch/sparc/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/sparc/entry/syscalls/syscall.tbl
@@ -517,3 +517,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl b/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl
index f832ebd2d79b0..b2eeddbe99be9 100644
--- a/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl
+++ b/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl
@@ -477,3 +477,5 @@
 469	i386	file_setattr		sys_file_setattr
 470	i386	listns			sys_listns
 471	i386	rseq_slice_yield	sys_rseq_slice_yield
+472	i386	pidfd_config		sys_pidfd_config
+473	i386	pidfd_spawn_run		sys_pidfd_spawn_run
diff --git a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da1..f0952f34c10a5 100644
--- a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
@@ -396,6 +396,8 @@
 469	common	file_setattr		sys_file_setattr
 470	common	listns			sys_listns
 471	common	rseq_slice_yield	sys_rseq_slice_yield
+472	common	pidfd_config		sys_pidfd_config
+473	common	pidfd_spawn_run		sys_pidfd_spawn_run
 
 #
 # Due to a historical design error, certain syscalls are numbered differently
diff --git a/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl b/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl
index a9bca4e484dec..45dd80ab94cf9 100644
--- a/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl
@@ -442,3 +442,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
diff --git a/tools/scripts/syscall.tbl b/tools/scripts/syscall.tbl
index 7a42b32b65776..29248f0ac1a07 100644
--- a/tools/scripts/syscall.tbl
+++ b/tools/scripts/syscall.tbl
@@ -412,3 +412,5 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	pidfd_config			sys_pidfd_config
+473	common	pidfd_spawn_run			sys_pidfd_spawn_run
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 23/24] selftests/pidfd: cover pidfd spawn builders
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Add comprehensive coverage for the pidfd spawn builder lifecycle and ABI.
Exercise taskless configuration, direct and staged executable paths,
one-shot execution, precise terminal errors, source-state inheritance,
and pidfs identity before and after task publication.

Cover ordered DUP2, CLOSE_RANGE, and FCHDIR actions, including validation,
same-fd FD_CLOEXEC semantics, child-only state, ordered effects, and
failures after publication. Verify run argument input-only behavior,
normal exec failure, status-127 children, and auto-reap metadata.

Use deterministic userfaultfd stalls and bounded monotonic waits to
exercise publication races, competing run claims, cancellation before
and after task creation, coredump suppression, and pre-uaccess claiming.

Verify ptrace, pidfd_getfd, procfs, source credential and PID namespace,
SCM_RIGHTS, RLIMIT_NPROC, and pids cgroup boundaries. Cover Landlock and
seccomp policy behavior, dedicated audit transactions, and compat pointer
containers.

The suite also checks pidfs reopen, bind-mount, xattr, and epoll behavior.
Keep feature-dependent cases skippable and request the required kselftest
configuration.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 tools/testing/selftests/landlock/audit.h      |    6 +-
 tools/testing/selftests/pidfd/.gitignore      |    9 +
 tools/testing/selftests/pidfd/Makefile        |   25 +-
 tools/testing/selftests/pidfd/config          |    6 +
 .../pidfd/pidfd_spawn_accounting_test.c       |  428 ++++++
 .../pidfd/pidfd_spawn_actions_test.c          |  474 +++++++
 .../selftests/pidfd/pidfd_spawn_audit_test.c  |  521 +++++++
 .../selftests/pidfd/pidfd_spawn_common.c      |  512 +++++++
 .../selftests/pidfd/pidfd_spawn_common.h      |   59 +
 .../selftests/pidfd/pidfd_spawn_compat.c      |  221 +++
 .../selftests/pidfd/pidfd_spawn_exec_test.c   |  301 ++++
 .../selftests/pidfd/pidfd_spawn_policy_test.c |  294 ++++
 .../selftests/pidfd/pidfd_spawn_race_test.c   |  923 ++++++++++++
 .../pidfd/pidfd_spawn_security_test.c         | 1242 +++++++++++++++++
 .../selftests/pidfd/pidfd_spawn_test.c        |  550 ++++++++
 15 files changed, 5568 insertions(+), 3 deletions(-)
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_common.c
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_common.h
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_compat.c
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_race_test.c
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_security_test.c
 create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_test.c

diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h
index f45fdef35681a..9530b8aca6c9f 100644
--- a/tools/testing/selftests/landlock/audit.h
+++ b/tools/testing/selftests/landlock/audit.h
@@ -550,7 +550,8 @@ static int audit_init_filter_exe(struct audit_filter *filter, const char *path)
 	return 0;
 }
 
-static int audit_cleanup(int audit_fd, struct audit_filter *filter)
+static int __maybe_unused
+audit_cleanup(int audit_fd, struct audit_filter *filter)
 {
 	struct audit_filter new_filter;
 	int err = 0;
@@ -581,7 +582,8 @@ static int audit_cleanup(int audit_fd, struct audit_filter *filter)
 	return err;
 }
 
-static int audit_init_with_exe_filter(struct audit_filter *filter)
+static int __maybe_unused
+audit_init_with_exe_filter(struct audit_filter *filter)
 {
 	int fd, err;
 
diff --git a/tools/testing/selftests/pidfd/.gitignore b/tools/testing/selftests/pidfd/.gitignore
index 4cd8ec7fd349a..72acb3371fadc 100644
--- a/tools/testing/selftests/pidfd/.gitignore
+++ b/tools/testing/selftests/pidfd/.gitignore
@@ -13,3 +13,12 @@ pidfd_exec_helper
 pidfd_xattr_test
 pidfd_setattr_test
 pidfd_autoreap_test
+pidfd_spawn_test
+pidfd_spawn_actions_test
+pidfd_spawn_exec_test
+pidfd_spawn_race_test
+pidfd_spawn_security_test
+pidfd_spawn_accounting_test
+pidfd_spawn_policy_test
+pidfd_spawn_audit_test
+pidfd_spawn_compat
diff --git a/tools/testing/selftests/pidfd/Makefile b/tools/testing/selftests/pidfd/Makefile
index 4211f91e9af85..1d58f76261200 100644
--- a/tools/testing/selftests/pidfd/Makefile
+++ b/tools/testing/selftests/pidfd/Makefile
@@ -1,12 +1,35 @@
 # SPDX-License-Identifier: GPL-2.0-only
 CFLAGS += -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) -pthread -Wall
 
+CAN_BUILD_I386 := $(shell ../x86/check_cc.sh "$(CC)" \
+	../x86/trivial_32bit_program.c -m32 -static)
+
+PIDFD_SPAWN_TESTS := pidfd_spawn_test pidfd_spawn_exec_test \
+	pidfd_spawn_actions_test pidfd_spawn_race_test \
+	pidfd_spawn_security_test pidfd_spawn_accounting_test \
+	pidfd_spawn_policy_test pidfd_spawn_audit_test
+
 TEST_GEN_PROGS := pidfd_test pidfd_fdinfo_test pidfd_open_test \
 	pidfd_poll_test pidfd_wait pidfd_getfd_test pidfd_setns_test \
 	pidfd_file_handle_test pidfd_bind_mount pidfd_info_test \
-	pidfd_xattr_test pidfd_setattr_test pidfd_autoreap_test
+	pidfd_xattr_test pidfd_setattr_test pidfd_autoreap_test \
+	$(PIDFD_SPAWN_TESTS)
+
+ifeq ($(CAN_BUILD_I386),1)
+TEST_GEN_PROGS += pidfd_spawn_compat
+endif
 
 TEST_GEN_PROGS_EXTENDED := pidfd_exec_helper
+LOCAL_HDRS += pidfd_spawn_common.h ../landlock/audit.h
 
 include ../lib.mk
 
+$(addprefix $(OUTPUT)/,$(PIDFD_SPAWN_TESTS)): \
+	pidfd_spawn_common.c pidfd_spawn_common.h
+
+ifeq ($(CAN_BUILD_I386),1)
+pidfd_spawn_compat: CFLAGS += -m32
+pidfd_spawn_compat: LDLIBS += -static
+$(OUTPUT)/pidfd_spawn_compat: CFLAGS += -m32
+$(OUTPUT)/pidfd_spawn_compat: LDLIBS += -static
+endif
diff --git a/tools/testing/selftests/pidfd/config b/tools/testing/selftests/pidfd/config
index cf7cc0ce02484..699d43aaa4093 100644
--- a/tools/testing/selftests/pidfd/config
+++ b/tools/testing/selftests/pidfd/config
@@ -5,4 +5,10 @@ CONFIG_PID_NS=y
 CONFIG_NET_NS=y
 CONFIG_TIME_NS=y
 CONFIG_CGROUPS=y
+CONFIG_CGROUP_PIDS=y
 CONFIG_CHECKPOINT_RESTORE=y
+CONFIG_USERFAULTFD=y
+CONFIG_SECURITY_LANDLOCK=y
+CONFIG_SECCOMP=y
+CONFIG_SECCOMP_FILTER=y
+CONFIG_AUDIT=y
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c
new file mode 100644
index 0000000000000..25fe493a34d44
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c
@@ -0,0 +1,428 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+static int write_string_file(const char *path, const char *value)
+{
+	size_t len = strlen(value);
+	ssize_t ret;
+	int saved_errno;
+	int fd;
+
+	fd = open(path, O_WRONLY | O_CLOEXEC);
+	if (fd < 0)
+		return -1;
+	ret = write(fd, value, len);
+	saved_errno = errno;
+	close(fd);
+	if (ret != (ssize_t)len) {
+		errno = ret < 0 ? saved_errno : EIO;
+		return -1;
+	}
+	return 0;
+}
+
+static int path_join(char **out, const char *dir, const char *name)
+{
+	if (asprintf(out, "%s/%s", dir, name) < 0) {
+		errno = ENOMEM;
+		return -1;
+	}
+	return 0;
+}
+
+static bool controller_list_has(const char *controllers, const char *name)
+{
+	size_t name_len = strlen(name);
+	const char *next = controllers;
+
+	while (next) {
+		const char *end = strchr(next, ',');
+		size_t len = end ? end - next : strlen(next);
+
+		if (len == name_len && !strncmp(next, name, len))
+			return true;
+		next = end ? end + 1 : NULL;
+	}
+	return false;
+}
+
+static int read_current_cgroup(char *buf, size_t size, bool *unified)
+{
+	char line[PATH_MAX];
+	FILE *file;
+
+	file = fopen("/proc/self/cgroup", "re");
+	if (!file)
+		return -1;
+	while (fgets(line, sizeof(line), file)) {
+		char *controllers;
+		char *path;
+		char *separator;
+
+		separator = strchr(line, ':');
+		if (!separator)
+			continue;
+		controllers = separator + 1;
+		separator = strchr(controllers, ':');
+		if (!separator)
+			continue;
+		*separator = '\0';
+		if (*controllers &&
+		    !controller_list_has(controllers, "pids"))
+			continue;
+		path = separator + 1;
+		path[strcspn(path, "\n")] = '\0';
+		if (strlen(path) >= size) {
+			fclose(file);
+			errno = ENAMETOOLONG;
+			return -1;
+		}
+		strcpy(buf, path);
+		*unified = !*controllers;
+		fclose(file);
+		return 0;
+	}
+	fclose(file);
+	errno = ENOENT;
+	return -1;
+}
+
+static int current_cgroup_path(char **pathp)
+{
+	char current[PATH_MAX];
+	const char *root;
+	bool unified;
+	int ret;
+
+	ret = read_current_cgroup(current, sizeof(current), &unified);
+	if (ret)
+		return ret;
+	root = unified ? "/sys/fs/cgroup" : "/sys/fs/cgroup/pids";
+	if (!strcmp(current, "/"))
+		ret = asprintf(pathp, "%s", root);
+	else
+		ret = asprintf(pathp, "%s%s", root, current);
+	if (ret < 0) {
+		errno = ENOMEM;
+		return -1;
+	}
+	return 0;
+}
+
+static int enable_pids_controller(const char *cgroup)
+{
+	char *controllers = NULL;
+	char *control = NULL;
+	int ret;
+
+	ret = path_join(&controllers, cgroup, "cgroup.controllers");
+	if (ret)
+		return ret;
+	ret = access(controllers, F_OK);
+	free(controllers);
+	if (ret && errno == ENOENT)
+		return 0;
+	if (ret)
+		return ret;
+	ret = path_join(&control, cgroup, "cgroup.subtree_control");
+	if (ret)
+		return ret;
+	ret = write_string_file(control, "+pids");
+	free(control);
+	return ret;
+}
+
+static int set_cgroup_pids_max(const char *cgroup, const char *value)
+{
+	char *max = NULL;
+	int ret;
+
+	ret = path_join(&max, cgroup, "pids.max");
+	if (ret)
+		return ret;
+	ret = write_string_file(max, value);
+	free(max);
+	return ret;
+}
+
+static int enter_cgroup(const char *cgroup)
+{
+	char pid[32];
+	char *procs = NULL;
+	int ret;
+
+	ret = snprintf(pid, sizeof(pid), "%d", getpid());
+	if (ret < 0 || ret >= (int)sizeof(pid)) {
+		errno = EINVAL;
+		return -1;
+	}
+	ret = path_join(&procs, cgroup, "cgroup.procs");
+	if (ret)
+		return ret;
+	ret = write_string_file(procs, pid);
+	free(procs);
+	return ret;
+}
+
+static int make_limited_pids_cgroup_at(const char *base, char **parentp,
+				       char **leafp)
+{
+	char name[64];
+	char *parent = NULL;
+	char *leaf = NULL;
+	char *max = NULL;
+	bool parent_created = false;
+	bool leaf_created = false;
+	int ret;
+
+	/* The controller may already be enabled for children. */
+	enable_pids_controller(base);
+	snprintf(name, sizeof(name), "pidfd-spawn-pids-%d", getpid());
+	ret = path_join(&parent, base, name);
+	if (ret)
+		goto out;
+	if (mkdir(parent, 0755)) {
+		ret = -1;
+		goto out;
+	}
+	parent_created = true;
+	ret = path_join(&max, parent, "pids.max");
+	if (ret)
+		goto out;
+	ret = access(max, W_OK);
+	free(max);
+	max = NULL;
+	if (ret)
+		goto out;
+	ret = enable_pids_controller(parent);
+	if (ret)
+		goto out;
+	ret = path_join(&leaf, parent, "leaf");
+	if (ret)
+		goto out;
+	if (mkdir(leaf, 0755)) {
+		ret = -1;
+		goto out;
+	}
+	leaf_created = true;
+	ret = set_cgroup_pids_max(leaf, "1");
+	if (ret)
+		goto out;
+
+	*parentp = parent;
+	*leafp = leaf;
+	parent_created = false;
+	leaf_created = false;
+	parent = NULL;
+	leaf = NULL;
+out:
+	if (leaf_created)
+		rmdir(leaf);
+	if (parent_created)
+		rmdir(parent);
+	free(max);
+	free(leaf);
+	free(parent);
+	return ret;
+}
+
+static int make_limited_pids_cgroup(char **parentp, char **leafp)
+{
+	char *base = NULL;
+	int saved_errno;
+	int ret;
+
+	ret = current_cgroup_path(&base);
+	if (ret)
+		return ret;
+	ret = make_limited_pids_cgroup_at(base, parentp, leafp);
+	saved_errno = errno;
+	free(base);
+	if (!ret)
+		return 0;
+	ret = make_limited_pids_cgroup_at("/sys/fs/cgroup", parentp, leafp);
+	if (!ret)
+		return 0;
+	errno = saved_errno;
+	return -1;
+}
+
+static int pidfd_spawn_pids_worker(const char *cgroup, const char *path)
+{
+	char * const argv[] = { "pidfd_spawn_accounting_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	int failed_builder;
+	int builder;
+
+	if (enter_cgroup(cgroup))
+		return 1;
+	failed_builder = sys_pidfd_empty_open();
+	if (failed_builder < 0)
+		return 2;
+	errno = 0;
+	if (spawn_run_path(failed_builder, path, argv, NULL, 0) != -1 ||
+	    errno != EAGAIN)
+		return 3;
+	errno = 0;
+	if (spawn_run_path(failed_builder, path, argv, NULL, 0) != -1 ||
+	    errno != EBUSY)
+		return 4;
+	if (set_cgroup_pids_max(cgroup, "2"))
+		return 5;
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		return 6;
+	if (spawn_run_path(builder, path, argv, NULL, 0))
+		return 7;
+	if (wait_pidfd_exit(builder, 0))
+		return 8;
+	if (close(builder) || close(failed_builder))
+		return 9;
+	return 0;
+}
+
+#define PIDFD_SPAWN_UID_FIRST 60000
+#define PIDFD_SPAWN_UID_LAST 65000
+#define PIDFD_SPAWN_UID_BUSY 78
+
+static int pidfd_spawn_rlimit_worker(const char *path, uid_t uid)
+{
+	char * const argv[] = { "pidfd_spawn_accounting_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct rlimit limit = {
+		.rlim_cur = 1,
+		.rlim_max = 2,
+	};
+	int failed_builder;
+	int builder;
+
+	if (setrlimit(RLIMIT_NPROC, &limit))
+		return 1;
+	if (setgid(uid) || setuid(uid))
+		return 77;
+
+	failed_builder = sys_pidfd_empty_open();
+	if (failed_builder < 0)
+		return 2;
+	errno = 0;
+	if (spawn_run_path(failed_builder, path, argv, NULL, 0) != -1 ||
+	    errno != EAGAIN)
+		return 3;
+	errno = 0;
+	if (spawn_run_path(failed_builder, path, argv, NULL, 0) != -1 ||
+	    errno != EBUSY)
+		return 4;
+
+	limit.rlim_cur = 2;
+	if (setrlimit(RLIMIT_NPROC, &limit))
+		return 5;
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		return 6;
+	if (spawn_run_path(builder, path, argv, NULL, 0))
+		return errno == EAGAIN ? PIDFD_SPAWN_UID_BUSY : 7;
+	if (wait_pidfd_exit(builder, 0))
+		return 8;
+	if (close(builder) || close(failed_builder))
+		return 9;
+	return 0;
+}
+
+TEST(pidfd_spawn_charges_rlimit_nproc_at_run)
+{
+	const char *path = self_exe_path();
+	pid_t worker;
+	pid_t waited;
+	uid_t uid;
+	int status;
+
+	if (getuid())
+		SKIP(return, "test requires root to select an unused uid");
+	ASSERT_NE(path, NULL);
+	for (uid = PIDFD_SPAWN_UID_FIRST; uid < PIDFD_SPAWN_UID_LAST; uid++) {
+		worker = fork();
+		ASSERT_GE(worker, 0);
+		if (!worker)
+			_exit(pidfd_spawn_rlimit_worker(path, uid));
+		waited = waitpid_timeout(worker, &status,
+					 PIDFD_SPAWN_TIMEOUT_MS);
+		ASSERT_EQ(waited, worker);
+		ASSERT_TRUE(WIFEXITED(status));
+		if (WEXITSTATUS(status) == 77)
+			SKIP(return, "changing uid is unavailable");
+		if (WEXITSTATUS(status) == PIDFD_SPAWN_UID_BUSY)
+			continue;
+		ASSERT_EQ(WEXITSTATUS(status), 0);
+		return;
+	}
+	SKIP(return, "no unused uid available for RLIMIT_NPROC");
+}
+
+TEST(pidfd_spawn_charges_pids_cgroup_at_run)
+{
+	const char *path = self_exe_path();
+	char *parent = NULL;
+	char *leaf = NULL;
+	int cleanup_leaf;
+	int cleanup_parent;
+	int saved_errno;
+	pid_t worker;
+	pid_t waited;
+	int status;
+
+	ASSERT_NE(path, NULL);
+	if (make_limited_pids_cgroup(&parent, &leaf)) {
+		saved_errno = errno;
+		SKIP(return, "writable pids cgroup is unavailable: %s",
+		     strerror(saved_errno));
+	}
+
+	worker = fork();
+	if (worker < 0) {
+		cleanup_leaf = rmdir(leaf);
+		cleanup_parent = rmdir(parent);
+		free(leaf);
+		free(parent);
+		ASSERT_GE(worker, 0);
+		ASSERT_EQ(cleanup_leaf, 0);
+		ASSERT_EQ(cleanup_parent, 0);
+	}
+	if (!worker)
+		_exit(pidfd_spawn_pids_worker(leaf, path));
+	waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	cleanup_leaf = rmdir(leaf);
+	cleanup_parent = rmdir(parent);
+	free(leaf);
+	free(parent);
+
+	ASSERT_EQ(cleanup_leaf, 0);
+	ASSERT_EQ(cleanup_parent, 0);
+	ASSERT_EQ(waited, worker);
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+int main(int argc, char **argv)
+{
+	int ret = helper_main(argc, argv);
+
+	if (ret >= 0)
+		return ret;
+	return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c
new file mode 100644
index 0000000000000..d44873280b6d2
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c
@@ -0,0 +1,474 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/close_range.h>
+#include <linux/pidfd_spawn.h>
+#include <poll.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+static ssize_t read_exact_timeout(int fd, void *buf, size_t size)
+{
+	struct pollfd pfd = {
+		.fd = fd,
+		.events = POLLIN,
+	};
+	size_t offset = 0;
+
+	while (offset < size) {
+		ssize_t len;
+		int ret;
+
+		ret = poll(&pfd, 1, PIDFD_SPAWN_TIMEOUT_MS);
+		if (ret <= 0) {
+			if (!ret)
+				errno = ETIMEDOUT;
+			return -1;
+		}
+		if (pfd.revents & (POLLERR | POLLNVAL)) {
+			errno = EIO;
+			return -1;
+		}
+		len = read(fd, (char *)buf + offset, size - offset);
+		if (len < 0) {
+			if (errno == EINTR)
+				continue;
+			return -1;
+		}
+		if (!len) {
+			errno = EPIPE;
+			return -1;
+		}
+		offset += len;
+	}
+	return offset;
+}
+
+static int run_args_fail_taskless(struct pidfd_spawn_run_args *args,
+				  size_t size, int expected_errno)
+{
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	const char *path = self_exe_path();
+	int saved_errno;
+	int ret;
+	int fd;
+
+	if (!path)
+		return -1;
+	fd = sys_pidfd_empty_open();
+	if (fd < 0)
+		return -1;
+	errno = 0;
+	ret = sys_pidfd_spawn_run(fd, args, size);
+	saved_errno = errno;
+	if (ret != -1 || saved_errno != expected_errno)
+		goto fail;
+	errno = 0;
+	if (ioctl(fd, PIDFD_GET_INFO, &info) != -1 || errno != ESRCH)
+		goto fail;
+	errno = 0;
+	if (config_path(fd, path) != -1 || errno != EBUSY)
+		goto fail;
+	return close(fd);
+
+fail:
+	saved_errno = errno;
+	close(fd);
+	errno = saved_errno;
+	return -1;
+}
+
+TEST(pidfd_spawn_run_rejects_bad_actions)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_actions_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_spawn_action action = {
+		.type = PIDFD_SPAWN_ACTION_DUP2,
+		.flags = 1,
+	};
+	struct pidfd_spawn_run_args args = {
+		.path = ptr_to_u64(path),
+		.argv = ptr_to_u64(argv),
+		.envp = ptr_to_u64(empty_envp),
+		.actions = ptr_to_u64(&action),
+		.nr_actions = 1,
+		.action_size = sizeof(action),
+	};
+
+	ASSERT_NE(path, NULL);
+	ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+	action.flags = 0;
+	action.reserved[0] = 1;
+	ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+	action.reserved[0] = 0;
+	action.reserved[1] = 1;
+	ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+	action.reserved[1] = 0;
+	action.type = UINT32_MAX;
+	ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EOPNOTSUPP), 0);
+
+	action.type = PIDFD_SPAWN_ACTION_CLOSE_RANGE;
+	action.fd = 10;
+	action.newfd = 9;
+	ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+
+	action.type = PIDFD_SPAWN_ACTION_DUP2;
+	action.fd = STDOUT_FILENO;
+	action.newfd = STDOUT_FILENO;
+	args.action_size = PIDFD_SPAWN_ACTION_SIZE_VER0 - sizeof(__u64);
+	ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+	args.action_size = PIDFD_SPAWN_ACTION_SIZE_VER0 + sizeof(__u32);
+	ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+	args.action_size = PIDFD_SPAWN_ACTION_SIZE_VER0;
+	args.nr_actions = 65536 / PIDFD_SPAWN_ACTION_SIZE_VER0 + 1;
+	ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), E2BIG), 0);
+
+	args.actions = 0;
+	args.nr_actions = 0;
+	ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), EINVAL), 0);
+}
+
+TEST(pidfd_spawn_run_accepts_versioned_action_elements)
+{
+	struct {
+		struct pidfd_spawn_action action;
+		__u64 tail;
+	} extended_action = {
+		.action = {
+			.type = PIDFD_SPAWN_ACTION_DUP2,
+			.fd = STDOUT_FILENO,
+			.newfd = STDOUT_FILENO,
+		},
+		.tail = 1,
+	};
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_actions_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_spawn_run_args args = {
+		.path = ptr_to_u64(path),
+		.argv = ptr_to_u64(argv),
+		.envp = ptr_to_u64(empty_envp),
+		.actions = ptr_to_u64(&extended_action),
+		.nr_actions = 1,
+		.action_size = sizeof(extended_action),
+	};
+	int child_pid;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	ASSERT_EQ(run_args_fail_taskless(&args, sizeof(args), E2BIG), 0);
+
+	extended_action.tail = 0;
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	child_pid = sys_pidfd_spawn_run(fd, &args, sizeof(args));
+	ASSERT_GT(child_pid, 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_action_failure_publishes_waitable_child)
+{
+	struct pidfd_spawn_action action = {
+		.type = PIDFD_SPAWN_ACTION_DUP2,
+		.fd = UINT_MAX,
+		.newfd = STDOUT_FILENO,
+	};
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_actions_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), -1);
+	ASSERT_EQ(errno, EBADF);
+	ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), 0);
+	ASSERT_GT(info.pid, 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 127), 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), -1);
+	ASSERT_EQ(errno, EBUSY);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_fchdir_rejects_regular_file)
+{
+	struct pidfd_spawn_action action = {
+		.type = PIDFD_SPAWN_ACTION_FCHDIR,
+	};
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_actions_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	int regular_fd;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	regular_fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(regular_fd, 0);
+	action.fd = regular_fd;
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), -1);
+	ASSERT_EQ(errno, ENOTDIR);
+	ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), 0);
+	ASSERT_GT(info.pid, 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 127), 0);
+	ASSERT_GE(fcntl(regular_fd, F_GETFD), 0);
+	ASSERT_EQ(close(fd), 0);
+	ASSERT_EQ(close(regular_fd), 0);
+}
+
+TEST(pidfd_spawn_dup2_captures_stdout)
+{
+	struct pidfd_spawn_action action = {
+		.type = PIDFD_SPAWN_ACTION_DUP2,
+		.newfd = STDOUT_FILENO,
+	};
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_actions_test",
+				"--pidfd-spawn-helper", "print", NULL };
+	char buf[13] = {};
+	int pipefd[2];
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	ASSERT_EQ(pipe2(pipefd, O_CLOEXEC), 0);
+	action.fd = pipefd[1];
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), 0);
+	ASSERT_EQ(close(pipefd[1]), 0);
+	ASSERT_EQ(read_exact_timeout(pipefd[0], buf, sizeof(buf) - 1),
+		  sizeof(buf) - 1);
+	ASSERT_STREQ(buf, "pidfd-spawn\n");
+	ASSERT_EQ(close(pipefd[0]), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_close_range_closes_only_child_fd)
+{
+	struct pidfd_spawn_action action = {
+		.type = PIDFD_SPAWN_ACTION_CLOSE_RANGE,
+	};
+	const char *path = self_exe_path();
+	char fdarg[32];
+	char * const argv[] = { "pidfd_spawn_actions_test",
+				"--pidfd-spawn-helper", "fd-closed", fdarg,
+				NULL };
+	int devnull;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	devnull = open("/dev/null", O_RDONLY);
+	ASSERT_GE(devnull, 0);
+	ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", devnull), 0);
+	action.fd = devnull;
+	action.newfd = devnull;
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_GE(fcntl(devnull, F_GETFD), 0);
+	ASSERT_EQ(close(fd), 0);
+	ASSERT_EQ(close(devnull), 0);
+}
+
+TEST(pidfd_spawn_dup2_same_fd_clears_child_cloexec)
+{
+	struct pidfd_spawn_action action = {
+		.type = PIDFD_SPAWN_ACTION_DUP2,
+	};
+	const char *path = self_exe_path();
+	char fdarg[32];
+	char * const argv[] = { "pidfd_spawn_actions_test",
+				"--pidfd-spawn-helper", "fd-open", fdarg,
+				NULL };
+	int devnull;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	devnull = open("/dev/null", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(devnull, 0);
+	ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", devnull), 0);
+	action.fd = devnull;
+	action.newfd = devnull;
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_NE(fcntl(devnull, F_GETFD) & FD_CLOEXEC, 0);
+	ASSERT_EQ(close(fd), 0);
+	ASSERT_EQ(close(devnull), 0);
+}
+
+TEST(pidfd_spawn_close_range_cloexec_is_child_local)
+{
+	struct pidfd_spawn_action action = {
+		.type = PIDFD_SPAWN_ACTION_CLOSE_RANGE,
+		.flags = CLOSE_RANGE_CLOEXEC,
+	};
+	const char *path = self_exe_path();
+	char fdarg[32];
+	char * const argv[] = { "pidfd_spawn_actions_test",
+				"--pidfd-spawn-helper", "fd-closed", fdarg,
+				NULL };
+	int devnull;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	devnull = open("/dev/null", O_RDONLY);
+	ASSERT_GE(devnull, 0);
+	ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", devnull), 0);
+	action.fd = devnull;
+	action.newfd = devnull;
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, &action, 1), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(fcntl(devnull, F_GETFD) & FD_CLOEXEC, 0);
+	ASSERT_EQ(close(fd), 0);
+	ASSERT_EQ(close(devnull), 0);
+}
+
+TEST(pidfd_spawn_fchdir_changes_only_child_cwd)
+{
+	struct pidfd_spawn_action actions[2] = {
+		{
+			.type = PIDFD_SPAWN_ACTION_FCHDIR,
+		},
+		{
+			.type = PIDFD_SPAWN_ACTION_DUP2,
+			.newfd = STDOUT_FILENO,
+		},
+	};
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_actions_test",
+				"--pidfd-spawn-helper", "cwd-file", NULL };
+	char parent_cwd[PATH_MAX];
+	char current_cwd[PATH_MAX];
+	char template[] = "/tmp/pidfd-spawn-cwd.XXXXXX";
+	char payload[PATH_MAX];
+	char buf[9] = {};
+	int pipefd[2];
+	int dirfd;
+	int filefd;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	ASSERT_NE(getcwd(parent_cwd, sizeof(parent_cwd)), NULL);
+	ASSERT_NE(mkdtemp(template), NULL);
+	ASSERT_LT(snprintf(payload, sizeof(payload), "%s/payload.txt", template),
+		  sizeof(payload));
+	filefd = open(payload, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
+	ASSERT_GE(filefd, 0);
+	ASSERT_EQ(write(filefd, "run-cwd\n", 8), 8);
+	ASSERT_EQ(close(filefd), 0);
+	dirfd = open(template, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dirfd, 0);
+	ASSERT_EQ(pipe2(pipefd, O_CLOEXEC), 0);
+	actions[0].fd = dirfd;
+	actions[1].fd = pipefd[1];
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, actions,
+				 ARRAY_SIZE(actions)), 0);
+	ASSERT_EQ(close(pipefd[1]), 0);
+	ASSERT_EQ(read_exact_timeout(pipefd[0], buf, sizeof(buf) - 1),
+		  sizeof(buf) - 1);
+	ASSERT_STREQ(buf, "run-cwd\n");
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_NE(getcwd(current_cwd, sizeof(current_cwd)), NULL);
+	ASSERT_STREQ(current_cwd, parent_cwd);
+	ASSERT_GE(fcntl(dirfd, F_GETFD), 0);
+	ASSERT_EQ(close(pipefd[0]), 0);
+	ASSERT_EQ(close(dirfd), 0);
+	ASSERT_EQ(close(fd), 0);
+	ASSERT_EQ(unlink(payload), 0);
+	ASSERT_EQ(rmdir(template), 0);
+}
+
+TEST(pidfd_spawn_file_actions_run_in_array_order)
+{
+	struct pidfd_spawn_action actions[2] = {
+		{
+			.type = PIDFD_SPAWN_ACTION_CLOSE_RANGE,
+		},
+		{
+			.type = PIDFD_SPAWN_ACTION_DUP2,
+		},
+	};
+	const char *path = self_exe_path();
+	char fdarg[32];
+	char * const open_argv[] = { "pidfd_spawn_actions_test",
+				     "--pidfd-spawn-helper", "fd-open", fdarg,
+				     NULL };
+	char * const closed_argv[] = { "pidfd_spawn_actions_test",
+				       "--pidfd-spawn-helper", "fd-closed",
+				       fdarg, NULL };
+	int target;
+	int devnull;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	devnull = open("/dev/null", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(devnull, 0);
+	target = fcntl(devnull, F_DUPFD_CLOEXEC, 100);
+	ASSERT_GE(target, 100);
+	ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", target), 0);
+	actions[0].fd = target;
+	actions[0].newfd = target;
+	actions[1].fd = devnull;
+	actions[1].newfd = target;
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, path, open_argv, actions,
+				 ARRAY_SIZE(actions)), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+
+	actions[0].type = PIDFD_SPAWN_ACTION_DUP2;
+	actions[0].fd = devnull;
+	actions[0].newfd = target;
+	actions[1].type = PIDFD_SPAWN_ACTION_CLOSE_RANGE;
+	actions[1].fd = target;
+	actions[1].newfd = target;
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, path, closed_argv, actions,
+				 ARRAY_SIZE(actions)), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_NE(fcntl(target, F_GETFD) & FD_CLOEXEC, 0);
+	ASSERT_EQ(close(fd), 0);
+	ASSERT_EQ(close(target), 0);
+	ASSERT_EQ(close(devnull), 0);
+}
+
+int main(int argc, char **argv)
+{
+	int ret = helper_main(argc, argv);
+
+	if (ret >= 0)
+		return ret;
+	return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c
new file mode 100644
index 0000000000000..759af7d6a68f8
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c
@@ -0,0 +1,521 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/audit.h>
+#include <linux/netlink.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+#include "../landlock/audit.h"
+
+struct pidfd_spawn_audit_event {
+	unsigned long long serial;
+	pid_t pid;
+	bool syscall;
+	bool pidfd_spawn;
+	bool execve;
+	bool cwd;
+	bool cwd_matches;
+	bool path;
+};
+
+struct pidfd_spawn_audit_observation {
+	bool parent_syscall;
+	bool child_syscall;
+	bool child_pidfd_spawn;
+	bool child_execve;
+	bool child_cwd;
+	bool child_cwd_matches;
+	bool child_path;
+};
+
+static int pidfd_spawn_audit_filter_syscall(int audit_fd, __u16 type)
+{
+	struct audit_message msg = {
+		.header = {
+			.nlmsg_len = NLMSG_SPACE(sizeof(msg.rule)),
+			.nlmsg_type = type,
+			.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
+		},
+		.rule = {
+			.flags = AUDIT_FILTER_EXIT,
+			.action = AUDIT_ALWAYS,
+		},
+	};
+
+	if (AUDIT_WORD(__NR_pidfd_spawn_run) >= AUDIT_BITMASK_SIZE)
+		return -E2BIG;
+	msg.rule.mask[AUDIT_WORD(__NR_pidfd_spawn_run)] =
+		AUDIT_BIT(__NR_pidfd_spawn_run);
+	return audit_request(audit_fd, &msg, NULL);
+}
+
+static int pidfd_spawn_audit_get_status(struct audit_status *status)
+{
+	const struct audit_message request = {
+		.header = {
+			.nlmsg_len = NLMSG_SPACE(0),
+			.nlmsg_type = AUDIT_GET,
+			.nlmsg_flags = NLM_F_REQUEST,
+		},
+	};
+	struct audit_message reply;
+	int audit_fd;
+	int ret;
+
+	audit_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
+	if (audit_fd < 0)
+		return -errno;
+	ret = audit_send(audit_fd, &request);
+	while (!ret) {
+		memset(&reply, 0, sizeof(reply));
+		ret = audit_recv(audit_fd, &reply);
+		if (ret)
+			continue;
+		if (reply.header.nlmsg_type == NLMSG_ERROR) {
+			ret = reply.err.error ? reply.err.error : -EIO;
+			break;
+		}
+		if (reply.header.nlmsg_type != AUDIT_GET)
+			continue;
+		*status = reply.status;
+		break;
+	}
+	close(audit_fd);
+	return ret;
+}
+
+static int pidfd_spawn_audit_init(int *audit_fd, bool *owner_set)
+{
+	int fd;
+	int ret;
+
+	fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
+	if (fd < 0)
+		return -errno;
+	*audit_fd = fd;
+	ret = audit_set_status(fd, AUDIT_STATUS_PID, getpid());
+	if (ret)
+		return ret;
+	*owner_set = true;
+	ret = audit_set_status(fd, AUDIT_STATUS_ENABLED, 1);
+	if (ret)
+		return ret;
+	ret = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast,
+			 sizeof(audit_tv_fast));
+	if (ret)
+		return -errno;
+	while (audit_recv(fd, NULL) == 0)
+		;
+	ret = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default,
+			 sizeof(audit_tv_default));
+	return ret ? -errno : 0;
+}
+
+static void pidfd_spawn_audit_record_cleanup(int *first_error, int error)
+{
+	if (!*first_error && error)
+		*first_error = error;
+}
+
+static int pidfd_spawn_audit_release(int audit_fd, __u32 enabled)
+{
+	const struct audit_message msg = {
+		.header = {
+			.nlmsg_len = NLMSG_SPACE(sizeof(msg.status)),
+			.nlmsg_type = AUDIT_SET,
+			.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
+		},
+		.status = {
+			.mask = AUDIT_STATUS_ENABLED | AUDIT_STATUS_PID,
+			.enabled = enabled,
+			.pid = 0,
+		},
+	};
+	int ret;
+
+	ret = audit_request(audit_fd, &msg, NULL);
+	return ret > 0 ? 0 : ret;
+}
+
+static unsigned long long pidfd_spawn_audit_serial(const char *record)
+{
+	const char *audit;
+	const char *separator;
+	char *end;
+	unsigned long long serial;
+
+	audit = strstr(record, "audit(");
+	if (!audit)
+		return 0;
+	separator = strchr(audit, ':');
+	if (!separator)
+		return 0;
+	errno = 0;
+	serial = strtoull(separator + 1, &end, 10);
+	if (errno || end == separator + 1 || *end != ')')
+		return 0;
+	return serial;
+}
+
+static int pidfd_spawn_audit_field(const char *record, const char *field,
+				   long *value)
+{
+	char pattern[32];
+	const char *start;
+	char *end;
+	int len;
+
+	len = snprintf(pattern, sizeof(pattern), " %s=", field);
+	if (len < 0 || len >= sizeof(pattern))
+		return -E2BIG;
+	start = strstr(record, pattern);
+	if (!start)
+		return -ENOENT;
+	errno = 0;
+	*value = strtol(start + len, &end, 10);
+	if (errno || end == start + len)
+		return -EINVAL;
+	return 0;
+}
+
+static bool pidfd_spawn_audit_cwd_matches(const char *record, const char *cwd)
+{
+	char field[PATH_MAX + sizeof("cwd=\"\"")];
+	int len;
+
+	len = snprintf(field, sizeof(field), "cwd=\"%s\"", cwd);
+	return len > 0 && len < (int)sizeof(field) && strstr(record, field);
+}
+
+static struct pidfd_spawn_audit_event *
+pidfd_spawn_audit_event(struct pidfd_spawn_audit_event *events,
+			unsigned int *nr_events, unsigned long long serial)
+{
+	unsigned int i;
+
+	for (i = 0; i < *nr_events; i++)
+		if (events[i].serial == serial)
+			return &events[i];
+	if (*nr_events == 16)
+		return NULL;
+	events[*nr_events].serial = serial;
+	return &events[(*nr_events)++];
+}
+
+static int
+pidfd_spawn_collect_audit(int audit_fd, pid_t parent, pid_t child,
+			  const char *expected_cwd,
+			  struct pidfd_spawn_audit_observation *obs)
+{
+	struct pidfd_spawn_audit_event events[16] = {};
+	unsigned int nr_events = 0;
+	struct audit_message msg;
+	unsigned int i;
+	int ret;
+
+	for (;;) {
+		struct pidfd_spawn_audit_event *event;
+		unsigned long long serial;
+
+		memset(&msg, 0, sizeof(msg));
+		ret = audit_recv(audit_fd, &msg);
+		if (ret == -EAGAIN)
+			break;
+		if (ret)
+			return ret;
+		serial = pidfd_spawn_audit_serial(msg.data);
+		if (!serial)
+			continue;
+		event = pidfd_spawn_audit_event(events, &nr_events, serial);
+		if (!event)
+			return -E2BIG;
+		switch (msg.header.nlmsg_type) {
+		case AUDIT_SYSCALL:
+		case AUDIT_PIDFD_SPAWN: {
+			long pid;
+			long syscall;
+
+			if (pidfd_spawn_audit_field(msg.data, "syscall", &syscall) ||
+			    syscall != __NR_pidfd_spawn_run ||
+			    pidfd_spawn_audit_field(msg.data, "pid", &pid))
+				break;
+			event->pid = pid;
+			if (msg.header.nlmsg_type == AUDIT_SYSCALL)
+				event->syscall = true;
+			else
+				event->pidfd_spawn = true;
+			break;
+		}
+		case AUDIT_EXECVE:
+			event->execve = true;
+			break;
+		case AUDIT_CWD:
+			event->cwd = true;
+			if (expected_cwd &&
+			    pidfd_spawn_audit_cwd_matches(msg.data, expected_cwd))
+				event->cwd_matches = true;
+			break;
+		case AUDIT_PATH:
+			event->path = true;
+			break;
+		}
+	}
+
+	for (i = 0; i < nr_events; i++) {
+		if (events[i].pid == parent)
+			obs->parent_syscall |= events[i].syscall;
+		if (events[i].pid != child)
+			continue;
+		obs->child_syscall |= events[i].syscall;
+		obs->child_pidfd_spawn |= events[i].pidfd_spawn;
+		obs->child_execve |= events[i].execve;
+		obs->child_cwd |= events[i].cwd;
+		obs->child_cwd_matches |= events[i].cwd_matches;
+		obs->child_path |= events[i].path;
+	}
+	return 0;
+}
+
+static int pidfd_spawn_audit_worker(int pipe_fd, const char *path, int cwd_fd)
+{
+	struct pidfd_spawn_action action = {
+		.type = PIDFD_SPAWN_ACTION_FCHDIR,
+		.fd = cwd_fd,
+	};
+	char * const argv[] = { "pidfd_spawn_audit_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	pid_t child;
+	int builder;
+
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		return 1;
+	child = spawn_run_path_pid(builder, path, argv,
+				   cwd_fd < 0 ? NULL : &action, cwd_fd < 0 ? 0 : 1);
+	if (child < 0) {
+		close(builder);
+		return 2;
+	}
+	if (write(pipe_fd, &child, sizeof(child)) != sizeof(child)) {
+		close(builder);
+		return 3;
+	}
+	if (wait_pidfd_exit(builder, 0)) {
+		close(builder);
+		return 4;
+	}
+	return close(builder) ? 5 : 0;
+}
+
+static int
+pidfd_spawn_observe_audit(struct pidfd_spawn_audit_observation *obs,
+			  bool fail_after_filter, const char *path, int cwd_fd,
+			  const char *expected_cwd)
+{
+	struct audit_records records;
+	struct audit_filter filter;
+	struct audit_status original = {};
+	bool audit_owner_set = false;
+	bool filter_added = false;
+	bool rule_added = false;
+	int pipe_fds[2] = { -1, -1 };
+	int audit_fd = -1;
+	int cleanup_error = 0;
+	int cleanup;
+	int status;
+	int ret;
+	pid_t child = 0;
+	pid_t source;
+	pid_t waited;
+	ssize_t len;
+
+	if (!path)
+		path = self_exe_path();
+	if (!path)
+		return -ENOENT;
+	ret = pidfd_spawn_audit_get_status(&original);
+	if (ret)
+		return ret;
+	if (original.pid)
+		return -EEXIST;
+	if (original.enabled == 2)
+		return -EPERM;
+	ret = pidfd_spawn_audit_init(&audit_fd, &audit_owner_set);
+	if (ret)
+		goto out;
+	ret = audit_init_filter_exe(&filter, NULL);
+	if (ret)
+		goto out;
+	ret = audit_filter_exe(audit_fd, &filter, AUDIT_ADD_RULE);
+	if (ret)
+		goto out;
+	filter_added = true;
+	if (fail_after_filter) {
+		ret = -ECANCELED;
+		goto out;
+	}
+	ret = pidfd_spawn_audit_filter_syscall(audit_fd, AUDIT_ADD_RULE);
+	if (ret)
+		goto out;
+	rule_added = true;
+	if (pipe2(pipe_fds, O_CLOEXEC)) {
+		ret = -errno;
+		goto out;
+	}
+	source = fork();
+	if (source < 0) {
+		ret = -errno;
+		goto out;
+	}
+	if (!source) {
+		close(pipe_fds[0]);
+		_exit(pidfd_spawn_audit_worker(pipe_fds[1], path, cwd_fd));
+	}
+	close(pipe_fds[1]);
+	pipe_fds[1] = -1;
+	do {
+		len = read(pipe_fds[0], &child, sizeof(child));
+	} while (len < 0 && errno == EINTR);
+	close(pipe_fds[0]);
+	pipe_fds[0] = -1;
+	waited = waitpid_timeout(source, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (waited != source || !WIFEXITED(status) || WEXITSTATUS(status)) {
+		ret = -ECHILD;
+		goto out;
+	}
+	if (len != sizeof(child)) {
+		ret = len < 0 ? -errno : -EIO;
+		goto out;
+	}
+	ret = pidfd_spawn_collect_audit(audit_fd, source, child, expected_cwd,
+					obs);
+	if (!ret)
+		ret = audit_count_records(audit_fd, &records);
+out:
+	if (pipe_fds[0] >= 0)
+		close(pipe_fds[0]);
+	if (pipe_fds[1] >= 0)
+		close(pipe_fds[1]);
+	if (audit_fd < 0)
+		return ret;
+	if (!audit_owner_set) {
+		close(audit_fd);
+		return ret;
+	}
+	if (rule_added) {
+		cleanup = pidfd_spawn_audit_filter_syscall(audit_fd,
+							   AUDIT_DEL_RULE);
+		pidfd_spawn_audit_record_cleanup(&cleanup_error, cleanup);
+	}
+	if (filter_added) {
+		cleanup = audit_filter_exe(audit_fd, &filter, AUDIT_DEL_RULE);
+		pidfd_spawn_audit_record_cleanup(&cleanup_error, cleanup);
+	}
+	cleanup = pidfd_spawn_audit_release(audit_fd, original.enabled);
+	pidfd_spawn_audit_record_cleanup(&cleanup_error, cleanup);
+	close(audit_fd);
+	return cleanup_error ? cleanup_error : ret;
+}
+
+TEST(pidfd_spawn_audit_records_child_exec)
+{
+	struct pidfd_spawn_audit_observation observation = {};
+	int ret;
+
+	ret = pidfd_spawn_observe_audit(&observation, false, NULL, -1, NULL);
+	if (ret == -EPERM || ret == -EACCES || ret == -EEXIST ||
+	    ret == -EAFNOSUPPORT || ret == -EPROTONOSUPPORT)
+		SKIP(return, "Audit control is unavailable: %s", strerror(-ret));
+	ASSERT_EQ(ret, 0);
+	ASSERT_TRUE(observation.parent_syscall);
+	ASSERT_FALSE(observation.child_syscall);
+	ASSERT_TRUE(observation.child_pidfd_spawn);
+	ASSERT_TRUE(observation.child_execve);
+	ASSERT_TRUE(observation.child_cwd);
+	ASSERT_TRUE(observation.child_path);
+}
+
+TEST(pidfd_spawn_audit_records_post_fchdir_cwd)
+{
+	struct pidfd_spawn_audit_observation observation = {};
+	const char *path = self_exe_path();
+	static const char relative_path[] = "pidfd-spawn-audit-helper";
+	char template[] = "/tmp/pidfd-spawn-audit.XXXXXX";
+	char link_path[PATH_MAX];
+	int dirfd;
+	int len;
+	int ret;
+
+	ASSERT_NE(path, NULL);
+	ASSERT_NE(mkdtemp(template), NULL);
+	len = snprintf(link_path, sizeof(link_path), "%s/%s", template,
+		       relative_path);
+	ASSERT_GT(len, 0);
+	ASSERT_LT(len, (int)sizeof(link_path));
+	ASSERT_EQ(symlink(path, link_path), 0);
+	dirfd = open(template, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dirfd, 0);
+
+	ret = pidfd_spawn_observe_audit(&observation, false, relative_path,
+					dirfd, template);
+	ASSERT_EQ(close(dirfd), 0);
+	ASSERT_EQ(unlink(link_path), 0);
+	ASSERT_EQ(rmdir(template), 0);
+	if (ret == -EPERM || ret == -EACCES || ret == -EEXIST ||
+	    ret == -EAFNOSUPPORT || ret == -EPROTONOSUPPORT)
+		SKIP(return, "Audit control is unavailable: %s", strerror(-ret));
+	ASSERT_EQ(ret, 0);
+	ASSERT_TRUE(observation.parent_syscall);
+	ASSERT_TRUE(observation.child_pidfd_spawn);
+	ASSERT_TRUE(observation.child_execve);
+	ASSERT_TRUE(observation.child_cwd);
+	ASSERT_TRUE(observation.child_cwd_matches);
+	ASSERT_TRUE(observation.child_path);
+}
+
+TEST(pidfd_spawn_audit_setup_failure_restores_status)
+{
+	struct pidfd_spawn_audit_observation observation = {};
+	struct audit_status original = {};
+	struct audit_status restored = {};
+	int ret;
+
+	ret = pidfd_spawn_audit_get_status(&original);
+	if (ret == -EPERM || ret == -EACCES ||
+	    ret == -EAFNOSUPPORT || ret == -EPROTONOSUPPORT)
+		SKIP(return, "Audit control is unavailable: %s", strerror(-ret));
+	ASSERT_EQ(ret, 0);
+	if (original.pid)
+		SKIP(return, "Audit is owned by pid %u", original.pid);
+	if (original.enabled == 2)
+		SKIP(return, "Audit configuration is immutable");
+	ret = pidfd_spawn_observe_audit(&observation, true, NULL, -1, NULL);
+	if (ret == -EPERM || ret == -EACCES || ret == -EEXIST ||
+	    ret == -EAFNOSUPPORT || ret == -EPROTONOSUPPORT)
+		SKIP(return, "Audit control is unavailable: %s", strerror(-ret));
+	ASSERT_EQ(ret, -ECANCELED);
+	ASSERT_EQ(pidfd_spawn_audit_get_status(&restored), 0);
+	ASSERT_EQ(restored.enabled, original.enabled);
+	ASSERT_EQ(restored.pid, original.pid);
+}
+
+int main(int argc, char **argv)
+{
+	int ret = helper_main(argc, argv);
+
+	if (ret >= 0)
+		return ret;
+	return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_common.c b/tools/testing/selftests/pidfd/pidfd_spawn_common.c
new file mode 100644
index 0000000000000..aab7211f5e2c7
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_common.c
@@ -0,0 +1,512 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "pidfd.h"
+#include "pidfd_spawn_common.h"
+
+char * const empty_envp[] = { NULL };
+
+int helper_main(int argc, char **argv)
+{
+	if (argc < 3 || strcmp(argv[1], "--pidfd-spawn-helper")) {
+		int fd = sys_pidfd_empty_open();
+
+		if (fd >= 0) {
+			close(fd);
+			return -1;
+		}
+		if (errno != EINVAL && errno != ENOSYS)
+			return -1;
+		ksft_print_msg("pidfd spawn is unavailable: %s\n",
+			       strerror(errno));
+		return KSFT_SKIP;
+	}
+
+	if (!strcmp(argv[2], "exit0"))
+		return 0;
+	if (!strcmp(argv[2], "empty-env"))
+		return environ[0] ? 1 : 0;
+	if (!strcmp(argv[2], "print")) {
+		if (write(STDOUT_FILENO, "pidfd-spawn\n", 12) != 12)
+			return 1;
+		return 0;
+	}
+	if (!strcmp(argv[2], "cwd-file")) {
+		char buf[32];
+		int fd;
+		ssize_t len;
+
+		fd = open("payload.txt", O_RDONLY | O_CLOEXEC);
+		if (fd < 0)
+			return 1;
+		len = read(fd, buf, sizeof(buf));
+		close(fd);
+		if (len < 0)
+			return 1;
+		if (write(STDOUT_FILENO, buf, len) != len)
+			return 1;
+		return 0;
+	}
+	if (!strcmp(argv[2], "fd-closed")) {
+		int fd;
+
+		if (argc != 4)
+			return 127;
+		fd = atoi(argv[3]);
+		if (fcntl(fd, F_GETFD) == -1 && errno == EBADF)
+			return 0;
+		return 1;
+	}
+	if (!strcmp(argv[2], "fd-open")) {
+		int fd;
+
+		if (argc != 4)
+			return 127;
+		fd = atoi(argv[3]);
+		return fcntl(fd, F_GETFD) >= 0 ? 0 : 1;
+	}
+	if (!strcmp(argv[2], "wait-fd")) {
+		char byte;
+		ssize_t len;
+		int fd;
+
+		if (argc != 4)
+			return 127;
+		fd = atoi(argv[3]);
+		do {
+			len = read(fd, &byte, sizeof(byte));
+		} while (len < 0 && errno == EINTR);
+		return len == sizeof(byte) ? 0 : 1;
+	}
+	if (!strcmp(argv[2], "ready-wait-fd")) {
+		char byte = 1;
+		ssize_t len;
+		int ready_fd;
+		int wait_fd;
+
+		if (argc != 5)
+			return 127;
+		ready_fd = atoi(argv[3]);
+		wait_fd = atoi(argv[4]);
+		if (write(ready_fd, &byte, sizeof(byte)) != sizeof(byte))
+			return 1;
+		do {
+			len = read(wait_fd, &byte, sizeof(byte));
+		} while (len < 0 && errno == EINTR);
+		return len == sizeof(byte) ? 0 : 1;
+	}
+	if (!strcmp(argv[2], "seccomp-actions")) {
+		char * const blocked_argv[] = { "blocked", NULL };
+		char cwd[PATH_MAX];
+		int closed_fd;
+		int cwd_fd;
+		int target;
+
+		if (argc != 7)
+			return 127;
+		target = atoi(argv[3]);
+		closed_fd = atoi(argv[4]);
+		cwd_fd = atoi(argv[5]);
+		if (fcntl(target, F_GETFD) < 0)
+			return 1;
+		if (fcntl(closed_fd, F_GETFD) != -1 || errno != EBADF)
+			return 2;
+		if (!getcwd(cwd, sizeof(cwd)) || strcmp(cwd, argv[6]))
+			return 3;
+#ifdef __NR_dup2
+		errno = 0;
+		if (dup2(target, target) != -1 || errno != EPERM)
+			return 4;
+#endif
+		errno = 0;
+		if (syscall(__NR_close_range, target, target, 0) != -1 ||
+		    errno != EPERM)
+			return 5;
+		errno = 0;
+		if (fchdir(cwd_fd) != -1 || errno != EPERM)
+			return 6;
+		errno = 0;
+		if (execve("/no/such/pidfd-spawn-seccomp", blocked_argv,
+			   empty_envp) != -1 || errno != EPERM)
+			return 7;
+		errno = 0;
+		if (syscall(__NR_execveat, AT_FDCWD,
+			    "/no/such/pidfd-spawn-seccomp", blocked_argv,
+			    empty_envp, 0) != -1 || errno != EPERM)
+			return 8;
+		return 0;
+	}
+	return 127;
+}
+
+const char *self_exe_path(void)
+{
+	static char path[PATH_MAX];
+	ssize_t len;
+
+	if (path[0])
+		return path;
+
+	len = readlink("/proc/self/exe", path, sizeof(path) - 1);
+	if (len < 0)
+		return NULL;
+	path[len] = '\0';
+	return path;
+}
+
+int enter_self_exe_directory(char **storage, const char **relative)
+{
+	const char *path = self_exe_path();
+	char *slash;
+	int cwd_fd;
+
+	if (!path) {
+		errno = ENOENT;
+		return -1;
+	}
+	*storage = strdup(path);
+	if (!*storage)
+		return -1;
+	slash = strrchr(*storage, '/');
+	if (!slash || !slash[1]) {
+		free(*storage);
+		*storage = NULL;
+		errno = EINVAL;
+		return -1;
+	}
+	*slash = '\0';
+	*relative = slash + 1;
+	cwd_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	if (cwd_fd < 0 || chdir(**storage ? *storage : "/")) {
+		int saved_errno = errno;
+
+		if (cwd_fd >= 0)
+			close(cwd_fd);
+		free(*storage);
+		*storage = NULL;
+		errno = saved_errno;
+		return -1;
+	}
+	return cwd_fd;
+}
+
+int leave_self_exe_directory(int cwd_fd, char *storage)
+{
+	int ret = fchdir(cwd_fd);
+	int saved_errno = errno;
+
+	if (close(cwd_fd) && !ret) {
+		ret = -1;
+		saved_errno = errno;
+	}
+	free(storage);
+	errno = saved_errno;
+	return ret;
+}
+
+int config_path(int fd, const char *path)
+{
+	return sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+				PIDFD_CONFIG_KEY_PATH, path, 0);
+}
+
+int spawn_run_path_pid(int fd, const char *path, char * const argv[],
+		       struct pidfd_spawn_action *actions,
+		       unsigned int nr_actions)
+{
+	struct pidfd_spawn_run_args args = {
+		.path = ptr_to_u64(path),
+		.argv = ptr_to_u64(argv),
+		.envp = ptr_to_u64(empty_envp),
+		.actions = ptr_to_u64(actions),
+		.nr_actions = nr_actions,
+		.action_size = nr_actions ? sizeof(*actions) : 0,
+	};
+
+	return sys_pidfd_spawn_run(fd, &args, sizeof(args));
+}
+
+int spawn_run_path(int fd, const char *path, char * const argv[],
+		   struct pidfd_spawn_action *actions,
+		   unsigned int nr_actions)
+{
+	return spawn_run_path_pid(fd, path, argv, actions, nr_actions) < 0 ?
+		-1 : 0;
+}
+
+int spawn_run_staged(int fd, char * const argv[],
+		     struct pidfd_spawn_action *actions,
+		     unsigned int nr_actions)
+{
+	return spawn_run_path(fd, NULL, argv, actions, nr_actions);
+}
+
+static int deadline_after_ms(struct timespec *deadline, unsigned int timeout_ms)
+{
+	if (clock_gettime(CLOCK_MONOTONIC, deadline))
+		return -1;
+	deadline->tv_sec += timeout_ms / 1000;
+	deadline->tv_nsec += (timeout_ms % 1000) * 1000000L;
+	if (deadline->tv_nsec >= 1000000000L) {
+		deadline->tv_sec++;
+		deadline->tv_nsec -= 1000000000L;
+	}
+	return 0;
+}
+
+static int deadline_remaining_ms(const struct timespec *deadline)
+{
+	struct timespec now;
+	int64_t remaining_ns;
+
+	if (clock_gettime(CLOCK_MONOTONIC, &now))
+		return -1;
+	remaining_ns = (deadline->tv_sec - now.tv_sec) * 1000000000LL +
+			       deadline->tv_nsec - now.tv_nsec;
+	if (remaining_ns <= 0)
+		return 0;
+	if (remaining_ns > (int64_t)INT_MAX * 1000000)
+		return INT_MAX;
+	return (remaining_ns + 999999) / 1000000;
+}
+
+static int poll_until(int fd, short events, const struct timespec *deadline)
+{
+	struct pollfd pfd = {
+		.fd = fd,
+		.events = events,
+	};
+	int timeout;
+	int ret;
+
+	for (;;) {
+		timeout = deadline_remaining_ms(deadline);
+		if (timeout <= 0) {
+			if (!timeout)
+				errno = ETIMEDOUT;
+			return -1;
+		}
+		ret = poll(&pfd, 1, timeout);
+		if (ret > 0) {
+			if (pfd.revents & POLLNVAL) {
+				errno = EBADF;
+				return -1;
+			}
+			if (pfd.revents & (events | POLLERR | POLLHUP))
+				return 0;
+			continue;
+		}
+		if (!ret) {
+			errno = ETIMEDOUT;
+			return -1;
+		}
+		if (errno != EINTR)
+			return -1;
+	}
+}
+
+int wait_pidfd_exit_info(int pidfd, struct pidfd_info *info)
+{
+	struct timespec deadline;
+
+	if (deadline_after_ms(&deadline, PIDFD_SPAWN_TIMEOUT_MS))
+		return -1;
+	for (;;) {
+		memset(info, 0, sizeof(*info));
+		info->mask = PIDFD_INFO_EXIT;
+		if (!ioctl(pidfd, PIDFD_GET_INFO, info)) {
+			if (info->mask & PIDFD_INFO_EXIT)
+				return 0;
+		} else if (errno != ESRCH && errno != EINTR) {
+			return -1;
+		}
+		if (poll_until(pidfd, POLLIN, &deadline))
+			return -1;
+	}
+}
+
+static int wait_pidfd_info(int pidfd, siginfo_t *info)
+{
+	struct timespec deadline;
+
+	if (deadline_after_ms(&deadline, PIDFD_SPAWN_TIMEOUT_MS))
+		return -1;
+	for (;;) {
+		memset(info, 0, sizeof(*info));
+		if (!sys_waitid(P_PIDFD, pidfd, info, WEXITED | WNOHANG)) {
+			if (info->si_pid)
+				return 0;
+		} else if (errno != EINTR) {
+			return -1;
+		}
+		if (poll_until(pidfd, POLLIN, &deadline))
+			return -1;
+	}
+}
+
+int wait_pidfd_exit(int pidfd, int status)
+{
+	siginfo_t info = {};
+
+	if (wait_pidfd_info(pidfd, &info))
+		return -1;
+	if (info.si_code != CLD_EXITED || info.si_status != status) {
+		errno = ECHILD;
+		return -1;
+	}
+	return 0;
+}
+
+int wait_pidfd_signal(int pidfd, int signal)
+{
+	siginfo_t info = {};
+
+	if (wait_pidfd_info(pidfd, &info))
+		return -1;
+	if (info.si_code != CLD_KILLED || info.si_status != signal) {
+		errno = ECHILD;
+		return -1;
+	}
+	return 0;
+}
+
+#define PIDFD_SPAWN_MAX_SENT_FDS 2
+
+int send_fds_status(int socket, int status, const int *fds, size_t nr_fds)
+{
+	union {
+		char buf[CMSG_SPACE(sizeof(int) * PIDFD_SPAWN_MAX_SENT_FDS)];
+		struct cmsghdr align;
+	} control = {};
+	struct iovec iov = {
+		.iov_base = &status,
+		.iov_len = sizeof(status),
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+	};
+	struct cmsghdr *cmsg;
+
+	if (nr_fds > PIDFD_SPAWN_MAX_SENT_FDS) {
+		errno = E2BIG;
+		return -1;
+	}
+	if (nr_fds) {
+		msg.msg_control = control.buf;
+		msg.msg_controllen = CMSG_SPACE(sizeof(*fds) * nr_fds);
+		cmsg = CMSG_FIRSTHDR(&msg);
+		cmsg->cmsg_level = SOL_SOCKET;
+		cmsg->cmsg_type = SCM_RIGHTS;
+		cmsg->cmsg_len = CMSG_LEN(sizeof(*fds) * nr_fds);
+		memcpy(CMSG_DATA(cmsg), fds, sizeof(*fds) * nr_fds);
+	}
+
+	return sendmsg(socket, &msg, 0) == sizeof(status) ? 0 : -1;
+}
+
+int recv_fds_status(int socket, int *status, int *fds, size_t nr_fds)
+{
+	union {
+		char buf[CMSG_SPACE(sizeof(int) * PIDFD_SPAWN_MAX_SENT_FDS)];
+		struct cmsghdr align;
+	} control = {};
+	struct iovec iov = {
+		.iov_base = status,
+		.iov_len = sizeof(*status),
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+		.msg_control = control.buf,
+		.msg_controllen = sizeof(control.buf),
+	};
+	struct cmsghdr *cmsg;
+
+	if (!nr_fds || nr_fds > PIDFD_SPAWN_MAX_SENT_FDS) {
+		errno = EINVAL;
+		return -1;
+	}
+	if (recvmsg(socket, &msg, MSG_CMSG_CLOEXEC) != sizeof(*status))
+		return -1;
+	if (*status)
+		return -1;
+
+	cmsg = CMSG_FIRSTHDR(&msg);
+	if (!cmsg || cmsg->cmsg_level != SOL_SOCKET ||
+	    cmsg->cmsg_type != SCM_RIGHTS ||
+	    cmsg->cmsg_len != CMSG_LEN(sizeof(*fds) * nr_fds) ||
+	    (msg.msg_flags & (MSG_CTRUNC | MSG_TRUNC))) {
+		errno = EBADMSG;
+		return -1;
+	}
+
+	memcpy(fds, CMSG_DATA(cmsg), sizeof(*fds) * nr_fds);
+	return 0;
+}
+
+pid_t waitpid_timeout(pid_t pid, int *status, unsigned int timeout_ms)
+{
+	struct timespec deadline;
+	pid_t ret;
+	int timeout;
+
+	if (deadline_after_ms(&deadline, timeout_ms))
+		return -1;
+	for (;;) {
+		ret = waitpid(pid, status, WNOHANG);
+		if (ret > 0)
+			return ret;
+		if (ret < 0 && errno != EINTR)
+			return -1;
+		timeout = deadline_remaining_ms(&deadline);
+		if (timeout <= 0)
+			break;
+		if (timeout > 10)
+			timeout = 10;
+		if (poll(NULL, 0, timeout) < 0 && errno != EINTR)
+			return -1;
+	}
+
+	errno = ETIMEDOUT;
+	return 0;
+}
+
+int pthread_join_timeout(pthread_t thread, void **retval,
+			 unsigned int timeout_ms)
+{
+	struct timespec deadline;
+	int timeout;
+	int ret;
+
+	if (deadline_after_ms(&deadline, timeout_ms))
+		return errno;
+	for (;;) {
+		ret = pthread_tryjoin_np(thread, retval);
+		if (ret != EBUSY)
+			return ret;
+		timeout = deadline_remaining_ms(&deadline);
+		if (timeout <= 0)
+			return timeout ? errno : ETIMEDOUT;
+		if (timeout > 10)
+			timeout = 10;
+		if (poll(NULL, 0, timeout) < 0 && errno != EINTR)
+			return errno;
+	}
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_common.h b/tools/testing/selftests/pidfd/pidfd_spawn_common.h
new file mode 100644
index 0000000000000..33f9b72655d67
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_common.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __PIDFD_SPAWN_COMMON_H
+#define __PIDFD_SPAWN_COMMON_H
+
+#include <linux/pidfd_spawn.h>
+#include <pthread.h>
+#include <stddef.h>
+
+#include "pidfd.h"
+
+#ifndef PIDFD_EMPTY
+#define PIDFD_EMPTY 0x08000000U
+#endif
+
+#define PIDFD_SPAWN_TIMEOUT_MS 5000
+
+extern char * const empty_envp[];
+
+int helper_main(int argc, char **argv);
+static inline int sys_pidfd_empty_open(void)
+{
+	return sys_pidfd_open(0, PIDFD_EMPTY);
+}
+
+static inline int sys_pidfd_config(int fd, unsigned int cmd,
+				   const char *key, const void *value, int aux)
+{
+	return syscall(__NR_pidfd_config, fd, cmd, key, value, aux);
+}
+
+static inline int sys_pidfd_spawn_run(int fd,
+				      struct pidfd_spawn_run_args *args,
+				      size_t size)
+{
+	return syscall(__NR_pidfd_spawn_run, fd, args, size);
+}
+
+const char *self_exe_path(void);
+int enter_self_exe_directory(char **storage, const char **relative);
+int leave_self_exe_directory(int cwd_fd, char *storage);
+int config_path(int fd, const char *path);
+int spawn_run_path_pid(int fd, const char *path, char * const argv[],
+		       struct pidfd_spawn_action *actions,
+		       unsigned int nr_actions);
+int spawn_run_path(int fd, const char *path, char * const argv[],
+		   struct pidfd_spawn_action *actions, unsigned int nr_actions);
+int spawn_run_staged(int fd, char * const argv[],
+		     struct pidfd_spawn_action *actions,
+		     unsigned int nr_actions);
+int wait_pidfd_exit_info(int pidfd, struct pidfd_info *info);
+int wait_pidfd_exit(int pidfd, int status);
+int wait_pidfd_signal(int pidfd, int signal);
+int send_fds_status(int socket, int status, const int *fds, size_t nr_fds);
+int recv_fds_status(int socket, int *status, int *fds, size_t nr_fds);
+pid_t waitpid_timeout(pid_t pid, int *status, unsigned int timeout_ms);
+int pthread_join_timeout(pthread_t thread, void **retval,
+			 unsigned int timeout_ms);
+
+#endif /* __PIDFD_SPAWN_COMMON_H */
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_compat.c b/tools/testing/selftests/pidfd/pidfd_spawn_compat.c
new file mode 100644
index 0000000000000..a4e31d271a5d5
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_compat.c
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <limits.h>
+#include <linux/fs.h>
+#include <linux/pidfd.h>
+#include <linux/pidfd_spawn.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../kselftest.h"
+
+#ifndef __NR_pidfd_config
+#define __NR_pidfd_config 472
+#endif
+#ifndef __NR_pidfd_spawn_run
+#define __NR_pidfd_spawn_run 473
+#endif
+
+static __u64 compat_ptr64(const void *ptr)
+{
+	return (__u64)(uintptr_t)ptr;
+}
+
+static __u64 invalid_full_width_ptr(const void *ptr)
+{
+	return compat_ptr64(ptr) | (1ULL << 32);
+}
+
+static int read_self_exe(char *path, size_t size)
+{
+	ssize_t len;
+
+	len = readlink("/proc/self/exe", path, size - 1);
+	if (len < 0 || (size_t)len >= size - 1)
+		return -1;
+	path[len] = '\0';
+	return 0;
+}
+
+static int run_child(bool staged)
+{
+	char * const argv[] = { "pidfd_spawn_compat", "--exit0", NULL };
+	char * const envp[] = { NULL };
+	char path[PATH_MAX] = {};
+	struct pidfd_spawn_action action = {
+		.type = PIDFD_SPAWN_ACTION_DUP2,
+		.fd = STDOUT_FILENO,
+		.newfd = STDOUT_FILENO,
+	};
+	struct pidfd_spawn_run_args args = {
+		.path = staged ? 0 : compat_ptr64(path),
+		.argv = compat_ptr64(argv),
+		.envp = compat_ptr64(envp),
+		.actions = compat_ptr64(&action),
+		.nr_actions = 1,
+		.action_size = sizeof(action),
+	};
+	siginfo_t info = {};
+	__u32 published_generation;
+	__u32 future_generation;
+	int fd;
+
+	if (read_self_exe(path, sizeof(path)))
+		return 1;
+	fd = syscall(__NR_pidfd_open, 0, PIDFD_EMPTY);
+	if (fd < 0)
+		return 2;
+	if (ioctl(fd, FS_IOC_GETVERSION, &future_generation))
+		return 3;
+	if (staged && syscall(__NR_pidfd_config, fd, PIDFD_CONFIG_SET_STRING,
+			      PIDFD_CONFIG_KEY_PATH, path, 0))
+		return 4;
+	if (syscall(__NR_pidfd_spawn_run, fd, &args, sizeof(args)) < 0)
+		return 5;
+	if (ioctl(fd, FS_IOC_GETVERSION, &published_generation))
+		return 6;
+	if (published_generation != future_generation)
+		return 7;
+	if (waitid(P_PIDFD, fd, &info, WEXITED))
+		return 8;
+	if (info.si_code != CLD_EXITED || info.si_status)
+		return 9;
+	return close(fd) ? 10 : 0;
+}
+
+static int run_empty_env(void)
+{
+	char * const argv[] = { "pidfd_spawn_compat", "--check-empty-env",
+				NULL };
+	char path[PATH_MAX];
+	struct pidfd_spawn_run_args args = {
+		.argv = compat_ptr64(argv),
+	};
+	siginfo_t info = {};
+	int fd;
+
+	if (read_self_exe(path, sizeof(path)))
+		return 1;
+	args.path = compat_ptr64(path);
+	fd = syscall(__NR_pidfd_open, 0, PIDFD_EMPTY);
+	if (fd < 0)
+		return 2;
+	if (syscall(__NR_pidfd_spawn_run, fd, &args, sizeof(args)) < 0)
+		return 3;
+	if (waitid(P_PIDFD, fd, &info, WEXITED))
+		return 4;
+	if (info.si_code != CLD_EXITED || info.si_status)
+		return 5;
+	return close(fd) ? 6 : 0;
+}
+
+enum bad_pointer {
+	BAD_PATH,
+	BAD_ARGV,
+	BAD_ENVP,
+	BAD_ACTIONS,
+};
+
+static int reject_bad_pointer(enum bad_pointer bad)
+{
+	char * const argv[] = { "true", NULL };
+	char * const envp[] = { NULL };
+	const char *path = "/bin/true";
+	struct pidfd_spawn_action action = {
+		.type = PIDFD_SPAWN_ACTION_DUP2,
+		.fd = STDOUT_FILENO,
+		.newfd = STDOUT_FILENO,
+	};
+	struct pidfd_spawn_run_args args = {
+		.path = compat_ptr64(path),
+		.argv = compat_ptr64(argv),
+		.envp = compat_ptr64(envp),
+		.actions = compat_ptr64(&action),
+		.nr_actions = 1,
+		.action_size = sizeof(action),
+	};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	int fd;
+
+	switch (bad) {
+	case BAD_PATH:
+		args.path = invalid_full_width_ptr(path);
+		break;
+	case BAD_ARGV:
+		args.argv = invalid_full_width_ptr(argv);
+		break;
+	case BAD_ENVP:
+		args.envp = invalid_full_width_ptr(envp);
+		break;
+	case BAD_ACTIONS:
+		args.actions = invalid_full_width_ptr(&action);
+		break;
+	}
+	fd = syscall(__NR_pidfd_open, 0, PIDFD_EMPTY);
+	if (fd < 0)
+		return 1;
+	errno = 0;
+	if (syscall(__NR_pidfd_spawn_run, fd, &args, sizeof(args)) != -1 ||
+	    errno != EFAULT)
+		return 2;
+	errno = 0;
+	if (ioctl(fd, PIDFD_GET_INFO, &info) != -1 || errno != ESRCH)
+		return 3;
+	errno = 0;
+	if (syscall(__NR_pidfd_spawn_run, fd, &args, sizeof(args)) != -1 ||
+	    errno != EBUSY)
+		return 4;
+	return close(fd) ? 5 : 0;
+}
+
+int main(int argc, char **argv)
+{
+	const char *stage = "direct path";
+	enum bad_pointer bad;
+	int fd;
+	int ret;
+
+	if (argc == 2 && !strcmp(argv[1], "--exit0"))
+		return 0;
+	if (argc == 2 && !strcmp(argv[1], "--check-empty-env"))
+		return environ[0] ? 1 : 0;
+	fd = syscall(__NR_pidfd_open, 0, PIDFD_EMPTY);
+	if (fd < 0 && (errno == EINVAL || errno == ENOSYS)) {
+		ksft_print_msg("pidfd spawn is unavailable: %s\n",
+			       strerror(errno));
+		return KSFT_SKIP;
+	}
+	if (fd < 0 || close(fd)) {
+		fprintf(stderr, "compat pidfd spawn feature probe failed: %s\n",
+			strerror(errno));
+		return 1;
+	}
+	ret = run_child(false);
+	if (!ret) {
+		stage = "staged path";
+		ret = run_child(true);
+	}
+	if (!ret) {
+		stage = "empty env";
+		ret = run_empty_env();
+	}
+	for (bad = BAD_PATH; bad <= BAD_ACTIONS && !ret; bad++) {
+		stage = "bad pointer";
+		ret = reject_bad_pointer(bad);
+	}
+	if (ret)
+		fprintf(stderr, "compat pidfd spawn %s failed at step %d: %s\n",
+			stage, ret, strerror(errno));
+	return ret ? KSFT_FAIL : KSFT_PASS;
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c
new file mode 100644
index 0000000000000..c03f8d2ee69c8
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/pidfd_spawn.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+TEST(pidfd_spawn_run_execs_path_without_config)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_exec_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_execs_proc_self_exe)
+{
+	char * const argv[] = { "pidfd_spawn_exec_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	int fd;
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, "/proc/self/exe", argv, NULL, 0), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_uses_current_source_cwd)
+{
+	char * const argv[] = { "pidfd_spawn_exec_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	const char *relative;
+	char *storage;
+	int cwd_fd;
+	int fd;
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	cwd_fd = enter_self_exe_directory(&storage, &relative);
+	ASSERT_GE(cwd_fd, 0);
+	ASSERT_EQ(spawn_run_path(fd, relative, argv, NULL, 0), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+	ASSERT_EQ(leave_self_exe_directory(cwd_fd, storage), 0);
+}
+
+TEST(pidfd_spawn_run_inherits_new_non_cloexec_fd)
+{
+	const char *path = self_exe_path();
+	char fdarg[32];
+	char * const argv[] = { "pidfd_spawn_exec_test",
+				"--pidfd-spawn-helper", "fd-open", fdarg,
+				NULL };
+	int inherited_fd;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	inherited_fd = open("/dev/null", O_RDONLY);
+	ASSERT_GE(inherited_fd, 0);
+	ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", inherited_fd), 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(inherited_fd), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_honors_new_cloexec_fd)
+{
+	const char *path = self_exe_path();
+	char fdarg[32];
+	char * const argv[] = { "pidfd_spawn_exec_test",
+				"--pidfd-spawn-helper", "fd-closed", fdarg,
+				NULL };
+	int inherited_fd;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	inherited_fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(inherited_fd, 0);
+	ASSERT_GT(snprintf(fdarg, sizeof(fdarg), "%d", inherited_fd), 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(inherited_fd), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_accepts_null_envp)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_exec_test",
+				"--pidfd-spawn-helper", "empty-env", NULL };
+	struct pidfd_spawn_run_args args = {
+		.path = ptr_to_u64(path),
+		.argv = ptr_to_u64(argv),
+	};
+	int child_pid;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	child_pid = sys_pidfd_spawn_run(fd, &args, sizeof(args));
+	ASSERT_GT(child_pid, 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_exec_failure_publishes_waitable_child)
+{
+	const char *path = self_exe_path();
+	char * const bad_argv[] = { "missing-pidfd-spawn-helper", NULL };
+	char * const good_argv[] = { "pidfd_spawn_exec_test",
+				     "--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	int ret;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	errno = 0;
+	ret = spawn_run_path(fd, "/no/such/pidfd-spawn-helper", bad_argv,
+			     NULL, 0);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, ENOENT);
+	ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), 0);
+	ASSERT_GT(info.pid, 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 127), 0);
+	ASSERT_EQ(spawn_run_path(fd, path, good_argv, NULL, 0), -1);
+	ASSERT_EQ(errno, EBUSY);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_pre_task_failure_is_terminal)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_exec_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_spawn_run_args args = {
+		.argv = ptr_to_u64(argv),
+		.envp = ptr_to_u64(empty_envp),
+	};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	struct epoll_event interest = {
+		.events = EPOLLIN,
+	};
+	struct epoll_event event = {};
+	struct pollfd pfd = {
+		.events = POLLIN,
+	};
+	int epoll_fd;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+	ASSERT_GE(epoll_fd, 0);
+	interest.data.fd = fd;
+	ASSERT_EQ(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &interest), 0);
+	ASSERT_EQ(sys_pidfd_spawn_run(fd, &args, sizeof(args)), -1);
+	ASSERT_EQ(errno, EINVAL);
+	pfd.fd = fd;
+	ASSERT_EQ(poll(&pfd, 1, 0), 1);
+	ASSERT_EQ(pfd.revents & (POLLERR | POLLHUP), POLLERR | POLLHUP);
+	ASSERT_EQ(epoll_wait(epoll_fd, &event, 1, 0), 1);
+	ASSERT_EQ(event.data.fd, fd);
+	ASSERT_EQ(event.events & (EPOLLERR | EPOLLHUP),
+		  EPOLLERR | EPOLLHUP);
+	ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), -1);
+	ASSERT_EQ(errno, ESRCH);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), -1);
+	ASSERT_EQ(errno, EBUSY);
+	ASSERT_EQ(close(epoll_fd), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_staged_path_conflict_precedes_path_uaccess)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_exec_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_spawn_run_args args = {
+		.path = ptr_to_u64(MAP_FAILED),
+		.argv = ptr_to_u64(argv),
+		.envp = ptr_to_u64(empty_envp),
+	};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(config_path(fd, path), 0);
+	ASSERT_EQ(sys_pidfd_spawn_run(fd, &args, sizeof(args)), -1);
+	ASSERT_EQ(errno, EINVAL);
+	ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), -1);
+	ASSERT_EQ(errno, ESRCH);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_exec_failure_survives_auto_reap)
+{
+	char * const argv[] = { "missing-pidfd-spawn-helper", NULL };
+	struct sigaction ignored = {
+		.sa_handler = SIG_IGN,
+	};
+	struct pidfd_info info;
+	struct sigaction old;
+	siginfo_t wait_info = {};
+	int saved_errno;
+	int ret;
+	int fd;
+
+	sigemptyset(&ignored.sa_mask);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(sigaction(SIGCHLD, &ignored, &old), 0);
+	ret = spawn_run_path(fd, "/no/such/pidfd-spawn-helper", argv, NULL, 0);
+	saved_errno = errno;
+	ASSERT_EQ(wait_pidfd_exit_info(fd, &info), 0);
+	ASSERT_EQ(sigaction(SIGCHLD, &old, NULL), 0);
+	errno = saved_errno;
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, ENOENT);
+	ASSERT_NE(info.mask & PIDFD_INFO_EXIT, 0);
+	ASSERT_EQ(info.exit_code, 127 << 8);
+	ASSERT_EQ(sys_waitid(P_PIDFD, fd, &wait_info, WEXITED | WNOHANG), -1);
+	ASSERT_EQ(errno, ECHILD);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_args_are_input_only)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_exec_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	long page_size = sysconf(_SC_PAGESIZE);
+	struct pidfd_spawn_run_args *args;
+	void *page;
+	int fd;
+
+	ASSERT_GT(page_size, 0);
+	ASSERT_NE(path, NULL);
+	page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+		    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(page, MAP_FAILED);
+	args = page;
+	*args = (struct pidfd_spawn_run_args) {
+		.path = ptr_to_u64(path),
+		.argv = ptr_to_u64(argv),
+		.envp = ptr_to_u64(empty_envp),
+	};
+	ASSERT_EQ(mprotect(page, page_size, PROT_READ), 0);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_GT(sys_pidfd_spawn_run(fd, args, sizeof(*args)), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+	ASSERT_EQ(munmap(page, page_size), 0);
+}
+
+int main(int argc, char **argv)
+{
+	int ret = helper_main(argc, argv);
+
+	if (ret >= 0)
+		return ret;
+	return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c
new file mode 100644
index 0000000000000..87c03007f793b
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/filter.h>
+#include <linux/landlock.h>
+#include <linux/pidfd_spawn.h>
+#include <linux/seccomp.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+static int
+pidfd_spawn_landlock_create_ruleset(const struct landlock_ruleset_attr *attr)
+{
+	return syscall(__NR_landlock_create_ruleset, attr, sizeof(*attr), 0);
+}
+
+static int pidfd_spawn_landlock_restrict_self(int ruleset_fd)
+{
+	return syscall(__NR_landlock_restrict_self, ruleset_fd, 0);
+}
+
+static int pidfd_spawn_landlock_worker(const char *path)
+{
+	char * const argv[] = { "pidfd_spawn_policy_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	const struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
+	};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	int ruleset_fd;
+	int builder;
+	int ret;
+
+	ruleset_fd = pidfd_spawn_landlock_create_ruleset(&ruleset_attr);
+	if (ruleset_fd < 0)
+		return errno == ENOMSG || errno == EOPNOTSUPP ? 77 : 1;
+	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) ||
+	    pidfd_spawn_landlock_restrict_self(ruleset_fd)) {
+		close(ruleset_fd);
+		return 2;
+	}
+	close(ruleset_fd);
+
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		return 3;
+	errno = 0;
+	ret = spawn_run_path(builder, path, argv, NULL, 0);
+	if (ret != -1 || errno != EACCES) {
+		if (!ret)
+			wait_pidfd_exit(builder, 0);
+		close(builder);
+		return 4;
+	}
+	if (ioctl(builder, PIDFD_GET_INFO, &info) || !info.pid) {
+		close(builder);
+		return 5;
+	}
+	if (wait_pidfd_exit(builder, 127)) {
+		close(builder);
+		return 6;
+	}
+	return close(builder) ? 7 : 0;
+}
+
+static int pidfd_spawn_install_seccomp(struct sock_filter *filter,
+				       size_t nr_filter)
+{
+	struct sock_fprog program = {
+		.len = nr_filter,
+		.filter = filter,
+	};
+
+	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
+		return -errno;
+	if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program))
+		return -errno;
+	return 0;
+}
+
+static int pidfd_spawn_seccomp_setup_worker(const char *path)
+{
+	struct sock_filter filter[] = {
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+			 offsetof(struct seccomp_data, nr)),
+#ifdef __NR_dup2
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_dup2, 0, 1),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
+#endif
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_close_range, 0, 1),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fchdir, 0, 1),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 0, 1),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execveat, 0, 1),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+	};
+	struct pidfd_spawn_action actions[] = {
+		{
+			.type = PIDFD_SPAWN_ACTION_DUP2,
+		},
+		{
+			.type = PIDFD_SPAWN_ACTION_CLOSE_RANGE,
+		},
+		{
+			.type = PIDFD_SPAWN_ACTION_FCHDIR,
+		},
+	};
+	char template[] = "/tmp/pidfd-spawn-seccomp.XXXXXX";
+	char target_arg[32];
+	char closed_arg[32];
+	char cwd_arg[32];
+	char * const argv[] = { "pidfd_spawn_policy_test",
+				"--pidfd-spawn-helper", "seccomp-actions",
+				target_arg, closed_arg, cwd_arg, template, NULL };
+	int target = 200;
+	int devnull = -1;
+	int builder = -1;
+	int cwd = -1;
+	int result = 0;
+	int ret;
+
+	if (!mkdtemp(template))
+		return 1;
+	cwd = open(template, O_PATH | O_DIRECTORY);
+	if (cwd < 0) {
+		result = 2;
+		goto out;
+	}
+	devnull = open("/dev/null", O_RDONLY | O_CLOEXEC);
+	if (devnull < 0) {
+		result = 3;
+		goto out;
+	}
+	builder = sys_pidfd_empty_open();
+	if (builder < 0) {
+		result = 4;
+		goto out;
+	}
+	actions[0].fd = devnull;
+	actions[0].newfd = target;
+	actions[1].fd = devnull;
+	actions[1].newfd = devnull;
+	actions[2].fd = cwd;
+	snprintf(target_arg, sizeof(target_arg), "%d", target);
+	snprintf(closed_arg, sizeof(closed_arg), "%d", devnull);
+	snprintf(cwd_arg, sizeof(cwd_arg), "%d", cwd);
+
+	ret = pidfd_spawn_install_seccomp(filter, ARRAY_SIZE(filter));
+	if (ret) {
+		result = ret == -EINVAL || ret == -ENOSYS ? 77 : 5;
+		goto out;
+	}
+	ret = spawn_run_path(builder, path, argv, actions,
+			     ARRAY_SIZE(actions));
+	if (ret) {
+		result = 6;
+		goto out;
+	}
+	if (wait_pidfd_exit(builder, 0))
+		result = 7;
+out:
+	if (builder >= 0 && close(builder) && !result)
+		result = 8;
+	if (devnull >= 0 && close(devnull) && !result)
+		result = 9;
+	if (cwd >= 0 && close(cwd) && !result)
+		result = 10;
+	if (rmdir(template) && !result)
+		result = 11;
+	return result;
+}
+
+static int pidfd_spawn_seccomp_run_worker(const char *path)
+{
+	struct sock_filter filter[] = {
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+			 offsetof(struct seccomp_data, nr)),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_pidfd_spawn_run,
+			 0, 1),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EACCES),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+	};
+	char * const argv[] = { "pidfd_spawn_policy_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	int builder;
+	int ret;
+
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		return 1;
+	ret = pidfd_spawn_install_seccomp(filter, ARRAY_SIZE(filter));
+	if (ret)
+		return ret == -EINVAL || ret == -ENOSYS ? 77 : 2;
+	errno = 0;
+	if (spawn_run_path(builder, path, argv, NULL, 0) != -1 ||
+	    errno != EACCES)
+		return 3;
+	errno = 0;
+	if (ioctl(builder, PIDFD_GET_INFO, &info) != -1 || errno != ESRCH)
+		return 4;
+	if (config_path(builder, path))
+		return 5;
+	return close(builder) ? 6 : 0;
+}
+
+TEST(pidfd_spawn_landlock_exec_denial_is_reported)
+{
+	const char *path = self_exe_path();
+	pid_t worker;
+	pid_t waited;
+	int status;
+
+	ASSERT_NE(path, NULL);
+	worker = fork();
+	ASSERT_GE(worker, 0);
+	if (!worker)
+		_exit(pidfd_spawn_landlock_worker(path));
+	waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	ASSERT_EQ(waited, worker);
+	ASSERT_TRUE(WIFEXITED(status));
+	if (WEXITSTATUS(status) == 77)
+		SKIP(return, "Landlock is unavailable");
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST(pidfd_spawn_seccomp_uses_spawn_syscall_boundary)
+{
+	const char *path = self_exe_path();
+	pid_t worker;
+	pid_t waited;
+	int status;
+
+	ASSERT_NE(path, NULL);
+	worker = fork();
+	ASSERT_GE(worker, 0);
+	if (!worker)
+		_exit(pidfd_spawn_seccomp_setup_worker(path));
+	waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	ASSERT_EQ(waited, worker);
+	ASSERT_TRUE(WIFEXITED(status));
+	if (WEXITSTATUS(status) == 77)
+		SKIP(return, "seccomp filters are unavailable");
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST(pidfd_spawn_seccomp_run_filter_keeps_builder_taskless)
+{
+	const char *path = self_exe_path();
+	pid_t worker;
+	pid_t waited;
+	int status;
+
+	ASSERT_NE(path, NULL);
+	worker = fork();
+	ASSERT_GE(worker, 0);
+	if (!worker)
+		_exit(pidfd_spawn_seccomp_run_worker(path));
+	waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	ASSERT_EQ(waited, worker);
+	ASSERT_TRUE(WIFEXITED(status));
+	if (WEXITSTATUS(status) == 77)
+		SKIP(return, "seccomp filters are unavailable");
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+int main(int argc, char **argv)
+{
+	int ret = helper_main(argc, argv);
+
+	if (ret >= 0)
+		return ret;
+	return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_race_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_race_test.c
new file mode 100644
index 0000000000000..19a82c77e5bc9
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_race_test.c
@@ -0,0 +1,923 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/pidfd_spawn.h>
+#include <linux/userfaultfd.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+struct pidfd_spawn_run_result {
+	int ret;
+	int error;
+	int wait_error;
+};
+
+struct pidfd_spawn_concurrent_run {
+	pthread_barrier_t *barrier;
+	char * const *argv;
+	const char *path;
+	int fd;
+	int ret;
+	int error;
+	int barrier_error;
+};
+
+static void *pidfd_spawn_concurrent_run(void *data)
+{
+	struct pidfd_spawn_concurrent_run *run = data;
+	int ret;
+
+	ret = pthread_barrier_wait(run->barrier);
+	if (ret && ret != PTHREAD_BARRIER_SERIAL_THREAD) {
+		run->barrier_error = ret;
+		return NULL;
+	}
+
+	errno = 0;
+	run->ret = spawn_run_path_pid(run->fd, run->path, run->argv,
+				      NULL, 0);
+	run->error = errno;
+	return NULL;
+}
+
+static int wait_readable(int fd, int timeout_ms)
+{
+	struct pollfd pfd = {
+		.fd = fd,
+		.events = POLLIN,
+	};
+	int ret;
+
+	do {
+		ret = poll(&pfd, 1, timeout_ms);
+	} while (ret < 0 && errno == EINTR);
+	if (ret <= 0) {
+		if (!ret)
+			errno = ETIMEDOUT;
+		return -1;
+	}
+	if (!(pfd.revents & (POLLIN | POLLHUP))) {
+		errno = EIO;
+		return -1;
+	}
+	return 0;
+}
+
+static int read_result(int socket, struct pidfd_spawn_run_result *result)
+{
+	ssize_t len;
+
+	if (wait_readable(socket, PIDFD_SPAWN_TIMEOUT_MS))
+		return -1;
+	do {
+		len = read(socket, result, sizeof(*result));
+	} while (len < 0 && errno == EINTR);
+	if (len != sizeof(*result)) {
+		errno = len < 0 ? errno : EIO;
+		return -1;
+	}
+	return 0;
+}
+
+static bool race_check(bool condition, int *failure_errno,
+		       int *failure_line, int line)
+{
+	if (condition)
+		return true;
+
+	*failure_errno = errno;
+	*failure_line = line;
+	return false;
+}
+
+static int open_missing_userfaultfd(void *fault_page, long page_size)
+{
+	struct uffdio_register registration = {
+		.range = {
+			.start = (uintptr_t)fault_page,
+			.len = page_size,
+		},
+		.mode = UFFDIO_REGISTER_MODE_MISSING,
+	};
+	struct uffdio_api api = {
+		.api = UFFD_API,
+	};
+	int saved_errno;
+	int uffd;
+
+	uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (uffd < 0)
+		return -1;
+	if (!ioctl(uffd, UFFDIO_API, &api) &&
+	    !ioctl(uffd, UFFDIO_REGISTER, &registration))
+		return uffd;
+
+	saved_errno = errno;
+	close(uffd);
+	errno = saved_errno;
+	return -1;
+}
+
+static void pidfd_spawn_blocked_exec_worker(int socket, void *fault_page,
+					    long page_size, pid_t parent,
+					    const char *path,
+					    int expected_signal)
+{
+	char * const argv[] = { fault_page, "--pidfd-spawn-helper", "exit0",
+				NULL };
+	struct pidfd_spawn_run_result result = {};
+	sigset_t unblocked;
+	int bootstrap[2];
+	int builder;
+	int uffd;
+
+	if (expected_signal) {
+		sigemptyset(&unblocked);
+		sigaddset(&unblocked, expected_signal);
+		if (signal(expected_signal, SIG_DFL) == SIG_ERR ||
+		    sigprocmask(SIG_UNBLOCK, &unblocked, NULL))
+			_exit(99);
+	}
+	if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+		_exit(100);
+	uffd = open_missing_userfaultfd(fault_page, page_size);
+	if (uffd < 0)
+		_exit(101);
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		_exit(102);
+	bootstrap[0] = builder;
+	bootstrap[1] = uffd;
+	if (send_fds_status(socket, 0, bootstrap, ARRAY_SIZE(bootstrap)))
+		_exit(103);
+	close(uffd);
+
+	errno = 0;
+	result.ret = spawn_run_path_pid(builder, path, argv, NULL, 0);
+	result.error = errno;
+	if (result.ret > 0 && expected_signal)
+		result.wait_error = wait_pidfd_signal(builder, expected_signal);
+	else if (result.ret > 0)
+		result.wait_error = wait_pidfd_exit(builder, 0);
+	else
+		result.wait_error = -1;
+	if (write(socket, &result, sizeof(result)) != sizeof(result))
+		_exit(104);
+	close(builder);
+	close(socket);
+	_exit(0);
+}
+
+static void pidfd_spawn_cancel_worker(int socket, void *fault_page,
+				      long page_size, pid_t parent,
+				      const char *path)
+{
+	char * const argv[] = { fault_page, "--pidfd-spawn-helper", "exit0",
+				NULL };
+	int bootstrap[2];
+	int builder;
+	int uffd;
+
+	if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+		_exit(110);
+	uffd = open_missing_userfaultfd(fault_page, page_size);
+	if (uffd < 0)
+		_exit(111);
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		_exit(112);
+	bootstrap[0] = builder;
+	bootstrap[1] = uffd;
+	if (send_fds_status(socket, 0, bootstrap, ARRAY_SIZE(bootstrap)))
+		_exit(113);
+	close(uffd);
+
+	spawn_run_path_pid(builder, path, argv, NULL, 0);
+	_exit(114);
+}
+
+static void pidfd_spawn_claim_worker(int socket, void *fault_page,
+				     long page_size, pid_t parent)
+{
+	struct pidfd_spawn_run_result result = {
+		.wait_error = -1,
+	};
+	int bootstrap[2];
+	int builder;
+	int uffd;
+
+	if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+		_exit(120);
+	uffd = open_missing_userfaultfd(fault_page, page_size);
+	if (uffd < 0)
+		_exit(121);
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		_exit(122);
+	bootstrap[0] = builder;
+	bootstrap[1] = uffd;
+	if (send_fds_status(socket, 0, bootstrap, ARRAY_SIZE(bootstrap)))
+		_exit(123);
+	close(uffd);
+
+	errno = 0;
+	result.ret = sys_pidfd_spawn_run(builder, fault_page,
+					 sizeof(struct pidfd_spawn_run_args));
+	result.error = errno;
+	if (write(socket, &result, sizeof(result)) != sizeof(result))
+		_exit(124);
+	close(builder);
+	close(socket);
+	_exit(0);
+}
+
+#define PIDFD_SPAWN_RUN_RACE_ITERATIONS 16
+
+TEST(pidfd_spawn_concurrent_runs_have_one_winner)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_race_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	unsigned int iteration;
+
+	ASSERT_NE(path, NULL);
+	for (iteration = 0; iteration < PIDFD_SPAWN_RUN_RACE_ITERATIONS;
+	     iteration++) {
+		struct pidfd_spawn_concurrent_run runs[2] = {};
+		pthread_barrier_t barrier;
+		pthread_t threads[2];
+		void *thread_ret;
+		unsigned int busy = 0;
+		unsigned int success = 0;
+		unsigned int i;
+		int ret;
+		int fd;
+
+		fd = sys_pidfd_empty_open();
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(pthread_barrier_init(&barrier, NULL, 3), 0);
+		for (i = 0; i < ARRAY_SIZE(runs); i++) {
+			runs[i].barrier = &barrier;
+			runs[i].argv = argv;
+			runs[i].path = path;
+			runs[i].fd = fd;
+			ret = pthread_create(&threads[i], NULL,
+					     pidfd_spawn_concurrent_run, &runs[i]);
+			ASSERT_EQ(ret, 0);
+		}
+		ret = pthread_barrier_wait(&barrier);
+		ASSERT_TRUE(!ret || ret == PTHREAD_BARRIER_SERIAL_THREAD);
+		for (i = 0; i < ARRAY_SIZE(runs); i++) {
+			ret = pthread_join_timeout(threads[i], &thread_ret,
+						   PIDFD_SPAWN_TIMEOUT_MS);
+			ASSERT_EQ(ret, 0);
+			ASSERT_EQ(thread_ret, NULL);
+			ASSERT_EQ(runs[i].barrier_error, 0);
+			if (runs[i].ret > 0) {
+				success++;
+				continue;
+			}
+			ASSERT_EQ(runs[i].ret, -1);
+			ASSERT_EQ(runs[i].error, EBUSY);
+			busy++;
+		}
+		ASSERT_EQ(success, 1);
+		ASSERT_EQ(busy, 1);
+		ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+		ASSERT_EQ(pthread_barrier_destroy(&barrier), 0);
+		ASSERT_EQ(close(fd), 0);
+	}
+}
+
+TEST(pidfd_spawn_fatal_signal_cancels_child)
+{
+	struct uffdio_zeropage zeropage = {};
+	struct uffdio_api api = {
+		.api = UFFD_API,
+	};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	struct uffd_msg message;
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_race_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	int sockets[2] = { -1, -1 };
+	int bootstrap[2] = { -1, -1 };
+	bool worker_reaped = false;
+	long page_size;
+	void *fault_page;
+	pid_t parent;
+	pid_t worker = -1;
+	pid_t waited;
+	int failure_errno = 0;
+	int failure_line = 0;
+	int builder = -1;
+	int status = -1;
+	int probe;
+	int ret;
+	int uffd = -1;
+
+#define RACE_CHECK(condition) \
+	race_check((condition), &failure_errno, &failure_line, __LINE__)
+
+	ASSERT_NE(path, NULL);
+	page_size = sysconf(_SC_PAGESIZE);
+	ASSERT_GT(page_size, 0);
+	probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+		SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+	ASSERT_GE(probe, 0);
+	ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+	ASSERT_EQ(close(probe), 0);
+
+	fault_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+			  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(fault_page, MAP_FAILED);
+	ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+			     sockets), 0);
+	parent = getpid();
+	worker = fork();
+	ASSERT_GE(worker, 0);
+	if (!worker) {
+		close(sockets[0]);
+		pidfd_spawn_cancel_worker(sockets[1], fault_page, page_size,
+					  parent, path);
+	}
+	close(sockets[1]);
+	sockets[1] = -1;
+	ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+	if (!RACE_CHECK(!ret))
+		goto cleanup;
+	ret = recv_fds_status(sockets[0], &status, bootstrap,
+			      ARRAY_SIZE(bootstrap));
+	if (!RACE_CHECK(!ret))
+		goto cleanup;
+	builder = bootstrap[0];
+	uffd = bootstrap[1];
+
+	if (!RACE_CHECK(!wait_readable(uffd, PIDFD_SPAWN_TIMEOUT_MS)))
+		goto cleanup;
+	if (!RACE_CHECK(read(uffd, &message, sizeof(message)) ==
+							 sizeof(message)))
+		goto cleanup;
+	if (!RACE_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+		goto cleanup;
+	if (!RACE_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+		goto cleanup;
+	if (!RACE_CHECK(info.pid > 0))
+		goto cleanup;
+	if (!RACE_CHECK(!kill(worker, SIGKILL)))
+		goto cleanup;
+	waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (!RACE_CHECK(waited == worker))
+		goto cleanup;
+	worker_reaped = true;
+	if (!RACE_CHECK(WIFSIGNALED(status)))
+		goto cleanup;
+	if (!RACE_CHECK(WTERMSIG(status) == SIGKILL))
+		goto cleanup;
+	if (!RACE_CHECK(!wait_pidfd_exit_info(builder, &info)))
+		goto cleanup;
+	if (!RACE_CHECK(WIFSIGNALED(info.exit_code)))
+		goto cleanup;
+	if (!RACE_CHECK(WTERMSIG(info.exit_code) == SIGKILL))
+		goto cleanup;
+	if (!RACE_CHECK(spawn_run_path_pid(builder, path, argv, NULL, 0) == -1))
+		goto cleanup;
+	if (!RACE_CHECK(errno == EBUSY))
+		goto cleanup;
+
+cleanup:
+	if (uffd >= 0) {
+		zeropage.range.start = (uintptr_t)fault_page;
+		zeropage.range.len = page_size;
+		ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage);
+	}
+	if (!worker_reaped && worker > 0) {
+		if (builder >= 0)
+			sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+		kill(worker, SIGKILL);
+		waited = waitpid_timeout(worker, &status,
+					 PIDFD_SPAWN_TIMEOUT_MS);
+		worker_reaped = waited == worker;
+	}
+	if (builder >= 0)
+		close(builder);
+	if (uffd >= 0)
+		close(uffd);
+	if (sockets[0] >= 0)
+		close(sockets[0]);
+	if (sockets[1] >= 0)
+		close(sockets[1]);
+	munmap(fault_page, page_size);
+	if (failure_line)
+		TH_LOG("cancellation test failed at line %d: %s", failure_line,
+		       strerror(failure_errno));
+	ASSERT_EQ(failure_line, 0);
+	ASSERT_TRUE(worker_reaped);
+
+#undef RACE_CHECK
+}
+
+TEST(pidfd_spawn_default_fatal_signal_wins_during_setup)
+{
+	struct uffdio_copy copy = {};
+	struct uffdio_zeropage zeropage = {};
+	struct uffdio_api api = {
+		.api = UFFD_API,
+	};
+	struct pidfd_spawn_run_result result = {};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	struct uffd_msg message;
+	const char *path = self_exe_path();
+	int sockets[2] = { -1, -1 };
+	int bootstrap[2] = { -1, -1 };
+	bool page_resolved = false;
+	bool worker_reaped = false;
+	long page_size;
+	void *fault_page;
+	void *source_page;
+	pid_t parent;
+	pid_t worker = -1;
+	pid_t waited;
+	int failure_errno = 0;
+	int failure_line = 0;
+	int builder = -1;
+	int child_pid = -1;
+	int status = -1;
+	int probe;
+	int ret;
+	int uffd = -1;
+
+#define RACE_CHECK(condition) \
+	race_check((condition), &failure_errno, &failure_line, __LINE__)
+
+	ASSERT_NE(path, NULL);
+	page_size = sysconf(_SC_PAGESIZE);
+	ASSERT_GT(page_size, 0);
+	probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+		SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+	ASSERT_GE(probe, 0);
+	ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+	ASSERT_EQ(close(probe), 0);
+
+	fault_page = mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE,
+			  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(fault_page, MAP_FAILED);
+	ASSERT_EQ(mprotect(fault_page + page_size, page_size, PROT_NONE), 0);
+	source_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+			   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(source_page, MAP_FAILED);
+	memset(source_page, 'x', page_size);
+	ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+			     sockets), 0);
+	parent = getpid();
+	worker = fork();
+	ASSERT_GE(worker, 0);
+	if (!worker) {
+		close(sockets[0]);
+		pidfd_spawn_blocked_exec_worker(sockets[1], fault_page,
+						page_size, parent, path,
+						SIGSEGV);
+	}
+	close(sockets[1]);
+	sockets[1] = -1;
+	ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+	if (!RACE_CHECK(!ret))
+		goto cleanup;
+	ret = recv_fds_status(sockets[0], &status, bootstrap,
+			      ARRAY_SIZE(bootstrap));
+	if (!RACE_CHECK(!ret))
+		goto cleanup;
+	builder = bootstrap[0];
+	uffd = bootstrap[1];
+
+	if (!RACE_CHECK(!wait_readable(uffd, PIDFD_SPAWN_TIMEOUT_MS)))
+		goto cleanup;
+	if (!RACE_CHECK(read(uffd, &message, sizeof(message)) ==
+							 sizeof(message)))
+		goto cleanup;
+	if (!RACE_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+		goto cleanup;
+	if (!RACE_CHECK((message.arg.pagefault.address & ~(page_size - 1)) ==
+			(uintptr_t)fault_page))
+		goto cleanup;
+	if (!RACE_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+		goto cleanup;
+	if (!RACE_CHECK(info.pid > 0))
+		goto cleanup;
+	child_pid = info.pid;
+	if (!RACE_CHECK(!sys_pidfd_send_signal(builder, SIGSEGV, NULL, 0)))
+		goto cleanup;
+	copy.src = (uintptr_t)source_page;
+	copy.dst = (uintptr_t)fault_page;
+	copy.len = page_size;
+	if (!RACE_CHECK(!ioctl(uffd, UFFDIO_COPY, &copy)))
+		goto cleanup;
+	page_resolved = true;
+	if (!RACE_CHECK(!read_result(sockets[0], &result)))
+		goto cleanup;
+	if (!RACE_CHECK(result.ret == child_pid))
+		goto cleanup;
+	if (!RACE_CHECK(!result.error))
+		goto cleanup;
+	if (!RACE_CHECK(!result.wait_error))
+		goto cleanup;
+	if (!RACE_CHECK(!wait_pidfd_exit_info(builder, &info)))
+		goto cleanup;
+	if (!RACE_CHECK(WIFSIGNALED(info.exit_code)))
+		goto cleanup;
+	if (!RACE_CHECK(WTERMSIG(info.exit_code) == SIGSEGV))
+		goto cleanup;
+	waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (!RACE_CHECK(waited == worker))
+		goto cleanup;
+	worker_reaped = true;
+	if (!RACE_CHECK(WIFEXITED(status)))
+		goto cleanup;
+	if (!RACE_CHECK(!WEXITSTATUS(status)))
+		goto cleanup;
+
+cleanup:
+	if (!page_resolved && uffd >= 0) {
+		zeropage.range.start = (uintptr_t)fault_page;
+		zeropage.range.len = page_size;
+		ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage);
+	}
+	if (!worker_reaped && worker > 0) {
+		if (builder >= 0)
+			sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+		kill(worker, SIGKILL);
+		waited = waitpid_timeout(worker, &status,
+					 PIDFD_SPAWN_TIMEOUT_MS);
+		worker_reaped = waited == worker;
+	}
+	if (builder >= 0)
+		close(builder);
+	if (uffd >= 0)
+		close(uffd);
+	if (sockets[0] >= 0)
+		close(sockets[0]);
+	if (sockets[1] >= 0)
+		close(sockets[1]);
+	munmap(source_page, page_size);
+	munmap(fault_page, page_size * 2);
+	if (failure_line)
+		TH_LOG("default-fatal test failed at line %d: %s",
+		       failure_line, strerror(failure_errno));
+	ASSERT_EQ(failure_line, 0);
+	ASSERT_TRUE(worker_reaped);
+
+#undef RACE_CHECK
+}
+
+TEST(pidfd_spawn_claim_precedes_run_uaccess)
+{
+	struct uffdio_zeropage zeropage = {};
+	struct uffdio_api api = {
+		.api = UFFD_API,
+	};
+	struct pidfd_spawn_run_result result = {};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	struct pollfd pidfd_poll = {
+		.events = POLLIN,
+	};
+	struct uffd_msg message;
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_race_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	int sockets[2] = { -1, -1 };
+	int bootstrap[2] = { -1, -1 };
+	bool worker_reaped = false;
+	bool page_resolved = false;
+	long page_size;
+	void *fault_page;
+	pid_t parent;
+	pid_t worker = -1;
+	pid_t waited;
+	int failure_errno = 0;
+	int failure_line = 0;
+	int builder = -1;
+	int status = -1;
+	int probe;
+	int ret;
+	int uffd = -1;
+
+#define RACE_CHECK(condition) \
+	race_check((condition), &failure_errno, &failure_line, __LINE__)
+
+	ASSERT_NE(path, NULL);
+	page_size = sysconf(_SC_PAGESIZE);
+	ASSERT_GT(page_size, 0);
+	probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+		SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+	ASSERT_GE(probe, 0);
+	ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+	ASSERT_EQ(close(probe), 0);
+
+	fault_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+			  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(fault_page, MAP_FAILED);
+	ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+			     sockets), 0);
+	parent = getpid();
+	worker = fork();
+	ASSERT_GE(worker, 0);
+	if (!worker) {
+		close(sockets[0]);
+		pidfd_spawn_claim_worker(sockets[1], fault_page, page_size,
+					 parent);
+	}
+	close(sockets[1]);
+	sockets[1] = -1;
+	ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+	if (!RACE_CHECK(!ret))
+		goto cleanup;
+	ret = recv_fds_status(sockets[0], &status, bootstrap,
+			      ARRAY_SIZE(bootstrap));
+	if (!RACE_CHECK(!ret))
+		goto cleanup;
+	builder = bootstrap[0];
+	uffd = bootstrap[1];
+	pidfd_poll.fd = builder;
+
+	if (!RACE_CHECK(!wait_readable(uffd, PIDFD_SPAWN_TIMEOUT_MS)))
+		goto cleanup;
+	if (!RACE_CHECK(read(uffd, &message, sizeof(message)) ==
+							 sizeof(message)))
+		goto cleanup;
+	if (!RACE_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+		goto cleanup;
+	if (!RACE_CHECK(config_path(builder, path) == -1))
+		goto cleanup;
+	if (!RACE_CHECK(errno == EBUSY))
+		goto cleanup;
+	if (!RACE_CHECK(spawn_run_path_pid(builder, path, argv, NULL, 0) == -1))
+		goto cleanup;
+	if (!RACE_CHECK(errno == EBUSY))
+		goto cleanup;
+	if (!RACE_CHECK(ioctl(builder, PIDFD_GET_INFO, &info) == -1))
+		goto cleanup;
+	if (!RACE_CHECK(errno == ESRCH))
+		goto cleanup;
+	if (!RACE_CHECK(sys_pidfd_send_signal(builder, 0, NULL, 0) == -1))
+		goto cleanup;
+	if (!RACE_CHECK(errno == ESRCH))
+		goto cleanup;
+	if (!RACE_CHECK(poll(&pidfd_poll, 1, 0) == 0))
+		goto cleanup;
+
+	zeropage.range.start = (uintptr_t)fault_page;
+	zeropage.range.len = page_size;
+	if (!RACE_CHECK(!ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage)))
+		goto cleanup;
+	page_resolved = true;
+	if (!RACE_CHECK(!read_result(sockets[0], &result)))
+		goto cleanup;
+	if (!RACE_CHECK(result.ret == -1))
+		goto cleanup;
+	if (!RACE_CHECK(result.error == EINVAL))
+		goto cleanup;
+	if (!RACE_CHECK(result.wait_error == -1))
+		goto cleanup;
+	if (!RACE_CHECK(spawn_run_path_pid(builder, path, argv, NULL, 0) == -1))
+		goto cleanup;
+	if (!RACE_CHECK(errno == EBUSY))
+		goto cleanup;
+	waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (!RACE_CHECK(waited == worker))
+		goto cleanup;
+	worker_reaped = true;
+	if (!RACE_CHECK(WIFEXITED(status)))
+		goto cleanup;
+	if (!RACE_CHECK(!WEXITSTATUS(status)))
+		goto cleanup;
+
+cleanup:
+	if (!page_resolved && uffd >= 0) {
+		zeropage.range.start = (uintptr_t)fault_page;
+		zeropage.range.len = page_size;
+		ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage);
+	}
+	if (!worker_reaped && worker > 0) {
+		kill(worker, SIGKILL);
+		waited = waitpid_timeout(worker, &status,
+					 PIDFD_SPAWN_TIMEOUT_MS);
+		worker_reaped = waited == worker;
+	}
+	if (builder >= 0)
+		close(builder);
+	if (uffd >= 0)
+		close(uffd);
+	if (sockets[0] >= 0)
+		close(sockets[0]);
+	if (sockets[1] >= 0)
+		close(sockets[1]);
+	munmap(fault_page, page_size);
+	if (failure_line)
+		TH_LOG("claim test failed at line %d: %s", failure_line,
+		       strerror(failure_errno));
+	ASSERT_EQ(failure_line, 0);
+	ASSERT_TRUE(worker_reaped);
+
+#undef RACE_CHECK
+}
+
+TEST(pidfd_spawn_publication_precedes_exec_completion)
+{
+	struct uffdio_zeropage zeropage = {};
+	struct uffdio_api api = {
+		.api = UFFD_API,
+	};
+	struct pidfd_spawn_run_result result = {};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID | PIDFD_INFO_COREDUMP,
+	};
+	struct pollfd result_poll = {
+		.events = POLLIN,
+	};
+	struct uffd_msg message;
+	const char *path = self_exe_path();
+	int sockets[2] = { -1, -1 };
+	int bootstrap[2] = { -1, -1 };
+	struct stat builder_stat;
+	struct stat opened_stat;
+	bool worker_reaped = false;
+	bool page_resolved = false;
+	long page_size;
+	void *fault_page;
+	pid_t parent;
+	pid_t worker = -1;
+	pid_t waited;
+	int failure_errno = 0;
+	int failure_line = 0;
+	int opened = -1;
+	int builder = -1;
+	int status = -1;
+	int probe;
+	int ret;
+	int uffd = -1;
+
+#define RACE_CHECK(condition) \
+	race_check((condition), &failure_errno, &failure_line, __LINE__)
+
+	ASSERT_NE(path, NULL);
+	page_size = sysconf(_SC_PAGESIZE);
+	ASSERT_GT(page_size, 0);
+	probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+		SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+	ASSERT_GE(probe, 0);
+	ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+	ASSERT_EQ(close(probe), 0);
+
+	fault_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+			  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(fault_page, MAP_FAILED);
+	ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+			     sockets), 0);
+	parent = getpid();
+	worker = fork();
+	ASSERT_GE(worker, 0);
+	if (!worker) {
+		close(sockets[0]);
+		pidfd_spawn_blocked_exec_worker(sockets[1], fault_page,
+						page_size, parent, path, 0);
+	}
+	close(sockets[1]);
+	sockets[1] = -1;
+	ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+	if (!RACE_CHECK(!ret))
+		goto cleanup;
+	ret = recv_fds_status(sockets[0], &status, bootstrap,
+			      ARRAY_SIZE(bootstrap));
+	if (!RACE_CHECK(!ret))
+		goto cleanup;
+	builder = bootstrap[0];
+	uffd = bootstrap[1];
+
+	if (!RACE_CHECK(!wait_readable(uffd, PIDFD_SPAWN_TIMEOUT_MS)))
+		goto cleanup;
+	if (!RACE_CHECK(read(uffd, &message, sizeof(message)) ==
+							 sizeof(message)))
+		goto cleanup;
+	if (!RACE_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+		goto cleanup;
+	if (!RACE_CHECK((message.arg.pagefault.address & ~(page_size - 1)) ==
+			(uintptr_t)fault_page))
+		goto cleanup;
+	if (!RACE_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+		goto cleanup;
+	if (!RACE_CHECK(info.pid > 0))
+		goto cleanup;
+	if (!RACE_CHECK(info.mask & PIDFD_INFO_COREDUMP))
+		goto cleanup;
+	if (!RACE_CHECK(info.coredump_mask == PIDFD_COREDUMP_SKIP))
+		goto cleanup;
+	if (!RACE_CHECK(!(info.mask & (PIDFD_INFO_COREDUMP_SIGNAL |
+				       PIDFD_INFO_COREDUMP_CODE))))
+		goto cleanup;
+	if (!RACE_CHECK(!info.coredump_signal && !info.coredump_code))
+		goto cleanup;
+	opened = sys_pidfd_open(info.pid, 0);
+	if (!RACE_CHECK(opened >= 0))
+		goto cleanup;
+	if (!RACE_CHECK(!fstat(builder, &builder_stat)))
+		goto cleanup;
+	if (!RACE_CHECK(!fstat(opened, &opened_stat)))
+		goto cleanup;
+	if (!RACE_CHECK(builder_stat.st_dev == opened_stat.st_dev))
+		goto cleanup;
+	if (!RACE_CHECK(builder_stat.st_ino == opened_stat.st_ino))
+		goto cleanup;
+	result_poll.fd = sockets[0];
+	if (!RACE_CHECK(poll(&result_poll, 1, 0) == 0))
+		goto cleanup;
+
+	zeropage.range.start = (uintptr_t)fault_page;
+	zeropage.range.len = page_size;
+	if (!RACE_CHECK(!ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage)))
+		goto cleanup;
+	page_resolved = true;
+	if (!RACE_CHECK(!read_result(sockets[0], &result)))
+		goto cleanup;
+	if (!RACE_CHECK(result.ret == info.pid))
+		goto cleanup;
+	if (!RACE_CHECK(!result.error))
+		goto cleanup;
+	if (!RACE_CHECK(!result.wait_error))
+		goto cleanup;
+	waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (!RACE_CHECK(waited == worker))
+		goto cleanup;
+	worker_reaped = true;
+	if (!RACE_CHECK(WIFEXITED(status)))
+		goto cleanup;
+	if (!RACE_CHECK(!WEXITSTATUS(status)))
+		goto cleanup;
+
+cleanup:
+	if (!page_resolved && uffd >= 0) {
+		zeropage.range.start = (uintptr_t)fault_page;
+		zeropage.range.len = page_size;
+		ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage);
+	}
+	if (!worker_reaped && worker > 0) {
+		if (builder >= 0)
+			sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+		kill(worker, SIGKILL);
+		waited = waitpid_timeout(worker, &status,
+					 PIDFD_SPAWN_TIMEOUT_MS);
+		worker_reaped = waited == worker;
+	}
+	if (opened >= 0)
+		close(opened);
+	if (builder >= 0)
+		close(builder);
+	if (uffd >= 0)
+		close(uffd);
+	if (sockets[0] >= 0)
+		close(sockets[0]);
+	if (sockets[1] >= 0)
+		close(sockets[1]);
+	munmap(fault_page, page_size);
+	if (failure_line)
+		TH_LOG("publication test failed at line %d: %s", failure_line,
+		       strerror(failure_errno));
+	ASSERT_EQ(failure_line, 0);
+	ASSERT_TRUE(worker_reaped);
+
+#undef RACE_CHECK
+}
+
+int main(int argc, char **argv)
+{
+	int ret = helper_main(argc, argv);
+
+	if (ret >= 0)
+		return ret;
+	return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_security_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_security_test.c
new file mode 100644
index 0000000000000..4da23384e8b79
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_security_test.c
@@ -0,0 +1,1242 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/pidfd_spawn.h>
+#include <linux/userfaultfd.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/fsuid.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/ptrace.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "pidfd_spawn_common.h"
+
+struct pidfd_spawn_run_result {
+	int ret;
+	int error;
+	int wait_error;
+};
+
+struct pidfd_spawn_cred_test {
+	const char *path;
+	int builder;
+	int setup_error;
+	int restore_error;
+	int config_ret;
+	int config_error;
+	int run_ret;
+	int run_error;
+};
+
+struct pidfd_spawn_pidns_test {
+	const char *path;
+	int builder;
+	int setup_error;
+	int config_ret;
+	int config_error;
+	int run_ret;
+	int run_error;
+};
+
+#define PIDFD_SPAWN_UNPRIVILEGED_ID 65534
+#define PIDFD_SPAWN_SLEEP_PATH "/bin/sleep"
+
+static void *pidfd_spawn_changed_cred_thread(void *data)
+{
+	struct pidfd_spawn_cred_test *test = data;
+	char * const argv[] = { "pidfd_spawn_security_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	uid_t original;
+	uid_t target;
+
+	original = setfsuid((uid_t)-1);
+	target = original ? 0 : 1;
+	setfsuid(target);
+	if (setfsuid((uid_t)-1) != target) {
+		test->setup_error = EPERM;
+		return NULL;
+	}
+
+	errno = 0;
+	test->config_ret = config_path(test->builder, test->path);
+	test->config_error = errno;
+	errno = 0;
+	test->run_ret = spawn_run_path_pid(test->builder, test->path, argv,
+					   NULL, 0);
+	test->run_error = errno;
+
+	setfsuid(original);
+	if (setfsuid((uid_t)-1) != original)
+		test->restore_error = EPERM;
+	return NULL;
+}
+
+static void *pidfd_spawn_changed_pidns_thread(void *data)
+{
+	struct pidfd_spawn_pidns_test *test = data;
+	char * const argv[] = { "pidfd_spawn_security_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+
+	if (unshare(CLONE_NEWPID)) {
+		test->setup_error = errno;
+		return NULL;
+	}
+
+	errno = 0;
+	test->config_ret = config_path(test->builder, test->path);
+	test->config_error = errno;
+	errno = 0;
+	test->run_ret = spawn_run_path_pid(test->builder, test->path, argv,
+					   NULL, 0);
+	test->run_error = errno;
+	return NULL;
+}
+
+static int wait_readable(int fd, int timeout_ms)
+{
+	struct pollfd pfd = {
+		.fd = fd,
+		.events = POLLIN,
+	};
+	int ret;
+
+	do {
+		ret = poll(&pfd, 1, timeout_ms);
+	} while (ret < 0 && errno == EINTR);
+	if (ret <= 0) {
+		if (!ret)
+			errno = ETIMEDOUT;
+		return -1;
+	}
+	if (!(pfd.revents & (POLLIN | POLLHUP))) {
+		errno = EIO;
+		return -1;
+	}
+	return 0;
+}
+
+static int read_result(int socket, struct pidfd_spawn_run_result *result)
+{
+	ssize_t len;
+
+	if (wait_readable(socket, PIDFD_SPAWN_TIMEOUT_MS))
+		return -1;
+	do {
+		len = read(socket, result, sizeof(*result));
+	} while (len < 0 && errno == EINTR);
+	if (len != sizeof(*result)) {
+		errno = len < 0 ? errno : EIO;
+		return -1;
+	}
+	return 0;
+}
+
+static bool security_check(bool condition, int *failure_errno,
+			   int *failure_line, int line)
+{
+	if (condition)
+		return true;
+
+	*failure_errno = errno;
+	*failure_line = line;
+	return false;
+}
+
+static int open_missing_userfaultfd(void *fault_page, long page_size)
+{
+	struct uffdio_register registration = {
+		.range = {
+			.start = (uintptr_t)fault_page,
+			.len = page_size,
+		},
+		.mode = UFFDIO_REGISTER_MODE_MISSING,
+	};
+	struct uffdio_api api = {
+		.api = UFFD_API,
+	};
+	int saved_errno;
+	int uffd;
+
+	uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (uffd < 0)
+		return -1;
+	if (!ioctl(uffd, UFFDIO_API, &api) &&
+	    !ioctl(uffd, UFFDIO_REGISTER, &registration))
+		return uffd;
+
+	saved_errno = errno;
+	close(uffd);
+	errno = saved_errno;
+	return -1;
+}
+
+static int pidfd_spawn_ptrace_preflight(const char *path)
+{
+	int ready_pipe[2] = { -1, -1 };
+	int release_pipe[2] = { -1, -1 };
+	bool attached = false;
+	char ready_arg[32];
+	char release_arg[32];
+	char byte = 1;
+	pid_t child = -1;
+	pid_t waited;
+	int saved_errno = 0;
+	int status = -1;
+	int ret = -1;
+
+	if (pipe(ready_pipe) || pipe(release_pipe))
+		goto out;
+	child = fork();
+	if (child < 0)
+		goto out;
+	if (!child) {
+		close(ready_pipe[0]);
+		close(release_pipe[1]);
+		if (snprintf(ready_arg, sizeof(ready_arg), "%d", ready_pipe[1]) < 0 ||
+		    snprintf(release_arg, sizeof(release_arg), "%d",
+			     release_pipe[0]) < 0)
+			_exit(120);
+		execl(path, path, "--pidfd-spawn-helper", "ready-wait-fd",
+		      ready_arg, release_arg, NULL);
+		_exit(121);
+	}
+	close(ready_pipe[1]);
+	ready_pipe[1] = -1;
+	close(release_pipe[0]);
+	release_pipe[0] = -1;
+	if (wait_readable(ready_pipe[0], PIDFD_SPAWN_TIMEOUT_MS))
+		goto out;
+	if (read(ready_pipe[0], &byte, sizeof(byte)) != sizeof(byte))
+		goto out;
+	if (ptrace(PTRACE_SEIZE, child, NULL, NULL))
+		goto out;
+	attached = true;
+	if (ptrace(PTRACE_INTERRUPT, child, NULL, NULL))
+		goto out;
+	waited = waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (waited != child || !WIFSTOPPED(status)) {
+		errno = EIO;
+		goto out;
+	}
+	if (ptrace(PTRACE_DETACH, child, NULL, NULL))
+		goto out;
+	attached = false;
+	if (write(release_pipe[1], &byte, sizeof(byte)) != sizeof(byte))
+		goto out;
+	waited = waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (waited != child || !WIFEXITED(status) || WEXITSTATUS(status)) {
+		errno = EIO;
+		goto out;
+	}
+	child = -1;
+	ret = 0;
+out:
+	saved_errno = errno;
+	if (attached && child > 0) {
+		ptrace(PTRACE_INTERRUPT, child, NULL, NULL);
+		waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+		ptrace(PTRACE_DETACH, child, NULL, NULL);
+	}
+	if (child > 0) {
+		kill(child, SIGKILL);
+		waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	}
+	if (ready_pipe[0] >= 0)
+		close(ready_pipe[0]);
+	if (ready_pipe[1] >= 0)
+		close(ready_pipe[1]);
+	if (release_pipe[0] >= 0)
+		close(release_pipe[0]);
+	if (release_pipe[1] >= 0)
+		close(release_pipe[1]);
+	errno = saved_errno;
+	return ret;
+}
+
+static void pidfd_spawn_ptrace_worker(int socket, void *fault_page,
+				      long page_size, pid_t parent,
+				      const char *path, int wait_fd,
+				      int release_fd)
+{
+	char fdarg[32];
+	char * const argv[] = { fault_page, "--pidfd-spawn-helper", "wait-fd",
+				fdarg, NULL };
+	struct pidfd_spawn_run_result result = {};
+	int bootstrap[2];
+	int builder;
+	int uffd;
+	int len;
+
+	if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+		_exit(100);
+	close(release_fd);
+	len = snprintf(fdarg, sizeof(fdarg), "%d", wait_fd);
+	if (len < 0 || len >= sizeof(fdarg))
+		_exit(101);
+	uffd = open_missing_userfaultfd(fault_page, page_size);
+	if (uffd < 0)
+		_exit(102);
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		_exit(103);
+	bootstrap[0] = builder;
+	bootstrap[1] = uffd;
+	if (send_fds_status(socket, 0, bootstrap, ARRAY_SIZE(bootstrap)))
+		_exit(104);
+	close(uffd);
+
+	errno = 0;
+	result.ret = spawn_run_path_pid(builder, path, argv, NULL, 0);
+	result.error = errno;
+	if (write(socket, &result, sizeof(result)) != sizeof(result))
+		_exit(105);
+	if (result.ret < 0)
+		_exit(106);
+	if (wait_pidfd_exit(builder, 0))
+		_exit(107);
+	close(wait_fd);
+	close(builder);
+	close(socket);
+	_exit(0);
+}
+
+static int pidfd_spawn_unprivileged_preflight(void)
+{
+	pid_t child;
+	pid_t waited;
+	int status;
+
+	child = fork();
+	if (child < 0)
+		return -1;
+	if (!child) {
+		if (setresgid(PIDFD_SPAWN_UNPRIVILEGED_ID,
+			      PIDFD_SPAWN_UNPRIVILEGED_ID,
+			      PIDFD_SPAWN_UNPRIVILEGED_ID) ||
+		    setresuid(PIDFD_SPAWN_UNPRIVILEGED_ID,
+			      PIDFD_SPAWN_UNPRIVILEGED_ID,
+			      PIDFD_SPAWN_UNPRIVILEGED_ID))
+			_exit(77);
+		if (access(PIDFD_SPAWN_SLEEP_PATH, X_OK))
+			_exit(78);
+		if (prctl(PR_SET_DUMPABLE, 1))
+			_exit(79);
+		_exit(0);
+	}
+
+	waited = waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (waited != child) {
+		kill(child, SIGKILL);
+		waitpid_timeout(child, &status, PIDFD_SPAWN_TIMEOUT_MS);
+		errno = ETIMEDOUT;
+		return -1;
+	}
+	if (!WIFEXITED(status)) {
+		errno = EIO;
+		return -1;
+	}
+	switch (WEXITSTATUS(status)) {
+	case 0:
+		return 0;
+	case 77:
+		errno = EPERM;
+		break;
+	case 78:
+		errno = EACCES;
+		break;
+	default:
+		errno = EINVAL;
+		break;
+	}
+	return -1;
+}
+
+static void pidfd_spawn_proc_worker(int socket, void *fault_page,
+				    long page_size, pid_t parent)
+{
+	char * const argv[] = { "sleep", fault_page, NULL };
+	struct pidfd_spawn_run_result result = {
+		.wait_error = -1,
+	};
+	char exec_path[64];
+	int bootstrap[2];
+	int builder;
+	int exec_fd;
+	int ret;
+	int uffd;
+
+	if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+		_exit(130);
+	uffd = open_missing_userfaultfd(fault_page, page_size);
+	if (uffd < 0)
+		_exit(131);
+	if (setresgid(PIDFD_SPAWN_UNPRIVILEGED_ID,
+		      PIDFD_SPAWN_UNPRIVILEGED_ID,
+		      PIDFD_SPAWN_UNPRIVILEGED_ID) ||
+	    setresuid(PIDFD_SPAWN_UNPRIVILEGED_ID,
+		      PIDFD_SPAWN_UNPRIVILEGED_ID,
+		      PIDFD_SPAWN_UNPRIVILEGED_ID) ||
+	    prctl(PR_SET_DUMPABLE, 1) ||
+	    prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+		_exit(132);
+	/*
+	 * Force the child to instantiate its numeric proc dentry before argv
+	 * copying reaches the userfaultfd stall. Keep the fd non-CLOEXEC so the
+	 * proc fd path remains usable throughout executable lookup.
+	 */
+	exec_fd = open(PIDFD_SPAWN_SLEEP_PATH, O_PATH);
+	if (exec_fd < 0)
+		_exit(138);
+	ret = snprintf(exec_path, sizeof(exec_path), "/proc/self/fd/%d", exec_fd);
+	if (ret < 0 || ret >= (int)sizeof(exec_path))
+		_exit(139);
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		_exit(133);
+	bootstrap[0] = builder;
+	bootstrap[1] = uffd;
+	if (send_fds_status(socket, 0, bootstrap, ARRAY_SIZE(bootstrap)))
+		_exit(134);
+	close(uffd);
+
+	errno = 0;
+	result.ret = spawn_run_path_pid(builder, exec_path, argv, NULL, 0);
+	result.error = errno;
+	close(exec_fd);
+	if (write(socket, &result, sizeof(result)) != sizeof(result))
+		_exit(135);
+	close(socket);
+	if (result.ret < 0)
+		_exit(136);
+	if (wait_pidfd_signal(builder, SIGKILL))
+		_exit(137);
+	close(builder);
+	_exit(0);
+}
+
+static void pidfd_spawn_traced_source_worker(int socket, pid_t parent,
+					     const char *path)
+{
+	char * const argv[] = { "pidfd_spawn_security_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_spawn_run_result result;
+	char command;
+	ssize_t len;
+	int builder;
+	int i;
+
+	if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+		_exit(120);
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		_exit(121);
+	if (send_fds_status(socket, 0, &builder, 1))
+		_exit(122);
+
+	for (i = 0; i < 2; i++) {
+		do {
+			len = read(socket, &command, sizeof(command));
+		} while (len < 0 && errno == EINTR);
+		if (len != sizeof(command))
+			_exit(123);
+
+		result = (struct pidfd_spawn_run_result) {};
+		errno = 0;
+		result.ret = spawn_run_path_pid(builder, path, argv, NULL, 0);
+		result.error = errno;
+		if (result.ret > 0)
+			result.wait_error = wait_pidfd_exit(builder, 0);
+		else
+			result.wait_error = -1;
+		if (write(socket, &result, sizeof(result)) != sizeof(result))
+			_exit(124);
+	}
+
+	close(builder);
+	close(socket);
+	_exit(0);
+}
+
+static void pidfd_spawn_authority_worker(int socket, pid_t parent,
+					 const char *path)
+{
+	char * const argv[] = { "pidfd_spawn_security_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_spawn_run_result result = {};
+	char command;
+	ssize_t len;
+	int builder;
+
+	if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
+		_exit(130);
+	builder = sys_pidfd_empty_open();
+	if (builder < 0)
+		_exit(131);
+	if (send_fds_status(socket, 0, &builder, 1))
+		_exit(132);
+	do {
+		len = read(socket, &command, sizeof(command));
+	} while (len < 0 && errno == EINTR);
+	if (len != sizeof(command))
+		_exit(133);
+
+	errno = 0;
+	result.ret = spawn_run_path_pid(builder, path, argv, NULL, 0);
+	result.error = errno;
+	if (result.ret > 0)
+		result.wait_error = wait_pidfd_exit(builder, 0);
+	else
+		result.wait_error = -1;
+	if (write(socket, &result, sizeof(result)) != sizeof(result))
+		_exit(134);
+	close(builder);
+	close(socket);
+	_exit(0);
+}
+
+TEST(pidfd_spawn_embryonic_task_denies_ptrace)
+{
+	struct uffdio_zeropage zeropage = {};
+	struct uffdio_api api = {
+		.api = UFFD_API,
+	};
+	struct pidfd_spawn_run_result result = {};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	struct uffd_msg message;
+	const char *path = self_exe_path();
+	int release_pipe[2] = { -1, -1 };
+	int sockets[2] = { -1, -1 };
+	int bootstrap[2] = { -1, -1 };
+	bool worker_reaped = false;
+	bool page_resolved = false;
+	bool child_released = false;
+	bool attached = false;
+	long page_size;
+	void *fault_page;
+	pid_t parent;
+	pid_t worker = -1;
+	pid_t waited;
+	int failure_errno = 0;
+	int failure_line = 0;
+	int builder = -1;
+	int child_pid = -1;
+	int child_wait_fd = -1;
+	int received_fd = -1;
+	int status = -1;
+	int probe;
+	int ret;
+	int uffd = -1;
+	char byte = 1;
+
+#define SECURITY_CHECK(condition) \
+	security_check((condition), &failure_errno, &failure_line, __LINE__)
+
+	ASSERT_NE(path, NULL);
+	ret = pidfd_spawn_ptrace_preflight(path);
+	if (ret && errno == EPERM)
+		SKIP(return, "ambient ptrace policy denies normal attach");
+	ASSERT_EQ(ret, 0);
+	page_size = sysconf(_SC_PAGESIZE);
+	ASSERT_GT(page_size, 0);
+	probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+		SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+	ASSERT_GE(probe, 0);
+	ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+	ASSERT_EQ(close(probe), 0);
+
+	fault_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+			  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(fault_page, MAP_FAILED);
+	ASSERT_EQ(pipe(release_pipe), 0);
+	ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+			     sockets), 0);
+	parent = getpid();
+	worker = fork();
+	ASSERT_GE(worker, 0);
+	if (!worker) {
+		close(sockets[0]);
+		pidfd_spawn_ptrace_worker(sockets[1], fault_page, page_size,
+					  parent, path, release_pipe[0],
+					  release_pipe[1]);
+	}
+	child_wait_fd = release_pipe[0];
+	close(sockets[1]);
+	sockets[1] = -1;
+	close(release_pipe[0]);
+	release_pipe[0] = -1;
+	ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+	if (!SECURITY_CHECK(!ret))
+		goto cleanup;
+	ret = recv_fds_status(sockets[0], &status, bootstrap,
+			      ARRAY_SIZE(bootstrap));
+	if (!SECURITY_CHECK(!ret))
+		goto cleanup;
+	builder = bootstrap[0];
+	uffd = bootstrap[1];
+
+	if (!SECURITY_CHECK(!wait_readable(uffd,
+					   PIDFD_SPAWN_TIMEOUT_MS)))
+		goto cleanup;
+	if (!SECURITY_CHECK(read(uffd, &message, sizeof(message)) ==
+							     sizeof(message)))
+		goto cleanup;
+	if (!SECURITY_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+		goto cleanup;
+	if (!SECURITY_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+		goto cleanup;
+	if (!SECURITY_CHECK(info.pid > 0))
+		goto cleanup;
+	child_pid = info.pid;
+
+	errno = 0;
+	ret = ptrace(PTRACE_SEIZE, child_pid, NULL, NULL);
+	if (!ret)
+		attached = true;
+	if (!SECURITY_CHECK(ret == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(errno == EPERM))
+		goto cleanup;
+	errno = 0;
+	received_fd = sys_pidfd_getfd(builder, child_wait_fd, 0);
+	if (!SECURITY_CHECK(received_fd == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(errno == EPERM))
+		goto cleanup;
+
+	zeropage.range.start = (uintptr_t)fault_page;
+	zeropage.range.len = page_size;
+	if (!SECURITY_CHECK(!ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage)))
+		goto cleanup;
+	page_resolved = true;
+	if (!SECURITY_CHECK(!read_result(sockets[0], &result)))
+		goto cleanup;
+	if (!SECURITY_CHECK(result.ret == child_pid))
+		goto cleanup;
+	if (!SECURITY_CHECK(!result.error))
+		goto cleanup;
+	received_fd = sys_pidfd_getfd(builder, child_wait_fd, 0);
+	if (!SECURITY_CHECK(received_fd >= 0))
+		goto cleanup;
+	if (!SECURITY_CHECK(!close(received_fd)))
+		goto cleanup;
+	received_fd = -1;
+
+	if (!SECURITY_CHECK(!ptrace(PTRACE_SEIZE, child_pid, NULL, NULL)))
+		goto cleanup;
+	attached = true;
+	if (!SECURITY_CHECK(!ptrace(PTRACE_INTERRUPT, child_pid, NULL, NULL)))
+		goto cleanup;
+	waited = waitpid_timeout(child_pid, &status,
+				 PIDFD_SPAWN_TIMEOUT_MS);
+	if (!SECURITY_CHECK(waited == child_pid))
+		goto cleanup;
+	if (!SECURITY_CHECK(WIFSTOPPED(status)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!ptrace(PTRACE_DETACH, child_pid, NULL, NULL)))
+		goto cleanup;
+	attached = false;
+	if (!SECURITY_CHECK(write(release_pipe[1], &byte, sizeof(byte)) ==
+							     sizeof(byte)))
+		goto cleanup;
+	child_released = true;
+	waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (!SECURITY_CHECK(waited == worker))
+		goto cleanup;
+	worker_reaped = true;
+	if (!SECURITY_CHECK(WIFEXITED(status)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!WEXITSTATUS(status)))
+		goto cleanup;
+
+cleanup:
+	if (attached && child_pid > 0) {
+		ptrace(PTRACE_INTERRUPT, child_pid, NULL, NULL);
+		waitpid_timeout(child_pid, &status, PIDFD_SPAWN_TIMEOUT_MS);
+		ptrace(PTRACE_DETACH, child_pid, NULL, NULL);
+	}
+	if (!page_resolved && uffd >= 0) {
+		zeropage.range.start = (uintptr_t)fault_page;
+		zeropage.range.len = page_size;
+		ioctl(uffd, UFFDIO_ZEROPAGE, &zeropage);
+	}
+	if (!child_released && release_pipe[1] >= 0)
+		write(release_pipe[1], &byte, sizeof(byte));
+	if (!worker_reaped && worker > 0) {
+		if (builder >= 0)
+			sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+		kill(worker, SIGKILL);
+		waited = waitpid_timeout(worker, &status,
+					 PIDFD_SPAWN_TIMEOUT_MS);
+		worker_reaped = waited == worker;
+	}
+	if (builder >= 0)
+		close(builder);
+	if (received_fd >= 0)
+		close(received_fd);
+	if (uffd >= 0)
+		close(uffd);
+	if (sockets[0] >= 0)
+		close(sockets[0]);
+	if (sockets[1] >= 0)
+		close(sockets[1]);
+	if (release_pipe[0] >= 0)
+		close(release_pipe[0]);
+	if (release_pipe[1] >= 0)
+		close(release_pipe[1]);
+	munmap(fault_page, page_size);
+	if (failure_line)
+		TH_LOG("ptrace test failed at line %d: %s", failure_line,
+		       strerror(failure_errno));
+	ASSERT_EQ(failure_line, 0);
+	ASSERT_TRUE(worker_reaped);
+
+#undef SECURITY_CHECK
+}
+
+TEST(pidfd_spawn_embryonic_task_is_hidden_from_proc)
+{
+	struct uffdio_copy uffd_copy = {};
+	struct uffdio_api api = {
+		.api = UFFD_API,
+	};
+	struct pidfd_spawn_run_result result = {};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	struct uffd_msg message;
+	struct stat proc_dir_stat;
+	struct stat proc_mem_stat;
+	int sockets[2] = { -1, -1 };
+	int bootstrap[2] = { -1, -1 };
+	bool child_killed = false;
+	bool worker_reaped = false;
+	bool page_resolved = false;
+	char proc_dir[64];
+	char proc_cmdline[64];
+	char proc_mem[64];
+	char cmdline[32];
+	char *fill_page;
+	long page_size;
+	void *fault_page;
+	void *mapping;
+	pid_t parent;
+	pid_t worker = -1;
+	pid_t waited;
+	int failure_errno = 0;
+	int failure_line = 0;
+	int builder = -1;
+	int cmdline_fd = -1;
+	int status = -1;
+	int probe;
+	int ret;
+	int uffd = -1;
+
+#define SECURITY_CHECK(condition) \
+	security_check((condition), &failure_errno, &failure_line, __LINE__)
+
+	if (geteuid())
+		SKIP(return, "test requires root to create a distinct task uid");
+	ret = pidfd_spawn_unprivileged_preflight();
+	if (ret && (errno == EPERM || errno == EACCES || errno == EINVAL))
+		SKIP(return, "unprivileged proc ownership setup is unavailable");
+	ASSERT_EQ(ret, 0);
+	page_size = sysconf(_SC_PAGESIZE);
+	ASSERT_GT(page_size, 0);
+	probe = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (probe < 0 && (errno == ENOSYS || errno == EPERM))
+		SKIP(return, "userfaultfd is unavailable: %s", strerror(errno));
+	ASSERT_GE(probe, 0);
+	ASSERT_EQ(ioctl(probe, UFFDIO_API, &api), 0);
+	ASSERT_EQ(close(probe), 0);
+
+	mapping = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE,
+		       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(mapping, MAP_FAILED);
+	fault_page = mapping;
+	fill_page = (char *)mapping + page_size;
+	memcpy(fill_page, "30", 3);
+	ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+			     sockets), 0);
+	parent = getpid();
+	worker = fork();
+	ASSERT_GE(worker, 0);
+	if (!worker) {
+		close(sockets[0]);
+		pidfd_spawn_proc_worker(sockets[1], fault_page, page_size,
+					parent);
+	}
+	close(sockets[1]);
+	sockets[1] = -1;
+	ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+	if (!SECURITY_CHECK(!ret))
+		goto cleanup;
+	ret = recv_fds_status(sockets[0], &status, bootstrap,
+			      ARRAY_SIZE(bootstrap));
+	if (!SECURITY_CHECK(!ret))
+		goto cleanup;
+	builder = bootstrap[0];
+	uffd = bootstrap[1];
+
+	if (!SECURITY_CHECK(!wait_readable(uffd,
+					   PIDFD_SPAWN_TIMEOUT_MS)))
+		goto cleanup;
+	if (!SECURITY_CHECK(read(uffd, &message, sizeof(message)) ==
+							     sizeof(message)))
+		goto cleanup;
+	if (!SECURITY_CHECK(message.event == UFFD_EVENT_PAGEFAULT))
+		goto cleanup;
+	if (!SECURITY_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+		goto cleanup;
+	if (!SECURITY_CHECK(info.pid > 0))
+		goto cleanup;
+	ret = snprintf(proc_dir, sizeof(proc_dir), "/proc/%u", info.pid);
+	if (!SECURITY_CHECK(ret > 0 && ret < sizeof(proc_dir)))
+		goto cleanup;
+	ret = snprintf(proc_cmdline, sizeof(proc_cmdline), "/proc/%u/cmdline",
+		       info.pid);
+	if (!SECURITY_CHECK(ret > 0 && ret < sizeof(proc_cmdline)))
+		goto cleanup;
+	ret = snprintf(proc_mem, sizeof(proc_mem), "/proc/%u/mem", info.pid);
+	if (!SECURITY_CHECK(ret > 0 && ret < sizeof(proc_mem)))
+		goto cleanup;
+	errno = 0;
+	if (!SECURITY_CHECK(stat(proc_dir, &proc_dir_stat) == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(errno == ENOENT))
+		goto cleanup;
+	errno = 0;
+	cmdline_fd = open(proc_cmdline, O_RDONLY | O_CLOEXEC);
+	if (!SECURITY_CHECK(cmdline_fd == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(errno == ENOENT))
+		goto cleanup;
+
+	uffd_copy.dst = (uintptr_t)fault_page;
+	uffd_copy.src = (uintptr_t)fill_page;
+	uffd_copy.len = page_size;
+	if (!SECURITY_CHECK(!ioctl(uffd, UFFDIO_COPY, &uffd_copy)))
+		goto cleanup;
+	page_resolved = true;
+	if (!SECURITY_CHECK(!read_result(sockets[0], &result)))
+		goto cleanup;
+	if (!SECURITY_CHECK(result.ret == info.pid))
+		goto cleanup;
+	if (!SECURITY_CHECK(!result.error))
+		goto cleanup;
+	if (!SECURITY_CHECK(result.wait_error == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(!stat(proc_dir, &proc_dir_stat)))
+		goto cleanup;
+	if (!SECURITY_CHECK(proc_dir_stat.st_uid ==
+				   PIDFD_SPAWN_UNPRIVILEGED_ID))
+		goto cleanup;
+	if (!SECURITY_CHECK(proc_dir_stat.st_gid ==
+				   PIDFD_SPAWN_UNPRIVILEGED_ID))
+		goto cleanup;
+	if (!SECURITY_CHECK(!stat(proc_mem, &proc_mem_stat)))
+		goto cleanup;
+	if (!SECURITY_CHECK(proc_mem_stat.st_uid ==
+				   PIDFD_SPAWN_UNPRIVILEGED_ID))
+		goto cleanup;
+	if (!SECURITY_CHECK(proc_mem_stat.st_gid ==
+				   PIDFD_SPAWN_UNPRIVILEGED_ID))
+		goto cleanup;
+	cmdline_fd = open(proc_cmdline, O_RDONLY | O_CLOEXEC);
+	if (!SECURITY_CHECK(cmdline_fd >= 0))
+		goto cleanup;
+	ret = read(cmdline_fd, cmdline, sizeof(cmdline));
+	if (!SECURITY_CHECK(ret >= (int)sizeof("sleep")))
+		goto cleanup;
+	if (!SECURITY_CHECK(!memcmp(cmdline, "sleep", sizeof("sleep"))))
+		goto cleanup;
+	if (!SECURITY_CHECK(!close(cmdline_fd)))
+		goto cleanup;
+	cmdline_fd = -1;
+	if (!SECURITY_CHECK(!sys_pidfd_send_signal(builder, SIGKILL, NULL, 0)))
+		goto cleanup;
+	child_killed = true;
+	waited = waitpid_timeout(worker, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (!SECURITY_CHECK(waited == worker))
+		goto cleanup;
+	worker_reaped = true;
+	if (!SECURITY_CHECK(WIFEXITED(status)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!WEXITSTATUS(status)))
+		goto cleanup;
+
+cleanup:
+	if (!page_resolved && uffd >= 0) {
+		uffd_copy.dst = (uintptr_t)fault_page;
+		uffd_copy.src = (uintptr_t)fill_page;
+		uffd_copy.len = page_size;
+		ioctl(uffd, UFFDIO_COPY, &uffd_copy);
+	}
+	if (!child_killed && builder >= 0)
+		sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+	if (!worker_reaped && worker > 0) {
+		kill(worker, SIGKILL);
+		waited = waitpid_timeout(worker, &status,
+					 PIDFD_SPAWN_TIMEOUT_MS);
+		worker_reaped = waited == worker;
+	}
+	if (builder >= 0)
+		close(builder);
+	if (cmdline_fd >= 0)
+		close(cmdline_fd);
+	if (uffd >= 0)
+		close(uffd);
+	if (sockets[0] >= 0)
+		close(sockets[0]);
+	if (sockets[1] >= 0)
+		close(sockets[1]);
+	munmap(mapping, 2 * page_size);
+	if (failure_line)
+		TH_LOG("proc visibility test failed at line %d: %s", failure_line,
+		       strerror(failure_errno));
+	ASSERT_EQ(failure_line, 0);
+	ASSERT_TRUE(worker_reaped);
+
+#undef SECURITY_CHECK
+}
+
+TEST(pidfd_spawn_traced_source_does_not_consume_builder)
+{
+	struct pidfd_spawn_run_result result = {};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	const char *path = self_exe_path();
+	int sockets[2] = { -1, -1 };
+	bool source_reaped = false;
+	bool attached = false;
+	pid_t source = -1;
+	pid_t waited;
+	int failure_errno = 0;
+	int failure_line = 0;
+	int builder = -1;
+	int status = -1;
+	char command = 1;
+	int ret;
+
+#define SECURITY_CHECK(condition) \
+	security_check((condition), &failure_errno, &failure_line, __LINE__)
+
+	ASSERT_NE(path, NULL);
+	ret = pidfd_spawn_ptrace_preflight(path);
+	if (ret && errno == EPERM)
+		SKIP(return, "ambient ptrace policy denies normal attach");
+	ASSERT_EQ(ret, 0);
+	ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+			     sockets), 0);
+	source = fork();
+	ASSERT_GE(source, 0);
+	if (!source) {
+		close(sockets[0]);
+		pidfd_spawn_traced_source_worker(sockets[1], getppid(), path);
+	}
+	close(sockets[1]);
+	sockets[1] = -1;
+	ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+	if (!SECURITY_CHECK(!ret))
+		goto cleanup;
+	ret = recv_fds_status(sockets[0], &status, &builder, 1);
+	if (!SECURITY_CHECK(!ret))
+		goto cleanup;
+
+	if (!SECURITY_CHECK(!ptrace(PTRACE_SEIZE, source, NULL, NULL)))
+		goto cleanup;
+	attached = true;
+	if (!SECURITY_CHECK(write(sockets[0], &command, sizeof(command)) ==
+							 sizeof(command)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!read_result(sockets[0], &result)))
+		goto cleanup;
+	if (!SECURITY_CHECK(result.ret == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(result.error == EPERM))
+		goto cleanup;
+	if (!SECURITY_CHECK(result.wait_error == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(ioctl(builder, PIDFD_GET_INFO, &info) == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(errno == ESRCH))
+		goto cleanup;
+
+	if (!SECURITY_CHECK(!ptrace(PTRACE_INTERRUPT, source, NULL, NULL)))
+		goto cleanup;
+	waited = waitpid_timeout(source, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (!SECURITY_CHECK(waited == source))
+		goto cleanup;
+	if (!SECURITY_CHECK(WIFSTOPPED(status)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!ptrace(PTRACE_DETACH, source, NULL, NULL)))
+		goto cleanup;
+	attached = false;
+	if (!SECURITY_CHECK(write(sockets[0], &command, sizeof(command)) ==
+							 sizeof(command)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!read_result(sockets[0], &result)))
+		goto cleanup;
+	if (!SECURITY_CHECK(result.ret > 0))
+		goto cleanup;
+	if (!SECURITY_CHECK(!result.error))
+		goto cleanup;
+	if (!SECURITY_CHECK(!result.wait_error))
+		goto cleanup;
+	info = (struct pidfd_info) {
+		.mask = PIDFD_INFO_EXIT,
+	};
+	if (!SECURITY_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+		goto cleanup;
+	if (!SECURITY_CHECK(info.mask & PIDFD_INFO_EXIT))
+		goto cleanup;
+	if (!SECURITY_CHECK(WIFEXITED(info.exit_code)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!WEXITSTATUS(info.exit_code)))
+		goto cleanup;
+	waited = waitpid_timeout(source, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (!SECURITY_CHECK(waited == source))
+		goto cleanup;
+	source_reaped = true;
+	if (!SECURITY_CHECK(WIFEXITED(status)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!WEXITSTATUS(status)))
+		goto cleanup;
+
+cleanup:
+	if (attached && source > 0) {
+		ptrace(PTRACE_INTERRUPT, source, NULL, NULL);
+		waitpid_timeout(source, &status, PIDFD_SPAWN_TIMEOUT_MS);
+		ptrace(PTRACE_DETACH, source, NULL, NULL);
+	}
+	if (!source_reaped && source > 0) {
+		if (builder >= 0)
+			sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+		kill(source, SIGKILL);
+		waited = waitpid_timeout(source, &status,
+					 PIDFD_SPAWN_TIMEOUT_MS);
+		source_reaped = waited == source;
+	}
+	if (builder >= 0)
+		close(builder);
+	if (sockets[0] >= 0)
+		close(sockets[0]);
+	if (sockets[1] >= 0)
+		close(sockets[1]);
+	if (failure_line)
+		TH_LOG("traced-source test failed at line %d: %s", failure_line,
+		       strerror(failure_errno));
+	ASSERT_EQ(failure_line, 0);
+	ASSERT_TRUE(source_reaped);
+
+#undef SECURITY_CHECK
+}
+
+TEST(pidfd_spawn_passed_builder_does_not_delegate_authority)
+{
+	char * const argv[] = { "pidfd_spawn_security_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_spawn_run_result result = {};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	const char *path = self_exe_path();
+	int sockets[2] = { -1, -1 };
+	bool source_reaped = false;
+	pid_t parent = getpid();
+	pid_t source = -1;
+	pid_t waited;
+	int failure_errno = 0;
+	int failure_line = 0;
+	int builder = -1;
+	int status = -1;
+	char command = 1;
+	int ret;
+
+#define SECURITY_CHECK(condition) \
+	security_check((condition), &failure_errno, &failure_line, __LINE__)
+
+	ASSERT_NE(path, NULL);
+	ASSERT_EQ(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
+			     sockets), 0);
+	source = fork();
+	ASSERT_GE(source, 0);
+	if (!source) {
+		close(sockets[0]);
+		pidfd_spawn_authority_worker(sockets[1], parent, path);
+	}
+	close(sockets[1]);
+	sockets[1] = -1;
+	ret = wait_readable(sockets[0], PIDFD_SPAWN_TIMEOUT_MS);
+	if (!SECURITY_CHECK(!ret))
+		goto cleanup;
+	ret = recv_fds_status(sockets[0], &status, &builder, 1);
+	if (!SECURITY_CHECK(!ret))
+		goto cleanup;
+
+	if (!SECURITY_CHECK(config_path(builder, path) == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(errno == EPERM))
+		goto cleanup;
+	if (!SECURITY_CHECK(spawn_run_path_pid(builder, path, argv,
+					       NULL, 0) == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(errno == EPERM))
+		goto cleanup;
+	if (!SECURITY_CHECK(ioctl(builder, PIDFD_GET_INFO, &info) == -1))
+		goto cleanup;
+	if (!SECURITY_CHECK(errno == ESRCH))
+		goto cleanup;
+
+	if (!SECURITY_CHECK(write(sockets[0], &command, sizeof(command)) ==
+							 sizeof(command)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!read_result(sockets[0], &result)))
+		goto cleanup;
+	if (!SECURITY_CHECK(result.ret > 0))
+		goto cleanup;
+	if (!SECURITY_CHECK(!result.error))
+		goto cleanup;
+	if (!SECURITY_CHECK(!result.wait_error))
+		goto cleanup;
+	info = (struct pidfd_info) {
+		.mask = PIDFD_INFO_EXIT,
+	};
+	if (!SECURITY_CHECK(!ioctl(builder, PIDFD_GET_INFO, &info)))
+		goto cleanup;
+	if (!SECURITY_CHECK(info.mask & PIDFD_INFO_EXIT))
+		goto cleanup;
+	if (!SECURITY_CHECK(WIFEXITED(info.exit_code)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!WEXITSTATUS(info.exit_code)))
+		goto cleanup;
+	waited = waitpid_timeout(source, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (!SECURITY_CHECK(waited == source))
+		goto cleanup;
+	source_reaped = true;
+	if (!SECURITY_CHECK(WIFEXITED(status)))
+		goto cleanup;
+	if (!SECURITY_CHECK(!WEXITSTATUS(status)))
+		goto cleanup;
+
+cleanup:
+	if (!source_reaped && source > 0) {
+		if (builder >= 0)
+			sys_pidfd_send_signal(builder, SIGKILL, NULL, 0);
+		kill(source, SIGKILL);
+		waited = waitpid_timeout(source, &status,
+					 PIDFD_SPAWN_TIMEOUT_MS);
+		source_reaped = waited == source;
+	}
+	if (builder >= 0)
+		close(builder);
+	if (sockets[0] >= 0)
+		close(sockets[0]);
+	if (sockets[1] >= 0)
+		close(sockets[1]);
+	if (failure_line)
+		TH_LOG("authority test failed at line %d: %s", failure_line,
+		       strerror(failure_errno));
+	ASSERT_EQ(failure_line, 0);
+	ASSERT_TRUE(source_reaped);
+
+#undef SECURITY_CHECK
+}
+
+TEST(pidfd_spawn_changed_cred_does_not_have_authority)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_security_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_spawn_cred_test test = {
+		.path = path,
+	};
+	pthread_t thread;
+	void *thread_ret;
+	int ret;
+
+	ASSERT_NE(path, NULL);
+	test.builder = sys_pidfd_empty_open();
+	ASSERT_GE(test.builder, 0);
+	ret = pthread_create(&thread, NULL, pidfd_spawn_changed_cred_thread,
+			     &test);
+	ASSERT_EQ(ret, 0);
+	ASSERT_EQ(pthread_join_timeout(thread, &thread_ret,
+				       PIDFD_SPAWN_TIMEOUT_MS), 0);
+	ASSERT_EQ(thread_ret, NULL);
+	if (test.setup_error) {
+		ASSERT_EQ(close(test.builder), 0);
+		SKIP(return, "changing fsuid is unavailable");
+	}
+	ASSERT_EQ(test.restore_error, 0);
+	ASSERT_EQ(test.config_ret, -1);
+	ASSERT_EQ(test.config_error, EPERM);
+	ASSERT_EQ(test.run_ret, -1);
+	ASSERT_EQ(test.run_error, EPERM);
+	ASSERT_GT(spawn_run_path_pid(test.builder, path, argv, NULL, 0), 0);
+	ASSERT_EQ(wait_pidfd_exit(test.builder, 0), 0);
+	ASSERT_EQ(close(test.builder), 0);
+}
+
+TEST(pidfd_spawn_changed_pidns_does_not_have_authority)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_security_test",
+				"--pidfd-spawn-helper", "exit0", NULL };
+	struct pidfd_spawn_pidns_test test = {
+		.path = path,
+	};
+	pthread_t thread;
+	void *thread_ret;
+	int ret;
+
+	ASSERT_NE(path, NULL);
+	test.builder = sys_pidfd_empty_open();
+	ASSERT_GE(test.builder, 0);
+	ret = pthread_create(&thread, NULL, pidfd_spawn_changed_pidns_thread,
+			     &test);
+	ASSERT_EQ(ret, 0);
+	ASSERT_EQ(pthread_join_timeout(thread, &thread_ret,
+				       PIDFD_SPAWN_TIMEOUT_MS), 0);
+	ASSERT_EQ(thread_ret, NULL);
+	if (test.setup_error) {
+		ASSERT_EQ(close(test.builder), 0);
+		if (test.setup_error == EPERM || test.setup_error == EINVAL)
+			SKIP(return, "PID namespace creation is unavailable");
+		ASSERT_EQ(test.setup_error, 0);
+	}
+	ASSERT_EQ(test.config_ret, -1);
+	ASSERT_EQ(test.config_error, EPERM);
+	ASSERT_EQ(test.run_ret, -1);
+	ASSERT_EQ(test.run_error, EPERM);
+	ASSERT_GT(spawn_run_path_pid(test.builder, path, argv, NULL, 0), 0);
+	ASSERT_EQ(wait_pidfd_exit(test.builder, 0), 0);
+	ASSERT_EQ(close(test.builder), 0);
+}
+
+int main(int argc, char **argv)
+{
+	int ret = helper_main(argc, argv);
+
+	if (ret >= 0)
+		return ret;
+	return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/pidfd/pidfd_spawn_test.c b/tools/testing/selftests/pidfd/pidfd_spawn_test.c
new file mode 100644
index 0000000000000..da28ad125a345
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_spawn_test.c
@@ -0,0 +1,550 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/fs.h>
+#include <linux/pidfd_spawn.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/epoll.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+#include <sys/xattr.h>
+#include <unistd.h>
+
+#include "pidfd.h"
+#include "kselftest_harness.h"
+#include "../filesystems/wrappers.h"
+
+#include "pidfd_spawn_common.h"
+
+struct pidfd_spawn_xattr_writer {
+	const char *value;
+	int fd;
+};
+
+static void *pidfd_spawn_xattr_writer(void *data)
+{
+	struct pidfd_spawn_xattr_writer *writer = data;
+	size_t len = strlen(writer->value);
+	int i;
+
+	for (i = 0; i < 1000; i++) {
+		if (fsetxattr(writer->fd, "trusted.pidfd_spawn", writer->value,
+			      len, 0))
+			return (void *)(intptr_t)errno;
+	}
+	return NULL;
+}
+
+static int read_fdinfo_pid(int fd)
+{
+	char path[64];
+	char line[128];
+	FILE *file;
+	int pid = INT_MIN;
+
+	snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", fd);
+	file = fopen(path, "re");
+	if (!file)
+		return INT_MIN;
+	while (fgets(line, sizeof(line), file)) {
+		if (sscanf(line, "Pid:\t%d", &pid) == 1)
+			break;
+	}
+	fclose(file);
+	return pid;
+}
+
+TEST(pidfd_open_empty_rejects_bad_flags)
+{
+	ASSERT_EQ(sys_pidfd_open(0, 0), -1);
+	ASSERT_EQ(errno, EINVAL);
+	ASSERT_EQ(sys_pidfd_open(0, PIDFD_EMPTY | PIDFD_THREAD), -1);
+	ASSERT_EQ(errno, EINVAL);
+	ASSERT_EQ(sys_pidfd_open(0, PIDFD_EMPTY | (1U << 26)), -1);
+	ASSERT_EQ(errno, EINVAL);
+	ASSERT_EQ(sys_pidfd_open(getpid(), PIDFD_EMPTY), -1);
+	ASSERT_EQ(errno, EINVAL);
+}
+
+TEST(pidfd_open_empty_close_discards_builder)
+{
+	int fd;
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_open_empty_accepts_nonblock)
+{
+	int fd;
+	int flags;
+
+	fd = sys_pidfd_open(0, PIDFD_EMPTY | PIDFD_NONBLOCK);
+	ASSERT_GE(fd, 0);
+
+	flags = fcntl(fd, F_GETFL);
+	ASSERT_GE(flags, 0);
+	ASSERT_NE(flags & O_NONBLOCK, 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_config_stages_absolute_path)
+{
+	const char *path = self_exe_path();
+	int fd;
+
+	ASSERT_NE(path, NULL);
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(config_path(fd, path), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_config_stages_relative_path)
+{
+	char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+				"exit0", NULL };
+	const char *relative;
+	char *storage;
+	int cwd_fd;
+	int fd;
+
+	cwd_fd = enter_self_exe_directory(&storage, &relative);
+	ASSERT_GE(cwd_fd, 0);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(config_path(fd, relative), 0);
+	ASSERT_EQ(spawn_run_staged(fd, argv, NULL, 0), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+	ASSERT_EQ(leave_self_exe_directory(cwd_fd, storage), 0);
+}
+
+TEST(pidfd_config_rejects_bad_arguments)
+{
+	char unterminated_key[256];
+	int fd;
+
+	memset(unterminated_key, 'x', sizeof(unterminated_key));
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+
+	ASSERT_EQ(sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING, NULL,
+				   "/bin/true", 0),
+		  -1);
+	ASSERT_EQ(errno, EINVAL);
+	ASSERT_EQ(sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+				   PIDFD_CONFIG_KEY_PATH, NULL, 0),
+		  -1);
+	ASSERT_EQ(errno, EINVAL);
+	ASSERT_EQ(sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+				   PIDFD_CONFIG_KEY_PATH, "/bin/true", 1),
+		  -1);
+	ASSERT_EQ(errno, EINVAL);
+	ASSERT_EQ(sys_pidfd_config(fd, UINT_MAX, PIDFD_CONFIG_KEY_PATH,
+				   "/bin/true", 0),
+		  -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+	ASSERT_EQ(sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING, "unknown",
+				   "/bin/true", 0),
+		  -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+	ASSERT_EQ(sys_pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+				   unterminated_key, "/bin/true", 0),
+		  -1);
+	ASSERT_EQ(errno, EINVAL);
+
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_open_empty_getversion_survives_publication)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+				"exit0", NULL };
+	__u32 published_generation;
+	__u32 future_generation;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(ioctl(fd, FS_IOC_GETVERSION, &future_generation), 0);
+	ASSERT_GT(spawn_run_path_pid(fd, path, argv, NULL, 0), 0);
+	ASSERT_EQ(ioctl(fd, FS_IOC_GETVERSION, &published_generation), 0);
+	ASSERT_EQ(published_generation, future_generation);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_reopened_file_does_not_cancel_builder)
+{
+	char proc_path[64];
+	const char *path = self_exe_path();
+	int reopened;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_GT(snprintf(proc_path, sizeof(proc_path), "/proc/self/fd/%d", fd),
+		  0);
+	reopened = open(proc_path, O_RDWR | O_CLOEXEC);
+	ASSERT_GE(reopened, 0);
+	ASSERT_EQ(close(reopened), 0);
+
+	ASSERT_EQ(config_path(fd, path), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+static int pidfd_spawn_bind_mount_child(const char *path)
+{
+	char template[] = P_tmpdir "/pidfd_spawn_mount_XXXXXX";
+	char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+				"exit0", NULL };
+	int mounted_fd = -1;
+	int mount_fd = -1;
+	int target_fd = -1;
+	bool mounted = false;
+	int ret = 1;
+	int fd = -1;
+
+	if (unshare(CLONE_NEWNS))
+		return errno == EPERM ? 77 : 1;
+	if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
+		return errno == EPERM ? 77 : 2;
+	target_fd = mkstemp(template);
+	if (target_fd < 0)
+		return 3;
+
+	fd = sys_pidfd_empty_open();
+	if (fd < 0) {
+		ret = 4;
+		goto out;
+	}
+	mount_fd = sys_open_tree(fd, "", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC |
+					    AT_EMPTY_PATH);
+	if (mount_fd < 0) {
+		ret = 5;
+		goto out;
+	}
+	if (move_mount(mount_fd, "", target_fd, "",
+		       MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH)) {
+		ret = 6;
+		goto out;
+	}
+	mounted = true;
+	close(mount_fd);
+	mount_fd = -1;
+	close(fd);
+	fd = -1;
+
+	mounted_fd = open(template, O_RDWR | O_CLOEXEC);
+	if (mounted_fd < 0) {
+		ret = 7;
+		goto out;
+	}
+	if (spawn_run_path(mounted_fd, path, argv, NULL, 0)) {
+		ret = 8;
+		goto out;
+	}
+	if (wait_pidfd_exit(mounted_fd, 0)) {
+		ret = 9;
+		goto out;
+	}
+	ret = 0;
+
+out:
+	if (mounted_fd >= 0)
+		close(mounted_fd);
+	if (mounted)
+		umount2(template, MNT_DETACH);
+	if (mount_fd >= 0)
+		close(mount_fd);
+	if (fd >= 0)
+		close(fd);
+	if (target_fd >= 0)
+		close(target_fd);
+	if (target_fd >= 0)
+		unlink(template);
+	return ret;
+}
+
+TEST(pidfd_spawn_bind_mount_keeps_builder_alive)
+{
+	const char *path = self_exe_path();
+	int status;
+	pid_t waited;
+	pid_t pid;
+
+	ASSERT_NE(path, NULL);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid)
+		_exit(pidfd_spawn_bind_mount_child(path));
+	waited = waitpid_timeout(pid, &status, PIDFD_SPAWN_TIMEOUT_MS);
+	if (!waited) {
+		kill(pid, SIGKILL);
+		waited = waitpid_timeout(pid, &status, 1000);
+	}
+	ASSERT_EQ(waited, pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	if (WEXITSTATUS(status) == 77)
+		SKIP(return, "mount namespaces are unavailable");
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST(pidfd_spawn_run_preserves_pidfd_identity)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+				"exit0", NULL };
+	struct stat builder_stat;
+	struct stat future_stat;
+	struct stat opened_stat;
+	struct stat ordinary_stat;
+	int opened;
+	int ordinary;
+	int child_pid;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	ordinary = sys_pidfd_open(getpid(), 0);
+	ASSERT_GE(ordinary, 0);
+	ASSERT_EQ(fstat(ordinary, &ordinary_stat), 0);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(fstat(fd, &future_stat), 0);
+	ASSERT_EQ(future_stat.st_mode, ordinary_stat.st_mode);
+	ASSERT_EQ(future_stat.st_uid, ordinary_stat.st_uid);
+	ASSERT_EQ(future_stat.st_gid, ordinary_stat.st_gid);
+	ASSERT_EQ(close(ordinary), 0);
+	child_pid = spawn_run_path_pid(fd, path, argv, NULL, 0);
+	ASSERT_GT(child_pid, 0);
+	opened = sys_pidfd_open(child_pid, 0);
+	ASSERT_GE(opened, 0);
+	ASSERT_EQ(fstat(fd, &builder_stat), 0);
+	ASSERT_EQ(fstat(opened, &opened_stat), 0);
+	ASSERT_EQ(future_stat.st_dev, builder_stat.st_dev);
+	ASSERT_EQ(future_stat.st_ino, builder_stat.st_ino);
+	ASSERT_EQ(future_stat.st_mode, builder_stat.st_mode);
+	ASSERT_EQ(future_stat.st_uid, builder_stat.st_uid);
+	ASSERT_EQ(future_stat.st_gid, builder_stat.st_gid);
+	ASSERT_EQ(builder_stat.st_dev, opened_stat.st_dev);
+	ASSERT_EQ(builder_stat.st_ino, opened_stat.st_ino);
+	ASSERT_EQ(builder_stat.st_mode, opened_stat.st_mode);
+	ASSERT_EQ(builder_stat.st_uid, opened_stat.st_uid);
+	ASSERT_EQ(builder_stat.st_gid, opened_stat.st_gid);
+
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(opened), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_preserves_pidfd_xattrs)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+				"exit0", NULL };
+	struct pidfd_spawn_xattr_writer writers[2];
+	pthread_t threads[2];
+	void *thread_ret;
+	int child_pid;
+	int opened;
+	int fd;
+	int ret;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	child_pid = spawn_run_path_pid(fd, path, argv, NULL, 0);
+	ASSERT_GT(child_pid, 0);
+	opened = sys_pidfd_open(child_pid, 0);
+	ASSERT_GE(opened, 0);
+
+	errno = 0;
+	ret = fsetxattr(fd, "trusted.pidfd_spawn", "probe", 5, 0);
+	if (ret && (errno == EPERM || errno == EACCES)) {
+		ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+		ASSERT_EQ(close(opened), 0);
+		ASSERT_EQ(close(fd), 0);
+		SKIP(return, "trusted xattrs require CAP_SYS_ADMIN");
+	}
+	ASSERT_EQ(ret, 0);
+
+	writers[0].fd = fd;
+	writers[0].value = "builder";
+	writers[1].fd = opened;
+	writers[1].value = "opened";
+	ASSERT_EQ(pthread_create(&threads[0], NULL, pidfd_spawn_xattr_writer,
+				 &writers[0]), 0);
+	ASSERT_EQ(pthread_create(&threads[1], NULL, pidfd_spawn_xattr_writer,
+				 &writers[1]), 0);
+	ASSERT_EQ(pthread_join_timeout(threads[0], &thread_ret,
+				       PIDFD_SPAWN_TIMEOUT_MS), 0);
+	ASSERT_EQ((intptr_t)thread_ret, 0);
+	ASSERT_EQ(pthread_join_timeout(threads[1], &thread_ret,
+				       PIDFD_SPAWN_TIMEOUT_MS), 0);
+	ASSERT_EQ((intptr_t)thread_ret, 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(opened), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_prerun_epoll_reports_child_exit)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+				"exit0", NULL };
+	struct epoll_event event = {
+		.events = EPOLLIN,
+	};
+	struct epoll_event ready = {};
+	int epoll_fd;
+	int fd;
+
+	ASSERT_NE(path, NULL);
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	event.data.fd = fd;
+	epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+	ASSERT_GE(epoll_fd, 0);
+	ASSERT_EQ(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event), 0);
+	ASSERT_GT(spawn_run_path_pid(fd, path, argv, NULL, 0), 0);
+	ASSERT_EQ(epoll_wait(epoll_fd, &ready, 1,
+			     PIDFD_SPAWN_TIMEOUT_MS), 1);
+	ASSERT_EQ(ready.data.fd, fd);
+	ASSERT_NE(ready.events & EPOLLIN, 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(epoll_fd), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_is_one_shot)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+				"exit0", NULL };
+	int fd;
+
+	ASSERT_NE(path, NULL);
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_GT(spawn_run_path_pid(fd, path, argv, NULL, 0), 0);
+	ASSERT_EQ(spawn_run_path_pid(fd, path, argv, NULL, 0), -1);
+	ASSERT_EQ(errno, EBUSY);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_execs_staged_path)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+				"exit0", NULL };
+	int fd;
+
+	ASSERT_NE(path, NULL);
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(config_path(fd, path), 0);
+	ASSERT_EQ(spawn_run_staged(fd, argv, NULL, 0), 0);
+	ASSERT_EQ(wait_pidfd_exit(fd, 0), 0);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_run_rejects_conflicting_paths)
+{
+	const char *path = self_exe_path();
+	char * const argv[] = { "pidfd_spawn_test", "--pidfd-spawn-helper",
+				"exit0", NULL };
+	int fd;
+
+	ASSERT_NE(path, NULL);
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(config_path(fd, path), 0);
+	ASSERT_EQ(spawn_run_path(fd, path, argv, NULL, 0), -1);
+	ASSERT_EQ(errno, EINVAL);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(pidfd_spawn_pending_pidfd_apis_return_esrch)
+{
+	char byte;
+	struct iovec iov = {
+		.iov_base = &byte,
+		.iov_len = sizeof(byte),
+	};
+	struct pidfd_info info = {
+		.mask = PIDFD_INFO_PID,
+	};
+	siginfo_t wait_info = {};
+	struct pollfd pfd = {};
+	int nsfd;
+	int fd;
+
+	fd = sys_pidfd_empty_open();
+	ASSERT_GE(fd, 0);
+
+	ASSERT_EQ(sys_pidfd_send_signal(fd, 0, NULL, 0), -1);
+	ASSERT_EQ(errno, ESRCH);
+	ASSERT_EQ(sys_pidfd_getfd(fd, STDIN_FILENO, 0), -1);
+	ASSERT_EQ(errno, ESRCH);
+	ASSERT_EQ(ioctl(fd, PIDFD_GET_INFO, &info), -1);
+	ASSERT_EQ(errno, ESRCH);
+	nsfd = ioctl(fd, PIDFD_GET_MNT_NAMESPACE, 0);
+	ASSERT_EQ(nsfd, -1);
+	ASSERT_EQ(errno, ESRCH);
+	ASSERT_EQ(setns(fd, CLONE_NEWNS), -1);
+	ASSERT_EQ(errno, ESRCH);
+	ASSERT_EQ(read_fdinfo_pid(fd), -1);
+	ASSERT_EQ(sys_waitid(P_PIDFD, fd, &wait_info, WEXITED | WNOHANG), -1);
+	ASSERT_EQ(errno, ESRCH);
+	ASSERT_EQ(syscall(__NR_process_madvise, fd, &iov, 1,
+			  MADV_DONTNEED, 0), -1);
+	ASSERT_EQ(errno, ESRCH);
+	ASSERT_EQ(syscall(__NR_process_mrelease, fd, 0), -1);
+	ASSERT_EQ(errno, ESRCH);
+	ASSERT_EQ(flistxattr(fd, NULL, 0), -1);
+	ASSERT_EQ(errno, ESRCH);
+
+	pfd.fd = fd;
+	pfd.events = POLLIN;
+	ASSERT_EQ(poll(&pfd, 1, 0), 0);
+
+	ASSERT_EQ(close(fd), 0);
+}
+
+int main(int argc, char **argv)
+{
+	int ret = helper_main(argc, argv);
+
+	if (ret >= 0)
+		return ret;
+	return test_harness_run(argc, argv);
+}
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 24/24] Documentation: describe pidfd spawn builders
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Kees Cook, Gabriel Krisman Bertazi, Josh Triplett, Mateusz Guzik,
	Andy Lutomirski, John Ericson, Jonathan Corbet, Shuah Khan,
	Arnd Bergmann, Oleg Nesterov, Andrew Morton, Paul Moore,
	Eric Paris, Mickaël Salaün, Günther Noack,
	Alexander Viro, Jan Kara, linux-api, linux-fsdevel, linux-kernel,
	linux-kselftest, linux-doc, audit, linux-security-module,
	linux-arch, linux-mm, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

Document the taskless pidfd lifecycle, source-state and authority
model, optional path configuration, run and file-action ABI, one-shot
failure classification, and embryonic process visibility.

Describe policy and audit interactions, current accounting behavior,
and the features still missing from the initial interface. Add the
document to the userspace API index and PIDFD API maintenance entry.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 Documentation/userspace-api/index.rst       |   1 +
 Documentation/userspace-api/pidfd_spawn.rst | 247 ++++++++++++++++++++
 MAINTAINERS                                 |   1 +
 3 files changed, 249 insertions(+)
 create mode 100644 Documentation/userspace-api/pidfd_spawn.rst

diff --git a/Documentation/userspace-api/index.rst b/Documentation/userspace-api/index.rst
index a68b1bea57a85..28da6a4da7c78 100644
--- a/Documentation/userspace-api/index.rst
+++ b/Documentation/userspace-api/index.rst
@@ -21,6 +21,7 @@ System calls
    ebpf/index
    ioctl/index
    mseal
+   pidfd_spawn
    rseq
 
 Security-related interfaces
diff --git a/Documentation/userspace-api/pidfd_spawn.rst b/Documentation/userspace-api/pidfd_spawn.rst
new file mode 100644
index 0000000000000..2aaf5db85f13b
--- /dev/null
+++ b/Documentation/userspace-api/pidfd_spawn.rst
@@ -0,0 +1,247 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+====================
+Pidfd spawn builders
+====================
+
+Pidfd spawn builders create a new process image with kernel-mediated setup.
+They do not expose a userspace child stub between process creation and exec.
+
+The interface is::
+
+    int fd = pidfd_open(0, PIDFD_EMPTY);
+
+    /* Optional executable-path staging. */
+    pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+                 PIDFD_CONFIG_KEY_PATH, path, 0);
+
+    pidfd_spawn_run(fd, &args, sizeof(args));
+
+The same fd starts as a taskless pidfs object and becomes the child's pidfd
+when a task is created. The current implementation supports an executable
+path, argv, envp, and ordered ``DUP2``, ``CLOSE_RANGE``, and ``FCHDIR`` file
+actions.
+
+Opening a builder
+=================
+
+``pidfd_open(0, PIDFD_EMPTY)`` returns a close-on-exec builder fd.
+``PIDFD_NONBLOCK`` may be combined with ``PIDFD_EMPTY``. Other flags are
+rejected. ``PIDFD_EMPTY`` is rejected for a nonzero pid.
+
+Opening the builder allocates a pidfs inode and builder state. It does not
+allocate a ``struct pid`` or task, and does not charge ``RLIMIT_NPROC`` or the
+pids cgroup controller. PID allocation and process-count accounting occur when
+``pidfd_spawn_run()`` calls the normal task-creation path.
+
+The pidfs inode has a stable identity before and after task creation. A pidfd
+opened from the eventual numeric pid refers to the same pidfs inode. A poll or
+epoll registration made while the fd is taskless remains attached across
+publication and can report the child's eventual exit.
+
+Process-dependent pidfd operations return ``ESRCH`` while the builder is
+taskless. This includes signal delivery, ``pidfd_getfd()``,
+``PIDFD_GET_INFO``, namespace ioctls, ``setns()``, process memory operations,
+and process-dependent pidfs xattrs. Argument validation may fail before the
+taskless-state check. ``poll()`` reports no event while the builder may still
+publish a task. After a terminal taskless failure, ``poll()`` and ``epoll``
+report ``POLLERR | POLLHUP`` and wake registrations made before the failure.
+Inode-only operations such as ``FS_IOC_GETVERSION`` remain available.
+
+The builder state follows the lifetime of the pidfs inode. Duplicated or
+reopened descriptors and VFS references to that inode keep it alive. Releasing
+the final reference before a run discards the taskless builder without
+creating a process.
+
+Source state and authority
+==========================
+
+The current mode is source-based. The child is created from the calling
+process at run time. State changed after ``pidfd_open()`` but before
+``pidfd_spawn_run()``, including the current directory and file descriptor
+table, is therefore visible to normal process creation. Non-close-on-exec file
+descriptors remain available to the new image unless a file action changes
+them. The exec path applies ``FD_CLOEXEC`` normally.
+
+File and filesystem state are private child copies before file actions run.
+The current backend shares the address space only for its internal
+vfork-style setup and replaces it during exec. Applications must not depend on
+that backend detail.
+
+Configuration and run authority is bound to the mm, exact credential object,
+and child PID namespace recorded when the builder is opened. Passing the fd
+through ``SCM_RIGHTS`` or another fd-passing mechanism does not by itself
+delegate authority to configure or run it. A rejected authority check does not
+consume the builder.
+
+Configuring an executable path
+==============================
+
+The optional configuration command is::
+
+    pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
+                 PIDFD_CONFIG_KEY_PATH, path, 0);
+
+The command copies ``path`` into builder state. Calling it again before a run
+replaces the staged string. The key and value must be valid NUL-terminated
+userspace strings, and ``aux`` must be zero. Unknown commands or keys return
+``EOPNOTSUPP``.
+
+Staging is not required when the run arguments provide ``path``. Supplying a
+run-time path when a path is already staged, or supplying neither form,
+returns ``EINVAL``. This selection is validated before a run-time path pointer
+is dereferenced, so a staged-path conflict takes precedence over a fault in
+that pointer. Relative paths are resolved after ordered file actions from the
+child's resulting working directory. Without an ``FCHDIR`` action, that is
+the working directory copied at run time. The kernel does not search ``PATH``.
+
+Run arguments
+=============
+
+Version 0 of ``struct pidfd_spawn_run_args`` contains:
+
+* ``path``: an optional executable-path pointer;
+* ``argv``: a required argument-vector pointer;
+* ``envp``: an environment-vector pointer, or zero for an empty environment;
+* ``actions``, ``nr_actions``, and ``action_size``: an optional ordered
+  file-action array.
+
+``flags`` and all reserved fields must be zero. The syscall size must be at
+least ``PIDFD_SPAWN_RUN_SIZE_VER0`` and no larger than ``PAGE_SIZE``. Unknown
+trailing bytes in a larger structure must be zero.
+
+``path``, ``argv``, ``envp``, and ``actions`` are full-width userspace virtual
+addresses stored in ``__aligned_u64`` fields on every ABI. Native calls must
+fit the native pointer width. Compat calls must fit ``compat_uptr_t``; nonzero
+upper bits are rejected with ``EFAULT`` before task creation. Entries read
+from compat argv and envp arrays are compat pointers.
+
+File actions
+============
+
+When ``nr_actions`` is zero, ``actions`` and ``action_size`` must also be zero.
+Otherwise ``actions`` must be nonzero and ``action_size`` must be at least
+``PIDFD_SPAWN_ACTION_SIZE_VER0`` and aligned to eight bytes. A larger element
+is accepted only when its unknown tail is zero. The complete action payload is
+limited to 64 KiB; the interface does not impose a fixed action count below
+that byte limit.
+
+Actions execute in array order against child-private file and filesystem
+state before exec:
+
+``PIDFD_SPAWN_ACTION_DUP2``
+    Duplicate ``fd`` onto ``newfd``. If both numbers are equal, clear
+    ``FD_CLOEXEC`` on that descriptor. ``flags`` and reserved fields must be
+    zero.
+
+``PIDFD_SPAWN_ACTION_CLOSE_RANGE``
+    Apply ``close_range(fd, newfd, flags)``. ``newfd`` must not be less than
+    ``fd``. Only ``CLOSE_RANGE_UNSHARE`` and ``CLOSE_RANGE_CLOEXEC`` are
+    accepted.
+
+``PIDFD_SPAWN_ACTION_FCHDIR``
+    Change the child working directory to ``fd``. ``flags``, ``newfd``, and
+    reserved fields must be zero.
+
+An unknown action type returns ``EOPNOTSUPP``. Invalid fields or arguments
+return ``EINVAL``. Errors from applying an otherwise valid action are reported
+as run failures after task publication.
+
+One-shot execution and results
+==============================
+
+After fd type, creator authority, and source ptrace checks succeed, the first
+``pidfd_spawn_run()`` attempt atomically claims the builder before copying its
+run arguments. Argument faults, validation errors, memory-allocation failures,
+process limits, and task-creation failures after that claim are terminal.
+Later configuration and run attempts return ``EBUSY``.
+
+Checks that fail before the claim do not consume the builder. These include an
+invalid fd type, creator-authority mismatch, or an already traced source.
+
+On successful setup and exec, ``pidfd_spawn_run()`` returns the positive child
+pid as seen in the caller's PID namespace. The pidfd remains the stable process
+identity.
+
+A failure before task creation returns a negative error and leaves the fd
+terminal but taskless. Ordinary pidfd operations continue to return ``ESRCH``.
+``poll()`` and ``epoll`` report ``POLLERR | POLLHUP``. The builder cannot be
+retried; a caller must open a new builder.
+
+A setup or exec failure after publication returns a negative error, leaves the
+same fd as the child's pidfd, and makes the child exit with status 127. Normal
+``SIGCHLD``, ``SIG_IGN``, and ``SA_NOCLDWAIT`` rules determine whether it must
+be reaped. ``PIDFD_GET_INFO`` with ``PIDFD_INFO_EXIT`` distinguishes this case
+from a terminal taskless failure.
+
+Internal restart errors after the claim are returned to userspace as
+``EINTR``. The kernel does not automatically restart a consumed run and
+replace its first result with ``EBUSY``.
+
+A fatal signal delivered to the published child determines the child's exit
+status if it wins during setup. It is not reported as an exec setup error, so
+the run can return the positive child pid even when that child exits from the
+signal.
+
+Embryonic process state
+=======================
+
+After task publication and before successful exec completion, the child is in
+an embryonic exec state. It is not yet a normal userspace process image and
+does not have a valid userspace register frame on every architecture.
+
+During this interval:
+
+* direct ptrace access and ``pidfd_getfd()`` are denied;
+* procfs PID lookup and iteration treat the task as absent to other tasks,
+  preventing access to the source address space temporarily shared by the
+  implementation; the embryonic task may resolve its own procfs entries for
+  executable and interpreter lookup;
+* ``PIDFD_GET_INFO`` reports ``PIDFD_COREDUMP_SKIP`` when coredump state is
+  requested;
+* ordinary pidfd identity, signal, wait, and poll mechanisms operate on the
+  published task as their normal permission checks allow.
+
+Successful exec releases the embryonic state before exec event publication. A
+failed setup never exposes the borrowed pre-exec frame to userspace; the child
+exits instead.
+
+Security policy and audit
+=========================
+
+File actions and exec run inside the child kernel path. They do not appear to
+seccomp as separate ``dup2()``, ``close_range()``, ``fchdir()``, or
+``execve()`` syscalls. A source-side seccomp filter can reject
+``pidfd_spawn_run()`` before the builder is claimed. A policy that intends to
+prevent this operation must filter ``pidfd_spawn_run()`` itself; denying only
+the syscall numbers corresponding to its file actions or exec does not mediate
+the equivalent in-kernel work. In particular, an exec-only denylist that
+allows unknown syscalls does not block the initial spawn exec; it must add
+``pidfd_spawn_run()``. A syscall allowlist does not permit the new operation
+until it adds that syscall. Seccomp state inherited by the child remains active
+after exec. LSM checks, including Landlock executable access, still mediate the
+underlying exec.
+
+Audit records the source syscall transaction and a separate
+``AUDIT_PIDFD_SPAWN`` child transaction for executable lookup and exec
+arguments. The child does not emit a synthetic second ``AUDIT_SYSCALL``
+record. The child transaction is selected using the originating
+``pidfd_spawn_run()`` syscall number, so an existing exit rule that names only
+``execve()`` or ``execveat()`` does not select it. Such rules must also select
+``pidfd_spawn_run()`` with the current audit model. File actions are not
+emitted as decoded per-action audit records.
+
+An already traced source is rejected before the claim. The current backend
+does not implement ptrace auto-attach events for the created child.
+
+Current limitations
+===================
+
+The interface does not yet implement the full ``posix_spawn()`` surface. It
+lacks open and close file actions, resetids, signal mask and default controls,
+scheduler attributes, affinity, process groups, sessions, explicit cgroup
+placement, ``PATH`` search, and exec by fd.
+
+It also does not provide pristine or no-source process creation, executable
+metadata caching, or reusable spawn templates. The current implementation is
+source-based and uses the existing vfork/exec machinery internally.
diff --git a/MAINTAINERS b/MAINTAINERS
index 85b1306cb2ff3..0b6d466787a60 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21288,6 +21288,7 @@ M:	Christian Brauner <christian@brauner.io>
 L:	linux-kernel@vger.kernel.org
 S:	Maintained
 T:	git git://git.kernel.org/pub/scm/linux/kernel/git/brauner/linux.git
+F:	Documentation/userspace-api/pidfd_spawn.rst
 F:	fs/pidfd_spawn.c
 F:	include/linux/pidfd_spawn.h
 F:	include/uapi/linux/pidfd_spawn.h
-- 
2.52.0


^ permalink raw reply related

* Re: [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks
From: Andy Lutomirski @ 2026-07-16 15:57 UTC (permalink / raw)
  To: Li Chen
  Cc: Christian Brauner, Kees Cook, Gabriel Krisman Bertazi,
	Josh Triplett, Mateusz Guzik, Andy Lutomirski, John Ericson,
	Jonathan Corbet, Shuah Khan, Arnd Bergmann, Oleg Nesterov,
	Andrew Morton, Paul Moore, Eric Paris, Mickaël Salaün,
	Günther Noack, Alexander Viro, Jan Kara, linux-api,
	linux-fsdevel, linux-kernel, linux-kselftest, linux-doc, audit,
	linux-security-module, linux-arch, linux-mm
In-Reply-To: <538e494dd8fcc677da24ec985c6e90dde554e7f3.1784204592.git.me@linux.beauty>

On Thu, Jul 16, 2026 at 8:52 AM Li Chen <me@linux.beauty> wrote:
>
> A kernel-created task can become visible before it has installed a new
> executable image or a valid userspace register frame. Exposing such a task
> through ptrace can disclose kernel setup state.
>
> Add a task-local embryonic flag and an internal clone argument for callers
> that need this lifecycle. Reject ptrace access until the creator clears the
> flag. Clear it with release ordering and observe it with acquire ordering.
> This orders visibility of the completed exec state with the transition.
>
> Existing fork, vfork, clone, and kernel-thread callers leave the argument
> unset and retain their current behavior.
>

> --- a/kernel/ptrace.c
> +++ b/kernel/ptrace.c
> @@ -56,6 +56,8 @@ bool ptracer_access_allowed(struct task_struct *tsk)
>         guard(rcu)();
>         if (ptrace_parent(tsk) != current)
>                 return false;
> +       if (task_is_embryonic_exec(tsk))
> +               return false;
>         es = task_exec_state_rcu(tsk);
>         return READ_ONCE(es->dumpable) == TASK_DUMPABLE_OWNER ||
>                ptracer_capable(tsk, es->user_ns);
> @@ -312,6 +314,8 @@ static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
>                 WARN(1, "denying ptrace access check without PTRACE_MODE_*CREDS\n");
>                 return -EPERM;
>         }
> +       if (task_is_embryonic_exec(task))
> +               return -EPERM;

Would it be better to use a different error code?  -ECONNREFUSED?
After all, this isn't a permission failure per se.

There's a not-locally-obvious gotcha here: reading other process
attributes prior to calling task_is_embryonic_exec may result in
(security-relevant!) data races.  This should at least be documented
-- it's critical to check task_is_embryonic_exec *before* trying to
read credentials.  Also, I think /proc and many pidfd APIs have the
same issue.

--Andy

^ permalink raw reply

* Re: [PATCH -next,v2] ima: add cond_resched() in ima_calc_file_hash_tfm loop
From: Mimi Zohar @ 2026-07-16 17:25 UTC (permalink / raw)
  To: Roberto Sassu, Gaosheng Cui, lujialin4, roberto.sassu,
	dmitry.kasatkin, eric.snowberg, paul, jmorris, serge
  Cc: linux-integrity, linux-security-module
In-Reply-To: <f6ecaaf1-e226-42a5-91f9-37ecce66a5d4@huaweicloud.com>

On Thu, 2026-07-16 at 09:29 +0200, Roberto Sassu wrote:
> On 7/14/2026 11:26 AM, Roberto Sassu wrote:
> > On Tue, 2026-07-14 at 17:17 +0800, Gaosheng Cui wrote:
> > > When hashing large files, the while loop in ima_calc_file_hash_tfm
> > > processes PAGE_SIZE chunks without any scheduling point, which can
> > > cause soft lockup warnings:
> > > watchdog: BUG: soft lockup - CPU#0 stuck for 50s!
> > > Call Trace:
> > >    _sha256_update+0x12d/0x1a0
> > >    ima_calc_file_hash_tfm+0xfb/0x150
> > >    ima_calc_file_hash+0x6e/0x160
> > >    ima_collect_measurement+0x202/0x340
> > >    process_measurement+0x3a9/0xb30
> > >    ima_file_check+0x56/0xa0
> > >    do_open+0x11b/0x250
> > >    path_openat+0x10b/0x1d0
> > >    do_filp_open+0xa9/0x150
> > >    do_sys_openat2+0x223/0x2a0
> > >    __x64_sys_openat+0x54/0xa0
> > >    do_syscall_64+0x59/0x110
> > >    entry_SYSCALL_64_after_hwframe+0x78/0xe2
> > > 
> > > Call cond_resched() every 4MB to yield the CPU when needed, rather
> > > than at every loop iteration, to reduce overhead.
> > > 
> > > Fixes: 3323eec921ef ("integrity: IMA as an integrity service provider")
> > > Signed-off-by: Gaosheng Cui <cuigaosheng1@huawei.com>
> > 
> > Reviewed-by: Roberto Sassu <roberto.sassu@huawei.com>
> 
> Actually, as Sashiko pointed out, alignment does not work well, since 
> __kernel_read() can return fewer than PAGE_SIZE bytes, causing the 
> offset to be always misaligned.

It would be interesting to know if short reads are a generic concern across all
filesystems or whether it is limited to remote or FUSE filesystems. The patch
probably won't change, but please update the patch description with more
details.

Mimi

> Instead, we can keep a variable with the 
> number of bytes read since last cond_resched(), which we reset when we 
> hit 4 MB.
> 
> Sashiko also noted that the same mechanism could apply for 
> calc_buffer_shash_tfm(), but we can add it later if we have a warning 
> (note that in this case we already have the buffer, we are not doing
> I/O).
> 
> Thanks
> 
> Roberto
> 
> > Thanks
> > 
> > Roberto
> > 
> > > ---
> > > v2: call cond_resched() every 4MB to yield the CPU when needed
> > >   security/integrity/ima/ima_crypto.c | 3 +++
> > >   1 file changed, 3 insertions(+)
> > > 
> > > diff --git a/security/integrity/ima/ima_crypto.c b/security/integrity/ima/ima_crypto.c
> > > index 0d72b48249ee..aab2349c0c33 100644
> > > --- a/security/integrity/ima/ima_crypto.c
> > > +++ b/security/integrity/ima/ima_crypto.c
> > > @@ -233,6 +233,9 @@ static int ima_calc_file_hash_tfm(struct file *file,
> > >   		rc = crypto_shash_update(shash, rbuf, rbuf_len);
> > >   		if (rc)
> > >   			break;
> > > +
> > > +		if (IS_ALIGNED(offset, SZ_4M))
> > > +			cond_resched();
> > >   	}
> > >   	kfree(rbuf);
> > >   out:
> > 
> 

^ permalink raw reply

* [PATCH] ima: Report errno for failed hash collection to audit
From: Frederick Lawler @ 2026-07-16 19:10 UTC (permalink / raw)
  To: Mimi Zohar, Roberto Sassu, Dmitry Kasatkin, Eric Snowberg,
	Paul Moore, James Morris, Serge E. Hallyn
  Cc: linux-integrity, linux-security-module, linux-kernel, kernel-team,
	Frederick Lawler

integrity_audit_msg() wraps integrity_audit_message() such that errno is
always set to zero. When debugging for failed hashes, it'd be nice to
understand the corresponding error code with the cause.

Signed-off-by: Frederick Lawler <fred@cloudflare.com>
---
 security/integrity/ima/ima_api.c          | 6 +++---
 security/integrity/ima/ima_template_lib.c | 6 +++---
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/security/integrity/ima/ima_api.c b/security/integrity/ima/ima_api.c
index 122d127e108dcca8ddae6575c908144859783fb6..15883e4f077e0aa950f3fdd4b573873929da68aa 100644
--- a/security/integrity/ima/ima_api.c
+++ b/security/integrity/ima/ima_api.c
@@ -328,9 +328,9 @@ int ima_collect_measurement(struct ima_iint_cache *iint, struct file *file,
 
 		take_dentry_name_snapshot(&filename, file->f_path.dentry);
 
-		integrity_audit_msg(AUDIT_INTEGRITY_DATA, inode,
-				    filename.name.name, "collect_data",
-				    audit_cause, result, 0);
+		integrity_audit_message(AUDIT_INTEGRITY_DATA, inode,
+					filename.name.name, "collect_data",
+					audit_cause, result, 0, result);
 
 		release_dentry_name_snapshot(&filename);
 	}
diff --git a/security/integrity/ima/ima_template_lib.c b/security/integrity/ima/ima_template_lib.c
index 8a89236f926c10af0dd2b2ef08fbe59bd8a78b66..6d163312fb4c47ce256766e997aac5f7375088a8 100644
--- a/security/integrity/ima/ima_template_lib.c
+++ b/security/integrity/ima/ima_template_lib.c
@@ -390,9 +390,9 @@ int ima_eventdigest_init(struct ima_event_data *event_data,
 	    ima_hash_algo : HASH_ALGO_SHA1;
 	result = ima_calc_file_hash(event_data->file, hash_hdr);
 	if (result) {
-		integrity_audit_msg(AUDIT_INTEGRITY_DATA, inode,
-				    event_data->filename, "collect_data",
-				    "failed", result, 0);
+		integrity_audit_message(AUDIT_INTEGRITY_DATA, inode,
+					event_data->filename, "collect_data",
+					"failed", result, 0, result);
 		return result;
 	}
 	cur_digest = hash_hdr->digest;

---
base-commit: 1da739feb31b4fecae465ebf87ba44a97e44101b
change-id: 20260716-report-hash-error-5203d6fe6e4e

Best regards,
-- 
Frederick Lawler <fred@cloudflare.com>


^ permalink raw reply related

* Re: [BUG] Landlock denies LANDLOCK_ACCESS_FS_EXECUTE despite a correctly-anchored PathBeneath rule (contradicts selftest layout1.execute)
From: Ken Grimes @ 2026-07-16 20:14 UTC (permalink / raw)
  To: Günther Noack, Mickaël Salaün
  Cc: linux-security-module@vger.kernel.org, landlock
In-Reply-To: <20260716.eijohyi0ieZa@digikod.net>


[-- Attachment #1.1: Type: text/plain, Size: 4448 bytes --]

Thanks so much Günther & Mickaël for the fast and thorough responses, it's very appreciated!

Günther, that's exactly what I found. I went down the path of 2; file-scoped LANDLOCK_ACCESS_FS_EXECUTE grant on dynamic loader, discovered at runtime since the loader's path varies across distros (/lib64/ld-linux-x86-64.so.2 here, but that's obviously not portable).

The fix resolves the target binary's PT_INTERP entry from ELF headers, then runs the interpreter in its own --list mode (invoked in a privilege-dropped, sandboxed child) to get the full shared-object closure. Not just the loader itself, but the DT_NEEDED dependencies (/etc/ld.so.cache and /etc/ld.so.preload) when present. I grant Execute|ReadFile on the interpreter and ReadFile on everything else, each file-scoped instead of directory wide, so not opening up all of /lib64. Just the files the loader will actually touch for the binary. I reproduced the failure standalone first (execveat under a single rule landlock domain, EACCESS, adding the loader's file as a second rule) before building the real fix, so I feel confident this is what you're describing.

Regarding the docs, yes please do add a note about this to LANDLOCK_ACCESS_FS_EXECUTE docs! That would have definitely saved a decent amount of effort, and I could see others running into the same issue.

Mickaël, thank you! I hadn't looked into the audit log support closely, I'll be sure to use this instead of building standalone reproductions, very helpful!

Thanks again for the help and all the great work you do.

-Ken


On Thursday, July 16th, 2026 at 2:37 AM, Mickaël Salaün <mic@digikod.net> wrote:

> Hi!
> 

> You should also get a look at the audit logs:
> https://docs.kernel.org/admin-guide/LSM/landlock.html#audit
> 

>  Mickaël
> 

> On Wed, Jul 15, 2026 at 10:39:29AM +0200, Günther Noack wrote:
> > Hello Ken!
> >
> > (Also adding landlock@lists.linux.dev to CC)
> >
> > On Tue, Jul 14, 2026 at 09:07:56PM +0000, Ken Grimes wrote:
> > > Hey all, this is my first bug report for linux. The issue was discovered 

> > > alongside llm-assisted coding on a downstream project. The 

> > > investigation/testing of the bug was a mostly manual process so I could 

> > > be sure this was something real. Please let me know if I can provide any
> > > further details or assistance. Hope this is helpful, thank you for all of your
> > > hard work!
> >
> > Welcome and thanks for reporting your first issue!
> >
> > I believe the issue you are observing is that the /bin/true program you are
> > starting is a dynamically linked executable.  As such, executing it requires
> > both the LANDLOCK_ACCESS_FS_EXECUTE right on the binary itself and on
> > the system's dynamic loader binary, which usually lives in /lib/ld-linux.so.*
> > (but there are symlinks and 32/64-bit differences at play as well, which
> > influence the actual final location).
> >
> > You can try this out with the following experiments:
> >
> > (1) Compile a "true" program statically and try using that:
> >
> >     $ echo 'int main() { return 0; }' > true.c
> >     $ CFLAGS=-static make true
> >
> >     This can be started with the test you have,
> >     unlike the dynamically linked version.
> >
> > (2) Alternatively, add execute permissions for the dynamic loader:
> >
> >     Add an additional "path beneath" rule that allow-lists the execution
> >     access right on /lib/ld-linux.so.2, /lib64/ld-linux-x86-64.so.2 or
> >     wherever else your dynamic loader is.  (You can discover the actual
> >     location using "ldd /bin/true".)
> >
> > With either one of these two changes, your standalone reproducer program
> > starts working again.  Or at least it does on my machine.  If it still doesn't
> > work on your end that way, please let us know. :)
> >
> > I admit that we should probably point this out in the Landlock documentation
> > for the LANDLOCK_ACCESS_FS_EXECUTE right, as dynamic linking is common and it
> > is a potential issue that many people might run into.
> >
> > For background on the dynamic loading mechanism, see the man page ld.so(8) [1]
> > and the LWN article "How programs get run: ELF binaries" [2] (specifically the
> > section "Dynamically linked programs").
> >
> > —Günther
> >
> >
> > [1] https://man7.org/linux/man-pages/man8/ld.so.8.html
> > [2] https://lwn.net/Articles/631631/
> >
> >
> 

[-- Attachment #1.2: publickey - ken@kengrimes.com - 0xEDFCC5BC.asc --]
[-- Type: application/pgp-keys, Size: 645 bytes --]

[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 343 bytes --]

^ permalink raw reply

* [GIT PULL] Landlock fix for v7.2-rc4
From: Mickaël Salaün @ 2026-07-16 19:57 UTC (permalink / raw)
  To: Linus Torvalds
  Cc: Mickaël Salaün, Günther Noack, Miguel Ojeda,
	Matthieu Buffet, Thomas Huth, linux-kernel, linux-security-module

Hi,

This PR fixes TCP Fast Open support, specific test environments, and doc
warnings.

Please pull these changes for v7.2-rc4 .  These commits merge cleanly
with your master branch.  Most kernel changes have been tested in the
latest linux-next releases for some weeks.

Test coverage for security/landlock is 91.8% of 2357 lines according to
LLVM 22, and it was 91.7% of 2351 lines before this PR.

Regards,
 Mickaël

--
The following changes since commit dc59e4fea9d83f03bad6bddf3fa2e52491777482:

  Linux 7.2-rc1 (2026-06-28 12:01:31 -0700)

are available in the Git repository at:

  https://git.kernel.org/pub/scm/linux/kernel/git/mic/linux.git tags/landlock-7.2-rc4

for you to fetch changes up to 5ab1dc6d110db6bee167a32fd94c53ea0e7ad6d2:

  selftests/landlock: Skip scoped_signal subtest with MSG_OOB if not available (2026-07-10 12:59:11 +0200)

----------------------------------------------------------------
Landlock fix for v7.2-rc4

----------------------------------------------------------------
Matthieu Buffet (2):
      landlock: Fix TCP Fast Open connection bypass
      selftests/landlock: Add test for TCP fast open

Mickaël Salaün (2):
      landlock: Fix kernel-doc for the nested quiet layer flag
      landlock: Update formatting

Thomas Huth (2):
      selftests/landlock: Fix screwed up pointers in the scoped_signal_test
      selftests/landlock: Skip scoped_signal subtest with MSG_OOB if not available

 security/landlock/net.c                            |  8 ++
 security/landlock/ruleset.h                        |  4 +-
 security/landlock/task.c                           |  9 +-
 tools/testing/selftests/landlock/net_test.c        | 97 ++++++++++++++++++++++
 .../selftests/landlock/scoped_signal_test.c        | 67 ++++++++++-----
 5 files changed, 154 insertions(+), 31 deletions(-)

^ permalink raw reply

* Re: [GIT PULL] Landlock fix for v7.2-rc4
From: pr-tracker-bot @ 2026-07-16 20:25 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Linus Torvalds, Mickaël Salaün, Günther Noack,
	Miguel Ojeda, Matthieu Buffet, Thomas Huth, linux-kernel,
	linux-security-module
In-Reply-To: <20260716195752.2326332-1-mic@digikod.net>

The pull request you sent on Thu, 16 Jul 2026 21:57:52 +0200:

> https://git.kernel.org/pub/scm/linux/kernel/git/mic/linux.git tags/landlock-7.2-rc4

has been merged into torvalds/linux.git:
https://git.kernel.org/torvalds/c/481ed5dd3ed7136f627b8ec372ba39f5b2e7d27f

Thank you!

-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/prtracker.html

^ permalink raw reply

* Re: [PATCH v5 1/3] security: rework inode_init_security xattr handling
From: Paul Moore @ 2026-07-16 21:55 UTC (permalink / raw)
  To: David Windsor, Alexei Starovoitov, Daniel Borkmann,
	Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman, Song Liu,
	Yonghong Song, John Fastabend, KP Singh, Jiri Olsa,
	Kumar Kartikeya Dwivedi, Emil Tsalapatis, Matt Bobrowski,
	James Morris, Serge E . Hallyn, Casey Schaufler, Stephen Smalley,
	Ondrej Mosnacek, Mimi Zohar, Roberto Sassu, Dmitry Kasatkin,
	Eric Snowberg, Alexander Viro, Christian Brauner, Jan Kara,
	Shuah Khan
  Cc: bpf, linux-security-module, linux-fsdevel, linux-integrity,
	selinux, linux-kselftest, linux-kernel, David Windsor
In-Reply-To: <20260708000956.46138-2-dwindsor@gmail.com>

On Jul  7, 2026 David Windsor <dwindsor@gmail.com> wrote:
> 
> In preparation for bpf_init_inode_xattr(), a kfunc that lets bpf LSM
> programs atomically label new inodes, rework how inode_init_security
> xattrs are managed.
> 
> inode_init_security receives the LSM xattr array and its count as
> separate parameters. For better compatibility with the bpf verifier,
> update inode_init_security and its callers to consolidate these
> parameters into a single context object: struct lsm_xattrs.
> 
> Also, add security_lsmxattr_add(), which claims a slot in the
> inode_init_security xattr array on behalf of the calling LSM and
> fills it with a copy of the given name and value.
> 
> Suggested-by: Paul Moore <paul@paul-moore.com>
> Signed-off-by: David Windsor <dwindsor@gmail.com>
> ---
>  include/linux/bpf_lsm.h           |   3 +
>  include/linux/evm.h               |   9 +--
>  include/linux/lsm_hook_defs.h     |   4 +-
>  include/linux/lsm_hooks.h         |  16 ++---
>  include/linux/security.h          |  15 +++++
>  security/bpf/hooks.c              |   1 +
>  security/integrity/evm/evm_main.c |   8 ++-
>  security/security.c               | 108 ++++++++++++++++++++++++++----
>  security/selinux/hooks.c          |   4 +-
>  security/smack/smack_lsm.c        |  27 ++++----
>  10 files changed, 148 insertions(+), 47 deletions(-)

...

> diff --git a/include/linux/security.h b/include/linux/security.h
> index 153e9043058f..647f7b88358b 100644
> --- a/include/linux/security.h
> +++ b/include/linux/security.h
> @@ -68,6 +68,11 @@ struct watch;
>  struct watch_notification;
>  struct lsm_ctx;
>  
> +struct lsm_xattrs {
> +	struct xattr *xattrs;
> +	unsigned int xattr_count;
> +};

Please separate out the 'struct lsm_xattrs' related changes into a
separate patch from the security_lsmxattr_add() changes.  I know they
are related, but they are different things.

> diff --git a/security/security.c b/security/security.c
> index 71aea8fdf014..261f68e17cfd 100644
> --- a/security/security.c
> +++ b/security/security.c
> @@ -12,6 +12,7 @@
>  #define pr_fmt(fmt) "LSM: " fmt
>  
>  #include <linux/bpf.h>
> +#include <linux/bpf_lsm.h>
>  #include <linux/capability.h>
>  #include <linux/dcache.h>
>  #include <linux/export.h>
> @@ -1333,8 +1334,8 @@ int security_inode_init_security(struct inode *inode, struct inode *dir,
>  				 const initxattrs initxattrs, void *fs_data)
>  {
>  	struct lsm_static_call *scall;
> -	struct xattr *new_xattrs = NULL;
> -	int ret = -EOPNOTSUPP, xattr_count = 0;
> +	struct lsm_xattrs xattrs = {};
> +	int ret = -EOPNOTSUPP;
>  
>  	if (unlikely(IS_PRIVATE(inode)))
>  		return 0;
> @@ -1344,15 +1345,15 @@ int security_inode_init_security(struct inode *inode, struct inode *dir,
>  
>  	if (initxattrs) {
>  		/* Allocate +1 as terminator. */
> -		new_xattrs = kcalloc(blob_sizes.lbs_xattr_count + 1,
> -				     sizeof(*new_xattrs), GFP_NOFS);
> -		if (!new_xattrs)
> +		xattrs.xattrs = kcalloc(blob_sizes.lbs_xattr_count + 1,
> +					sizeof(*xattrs.xattrs), GFP_NOFS);
> +		if (!xattrs.xattrs)
>  			return -ENOMEM;
>  	}
>  
>  	lsm_for_each_hook(scall, inode_init_security) {
> -		ret = scall->hl->hook.inode_init_security(inode, dir, qstr, new_xattrs,
> -						  &xattr_count);
> +		ret = scall->hl->hook.inode_init_security(inode, dir, qstr,
> +							  &xattrs);
>  		if (ret && ret != -EOPNOTSUPP)
>  			goto out;
>  		/*
> @@ -1364,18 +1365,101 @@ int security_inode_init_security(struct inode *inode, struct inode *dir,
>  	}
>  
>  	/* If initxattrs() is NULL, xattr_count is zero, skip the call. */
> -	if (!xattr_count)
> +	if (!xattrs.xattr_count)
>  		goto out;
>  
> -	ret = initxattrs(inode, new_xattrs, fs_data);
> +	ret = initxattrs(inode, xattrs.xattrs, fs_data);
>  out:
> -	for (; xattr_count > 0; xattr_count--)
> -		kfree(new_xattrs[xattr_count - 1].value);
> -	kfree(new_xattrs);
> +	for (; xattrs.xattr_count > 0; xattrs.xattr_count--)
> +		kfree(xattrs.xattrs[xattrs.xattr_count - 1].value);
> +	kfree(xattrs.xattrs);
>  	return (ret == -EOPNOTSUPP) ? 0 : ret;
>  }
>  EXPORT_SYMBOL(security_inode_init_security);
>  
> +#ifdef CONFIG_BPF_LSM
> +static unsigned int lsm_xattrs_used(const struct lsm_xattrs *xattrs,
> +				    const char *prefix)
> +{
> +	size_t prefix_len = strlen(prefix);
> +	unsigned int i, n = 0;
> +
> +	for (i = 0; i < xattrs->xattr_count; i++) {
> +		const char *name = xattrs->xattrs[i].name;
> +
> +		if (name && !strncmp(name, prefix, prefix_len))
> +			n++;
> +	}
> +	return n;
> +}
> +#endif /* CONFIG_BPF_LSM */

More on this below, but this function isn't strictly BPF LSM related so
let's drop the CONFIG_BPF_LSM macro bracketing.

> +/**
> + * security_lsmxattr_add() - Add an xattr during inode_init_security
> + * @xattrs: xattr state shared by inode_init_security hooks
> + * @lsm_id: LSM_ID_* value identifying the calling LSM
> + * @name: xattr name suffix

For BPF you currently pass "bpf.foo" for the name and other LSMs would
pass their own xattr name, likely without a LSM specific sub-namespace
(for example, SELinux would be just "selinux").  In both cases the
'name' parameter always starts with a well known suffix as defined by
the 'lsm_id" parameter.

Since we are already passing the lsm_id parameter, let's do away with
the standard LSM suffixes, e.g. XATTR_BPF_LSM_SUFFIX, and just pass in
any additional name components.  For example, instead of passing
"bpf.foo" in the BPF LSM case, you would just pass "foo"; LSMs without
their own sub-namespace, e.g. SELinux, would pass NULL for the name
parameter as XATTR_SELINUX_SUFFIX is all that is needed.  While doing
this I would also suggest changing the name of the 'name' parameter to
'name_extra', 'namespace_extra', or something similar to indicate that
it isn't the full name, but rather an additional suffix beyond the
standard suffix associated with the given LSM.

> + * @value: xattr value
> + * @value_len: length of @value
> + *
> + * Claim an xattr slot in @xattrs on behalf of the LSM identified by
> + * @lsm_id and fill it with a copy of @name and @value. Callers can invoke
> + * this function from non-sleepable context.
> + *
> + * Return: Returns 0 on success, -ENOSPC if the calling LSM's slot budget
> + *         is exhausted, negative values on other errors.
> + */
> +int security_lsmxattr_add(struct lsm_xattrs *xattrs, u64 lsm_id,
> +			  const char *name, const void *value,
> +			  size_t value_len)
> +{
> +	struct xattr *xattr;
> +	void *xattr_value;
> +	size_t name_len;
> +
> +	if (!xattrs || !xattrs->xattrs || !name || !value)
> +		return -EINVAL;

Sashiko raised a good point about xattrs->xattrs being NULL not
necessarily being a good reason for -EINVAL.  If xattrs is NULL, yes,
something has gone wrong and -EINVAL seems reasonable, but the
xattr->xattrs NULL case does seem like it should simply return early
with a value of 0 (see SELinux's handling of this case as an example).

> +	name_len = strlen(name);
> +	if (name_len == 0 || name_len > XATTR_NAME_MAX)
> +		return -EINVAL;
> +	if (value_len == 0 || value_len > XATTR_SIZE_MAX)
> +		return -EINVAL;
> +
> +	switch (lsm_id) {
> +#ifdef CONFIG_BPF_LSM
> +	case LSM_ID_BPF:
> +		if (lsm_xattrs_used(xattrs, XATTR_BPF_LSM_SUFFIX) >=
> +		    BPF_LSM_INODE_INIT_XATTRS)
> +			return -ENOSPC;
> +		break;
> +#endif /* CONFIG_BPF_LSM */

I like to avoid macro conditional code inside functions whenever
possible, and I think this is a case where we could avoid the conditional
block with a little work.

The LSM_ID_BPF macro is already defined as part of the UAPI so that will
always be available.  While BPF_LSM_INODE_INIT_XATTRS is dependent on
CONFIG_BPF_LSM in this revision, that should be easy enough to move
outside the CONFIG_BPF_LSM conditional in bpf_lsm.h.  Eventually we
should probably expand the lsm_id struct to carry this info, likely just
a permanent/local copy of the LSM's lsm_blob_sizes passed during
registration, but I can take care of that later; just make the
BPF_LSM_INODE_INIT_XATTRS macro always accessible now.

We should also probably record the xattr suffix/length when the LSM is
registered, but that can also be done later with the other lsm_id
additions.

> +	default:
> +		return -EINVAL;
> +	}
> +
> +	/* Combine xattr value + name into one allocation. */
> +	xattr_value = kmalloc(value_len + name_len + 1, GFP_NOWAIT);
> +	if (!xattr_value)
> +		return -ENOMEM;
> +
> +	memcpy(xattr_value, value, value_len);
> +	memcpy(xattr_value + value_len, name, name_len);
> +	((char *)xattr_value)[value_len + name_len] = '\0';

You'll need to add an additional memcpy() here, likely in a switch
statement to handle the different LSMs, to copy over the LSM specific
prefix.  It's a little ugly, but when we have things captured in the
lsm_id struct it will get a lot cleaner.

> +	xattr = lsm_get_xattr_slot(xattrs);
> +	if (!xattr) {
> +		kfree(xattr_value);
> +		return -ENOSPC;
> +	}
> +
> +	xattr->value = xattr_value;
> +	xattr->name = (const char *)xattr_value + value_len;
> +	xattr->value_len = value_len;
> +
> +	return 0;
> +}

--
paul-moore.com

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox