Linux Security Modules development
 help / color / mirror / Atom feed
* [PATCH v2 3/3] landlock: Document LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
From: Justin Suess @ 2026-07-17 22:03 UTC (permalink / raw)
  To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
In-Reply-To: <20260717220320.1030123-1-utilityemal77@gmail.com>

Document atomically setting no_new_privs with ruleset enforcement,
following the same compatibility section style as previous ABI
additions.

Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
 Documentation/userspace-api/landlock.rst | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 5a63d4476c1c..ec87d35f4715 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -8,7 +8,7 @@ Landlock: unprivileged access control
 =====================================
 
 :Author: Mickaël Salaün
-:Date: June 2026
+:Date: July 2026
 
 The goal of Landlock is to enable restriction of ambient rights (e.g. global
 filesystem or network access) for a set of processes.  Because Landlock
@@ -789,6 +789,18 @@ when at least one sys_landlock_add_rule() call is made for it with the
 ``LANDLOCK_ADD_RULE_QUIET`` flag, additional add-rule calls for the same
 object without this flag do not clear it.
 
+Atomic no_new_privs (ABI < 11)
+------------------------------
+
+Starting with the Landlock ABI version 11, sys_landlock_restrict_self()
+accepts the ``LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS`` flag, which sets the
+no_new_privs attribute of the calling thread atomically with the enforcement
+of the ruleset: no_new_privs is set if and only if the call succeeds.  This
+removes the need for a prior :manpage:`prctl(2)` ``PR_SET_NO_NEW_PRIVS``
+call, and with it the ``CAP_SYS_ADMIN`` requirement.  When combined with
+``LANDLOCK_RESTRICT_SELF_TSYNC``, no_new_privs is set on all threads of the
+process.
+
 .. _kernel_support:
 
 Kernel support
-- 
2.54.0


^ permalink raw reply related

* [PATCH v2 2/3] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
From: Justin Suess @ 2026-07-17 22:03 UTC (permalink / raw)
  To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
In-Reply-To: <20260717220320.1030123-1-utilityemal77@gmail.com>

Check that a successful landlock_restrict_self(2) call with
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS sets no_new_privs without a prior
prctl(2) call nor CAP_SYS_ADMIN, that a failed call leaves the attribute
unchanged, and that LANDLOCK_RESTRICT_SELF_TSYNC extends it to sibling
threads.  Also check that this flag requires a ruleset, and update the
restrict_self_checks_ordering EPERM checks since this flag is now
checked before the flags validity.

Update the ABI version and last-flag checks accordingly.

Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
 tools/testing/selftests/landlock/base_test.c  | 65 +++++++++++++++++--
 tools/testing/selftests/landlock/tsync_test.c | 33 ++++++++++
 2 files changed, 93 insertions(+), 5 deletions(-)

diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index cbd3c1669951..2d4903588903 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -76,7 +76,7 @@ TEST(abi_version)
 	const struct landlock_ruleset_attr ruleset_attr = {
 		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
 	};
-	ASSERT_EQ(10, landlock_create_ruleset(NULL, 0,
+	ASSERT_EQ(11, landlock_create_ruleset(NULL, 0,
 					      LANDLOCK_CREATE_RULESET_VERSION));
 
 	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
@@ -255,8 +255,15 @@ TEST(restrict_self_checks_ordering)
 
 	/* Checks unprivileged enforcement without no_new_privs. */
 	drop_caps(_metadata);
-	ASSERT_EQ(-1, landlock_restrict_self(-1, -1));
+	ASSERT_EQ(-1, landlock_restrict_self(
+			      -1, ~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
 	ASSERT_EQ(EPERM, errno);
+	/*
+	 * LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills the no_new_privs /
+	 * CAP_SYS_ADMIN requirement, so the invalid flags are checked first.
+	 */
+	ASSERT_EQ(-1, landlock_restrict_self(-1, -1));
+	ASSERT_EQ(EINVAL, errno);
 	ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
 	ASSERT_EQ(EPERM, errno);
 	ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, 0));
@@ -288,7 +295,7 @@ TEST(restrict_self_fd)
 	EXPECT_EQ(EBADFD, errno);
 }
 
-TEST(restrict_self_fd_logging_flags)
+TEST(restrict_self_fd_flags)
 {
 	int fd;
 
@@ -302,11 +309,16 @@ TEST(restrict_self_fd_logging_flags)
 	EXPECT_EQ(-1, landlock_restrict_self(
 			      fd, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
 	EXPECT_EQ(EBADFD, errno);
+
+	/* LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS requires a ruleset FD. */
+	EXPECT_EQ(-1, landlock_restrict_self(
+			      fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+	EXPECT_EQ(EBADFD, errno);
 }
 
-TEST(restrict_self_logging_flags)
+TEST(restrict_self_flags)
 {
-	const __u32 last_flag = LANDLOCK_RESTRICT_SELF_TSYNC;
+	const __u32 last_flag = LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
 
 	/* Tests invalid flag combinations. */
 
@@ -349,6 +361,18 @@ TEST(restrict_self_logging_flags)
 				      LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON));
 	EXPECT_EQ(EBADF, errno);
 
+	/* LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS requires a ruleset FD. */
+
+	EXPECT_EQ(-1, landlock_restrict_self(
+			      -1, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+	EXPECT_EQ(EBADF, errno);
+
+	EXPECT_EQ(-1,
+		  landlock_restrict_self(
+			  -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+				      LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+	EXPECT_EQ(EBADF, errno);
+
 	/* Tests with an invalid ruleset_fd. */
 
 	EXPECT_EQ(-1, landlock_restrict_self(
@@ -359,6 +383,37 @@ TEST(restrict_self_logging_flags)
 			     -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
 }
 
+TEST(restrict_self_no_new_privs)
+{
+	const struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
+	};
+	const int ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+
+	ASSERT_LE(0, ruleset_fd);
+
+	/*
+	 * The calling thread does not need CAP_SYS_ADMIN nor an explicit
+	 * prctl(2) PR_SET_NO_NEW_PRIVS call.
+	 */
+	drop_caps(_metadata);
+	ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+	/* Checks that a failed call does not set no_new_privs. */
+	EXPECT_EQ(-1, landlock_restrict_self(
+			      -1, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+	EXPECT_EQ(EBADF, errno);
+	EXPECT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+	/* Checks that a successful call sets no_new_privs. */
+	ASSERT_EQ(0, landlock_restrict_self(
+			     ruleset_fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+	EXPECT_EQ(1, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+	EXPECT_EQ(0, close(ruleset_fd));
+}
+
 TEST(ruleset_fd_io)
 {
 	struct landlock_ruleset_attr ruleset_attr = {
diff --git a/tools/testing/selftests/landlock/tsync_test.c b/tools/testing/selftests/landlock/tsync_test.c
index 9cf1491bbaaf..d5336186b2c7 100644
--- a/tools/testing/selftests/landlock/tsync_test.c
+++ b/tools/testing/selftests/landlock/tsync_test.c
@@ -90,6 +90,39 @@ TEST(multi_threaded_success)
 	EXPECT_EQ(0, close(ruleset_fd));
 }
 
+TEST(multi_threaded_no_new_privs)
+{
+	pthread_t t1, t2;
+	bool no_new_privs1, no_new_privs2;
+	const int ruleset_fd = create_ruleset(_metadata);
+
+	disable_caps(_metadata);
+
+	ASSERT_EQ(0, pthread_create(&t1, NULL, idle, &no_new_privs1));
+	ASSERT_EQ(0, pthread_create(&t2, NULL, idle, &no_new_privs2));
+
+	/* No prior prctl(2) PR_SET_NO_NEW_PRIVS call. */
+	ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+	EXPECT_EQ(0, landlock_restrict_self(
+			     ruleset_fd,
+			     LANDLOCK_RESTRICT_SELF_TSYNC |
+				     LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+
+	EXPECT_EQ(1, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+	ASSERT_EQ(0, pthread_cancel(t1));
+	ASSERT_EQ(0, pthread_cancel(t2));
+	ASSERT_EQ(0, pthread_join(t1, NULL));
+	ASSERT_EQ(0, pthread_join(t2, NULL));
+
+	/* The no_new_privs flag was enabled on all threads. */
+	EXPECT_TRUE(no_new_privs1);
+	EXPECT_TRUE(no_new_privs2);
+
+	EXPECT_EQ(0, close(ruleset_fd));
+}
+
 TEST(multi_threaded_success_despite_diverging_domains)
 {
 	pthread_t t1, t2;
-- 
2.54.0


^ permalink raw reply related

* [PATCH v2 1/3] landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
From: Justin Suess @ 2026-07-17 22:03 UTC (permalink / raw)
  To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
In-Reply-To: <20260717220320.1030123-1-utilityemal77@gmail.com>

Add a landlock_restrict_self(2) flag to set the no_new_privs attribute
of the calling thread atomically with the enforcement of the ruleset:
no_new_privs is set if and only if the call succeeds.  This removes the
need for a prior prctl(2) PR_SET_NO_NEW_PRIVS call and guarantees that a
failed enforcement leaves the attribute unchanged.

Because no_new_privs is set by the call itself, the no_new_privs /
CAP_SYS_ADMIN requirement of landlock_restrict_self(2) is fulfilled by
construction, and the related EPERM check is skipped.  As a consequence,
an unprivileged caller passing unknown flags along with this flag gets
EINVAL instead of EPERM.

The attribute is only set past the last point of failure, just before
committing the new credentials.  When combined with
LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on the sibling threads
as well, in their commit phase, with the same atomicity.

Bump the Landlock ABI version to 11.

Cc: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
 include/uapi/linux/landlock.h | 13 +++++++++++++
 security/landlock/limits.h    |  2 +-
 security/landlock/syscalls.c  | 28 +++++++++++++++++++++-------
 security/landlock/tsync.c     |  8 ++++++--
 security/landlock/tsync.h     |  4 +++-
 5 files changed, 44 insertions(+), 11 deletions(-)

diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 272f047df438..77820e430ab8 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -191,12 +191,25 @@ struct landlock_ruleset_attr {
  *
  *     If the calling thread is running with no_new_privs, this operation
  *     enables no_new_privs on the sibling threads as well.
+ *
+ * The following flag ties the no_new_privs attribute to the ruleset
+ * enforcement:
+ *
+ * %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
+ *     Sets the no_new_privs attribute of the calling thread atomically with
+ *     the enforcement of the ruleset: no_new_privs is set if and only if
+ *     sys_landlock_restrict_self() succeeds.  This removes the need for a
+ *     prior :manpage:`prctl(2)` ``PR_SET_NO_NEW_PRIVS`` call, and with it the
+ *     %CAP_SYS_ADMIN requirement.  This flag requires a ruleset.  When
+ *     combined with %LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on the
+ *     sibling threads as well.
  */
 /* clang-format off */
 #define LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF		(1U << 0)
 #define LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON			(1U << 1)
 #define LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF		(1U << 2)
 #define LANDLOCK_RESTRICT_SELF_TSYNC				(1U << 3)
+#define LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS			(1U << 4)
 /* clang-format on */
 
 /**
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 08d5f2f6d321..1a7c5fb8f6fd 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -34,7 +34,7 @@
 #define LANDLOCK_NUM_ACCESS_MAX \
 	MAX(MAX(LANDLOCK_NUM_ACCESS_FS, LANDLOCK_NUM_ACCESS_NET), LANDLOCK_NUM_SCOPE)
 
-#define LANDLOCK_LAST_RESTRICT_SELF	LANDLOCK_RESTRICT_SELF_TSYNC
+#define LANDLOCK_LAST_RESTRICT_SELF	LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
 #define LANDLOCK_MASK_RESTRICT_SELF	((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1)
 
 /* clang-format on */
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 36b02892c62f..36b8a3fb506f 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -169,7 +169,7 @@ static const struct file_operations ruleset_fops = {
  * If the change involves a fix that requires userspace awareness, also update
  * the errata documentation in Documentation/userspace-api/landlock.rst .
  */
-const int landlock_abi_version = 10;
+const int landlock_abi_version = 11;
 
 /**
  * sys_landlock_create_ruleset - Create a new ruleset
@@ -502,21 +502,28 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
  *         - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON
  *         - %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF
  *         - %LANDLOCK_RESTRICT_SELF_TSYNC
+ *         - %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
  *
  * This system call enforces a Landlock ruleset on the current thread.
  * Enforcing a ruleset requires that the task has %CAP_SYS_ADMIN in its
  * namespace or is running with no_new_privs.  This avoids scenarios where
  * unprivileged tasks can affect the behavior of privileged children.
  *
+ * With %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS, the no_new_privs attribute of the
+ * calling thread is set atomically with the enforcement of the ruleset, which
+ * fulfills the above requirement: no_new_privs is set if and only if the call
+ * succeeds.
+ *
  * Return: 0 on success, or -errno on failure.  Possible returned errors are:
  *
  * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
  * - %EINVAL: @flags contains an unknown bit.
  * - %EBADF: @ruleset_fd is not a file descriptor for the current thread;
  * - %EBADFD: @ruleset_fd is not a ruleset file descriptor;
- * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or the
- *   current thread is not running with no_new_privs, or it doesn't have
- *   %CAP_SYS_ADMIN in its namespace.
+ * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or
+ *   %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS is not set while the current thread
+ *   is not running with no_new_privs and doesn't have %CAP_SYS_ADMIN in its
+ *   namespace.
  * - %E2BIG: The maximum number of stacked rulesets is reached for the current
  *   thread.
  *
@@ -529,6 +536,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 	struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
 	struct cred *new_cred;
 	struct landlock_cred_security *new_llcred;
+	const bool set_no_new_privs =
+		!!(flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS);
 	bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
 		prev_log_subdomains;
 
@@ -537,9 +546,10 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 
 	/*
 	 * Similar checks as for seccomp(2), except that an -EPERM may be
-	 * returned.
+	 * returned.  LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills this
+	 * requirement.
 	 */
-	if (!task_no_new_privs(current) &&
+	if (!set_no_new_privs && !task_no_new_privs(current) &&
 	    !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
 		return -EPERM;
 
@@ -620,12 +630,16 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 
 	if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
 		const int err = landlock_restrict_sibling_threads(
-			current_cred(), new_cred);
+			current_cred(), new_cred, flags);
 		if (err) {
 			abort_creds(new_cred);
 			return err;
 		}
 	}
 
+	/* Sets no_new_privs past the last point of failure. */
+	if (set_no_new_privs)
+		task_set_no_new_privs(current);
+
 	return commit_creds(new_cred);
 }
diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c
index c5730bbd9ed3..0b71e158c3f5 100644
--- a/security/landlock/tsync.c
+++ b/security/landlock/tsync.c
@@ -17,6 +17,7 @@
 #include <linux/sched/task.h>
 #include <linux/slab.h>
 #include <linux/task_work.h>
+#include <uapi/linux/landlock.h>
 
 #include "cred.h"
 #include "tsync.h"
@@ -466,7 +467,8 @@ static void cancel_tsync_works(const struct tsync_works *works,
  * restrict_sibling_threads - enables a Landlock policy for all sibling threads
  */
 int landlock_restrict_sibling_threads(const struct cred *old_cred,
-				      const struct cred *new_cred)
+				      const struct cred *new_cred,
+				      const u32 restrict_flags)
 {
 	int err;
 	struct tsync_shared_context shared_ctx;
@@ -481,7 +483,9 @@ int landlock_restrict_sibling_threads(const struct cred *old_cred,
 	init_completion(&shared_ctx.all_finished);
 	shared_ctx.old_cred = old_cred;
 	shared_ctx.new_cred = new_cred;
-	shared_ctx.set_no_new_privs = task_no_new_privs(current);
+	shared_ctx.set_no_new_privs =
+		(restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) ||
+		task_no_new_privs(current);
 
 	/*
 	 * Serialize concurrent TSYNC operations to prevent deadlocks when
diff --git a/security/landlock/tsync.h b/security/landlock/tsync.h
index ef86bb61c2f6..2ae4f938ca00 100644
--- a/security/landlock/tsync.h
+++ b/security/landlock/tsync.h
@@ -9,8 +9,10 @@
 #define _SECURITY_LANDLOCK_TSYNC_H
 
 #include <linux/cred.h>
+#include <linux/types.h>
 
 int landlock_restrict_sibling_threads(const struct cred *old_cred,
-				      const struct cred *new_cred);
+				      const struct cred *new_cred,
+				      u32 restrict_flags);
 
 #endif /* _SECURITY_LANDLOCK_TSYNC_H */
-- 
2.54.0


^ permalink raw reply related

* [PATCH v2 0/3] Implement LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
From: Justin Suess @ 2026-07-17 22:03 UTC (permalink / raw)
  To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess

Howdy

This series adds a new landlock_restrict_self(2) flag:
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS.

This is a redesign of v1 [1], which added
LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC, a flag staging no_new_privs so that
it was only set at the next execve(2).  Following Mickaël's feedback,
[2] the staging mechanism (credential bit and bprm_committing_creds hook)
is dropped entirely.  The new flag instead sets the no_new_privs
attribute of the calling thread atomically with the enforcement of the
ruleset: no_new_privs is set if and only if the
landlock_restrict_self(2) call succeeds.

Semantics:

A single call replaces the usual prctl(PR_SET_NO_NEW_PRIVS) +
landlock_restrict_self(2) pair.  Because no_new_privs is set by the
call itself, the no_new_privs/CAP_SYS_ADMIN precondition is fulfilled
by construction, so the flag is usable by unprivileged processes.  This
is safe for the same reason the prctl(2) pair is: the executed programs
can either gain privileges or be restricted, never both.

The two states cannot diverge.  A failed call (invalid ruleset FD,
E2BIG, ENOMEM, interrupted TSYNC, ...) leaves no_new_privs unchanged,
and a successful call never returns without no_new_privs set: the
attribute is set past the last point of failure, right before
commit_creds(), which cannot fail.

Combined with LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on all
threads with the same guarantee: each sibling thread sets it in the
commit phase of the TSYNC protocol, after its all-or-nothing barrier,
so either every thread gets both the domain and no_new_privs, or none
does.  This also makes it possible to atomically set no_new_privs
process-wide, which prctl(2) cannot do.

Unlike the v1 flag, this flag requires a ruleset: calls with a
ruleset_fd of -1 are rejected.  As a consequence of the fulfilled
precondition, an unprivileged caller passing unknown flag bits together
with this flag receives EINVAL instead of EPERM; the selftests pin this
error ordering as well.

The reason why -1 ruleset_fd is rejected is basically then we are
making a Landlock-flaved prctl(nnp) call that doesn't do anything
special. It seems better to be able to have the option to define
behavior later rather than have a useless feature stuck in the
syscall abi. So we reject the -1 ruleset_fd for now.

The Landlock ABI version is bumped to 11.

Test coverage:

base_test checks that a successful call sets no_new_privs without a
prior prctl(2) nor CAP_SYS_ADMIN, that a failed call leaves it
unchanged, that the flag requires a ruleset FD, and the updated
EPERM/EINVAL ordering.  tsync_test checks that TSYNC sets no_new_privs
on sibling threads along with the domain.

Changes since v1:

- Renamed LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC to
  LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS, per Mickaël's feedback.
- Dropped the staged "on exec" design: removed the credential bit and
  the bprm_committing_creds hook; no_new_privs is now set atomically
  with the ruleset enforcement.
- The flag now fulfills the no_new_privs/CAP_SYS_ADMIN precondition,
  a significant departure from the v1.
- The flag now requires a ruleset (no ruleset_fd = -1). Otherwise such
  a call would just == a vanilla prctl no-new-privs call. This is left
  in case we want to repurpose it later rather than defining useless
  redundant behavior.

[1] https://lore.kernel.org/linux-security-module/20260708133928.852999-1-utilityemal77@gmail.com/
[2] https://lore.kernel.org/linux-security-module/20260709.Eaphooyoh6sh@digikod.net/

Justin Suess (3):
  landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
  selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
  landlock: Document LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS

 Documentation/userspace-api/landlock.rst      | 14 +++-
 include/uapi/linux/landlock.h                 | 13 ++++
 security/landlock/limits.h                    |  2 +-
 security/landlock/syscalls.c                  | 28 ++++++--
 security/landlock/tsync.c                     |  8 ++-
 security/landlock/tsync.h                     |  4 +-
 tools/testing/selftests/landlock/base_test.c  | 65 +++++++++++++++++--
 tools/testing/selftests/landlock/tsync_test.c | 33 ++++++++++
 8 files changed, 150 insertions(+), 17 deletions(-)


base-commit: 55f82176ef8dde632ea3eb94a6224950ed809d7c
-- 
2.54.0


^ permalink raw reply

* Re: [PATCH] apparmor: leverage audit_log_n_untrustedstring() when possible
From: Paul Moore @ 2026-07-17 21:57 UTC (permalink / raw)
  To: Georgia Garcia; +Cc: John Johansen, linux-security-module
In-Reply-To: <CAHC9VhRcJGj3Pnrpmpi0uRi3ndcMVGGQorsU4dJ-vA_uQmRuQA@mail.gmail.com>

On Fri, Jul 17, 2026 at 5:54 PM Paul Moore <paul@paul-moore.com> wrote:
> On Fri, Jul 17, 2026 at 5:52 PM Paul Moore <paul@paul-moore.com> wrote:
> >
> > Make use of the audit_log_n_untrustedstring() function to simplify the
> > code in aa_label_xaudit().
> >
> > Signed-off-by: Paul Moore <paul@paul-moore.com>
> > ---
> >  security/apparmor/label.c | 5 +----
> >  1 file changed, 1 insertion(+), 4 deletions(-)
>
> Untested beyond a basic compile, but I noticed this while looking at
> something else (unrelated) and wanted to send it to the list before I
> forgot about it ...

My apologies Georgia, I thought I had copied your email into the
original posting but it appears I copied John's email twice into my
posting script (which helpfully de-duped it).

Sorry about that.

> > diff --git a/security/apparmor/label.c b/security/apparmor/label.c
> > index 3fd384d8c41a..a165cadf8249 100644
> > --- a/security/apparmor/label.c
> > +++ b/security/apparmor/label.c
> > @@ -1743,10 +1743,7 @@ void aa_label_xaudit(struct audit_buffer *ab, struct aa_ns *ns,
> >                 str = (char *) label->hname;
> >                 len = strlen(str);
> >         }
> > -       if (audit_string_contains_control(str, len))
> > -               audit_log_n_hex(ab, str, len);
> > -       else
> > -               audit_log_n_string(ab, str, len);
> > +       audit_log_n_untrustedstring(ab, str, len);
> >
> >         kfree(name);
> >  }
> > --
> > 2.55.0

-- 
paul-moore.com

^ permalink raw reply

* Re: [PATCH] apparmor: leverage audit_log_n_untrustedstring() when possible
From: Paul Moore @ 2026-07-17 21:54 UTC (permalink / raw)
  To: linux-security-module; +Cc: John Johansen
In-Reply-To: <20260717215254.383183-2-paul@paul-moore.com>

On Fri, Jul 17, 2026 at 5:52 PM Paul Moore <paul@paul-moore.com> wrote:
>
> Make use of the audit_log_n_untrustedstring() function to simplify the
> code in aa_label_xaudit().
>
> Signed-off-by: Paul Moore <paul@paul-moore.com>
> ---
>  security/apparmor/label.c | 5 +----
>  1 file changed, 1 insertion(+), 4 deletions(-)

Untested beyond a basic compile, but I noticed this while looking at
something else (unrelated) and wanted to send it to the list before I
forgot about it ...

> diff --git a/security/apparmor/label.c b/security/apparmor/label.c
> index 3fd384d8c41a..a165cadf8249 100644
> --- a/security/apparmor/label.c
> +++ b/security/apparmor/label.c
> @@ -1743,10 +1743,7 @@ void aa_label_xaudit(struct audit_buffer *ab, struct aa_ns *ns,
>                 str = (char *) label->hname;
>                 len = strlen(str);
>         }
> -       if (audit_string_contains_control(str, len))
> -               audit_log_n_hex(ab, str, len);
> -       else
> -               audit_log_n_string(ab, str, len);
> +       audit_log_n_untrustedstring(ab, str, len);
>
>         kfree(name);
>  }
> --
> 2.55.0

-- 
paul-moore.com

^ permalink raw reply

* [PATCH] apparmor: leverage audit_log_n_untrustedstring() when possible
From: Paul Moore @ 2026-07-17 21:52 UTC (permalink / raw)
  To: linux-security-module; +Cc: John Johansen

Make use of the audit_log_n_untrustedstring() function to simplify the
code in aa_label_xaudit().

Signed-off-by: Paul Moore <paul@paul-moore.com>
---
 security/apparmor/label.c | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/security/apparmor/label.c b/security/apparmor/label.c
index 3fd384d8c41a..a165cadf8249 100644
--- a/security/apparmor/label.c
+++ b/security/apparmor/label.c
@@ -1743,10 +1743,7 @@ void aa_label_xaudit(struct audit_buffer *ab, struct aa_ns *ns,
 		str = (char *) label->hname;
 		len = strlen(str);
 	}
-	if (audit_string_contains_control(str, len))
-		audit_log_n_hex(ab, str, len);
-	else
-		audit_log_n_string(ab, str, len);
+	audit_log_n_untrustedstring(ab, str, len);
 
 	kfree(name);
 }
-- 
2.55.0


^ permalink raw reply related

* Re: [GIT PULL] selinux/selinux-pr-20260717
From: pr-tracker-bot @ 2026-07-17 20:17 UTC (permalink / raw)
  To: Paul Moore; +Cc: Linus Torvalds, selinux, linux-security-module, linux-kernel
In-Reply-To: <fa44478822ef189fb7a5ce16135dadd8@paul-moore.com>

The pull request you sent on Fri, 17 Jul 2026 13:03:30 -0400:

> https://git.kernel.org/pub/scm/linux/kernel/git/pcmoore/selinux.git tags/selinux-pr-20260717

has been merged into torvalds/linux.git:
https://git.kernel.org/torvalds/c/8fc574321e59a2484063b2e75016772815038608

Thank you!

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

^ permalink raw reply

* [GIT PULL] selinux/selinux-pr-20260717
From: Paul Moore @ 2026-07-17 17:03 UTC (permalink / raw)
  To: Linus Torvalds; +Cc: selinux, linux-security-module, linux-kernel

Linus,

A single SELinux patch to correct a problem with the overlayfs mmap()
and mprotect() fixes from earlier this year where we inadvertenly
included an additional SELinux execmem permission check on some
operations.  Please merge for an upcoming v7.2-rcX release.

Paul

--
The following changes since commit a13c140cc289c0b7b3770bce5b3ad42ab35074aa:

  Linux 7.2-rc3 (2026-07-12 14:16:39 -0700)

are available in the Git repository at:

  https://git.kernel.org/pub/scm/linux/kernel/git/pcmoore/selinux.git
    tags/selinux-pr-20260717

for you to fetch changes up to 9fe595fad54d4ac6a402edb3f60bec859d52cea6:

  selinux: fix incorrect execmem checks on overlayfs
    (2026-07-14 18:10:20 -0400)

----------------------------------------------------------------
selinux/stable-7.2 PR 20260717
----------------------------------------------------------------

Ondrej Mosnacek (1):
      selinux: fix incorrect execmem checks on overlayfs

 security/selinux/hooks.c |   42 ++++++++++++++++++++++-----------------
 1 file changed, 24 insertions(+), 18 deletions(-)

--
paul-moore.com

^ permalink raw reply

* Re: [PATCH v3 1/3] landlock: Require LANDLOCK_ACCESS_FS_MAKE_WHITEOUT for RENAME_WHITEOUT
From: Günther Noack @ 2026-07-17  9:23 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Christian Brauner, linux-security-module, Paul Moore,
	Amir Goldstein, Miklos Szeredi, Serge Hallyn, Stephen Smalley
In-Reply-To: <aivEo6bV_phFFJg5@google.com>

On Fri, Jun 12, 2026 at 10:34:43AM +0200, Günther Noack wrote:
> On Wed, Jun 10, 2026 at 03:38:56PM +0200, Mickaël Salaün wrote:
> > Making MAKE_CHAR not covering MAKE_WHITEOUT is not addressed (see
> > previous discussion).  MAKE_CHAR should not restrict whiteout creation
> > *if* MAKE_WHITEOUT is handled.
> 
> (This is option (3) from your reply to V1 [1].)
>         
> I am skeptical of this approach, because it complicates how userspace
> needs to deal with this access right.  Consider the following
> scenario: A program wants to install the policy:
> 
>  * DENY  MAKE_WHITEOUT, MAKE_CHAR
>  * ALLOW MAKE_WHITEOUT             in /foo  (path_beneath rule)
> 
> Then, if the kernel ABI predates make-whiteout, with the usual
> best-effort fallback (clearing out the unsupported bits), this ruleset
> becomes:
> 
>  * DENY  MAKE_CHAR
>  * (no ALLOW rule)
> 
> But this ruleset is incorrect, because it denies mknod("/foo/x",
> S_IFCHR | mode, makedev(0, 0)) in /foo, which was explicitly allowed
> in the earlier ruleset.
> 
> So in order to implement the best-effort fallback, I guess userspace
> libraries would now have to take into account whether there are any
> rules where MAKE_WHITEOUT is specifically allowed, and if so, they
> can't restrict MAKE_CHAR either?  I find this a bit complicated and I
> think it's foreseeable that library implementers will predominantly
> get this wrong.
> 
> 
> Let me circle back to the other options you mentioned in [1], quoting
> them here for reference:
> > I see four options:
> > 
> > 1. Consider whiteouts as regular files and make them handled by
> >    LANDLOCK_ACCESS_FS_MAKE_REG.  This would require an erratum and would
> >    make sense for direct mknod calls, but it would be weird for
> >    renameat2 calls than move a file and should only require
> >    LANDLOCK_ACCESS_FS_REMOVE_FILE from the user point of view.
> 
> It would be weird for renameat2 calls to require MAKE_REG in the
> source directory, but the weirdness would only affect
> fuse-overlayfs-style programs and could be documented explicitly for
> them for the case that they start using Landlock.
> 
> Normal programs that just call rename() on an existing FUSE-Overlayfs
> filesystem would *not* require the MAKE_REG right, because the FUSE
> process would do that on their behalf with the FUSE processes'
> credentials.
> 
> > 
> > 2. Add a new LANDLOCK_ACCESS_FS_MAKE_WHITEOUT right to handle whitout
> >    creation (direct and indirect?) and keep LANDLOCK_ACCESS_FS_MAKE_CHAR
> >    handle direct whiteout creation (and don't backport anything).  It
> >    looks inconsistent from an access control point of view.
> 
> MAKE_WHITEOUT to handle rename(RENAME_WHITEOUT) and MAKE_CHAR to
> handle mknod(chardev (0, 0)) -- This is a bit inconsistent, but it
> does not make a difference for any programs other than the ones
> calling rename(RENAME_WHITEOUT) (i.e., overlayfs-fuse), and it could
> be documented for that one use case.
> 
> I find this a pragmatic balance, and it does not require special logic
> for the best-effort fallback either.  Could you be persuaded to go
> this route instead?
> 
> > 3. Add a new LANDLOCK_ACCESS_FS_MAKE_WHITEOUT right and, when handled,
> >    make LANDLOCK_ACCESS_FS_MAKE_CHAR not handle whiteout.  This would be
> >    a bit weird from a kernel point of view but it should work well for
> >    users while still forbidding direct whiteout creation.
> 
> Except for the best-effort fallback, which is IMHO prone to
> implementation bugs. (see above)
> 
> On the side, the implementation of this is also non-trivial: In order
> to check for mknod(..., makedev(0, 0)), we need to check
> layer-by-layer whether the layer handles MAKE_WHITEOUT and then either
> check for MAKE_CHAR or MAKE_WHITEOUT.
> 
> 
> > 4. Add a new LANDLOCK_ACCESS_FS_MAKE_WHITEOUT right and make
> >    LANDLOCK_ACCESS_FS_MAKE_CHAR never handle whiteout (and backport
> >    MAKE_CHAR fix with an errata).  This would be consistent but backport
> >    a way to directly create whiteouts (e.g. with mknod).
> 
> It's mostly theoretical, but lifting the mknod(chardev (0,0))
> restriction for normal mknod() calls and calling it an erratum seems
> surprising as well, because it would relax security guarantees for
> existing programs.
> 
> I also pondered the alternative of creating an erratum but
> intentionally *not* backporting it, but even in that case, that
> surprising erratum still affects older programs which are deployed on
> newer kernels.
> 
> 
> Revisiting this discussion, I'd lean towards option 1 or 2 -- could
> you be persuaded towards one of these?

Friendly ping, Mickaël; I would like to have some feedback on this approach
before sending v4.  Could you please have a look?

Thanks,
—Günther


> I have a slight preference for option 1 (using MAKE_REG) because it
> would be a narrow fix that could be backported to older kernels as
> well and would not require a new access right.  Given that the use
> case for RENAME_WHITEOUT is really only for FUSE-OverlayFS and given
> that FUSE-OverlayFS anyway needs MAKE_REG permissions there, I have
> trouble imagining a scenario where a separate access right for
> MAKE_WHITEOUT is needed in a policy.  It seems like a pragmatic
> choice.
> 
> 
> > Specific tests should check that all
> > these cases are proprely handled.
> >
> > There is no documentation update related to the new feature.  A note
> > should also explain what exactly is a whiteout and why it is not
> > considered a character device (see previous discussions).
> > 
> > The sandboxer is not updated.
> > 
> > There is no audit tests.
> 
> Acknowledged, these were missing.
> 
> (I was initially hoping that this bug report wouldn't expand into a
> full-fledged feature with its own access right constant, but it is
> correct that this is all required in that case... :-/)
> 
> Will add this for the next patch set revision if it is still needed.
> 
> —Günther
> 
> [1] https://lore.kernel.org/all/20260414.Lae5ida1eeGh@digikod.net/


^ permalink raw reply

* Re: [PATCH v5 2/3] bpf: add bpf_init_inode_xattr kfunc for atomic  inode labeling
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-3-dwindsor@gmail.com>

On Jul  7, 2026 David Windsor <dwindsor@gmail.com> wrote:
> 
> Add bpf_init_inode_xattr() kfunc for BPF LSM programs to atomically set
> xattrs via the inode_init_security hook using security_lsmxattr_add().
> The hook now passes its xattr state as a single struct lsm_xattrs
> object, which the kfunc takes directly.
> 
> This kfunc is only callable from inode_init_security; the verifier
> rejects attempts to call it elsewhere.
> 
> A previous attempt [1] required a kmalloc string output protocol for
> the xattr name. Since commit 6bcdfd2cac55 ("security: Allow all LSMs to
> provide xattrs for inode_init_security hook") [2], the xattr name is no
> longer allocated; it is a static constant.
> 
> Link: https://kernsec.org/pipermail/linux-security-module-archive/2022-October/034878.html [1]
> Link: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=6bcdfd2cac55 [2]
> Suggested-by: Song Liu <song@kernel.org>
> Signed-off-by: David Windsor <dwindsor@gmail.com>
> ---
>  fs/bpf_fs_kfuncs.c | 36 ++++++++++++++++++++++++++++++++++++
>  1 file changed, 36 insertions(+)
> 
> diff --git a/fs/bpf_fs_kfuncs.c b/fs/bpf_fs_kfuncs.c
> index 768aca2dc0f0..ad0025f2264a 100644
> --- a/fs/bpf_fs_kfuncs.c
> +++ b/fs/bpf_fs_kfuncs.c
> @@ -11,7 +11,9 @@
>  #include <linux/file.h>
>  #include <linux/kernfs.h>
>  #include <linux/mm.h>
> +#include <linux/security.h>
>  #include <linux/xattr.h>
> +#include <uapi/linux/lsm.h>
>  
>  __bpf_kfunc_start_defs();
>  
> @@ -374,6 +376,39 @@ __bpf_kfunc struct inode *bpf_real_inode(struct dentry *dentry)
>  	return d_real_inode(dentry);
>  }
>  
> +/**
> + * bpf_init_inode_xattr - set an xattr on a new inode from inode_init_security
> + * @xattrs: inode_init_security xattr state from the hook context
> + * @name__str: xattr name (e.g., "bpf.file_label")
> + * @value_p: dynptr containing the xattr value
> + *
> + * Only callable from lsm/inode_init_security programs.
> + *
> + * Return: 0 on success, negative error on failure.
> + */
> +__bpf_kfunc int bpf_init_inode_xattr(struct lsm_xattrs *xattrs,
> +				     const char *name__str,
> +				     const struct bpf_dynptr *value_p)
> +{
> +	struct bpf_dynptr_kern *value_ptr = (struct bpf_dynptr_kern *)value_p;
> +	const void *value;
> +	u32 value_len;
> +
> +	if (!name__str)
> +		return -EINVAL;
> +	if (strncmp(name__str, XATTR_BPF_LSM_SUFFIX,
> +		    sizeof(XATTR_BPF_LSM_SUFFIX) - 1))
> +		return -EPERM;
> +
> +	value_len = __bpf_dynptr_size(value_ptr);
> +	value = __bpf_dynptr_data(value_ptr, value_len);
> +	if (!value)
> +		return -EINVAL;
> +
> +	return security_lsmxattr_add(xattrs, LSM_ID_BPF, name__str, value,
> +				     value_len);
> +}

I'm sorry David, now that I'm seeing this function again, especially
with the LSM specific bits extracted into a LSM function, this absolutely
belongs somewhere under security/.  It's only callable from within a
BPF LSM callback and all it does outside of some BPF pointer boilerplate
is call right back into a LSM helper function.

If the BPF maintainers aren't willing to accept that, then we will all
need to find another way.

>  __bpf_kfunc_end_defs();
>  
>  BTF_KFUNCS_START(bpf_fs_kfunc_set_ids)
> @@ -385,6 +420,7 @@ BTF_ID_FLAGS(func, bpf_get_file_xattr, KF_SLEEPABLE)
>  BTF_ID_FLAGS(func, bpf_set_dentry_xattr, KF_SLEEPABLE)
>  BTF_ID_FLAGS(func, bpf_remove_dentry_xattr, KF_SLEEPABLE)
>  BTF_ID_FLAGS(func, bpf_real_inode, KF_SLEEPABLE | KF_RET_NULL)
> +BTF_ID_FLAGS(func, bpf_init_inode_xattr)
>  BTF_KFUNCS_END(bpf_fs_kfunc_set_ids)
>  
>  static int bpf_fs_kfuncs_filter(const struct bpf_prog *prog, u32 kfunc_id)
> -- 
> 2.53.0

--
paul-moore.com

^ 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

* 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

* [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: [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

* [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: [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

* 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

* [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

* [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 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 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 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 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 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


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