Linux userland API discussions
 help / color / mirror / Atom feed
* Re: PATCH v2 0/2] Power: supply: Add PbAc, NiZn, RAM, and ZnAr support
From: Sebastian Reichel @ 2026-07-20 21:43 UTC (permalink / raw)
  To: Sebastian Reichel, Shuah Khan, Boris Shtrasman
  Cc: linux-pm, linux-kernel, linux-kselftest, linux-api
In-Reply-To: <20260624135718.286771-1-borissh1983@gmail.com>


On Wed, 24 Jun 2026 16:57:16 +0300, Boris Shtrasman wrote:
> These series adds support for PbAc, NiZn, RAM, and ZnAr chemistries as
> defined in the Smart Battery Data Specification v1.1 (Section 5.1.30
> DeviceChemistry).
> 
> Currently, the sbs-battery driver only handles LION, LiP, NiCd and NiMH.
> The Smart Battery specification defines 8 possible values:
>  - Lead Acid (PbAc)
>  - Lithium Ion (LION)
>  - Nickel Cadmium (NiCd)
>  - Nickel Metal Hydride (NiMH)
>  - Nickel Zinc (NiZn)
>  - Rechargeable Alkaline-Manganese (RAM)
>  - Zinc Air (ZnAr)
>  - Lithium Polymer (LiP)
> 
> [...]

Applied, thanks!

[1/2] power: supply: Add PbAc, NiZn, RAM, and ZnAr support
      commit: defb072f411c630337946f3babf1bf9b38b23a84
[2/2] power: supply: sbs-battery: Add PbAc, NiZn, RAM, and ZnAr support
      commit: 751253c3c3a40c43de8014a9d81e9e9a9f7b1039

Best regards,
-- 
Sebastian Reichel <sebastian.reichel@collabora.com>


^ permalink raw reply

* [PATCH 4/4] ntsync: reject wait ioctls with zero owner
From: Elizabeth Figura @ 2026-07-20 17:17 UTC (permalink / raw)
  To: Arnd Bergmann, Greg Kroah-Hartman
  Cc: linux-kernel, linux-api, wine-devel, linux-kselftest,
	Iván Ezequiel Rodriguez, Elizabeth Figura
In-Reply-To: <20260720171740.447035-1-zfigura@codeweavers.com>

From: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>

setup_wait() already validates pad and flags but not owner, while
Documentation/userspace-api/ntsync.rst requires EINVAL when owner is
zero. Reject early before queueing waiters.

Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
Reviewed-by: Elizabeth Figura <zfigura@codeweavers.com>
Signed-off-by: Elizabeth Figura <zfigura@codeweavers.com>
---
 drivers/misc/ntsync.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/drivers/misc/ntsync.c b/drivers/misc/ntsync.c
index 02c9d1192812..4a805919bb0c 100644
--- a/drivers/misc/ntsync.c
+++ b/drivers/misc/ntsync.c
@@ -875,6 +875,9 @@ static int setup_wait(struct ntsync_device *dev,
 	if (args->pad || (args->flags & ~NTSYNC_WAIT_REALTIME))
 		return -EINVAL;
 
+	if (!args->owner)
+		return -EINVAL;
+
 	if (size >= sizeof(fds))
 		return -EINVAL;
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH 0/4] ntsync miscellaneous patches
From: Elizabeth Figura @ 2026-07-20 17:17 UTC (permalink / raw)
  To: Arnd Bergmann, Greg Kroah-Hartman
  Cc: linux-kernel, linux-api, wine-devel, linux-kselftest,
	Elizabeth Figura

This is a resend of a series originally submitted as [1], now sent to char-misc.
The following is the original cover letter sent with that series.

[1] https://lore.kernel.org/all/20260628024239.152852-1-ivanrwcm25@gmail.com/

===

This series improves ntsync without changing wait/wake semantics:

 1/4 — Align Documentation/userspace-api/ntsync.rst with
       include/uapi/linux/ntsync.h (ioctl macro names and struct layout).

 2/4 — Fix wake_all selftest: CREATE_EVENT returns an fd, not zero.

 3/4 — Add selftests for documented EINVAL cases (zero owner, non-zero
       pad, cross-instance object use).

 4/4 — Reject wait ioctls when owner is zero, matching the documented
       uAPI (3/4 depends on 4/4 for the owner tests).

Patch 4/4 closes a spec gap: Documentation/userspace-api/ntsync.rst
requires EINVAL when wait owner is zero, but setup_wait() only validated
pad and flags.  Unlock/kill mutex ioctls already reject owner == 0.

Testing:
- scripts/checkpatch.pl --strict --no-tree: clean (4/4 patches)
- make headers && make -C tools/testing/selftests TARGETS=drivers/ntsync
- Kernel 7.1.0-ntsync-test+ (CONFIG_NTSYNC=y), QEMU x86_64 initramfs:
  tools/testing/selftests/drivers/ntsync/ntsync — 12/12 PASS,
  including wake_all and wait_args_validation
- On 6.17.0-35-generic with the distro ntsync.ko (without patch 4/4):
  wait_args_validation fails on owner==0 (wait proceeds instead of
  EINVAL), confirming the gap this series fixes


Iván Ezequiel Rodriguez (4):
  docs: ntsync: align uAPI ioctl names and struct layout with ntsync.h
  selftests: ntsync: fix wake_all CREATE_EVENT fd expectation
  selftests: ntsync: add wait argument validation tests
  ntsync: reject wait ioctls with zero owner

 Documentation/userspace-api/ntsync.rst        | 22 ++++-----
 drivers/misc/ntsync.c                         |  3 ++
 .../testing/selftests/drivers/ntsync/ntsync.c | 46 ++++++++++++++++++-
 3 files changed, 59 insertions(+), 12 deletions(-)


base-commit: 2cedf2272f1bb42471e646868ac572cc5752bd91
-- 
2.53.0


^ permalink raw reply

* [PATCH 1/4] docs: ntsync: align uAPI ioctl names and struct layout with ntsync.h
From: Elizabeth Figura @ 2026-07-20 17:17 UTC (permalink / raw)
  To: Arnd Bergmann, Greg Kroah-Hartman
  Cc: linux-kernel, linux-api, wine-devel, linux-kselftest,
	Iván Ezequiel Rodriguez, Elizabeth Figura
In-Reply-To: <20260720171740.447035-1-zfigura@codeweavers.com>

From: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>

The userspace-api reference used stale macro names (SEM_POST, SET_EVENT,
READ_*, KILL_OWNER) and struct field order that did not match
include/uapi/linux/ntsync.h. Update the documentation to match the
published uapi so Wine and other consumers grep the correct symbols.

Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
Reviewed-by: Elizabeth Figura <zfigura@codeweavers.com>
Signed-off-by: Elizabeth Figura <zfigura@codeweavers.com>
---
 Documentation/userspace-api/ntsync.rst | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/Documentation/userspace-api/ntsync.rst b/Documentation/userspace-api/ntsync.rst
index 25e7c4aef968..535585331380 100644
--- a/Documentation/userspace-api/ntsync.rst
+++ b/Documentation/userspace-api/ntsync.rst
@@ -83,18 +83,18 @@ structures used in ioctl calls::
    };
 
    struct ntsync_event_args {
-   	__u32 signaled;
    	__u32 manual;
+   	__u32 signaled;
    };
 
    struct ntsync_wait_args {
    	__u64 timeout;
    	__u64 objs;
    	__u32 count;
-   	__u32 owner;
    	__u32 index;
-   	__u32 alert;
    	__u32 flags;
+   	__u32 owner;
+   	__u32 alert;
    	__u32 pad;
    };
 
@@ -152,7 +152,7 @@ The ioctls on the device file are as follows:
 
 The ioctls on the individual objects are as follows:
 
-.. c:macro:: NTSYNC_IOC_SEM_POST
+.. c:macro:: NTSYNC_IOC_SEM_RELEASE
 
   Post to a semaphore object. Takes a pointer to a 32-bit integer,
   which on input holds the count to be added to the semaphore, and on
@@ -186,7 +186,7 @@ The ioctls on the individual objects are as follows:
   unowned and signaled, and eligible threads waiting on it will be
   woken as appropriate.
 
-.. c:macro:: NTSYNC_IOC_SET_EVENT
+.. c:macro:: NTSYNC_IOC_EVENT_SET
 
   Signal an event object. Takes a pointer to a 32-bit integer, which on
   output contains the previous state of the event.
@@ -194,12 +194,12 @@ The ioctls on the individual objects are as follows:
   Eligible threads will be woken, and auto-reset events will be
   designaled appropriately.
 
-.. c:macro:: NTSYNC_IOC_RESET_EVENT
+.. c:macro:: NTSYNC_IOC_EVENT_RESET
 
   Designal an event object. Takes a pointer to a 32-bit integer, which
   on output contains the previous state of the event.
 
-.. c:macro:: NTSYNC_IOC_PULSE_EVENT
+.. c:macro:: NTSYNC_IOC_EVENT_PULSE
 
   Wake threads waiting on an event object while leaving it in an
   unsignaled state. Takes a pointer to a 32-bit integer, which on
@@ -213,7 +213,7 @@ The ioctls on the individual objects are as follows:
   afterwards, and a simultaneous read operation will always report the
   event as unsignaled.
 
-.. c:macro:: NTSYNC_IOC_READ_SEM
+.. c:macro:: NTSYNC_IOC_SEM_READ
 
   Read the current state of a semaphore object. Takes a pointer to
   struct :c:type:`ntsync_sem_args`, which is used as follows:
@@ -225,7 +225,7 @@ The ioctls on the individual objects are as follows:
      * - ``max``
        - On output, contains the maximum count of the semaphore.
 
-.. c:macro:: NTSYNC_IOC_READ_MUTEX
+.. c:macro:: NTSYNC_IOC_MUTEX_READ
 
   Read the current state of a mutex object. Takes a pointer to struct
   :c:type:`ntsync_mutex_args`, which is used as follows:
@@ -242,7 +242,7 @@ The ioctls on the individual objects are as follows:
   ``EOWNERDEAD``. In this case, ``count`` and ``owner`` are set to
   zero.
 
-.. c:macro:: NTSYNC_IOC_READ_EVENT
+.. c:macro:: NTSYNC_IOC_EVENT_READ
 
   Read the current state of an event object. Takes a pointer to struct
   :c:type:`ntsync_event_args`, which is used as follows:
@@ -255,7 +255,7 @@ The ioctls on the individual objects are as follows:
        - On output, contains 1 if the event is a manual-reset event,
          and 0 otherwise.
 
-.. c:macro:: NTSYNC_IOC_KILL_OWNER
+.. c:macro:: NTSYNC_IOC_MUTEX_KILL
 
   Mark a mutex as unowned and abandoned if it is owned by the given
   owner. Takes an input-only pointer to a 32-bit integer denoting the
-- 
2.53.0


^ permalink raw reply related

* [PATCH 3/4] selftests: ntsync: add wait argument validation tests
From: Elizabeth Figura @ 2026-07-20 17:17 UTC (permalink / raw)
  To: Arnd Bergmann, Greg Kroah-Hartman
  Cc: linux-kernel, linux-api, wine-devel, linux-kselftest,
	Iván Ezequiel Rodriguez, Elizabeth Figura
In-Reply-To: <20260720171740.447035-1-zfigura@codeweavers.com>

From: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>

Add coverage for documented EINVAL cases: zero owner on wait any/all,
non-zero pad, and objects from a different /dev/ntsync instance.

Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
Reviewed-by: Elizabeth Figura <zfigura@codeweavers.com>
Signed-off-by: Elizabeth Figura <zfigura@codeweavers.com>
---
 .../testing/selftests/drivers/ntsync/ntsync.c | 44 +++++++++++++++++++
 1 file changed, 44 insertions(+)

diff --git a/tools/testing/selftests/drivers/ntsync/ntsync.c b/tools/testing/selftests/drivers/ntsync/ntsync.c
index 12b4b81edf7f..c9fe4d5987ec 100644
--- a/tools/testing/selftests/drivers/ntsync/ntsync.c
+++ b/tools/testing/selftests/drivers/ntsync/ntsync.c
@@ -1340,4 +1340,48 @@ TEST(stress_wait)
 	close(stress_device);
 }
 
+TEST(wait_args_validation)
+{
+	struct ntsync_sem_args sem_args = { .count = 1, .max = 1 };
+	struct ntsync_wait_args wait_args = {0};
+	struct timespec timeout;
+	int fd, fd2, sem, ret;
+	__u32 index;
+
+	fd = open("/dev/ntsync", O_CLOEXEC | O_RDONLY);
+	ASSERT_GE(fd, 0);
+
+	fd2 = open("/dev/ntsync", O_CLOEXEC | O_RDONLY);
+	ASSERT_GE(fd2, 0);
+
+	sem = ioctl(fd, NTSYNC_IOC_CREATE_SEM, &sem_args);
+	EXPECT_GE(sem, 0);
+
+	ret = wait_any(fd, 1, &sem, 0, &index);
+	EXPECT_EQ(-1, ret);
+	EXPECT_EQ(EINVAL, errno);
+
+	ret = wait_all(fd, 1, &sem, 0, &index);
+	EXPECT_EQ(-1, ret);
+	EXPECT_EQ(EINVAL, errno);
+
+	clock_gettime(CLOCK_MONOTONIC, &timeout);
+	wait_args.timeout = timeout.tv_sec * 1000000000ULL + timeout.tv_nsec;
+	wait_args.count = 0;
+	wait_args.objs = 0;
+	wait_args.owner = 123;
+	wait_args.pad = 1;
+	ret = ioctl(fd, NTSYNC_IOC_WAIT_ANY, &wait_args);
+	EXPECT_EQ(-1, ret);
+	EXPECT_EQ(EINVAL, errno);
+
+	ret = wait_any(fd2, 1, &sem, 123, &index);
+	EXPECT_EQ(-1, ret);
+	EXPECT_EQ(EINVAL, errno);
+
+	close(sem);
+	close(fd2);
+	close(fd);
+}
+
 TEST_HARNESS_MAIN
-- 
2.53.0


^ permalink raw reply related

* [PATCH 2/4] selftests: ntsync: fix wake_all CREATE_EVENT fd expectation
From: Elizabeth Figura @ 2026-07-20 17:17 UTC (permalink / raw)
  To: Arnd Bergmann, Greg Kroah-Hartman
  Cc: linux-kernel, linux-api, wine-devel, linux-kselftest,
	Iván Ezequiel Rodriguez, Elizabeth Figura
In-Reply-To: <20260720171740.447035-1-zfigura@codeweavers.com>

From: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>

wake_all used EXPECT_EQ(0, objs[3]) after NTSYNC_IOC_CREATE_EVENT.
The ioctl returns a non-negative file descriptor on success; check
EXPECT_LE(0, objs[3]) like the other CREATE_* paths. The incorrect
expectation was noted on list (Mar 2025) but is still present in
mainline.

Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
Reviewed-by: Elizabeth Figura <zfigura@codeweavers.com>
Signed-off-by: Elizabeth Figura <zfigura@codeweavers.com>
---
 tools/testing/selftests/drivers/ntsync/ntsync.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/testing/selftests/drivers/ntsync/ntsync.c b/tools/testing/selftests/drivers/ntsync/ntsync.c
index e6a37214aa46..12b4b81edf7f 100644
--- a/tools/testing/selftests/drivers/ntsync/ntsync.c
+++ b/tools/testing/selftests/drivers/ntsync/ntsync.c
@@ -968,7 +968,7 @@ TEST(wake_all)
 	auto_event_args.manual = false;
 	auto_event_args.signaled = true;
 	objs[3] = ioctl(fd, NTSYNC_IOC_CREATE_EVENT, &auto_event_args);
-	EXPECT_EQ(0, objs[3]);
+	EXPECT_LE(0, objs[3]);
 
 	wait_args.timeout = get_abs_timeout(1000);
 	wait_args.objs = (uintptr_t)objs;
-- 
2.53.0


^ permalink raw reply related

* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Miklos Szeredi @ 2026-07-20  9:22 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Miklos Szeredi, Giuseppe Scrivano, linux-fsdevel, linux-unionfs,
	linux-api, linux-erofs, Amir Goldstein, Gao Xiang
In-Reply-To: <CALCETrXYHX+OSwm6vhzQqvdk9Fkbxzh_dGuVm2tjvPJG_x1XdA@mail.gmail.com>

On Sun, 19 Jul 2026 at 16:13, Andy Lutomirski <luto@amacapital.net> wrote:

> This gives me the willies a bit.  It's very cool, but it has some
> potential security issues that I think we need to watch out for.  As
> sort-of prior art, NTFS supports alternate streams and Reiser4, ahem,
> supported files-as-directories.

I realized that we can keep the creepiness factor minimal by
introducing this first in procfs only.   That way zero changes are
needed in the path lookup code:

sprintf(buf, "/proc/self/fd-alt/%d/%s", base_fd, alt_path);
alt_fd = open(buf, O_PATH);

Thanks,
Miklos

^ permalink raw reply

* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Miklos Szeredi @ 2026-07-19 18:42 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Miklos Szeredi, Giuseppe Scrivano, linux-fsdevel, linux-unionfs,
	linux-api, linux-erofs, Amir Goldstein, Gao Xiang
In-Reply-To: <CALCETrXYHX+OSwm6vhzQqvdk9Fkbxzh_dGuVm2tjvPJG_x1XdA@mail.gmail.com>

On Sun, 19 Jul 2026 at 16:13, Andy Lutomirski <luto@amacapital.net> wrote:

> This gives me the willies a bit.  It's very cool, but it has some
> potential security issues that I think we need to watch out for.  As
> sort-of prior art, NTFS supports alternate streams and Reiser4, ahem,
> supported files-as-directories.
>
> Some questions that we would need to consider:
>
> 1. Can you open an fd referring to a "directory" within an O_ALT tree?

Yes.

>  What happens when an unsuspecting process gets such an fd?  Can you
> fchdir to it?  chroot to it?

Yes.   However you cannot escape from that particular base file's O_ALT tree.

>  clone its tree and possibly bind-mount
> it somewhere?

No, it's an "internal" mount, so no cloning.

> 2. Can you bind-mount *onto* something inside O_ALT?

Again, no, because it's an internal mount, not in any mount namespace.

> 3. Whose creds are used for which operations?

The creds of the process doing the operation.

> 4. Can you mmap the things in O_ALT?  Is this determined per object or
> generically?

Per object.

> 5. Would we ever allow FUSE to expose custom things inside O_ALT?
> What contents of O_ALT can a program trust come from the kernel and
> have the expected semantics?

I think adding such a plugin would be similar to adding a mount.  So
I'd suggest allowing it with the same privs (CAP_SYS_ADMIN) and
constraints (limited to the current mount namespace).

Thanks,
Miklos

^ permalink raw reply

* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Andy Lutomirski @ 2026-07-19 14:12 UTC (permalink / raw)
  To: Miklos Szeredi
  Cc: Giuseppe Scrivano, linux-fsdevel, linux-unionfs, linux-api,
	linux-erofs, Amir Goldstein, Gao Xiang
In-Reply-To: <20260715101107.973997-1-mszeredi@redhat.com>

On Wed, Jul 15, 2026 at 3:17 AM Miklos Szeredi <mszeredi@redhat.com> wrote:
>
> This is a prototype patch. Needs to be split up!
>
> 1) O_ALT / OPEN_TREE_ALT
>
>   These open an alternative namespace rooted at dfd - instead of resolving
>   the path in the real filesystem, it resolves it in a virtual tree that
>   exposes metadata about that file.
>
>   This is intended to provide an alternative to adding new ioctls:
>
>    - provides structured namespace
>    - allows accessing data through fs ops
>
>   Could call this O_META, but O_ALT seems more generic and there could be
>   uses beyond metadata (e.g. seekable, in-place decompression).

This gives me the willies a bit.  It's very cool, but it has some
potential security issues that I think we need to watch out for.  As
sort-of prior art, NTFS supports alternate streams and Reiser4, ahem,
supported files-as-directories.

Some questions that we would need to consider:

1. Can you open an fd referring to a "directory" within an O_ALT tree?
 What happens when an unsuspecting process gets such an fd?  Can you
fchdir to it?  chroot to it?  clone its tree and possibly bind-mount
it somewhere?

2. Can you bind-mount *onto* something inside O_ALT?

3. Whose creds are used for which operations?

4. Can you mmap the things in O_ALT?  Is this determined per object or
generically?

5. Would we ever allow FUSE to expose custom things inside O_ALT?
What contents of O_ALT can a program trust come from the kernel and
have the expected semantics?

--Andy

-- 
Andy Lutomirski
AMA Capital Management, LLC

^ permalink raw reply

* Re: [RFC] Null Namespaces
From: Andy Lutomirski @ 2026-07-18 19:39 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Andy Lutomirski, Jann Horn, John Ericson, Li Chen, Cong Wang,
	linux-arch, linux-kernel, linux-fsdevel, linux-api, Arnd Bergmann,
	Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen,
	H. Peter Anvin, Jan Kara, Jonathan Corbet, Shuah Khan,
	Alexander Viro, Kees Cook, Sergei Zimmerman, Farid Zakaria
In-Reply-To: <20260706-dabei-radeln-glitzer-71ecb835029c@brauner>

On Mon, Jul 6, 2026 at 8:31 AM Christian Brauner <brauner@kernel.org> wrote:
>
> On Thu, Jul 02, 2026 at 11:34:01AM +0200, Christian Brauner wrote:
> > On Mon, Jun 29, 2026 at 02:06:55PM -0700, Andy Lutomirski wrote:
> > > On Mon, Jun 29, 2026 at 4:45 AM Christian Brauner <brauner@kernel.org> wrote:
> > > >
> > >
> > > > But I guess the even simpler model would be to copy what I've been doing
> > > > for pidfs:
> > > >
> > > > +static struct path nullfs_root_path = {};
> > > > +
> > > > +void nullfs_get_root(struct path *path)
> > > > +{
> > > > +       *path = nullfs_root_path;
> > > > +       path_get(path);
> > > > +}
> > > > +
> > > >  static void __init init_mount_tree(void)
> > > >  {
> > > >         struct vfsmount *mnt, *nullfs_mnt;
> > > > @@ -6209,6 +6217,8 @@ static void __init init_mount_tree(void)
> > > >         /* Mount mutable rootfs on top of nullfs. */
> > > >         root.mnt                = nullfs_mnt;
> > > >         root.dentry             = nullfs_mnt->mnt_root;
> > > > +       nullfs_root_path.mnt    = nullfs_mnt;
> > > > +       pidfs_root_path.dentry  = nullfs_mnt->mnt_root;
> > > >
> > > >         LOCK_MOUNT_EXACT(mp, &root);
> > > >         if (unlikely(IS_ERR(mp.parent)))
> > > > diff --git a/include/uapi/linux/fcntl.h b/include/uapi/linux/fcntl.h
> > > > index aadfbf6e0cb3..f55c87c70b78 100644
> > > > --- a/include/uapi/linux/fcntl.h
> > > > +++ b/include/uapi/linux/fcntl.h
> > > > @@ -124,6 +124,7 @@ struct delegation {
> > > >
> > > >  #define FD_PIDFS_ROOT                  -10002 /* Root of the pidfs filesystem */
> > > >  #define FD_NSFS_ROOT                   -10003 /* Root of the nsfs filesystem */
> > > > +#define FD_NULLFS_ROOT                 -10004 /* Root of the nullfs filesystem */
> > > >  #define FD_INVALID                     -10009 /* Invalid file descriptor: -10000 - EBADF = -10009 */
> > > >
> > > >  /* Generic flags for the *at(2) family of syscalls. */
> > > >
> > > > we then add fchroot() (overdue anyway) and then teach both fchdir() and
> > > > fchroot() to honor FD_NULLFS_ROOT. Then a process may shed its fs state
> > > > and move itself into nullfs. Restrict *chdir() and *chroot() for said
> > > > process via seccomp and it's locked in forever as well.
> > > >
> > >
> > > One thing comes to mind that might need a bit of care: this would give
> > > an API for any task to get an fd to a directory that lives in the init
> > > mount namespace.  It's not at all obvious to me that this is dangerous
> > > or even observable (you're not about to find a setuid program in
> > > nullfs), but I think it's at least worth a tiny bit of consideration.
> >
> > Yes, I thought about this as well. But it doesn't have to be this way.
> > Every mount namespaces has nullfs as it's root ever since I introduced
> > it. Which means FD_NULLFS_ROOT can also just mean "nullfs within that
> > specific mount namespace". That's fine.
> >
> > For my FD_FAILFS_ROOT proposal it would be enough if we make failfs
> > SB_KERNMOUNT which means it's logically distinct from every mount
> > namespace. I think that might be the right thing to do. I need to spend
> > one or more brain cycles on this though.
>
> I had to take a long drive on Sunday and I kept thinking about both
> FD_NULLFS_ROOT and FD_FAILFS_ROOT and ofc there are some things to
> consider/discuss.
>
> I think the straightforward solution to FD_NULLFS_ROOT would be to just:
>
> - make it always available
> - refer to the caller's mount namespace nullfs
> - work with fchroot()/fchdir()
>
> So I considered two chroot() use-cases for the sake of simplicity:
>
> (1) You want to isolate yourself for the sake of lookup
>
> (2) You want to isolate yourself to assemble a "private mount tree" but
>     not really be in a separate namespace (very odd use-case... but it
>     helps to make a point).
>
> The problem with this approach is that everyone who chroots into the
> nullfs root would suffer from the problem that any mount on top of it is
> still visible. So that kinda makes it pointless for both (1) and (2).
>
> Also all mounts that someone else would do would also be visible
> allowing multiple chroot()ers to affect each others state. That also
> would somewhat defeat the purpose of the chroot(). So I'm not convinced
> this is what we should do.
>

After some contemplation and a long place flight: are we talking about
nullfs or failfs?  Because I would expect that it's entirely
impossible to mount anything on top of failfs.  So failfs would be
useless for #2 but would still solve #1.

--Andy

^ permalink raw reply

* Re: [PATCH v2] tpm: Make the TPM character devices non-seekable
From: Jarkko Sakkinen @ 2026-07-18 18:20 UTC (permalink / raw)
  To: Jaewon Yang
  Cc: Peter Huewe, Jason Gunthorpe, linux-integrity, linux-kernel,
	security, linux-api
In-Reply-To: <alvCRtFvf15hG7tJ@kernel.org>

oN Sat, Jul 18, 2026 at 09:13:30PM +0300, Jarkko Sakkinen wrote:
> On Mon, Jul 13, 2026 at 02:11:47AM +0900, Jaewon Yang wrote:
> > The TPM character devices expose a sequential command/response
> > interface, but their open handlers leave FMODE_PREAD and FMODE_PWRITE
> > enabled.
> > 
> > After a command leaves a response pending, pread(fd, buf, 16, 0x1400)
> > passes 0x1400 as *off to tpm_common_read(). The transfer length is
> > bounded by response_length, but the offset is used unchecked when
> > forming data_buffer + *off. A sufficiently large offset therefore
> > causes an out-of-bounds heap read through copy_to_user() and, if the
> > copy succeeds, an out-of-bounds zero-write through the following
> > memset().
> > 
> > Positional I/O does not provide coherent semantics for this interface.
> > An arbitrary pread offset cannot represent how much of a response has
> > been consumed sequentially. The write callback always stores a command
> > at the start of data_buffer, while pwrite() does not update file->f_pos
> > and can leave the sequential read cursor stale.
> > 
> > Call nonseekable_open() from both open handlers. This removes
> > FMODE_PREAD and FMODE_PWRITE, causing positional reads and writes to
> > fail with -ESPIPE before reaching the TPM callbacks, and explicitly
> > marks the files non-seekable. Normal read() and write() continue to use
> > the existing sequential f_pos cursor, leaving the response state
> > machine unchanged.
> > 
> > Tested on Linux 6.12 with KASAN and a swtpm TPM2 device:
> > 
> > - sequential partial reads returned the complete response;
> > - pread() and preadv() with offset 0x1400 returned -ESPIPE;
> > - pwrite() and pwritev() with offset zero returned -ESPIPE;
> > - the pending response remained intact after the rejected operations;
> > - a subsequent normal command/response cycle completed normally; and
> > - no KASAN report was produced.
> > 
> > Fixes: 9488585b21be ("tpm: add support for partial reads")
> > Link: https://lore.kernel.org/all/20260710090217.191289-1-yong010301@gmail.com/
> > Cc: stable@vger.kernel.org
> > Signed-off-by: Jaewon Yang <yong010301@gmail.com>
> > ---
> > Changes in v2:
> > - replace the response-buffer bounds check with nonseekable_open();
> > - reject positional read and write at open time;
> > - preserve the existing sequential read/write state machine.
> > 
> > The alternative response_length rework proposed during review was tested
> > and not taken: a read-until-EOF loop hangs because cleanup resets *off
> > without clearing response_length. It also treats an arbitrary positional
> > offset as the consumption cursor; for example,
> > 
> >     pread(fd, &c, 1, 99)
> > 
> > on a 100-byte response can discard bytes 0 through 98 without returning
> > them.
> > 
> >  drivers/char/tpm/tpm-dev.c   | 2 +-
> >  drivers/char/tpm/tpmrm-dev.c | 2 +-
> >  2 files changed, 2 insertions(+), 2 deletions(-)
> > 
> > diff --git a/drivers/char/tpm/tpm-dev.c b/drivers/char/tpm/tpm-dev.c
> > index 2779a8738..74488f0a7 100644
> > --- a/drivers/char/tpm/tpm-dev.c
> > +++ b/drivers/char/tpm/tpm-dev.c
> > @@ -36,7 +36,7 @@ static int tpm_open(struct inode *inode, struct file *file)
> >  
> >  	tpm_common_open(file, chip, priv, NULL);
> >  
> > -	return 0;
> > +	return nonseekable_open(inode, file);
> >  
> >   out:
> >  	clear_bit(0, &chip->is_open);
> > diff --git a/drivers/char/tpm/tpmrm-dev.c b/drivers/char/tpm/tpmrm-dev.c
> > index f48d4d9e1..19e8f2779 100644
> > --- a/drivers/char/tpm/tpmrm-dev.c
> > +++ b/drivers/char/tpm/tpmrm-dev.c
> > @@ -29,7 +29,7 @@ static int tpmrm_open(struct inode *inode, struct file *file)
> >  
> >  	tpm_common_open(file, chip, &priv->priv, &priv->space);
> >  
> > -	return 0;
> > +	return nonseekable_open(inode, file);
> >  }
> >  
> >  static int tpmrm_release(struct inode *inode, struct file *file)
> > -- 
> > 2.43.0
> > 
> 
> LGTM
> 
> 
> Acked-by: Jarkko Sakkinen <jarkko@kernel.org>

[A slow response time due on holiday until end of this Month.]

BR, Jarkko

^ permalink raw reply

* Re: [PATCH v2] tpm: Make the TPM character devices non-seekable
From: Jarkko Sakkinen @ 2026-07-18 18:13 UTC (permalink / raw)
  To: Jaewon Yang
  Cc: Peter Huewe, Jason Gunthorpe, linux-integrity, linux-kernel,
	security, linux-api
In-Reply-To: <20260712171147.323213-1-yong010301@gmail.com>

On Mon, Jul 13, 2026 at 02:11:47AM +0900, Jaewon Yang wrote:
> The TPM character devices expose a sequential command/response
> interface, but their open handlers leave FMODE_PREAD and FMODE_PWRITE
> enabled.
> 
> After a command leaves a response pending, pread(fd, buf, 16, 0x1400)
> passes 0x1400 as *off to tpm_common_read(). The transfer length is
> bounded by response_length, but the offset is used unchecked when
> forming data_buffer + *off. A sufficiently large offset therefore
> causes an out-of-bounds heap read through copy_to_user() and, if the
> copy succeeds, an out-of-bounds zero-write through the following
> memset().
> 
> Positional I/O does not provide coherent semantics for this interface.
> An arbitrary pread offset cannot represent how much of a response has
> been consumed sequentially. The write callback always stores a command
> at the start of data_buffer, while pwrite() does not update file->f_pos
> and can leave the sequential read cursor stale.
> 
> Call nonseekable_open() from both open handlers. This removes
> FMODE_PREAD and FMODE_PWRITE, causing positional reads and writes to
> fail with -ESPIPE before reaching the TPM callbacks, and explicitly
> marks the files non-seekable. Normal read() and write() continue to use
> the existing sequential f_pos cursor, leaving the response state
> machine unchanged.
> 
> Tested on Linux 6.12 with KASAN and a swtpm TPM2 device:
> 
> - sequential partial reads returned the complete response;
> - pread() and preadv() with offset 0x1400 returned -ESPIPE;
> - pwrite() and pwritev() with offset zero returned -ESPIPE;
> - the pending response remained intact after the rejected operations;
> - a subsequent normal command/response cycle completed normally; and
> - no KASAN report was produced.
> 
> Fixes: 9488585b21be ("tpm: add support for partial reads")
> Link: https://lore.kernel.org/all/20260710090217.191289-1-yong010301@gmail.com/
> Cc: stable@vger.kernel.org
> Signed-off-by: Jaewon Yang <yong010301@gmail.com>
> ---
> Changes in v2:
> - replace the response-buffer bounds check with nonseekable_open();
> - reject positional read and write at open time;
> - preserve the existing sequential read/write state machine.
> 
> The alternative response_length rework proposed during review was tested
> and not taken: a read-until-EOF loop hangs because cleanup resets *off
> without clearing response_length. It also treats an arbitrary positional
> offset as the consumption cursor; for example,
> 
>     pread(fd, &c, 1, 99)
> 
> on a 100-byte response can discard bytes 0 through 98 without returning
> them.
> 
>  drivers/char/tpm/tpm-dev.c   | 2 +-
>  drivers/char/tpm/tpmrm-dev.c | 2 +-
>  2 files changed, 2 insertions(+), 2 deletions(-)
> 
> diff --git a/drivers/char/tpm/tpm-dev.c b/drivers/char/tpm/tpm-dev.c
> index 2779a8738..74488f0a7 100644
> --- a/drivers/char/tpm/tpm-dev.c
> +++ b/drivers/char/tpm/tpm-dev.c
> @@ -36,7 +36,7 @@ static int tpm_open(struct inode *inode, struct file *file)
>  
>  	tpm_common_open(file, chip, priv, NULL);
>  
> -	return 0;
> +	return nonseekable_open(inode, file);
>  
>   out:
>  	clear_bit(0, &chip->is_open);
> diff --git a/drivers/char/tpm/tpmrm-dev.c b/drivers/char/tpm/tpmrm-dev.c
> index f48d4d9e1..19e8f2779 100644
> --- a/drivers/char/tpm/tpmrm-dev.c
> +++ b/drivers/char/tpm/tpmrm-dev.c
> @@ -29,7 +29,7 @@ static int tpmrm_open(struct inode *inode, struct file *file)
>  
>  	tpm_common_open(file, chip, &priv->priv, &priv->space);
>  
> -	return 0;
> +	return nonseekable_open(inode, file);
>  }
>  
>  static int tpmrm_release(struct inode *inode, struct file *file)
> -- 
> 2.43.0
> 

LGTM


Acked-by: Jarkko Sakkinen <jarkko@kernel.org>

BR, Jarkko

^ permalink raw reply

* [PATCH v4 6/6] selftests: prctl: Add test for long thread names
From: André Almeida @ 2026-07-17 13:54 UTC (permalink / raw)
  To: Peter Zijlstra, Juri Lelli, Vincent Guittot, Steven Rostedt,
	Christian Brauner, Kees Cook, Shuah Khan, willy,
	mathieu.desnoyers, David Laight, Linus Torvalds, akpm,
	Yafang Shao, andrii.nakryiko, arnaldo.melo, Petr Mladek
  Cc: linux-kernel, kernel-dev, linux-mm, linux-api, André Almeida
In-Reply-To: <20260717-tonyk-long_name-v4-0-1fedfc870d21@igalia.com>

Add tests for the new interface to set and get long thread names. The
kernel should accept the LONG_NAME and returning it accordingly. For the
old PR_GET_NAME interface, the kernel should truncate the name up to 16
chars. /proc/<task>/comm should return the same string ad PR_GET_NAME.

Signed-off-by: André Almeida <andrealmeid@igalia.com>
---
 tools/testing/selftests/prctl/set-process-name.c | 36 ++++++++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/tools/testing/selftests/prctl/set-process-name.c b/tools/testing/selftests/prctl/set-process-name.c
index 3f7b146d36df..0f20f7deac67 100644
--- a/tools/testing/selftests/prctl/set-process-name.c
+++ b/tools/testing/selftests/prctl/set-process-name.c
@@ -9,9 +9,17 @@
 
 #include "kselftest_harness.h"
 
+#ifndef PR_SET_EXT_NAME
+# define PR_SET_EXT_NAME 17
+# define PR_GET_EXT_NAME 18
+#endif
+
 #define CHANGE_NAME "changename"
+#define LONG_NAME	"change_to_very_long_extended_name"
+#define LONG_NAME_CAP	"change_to_very_"
 #define EMPTY_NAME ""
 #define TASK_COMM_LEN 16
+#define TASK_COMM_EXT_LEN 64
 #define MAX_PATH_LEN 50
 
 int set_name(char *name)
@@ -25,6 +33,16 @@ int set_name(char *name)
 	return res;
 }
 
+int set_ext_name(char *name)
+{
+	int res;
+
+	res = prctl(PR_SET_EXT_NAME, name, NULL, NULL, NULL);
+
+	if (res < 0)
+		return -errno;
+}
+
 int check_is_name_correct(char *check_name)
 {
 	char name[TASK_COMM_LEN];
@@ -38,6 +56,19 @@ int check_is_name_correct(char *check_name)
 	return !strcmp(name, check_name);
 }
 
+int check_is_ext_name_correct(char *check_name)
+{
+	char name[TASK_COMM_EXT_LEN];
+	int res;
+
+	res = prctl(PR_GET_EXT_NAME, name, NULL, NULL, NULL);
+
+	if (res < 0)
+		return -errno;
+
+	return !strcmp(name, check_name);
+}
+
 int check_null_pointer(char *check_name)
 {
 	char *name = NULL;
@@ -82,6 +113,11 @@ TEST(rename_process) {
 	EXPECT_GE(set_name(CHANGE_NAME), 0);
 	EXPECT_TRUE(check_is_name_correct(CHANGE_NAME));
 
+	EXPECT_GE(set_ext_name(LONG_NAME), 0);
+	EXPECT_TRUE(check_is_ext_name_correct(LONG_NAME));
+	EXPECT_TRUE(check_is_name_correct(LONG_NAME_CAP));
+	EXPECT_TRUE(check_name());
+
 	EXPECT_GE(set_name(EMPTY_NAME), 0);
 	EXPECT_TRUE(check_is_name_correct(EMPTY_NAME));
 

-- 
2.55.0


^ permalink raw reply related

* [PATCH v4 5/6] prctl: Add support for long user thread names
From: André Almeida @ 2026-07-17 13:54 UTC (permalink / raw)
  To: Peter Zijlstra, Juri Lelli, Vincent Guittot, Steven Rostedt,
	Christian Brauner, Kees Cook, Shuah Khan, willy,
	mathieu.desnoyers, David Laight, Linus Torvalds, akpm,
	Yafang Shao, andrii.nakryiko, arnaldo.melo, Petr Mladek
  Cc: linux-kernel, kernel-dev, linux-mm, linux-api, André Almeida
In-Reply-To: <20260717-tonyk-long_name-v4-0-1fedfc870d21@igalia.com>

Add support for getting and setting long user thread names with
PR_{SET,GET}_EXT_NAME.

Signed-off-by: André Almeida <andrealmeid@igalia.com>
---
 include/linux/sched.h      |  2 +-
 include/uapi/linux/prctl.h |  3 +++
 kernel/sys.c               | 15 ++++++++++++++-
 3 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/include/linux/sched.h b/include/linux/sched.h
index 6a48517f01cc..0f1b40a5dbc6 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -2018,7 +2018,7 @@ extern void kick_process(struct task_struct *tsk);
 
 extern void __set_task_comm(struct task_struct *tsk, const char *from, bool exec);
 #define set_task_comm(tsk, from) ({			\
-	BUILD_BUG_ON(sizeof(from) != TASK_COMM_LEN);	\
+	BUILD_BUG_ON(sizeof(from) < TASK_COMM_LEN);	\
 	__set_task_comm(tsk, from, false);		\
 })
 
diff --git a/include/uapi/linux/prctl.h b/include/uapi/linux/prctl.h
index b6ec6f693719..a07f8edadd65 100644
--- a/include/uapi/linux/prctl.h
+++ b/include/uapi/linux/prctl.h
@@ -56,6 +56,9 @@
 #define PR_SET_NAME    15		/* Set process name */
 #define PR_GET_NAME    16		/* Get process name */
 
+#define PR_SET_EXT_NAME    17		/* Set extended process name */
+#define PR_GET_EXT_NAME    18		/* Get extended process name */
+
 /* Get/set process endian */
 #define PR_GET_ENDIAN	19
 #define PR_SET_ENDIAN	20
diff --git a/kernel/sys.c b/kernel/sys.c
index 3c37b5c1c072..26d5026f9590 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -2535,7 +2535,7 @@ SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
 		unsigned long, arg4, unsigned long, arg5)
 {
 	struct task_struct *me = current;
-	unsigned char comm[TASK_COMM_LEN];
+	unsigned char comm[TASK_COMM_EXT_LEN];
 	long error;
 
 	error = security_task_prctl(option, arg2, arg3, arg4, arg5);
@@ -2613,6 +2613,19 @@ SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
 		if (copy_to_user((char __user *)arg2, comm, TASK_COMM_LEN))
 			return -EFAULT;
 		break;
+	case PR_SET_EXT_NAME:
+		comm[TASK_COMM_EXT_LEN - 1] = 0;
+		if (strncpy_from_user(comm, (char __user *)arg2,
+				      TASK_COMM_EXT_LEN - 1) < 0)
+			return -EFAULT;
+		set_task_comm(me, comm);
+		proc_comm_connector(me);
+		break;
+	case PR_GET_EXT_NAME:
+		strscpy_pad(comm, me->comm, TASK_COMM_EXT_LEN);
+		if (copy_to_user((char __user *)arg2, comm, TASK_COMM_EXT_LEN))
+			return -EFAULT;
+		break;
 	case PR_GET_ENDIAN:
 		error = GET_ENDIAN(me, arg2);
 		break;

-- 
2.55.0


^ permalink raw reply related

* [PATCH v4 4/6] sched: Extend task command name with TASK_COMM_EXT_LEN
From: André Almeida @ 2026-07-17 13:54 UTC (permalink / raw)
  To: Peter Zijlstra, Juri Lelli, Vincent Guittot, Steven Rostedt,
	Christian Brauner, Kees Cook, Shuah Khan, willy,
	mathieu.desnoyers, David Laight, Linus Torvalds, akpm,
	Yafang Shao, andrii.nakryiko, arnaldo.melo, Petr Mladek
  Cc: linux-kernel, kernel-dev, linux-mm, linux-api, André Almeida
In-Reply-To: <20260717-tonyk-long_name-v4-0-1fedfc870d21@igalia.com>

Command name has been restrict to only 16 bytes, which is too limiting,
specially when debugging and tracing complex software with thousands of
threads and the need to differentiate them.

Just as it was done with kthreads in commit 6b59808bfe48 ("workqueue:
Show the latest workqueue name in /proc/PID/{comm,stat,status}"), support
long names for userspace threads as well.

To avoid buffer overflows, cap all existing userspace APIs to
TASK_COMM_LEN, and leave the full extended name for a new interface.

Signed-off-by: André Almeida <andrealmeid@igalia.com>
---
 fs/proc/array.c          |  2 +-
 include/linux/sched.h    |  3 ++-
 kernel/sys.c             | 10 +++++-----
 lib/tests/string_kunit.c |  2 +-
 4 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/fs/proc/array.c b/fs/proc/array.c
index 905d910b598f..0490b9ef2788 100644
--- a/fs/proc/array.c
+++ b/fs/proc/array.c
@@ -110,7 +110,7 @@ void proc_task_name(struct seq_file *m, struct task_struct *p, bool escape)
 	else if (p->flags & PF_KTHREAD)
 		get_kthread_comm(tcomm, sizeof(tcomm), p);
 	else
-		strscpy_pad(tcomm, p->comm);
+		strscpy_pad(tcomm, p->comm, TASK_COMM_LEN);
 
 	if (escape)
 		seq_escape_str(m, tcomm, ESCAPE_SPACE | ESCAPE_SPECIAL, "\n\\");
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 9b6a413f6618..6a48517f01cc 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -323,6 +323,7 @@ struct user_event_mm;
  */
 enum {
 	TASK_COMM_LEN = 16,
+	TASK_COMM_EXT_LEN = 64,
 };
 
 extern void sched_tick(void);
@@ -1178,7 +1179,7 @@ struct task_struct {
 	 * - set it with set_task_comm() to ensure it is always
 	 *   NUL-terminated and zero-padded
 	 */
-	char				comm[TASK_COMM_LEN];
+	char				comm[TASK_COMM_EXT_LEN];
 
 	struct nameidata		*nameidata;
 
diff --git a/kernel/sys.c b/kernel/sys.c
index 74c22d6dded0..3c37b5c1c072 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -2535,7 +2535,7 @@ SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
 		unsigned long, arg4, unsigned long, arg5)
 {
 	struct task_struct *me = current;
-	unsigned char comm[sizeof(me->comm)];
+	unsigned char comm[TASK_COMM_LEN];
 	long error;
 
 	error = security_task_prctl(option, arg2, arg3, arg4, arg5);
@@ -2601,16 +2601,16 @@ SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
 			error = -EINVAL;
 		break;
 	case PR_SET_NAME:
-		comm[sizeof(me->comm) - 1] = 0;
+		comm[TASK_COMM_LEN - 1] = 0;
 		if (strncpy_from_user(comm, (char __user *)arg2,
-				      sizeof(me->comm) - 1) < 0)
+				      TASK_COMM_LEN - 1) < 0)
 			return -EFAULT;
 		set_task_comm(me, comm);
 		proc_comm_connector(me);
 		break;
 	case PR_GET_NAME:
-		strscpy_pad(comm, me->comm);
-		if (copy_to_user((char __user *)arg2, comm, sizeof(comm)))
+		strscpy_pad(comm, me->comm, TASK_COMM_LEN);
+		if (copy_to_user((char __user *)arg2, comm, TASK_COMM_LEN))
 			return -EFAULT;
 		break;
 	case PR_GET_ENDIAN:
diff --git a/lib/tests/string_kunit.c b/lib/tests/string_kunit.c
index b64d7f0e54a3..5d26029d2d01 100644
--- a/lib/tests/string_kunit.c
+++ b/lib/tests/string_kunit.c
@@ -883,7 +883,7 @@ static void string_bench_strrchr(struct kunit *test)
 
 #define TASK_NAME "task_name"
 #define TASK_NAME_LEN 9
-#define TASK_MAX_LEN TASK_COMM_LEN
+#define TASK_MAX_LEN TASK_COMM_EXT_LEN
 #define SMALLER_LEN TASK_NAME_LEN - 3
 #define BIGGER_LEN TASK_MAX_LEN + 3
 

-- 
2.55.0


^ permalink raw reply related

* [PATCH v4 3/6] lib/string_kunit: Add test for copy_task_comm()
From: André Almeida @ 2026-07-17 13:54 UTC (permalink / raw)
  To: Peter Zijlstra, Juri Lelli, Vincent Guittot, Steven Rostedt,
	Christian Brauner, Kees Cook, Shuah Khan, willy,
	mathieu.desnoyers, David Laight, Linus Torvalds, akpm,
	Yafang Shao, andrii.nakryiko, arnaldo.melo, Petr Mladek
  Cc: linux-kernel, kernel-dev, linux-mm, linux-api, André Almeida
In-Reply-To: <20260717-tonyk-long_name-v4-0-1fedfc870d21@igalia.com>

Add a new test for copy_task_comm(). Check if a copy from a task_struct
works, and special cases when the size of source and destination buffer
mismatches.

Signed-off-by: André Almeida <andrealmeid@igalia.com>
---
 lib/tests/string_kunit.c | 38 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 38 insertions(+)

diff --git a/lib/tests/string_kunit.c b/lib/tests/string_kunit.c
index 0819ace5b027..b64d7f0e54a3 100644
--- a/lib/tests/string_kunit.c
+++ b/lib/tests/string_kunit.c
@@ -881,6 +881,43 @@ static void string_bench_strrchr(struct kunit *test)
 	STRING_BENCH_BUF(test, buf, len, strrchr, buf, '\0');
 }
 
+#define TASK_NAME "task_name"
+#define TASK_NAME_LEN 9
+#define TASK_MAX_LEN TASK_COMM_LEN
+#define SMALLER_LEN TASK_NAME_LEN - 3
+#define BIGGER_LEN TASK_MAX_LEN + 3
+
+static void string_copy_task_comm(struct kunit *test)
+{
+	char str[TASK_MAX_LEN] = TASK_NAME, copy[TASK_MAX_LEN],
+	     smaller_buf[SMALLER_LEN], bigger_buf[BIGGER_LEN];
+	static struct task_struct task, *tsk = &task;
+	int len1, len2, i;
+
+	/* set and get task name */
+	set_task_comm(tsk, str);
+	copy_task_comm(copy, tsk, TASK_COMM_LEN);
+
+	len1 = strlen(str);
+	len2 = strlen(copy);
+
+	KUNIT_ASSERT_EQ(test, len1, len2);
+	KUNIT_ASSERT_EQ(test, len2, TASK_NAME_LEN);
+	KUNIT_ASSERT_EQ(test, copy[len2], '\0');
+	KUNIT_ASSERT_TRUE(test, !strcmp(str, copy));
+
+	/* copy to a smaller dst buffer */
+	copy_task_comm(smaller_buf, tsk, sizeof(smaller_buf));
+	KUNIT_ASSERT_TRUE(test, !strncmp(str, smaller_buf, SMALLER_LEN - 1));
+	KUNIT_ASSERT_EQ(test, smaller_buf[SMALLER_LEN - 1], '\0');
+
+	/* copy to a bigger dst buffer */
+	copy_task_comm(bigger_buf, tsk, sizeof(bigger_buf));
+	KUNIT_ASSERT_TRUE(test, !strncmp(str, bigger_buf, TASK_NAME_LEN));
+	for (i = TASK_NAME_LEN; i < BIGGER_LEN; i++)
+		KUNIT_ASSERT_EQ(test, bigger_buf[i], '\0');
+}
+
 static struct kunit_case string_test_cases[] = {
 	KUNIT_CASE(string_test_memset16),
 	KUNIT_CASE(string_test_memset32),
@@ -910,6 +947,7 @@ static struct kunit_case string_test_cases[] = {
 	KUNIT_CASE(string_bench_strnlen),
 	KUNIT_CASE(string_bench_strchr),
 	KUNIT_CASE(string_bench_strrchr),
+	KUNIT_CASE(string_copy_task_comm),
 	{}
 };
 

-- 
2.55.0


^ permalink raw reply related

* [PATCH v4 2/6] treewide: Replace memcpy(..., current->comm) with copy_task_comm()
From: André Almeida @ 2026-07-17 13:54 UTC (permalink / raw)
  To: Peter Zijlstra, Juri Lelli, Vincent Guittot, Steven Rostedt,
	Christian Brauner, Kees Cook, Shuah Khan, willy,
	mathieu.desnoyers, David Laight, Linus Torvalds, akpm,
	Yafang Shao, andrii.nakryiko, arnaldo.melo, Petr Mladek
  Cc: linux-kernel, kernel-dev, linux-mm, linux-api, André Almeida
In-Reply-To: <20260717-tonyk-long_name-v4-0-1fedfc870d21@igalia.com>

In order to increase the size of current->comm[] and to avoid breaking any
existing code, replace memcpy() with copy_task_comm(). This new function
makes sure that the copy is NUL terminated. This is crucial given that the
source buffer might be larger than the destination buffer and could
truncate the NUL character out of it.

Signed-off-by: André Almeida <andrealmeid@igalia.com>
---
Changes from v3:
 - Simplify copy_task_comm() to a memcpy + NUL char at the end of buffer

Changes from v2:
 - Bring back custom function.

Changes from v1:
 - New patch, dropped strtostr() from last version
---
 include/linux/coredump.h        |  2 +-
 include/linux/sched.h           | 11 +++++++++++
 include/linux/tracepoint.h      |  4 ++--
 include/trace/events/block.h    | 10 +++++-----
 include/trace/events/coredump.h |  2 +-
 include/trace/events/f2fs.h     |  4 ++--
 include/trace/events/oom.h      |  2 +-
 include/trace/events/osnoise.h  |  2 +-
 include/trace/events/sched.h    | 10 +++++-----
 include/trace/events/signal.h   |  2 +-
 include/trace/events/task.h     |  7 ++++---
 kernel/printk/nbcon.c           |  2 +-
 kernel/printk/printk.c          |  2 +-
 13 files changed, 36 insertions(+), 24 deletions(-)

diff --git a/include/linux/coredump.h b/include/linux/coredump.h
index 7b38ee2e7913..dee82e3c350a 100644
--- a/include/linux/coredump.h
+++ b/include/linux/coredump.h
@@ -58,7 +58,7 @@ extern void vfs_coredump(const kernel_siginfo_t *siginfo);
 	do {	\
 		char comm[TASK_COMM_LEN];	\
 		/* This will always be NUL terminated. */ \
-		memcpy(comm, current->comm, sizeof(comm)); \
+		copy_task_comm(comm, current, sizeof(comm)); \
 		printk_ratelimited(Level "coredump: %d(%*pE): " Format "\n",	\
 			task_tgid_vnr(current), (int)strlen(comm), comm, ##__VA_ARGS__);	\
 	} while (0)	\
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 2714c81bc708..9b6a413f6618 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -2021,6 +2021,17 @@ extern void __set_task_comm(struct task_struct *tsk, const char *from, bool exec
 	__set_task_comm(tsk, from, false);		\
 })
 
+/*
+ * Copy task name to a buffer. Final result is always a NUL-terminated string.
+ */
+static __always_inline void copy_task_comm(char *dst, struct task_struct *tsk, size_t len)
+{
+	const char *_src = tsk->comm;
+
+	memcpy(dst, _src, len);
+	dst[len - 1] = '\0';
+}
+
 static __always_inline void scheduler_ipi(void)
 {
 	/*
diff --git a/include/linux/tracepoint.h b/include/linux/tracepoint.h
index e0d838c9ce93..9b0094fa44c6 100644
--- a/include/linux/tracepoint.h
+++ b/include/linux/tracepoint.h
@@ -637,10 +637,10 @@ static inline struct tracepoint *tracepoint_ptr_deref(tracepoint_ptr_t *p)
  *	*
  *
  *	TP_fast_assign(
- *		memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
+ *		copy_task_comm(__entry->next_comm, next, TASK_COMM_LEN);
  *		__entry->prev_pid	= prev->pid;
  *		__entry->prev_prio	= prev->prio;
- *		memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
+ *		copy_task_comm(__entry->prev_comm, prev, TASK_COMM_LEN);
  *		__entry->next_pid	= next->pid;
  *		__entry->next_prio	= next->prio;
  *	),
diff --git a/include/trace/events/block.h b/include/trace/events/block.h
index 9c97a16850b9..1e3a9ded03bd 100644
--- a/include/trace/events/block.h
+++ b/include/trace/events/block.h
@@ -213,7 +213,7 @@ DECLARE_EVENT_CLASS(block_rq,
 
 		blk_fill_rwbs(__entry->rwbs, rq->cmd_flags);
 		__get_str(cmd)[0] = '\0';
-		memcpy(__entry->comm, current->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, current, TASK_COMM_LEN);
 	),
 
 	TP_printk("%d,%d %s %u (%s) %llu + %u %s,%u,%u [%s]",
@@ -410,7 +410,7 @@ DECLARE_EVENT_CLASS(block_bio,
 		__entry->sector		= bio->bi_iter.bi_sector;
 		__entry->nr_sector	= bio_sectors(bio);
 		blk_fill_rwbs(__entry->rwbs, bio->bi_opf);
-		memcpy(__entry->comm, current->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, current, TASK_COMM_LEN);
 	),
 
 	TP_printk("%d,%d %s %llu + %u [%s]",
@@ -493,7 +493,7 @@ TRACE_EVENT(block_plug,
 	),
 
 	TP_fast_assign(
-		memcpy(__entry->comm, current->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, current, TASK_COMM_LEN);
 	),
 
 	TP_printk("[%s]", __entry->comm)
@@ -512,7 +512,7 @@ DECLARE_EVENT_CLASS(block_unplug,
 
 	TP_fast_assign(
 		__entry->nr_rq = depth;
-		memcpy(__entry->comm, current->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, current, TASK_COMM_LEN);
 	),
 
 	TP_printk("[%s] %d", __entry->comm, __entry->nr_rq)
@@ -563,7 +563,7 @@ TRACE_EVENT(block_split,
 		__entry->sector		= bio->bi_iter.bi_sector;
 		__entry->new_sector	= new_sector;
 		blk_fill_rwbs(__entry->rwbs, bio->bi_opf);
-		memcpy(__entry->comm, current->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, current, TASK_COMM_LEN);
 	),
 
 	TP_printk("%d,%d %s %llu / %llu [%s]",
diff --git a/include/trace/events/coredump.h b/include/trace/events/coredump.h
index c7b9c53fc498..fdd20bc46bb0 100644
--- a/include/trace/events/coredump.h
+++ b/include/trace/events/coredump.h
@@ -32,7 +32,7 @@ TRACE_EVENT(coredump,
 
 	TP_fast_assign(
 		__entry->sig = sig;
-		memcpy(__entry->comm, current->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, current, TASK_COMM_LEN);
 	),
 
 	TP_printk("sig=%d comm=%s",
diff --git a/include/trace/events/f2fs.h b/include/trace/events/f2fs.h
index 270c1a2c24c4..7d22650b4c78 100644
--- a/include/trace/events/f2fs.h
+++ b/include/trace/events/f2fs.h
@@ -2505,7 +2505,7 @@ TRACE_EVENT(f2fs_lock_elapsed_time,
 
 	TP_fast_assign(
 		__entry->dev		= sbi->sb->s_dev;
-		memcpy(__entry->comm, p->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, p, TASK_COMM_LEN);
 		__entry->pid		= p->pid;
 		__entry->prio		= p->prio;
 		__entry->ioprio_class	= IOPRIO_PRIO_CLASS(ioprio);
@@ -2558,7 +2558,7 @@ DECLARE_EVENT_CLASS(f2fs_priority_update,
 
 	TP_fast_assign(
 		__entry->dev		= sbi->sb->s_dev;
-		memcpy(__entry->comm, p->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, p, TASK_COMM_LEN);
 		__entry->pid		= p->pid;
 		__entry->lock_name	= lock_name;
 		__entry->is_write	= is_write;
diff --git a/include/trace/events/oom.h b/include/trace/events/oom.h
index 9f0a5d1482c4..8bcdc4ffc8d3 100644
--- a/include/trace/events/oom.h
+++ b/include/trace/events/oom.h
@@ -23,7 +23,7 @@ TRACE_EVENT(oom_score_adj_update,
 
 	TP_fast_assign(
 		__entry->pid = task->pid;
-		memcpy(__entry->comm, task->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, task, TASK_COMM_LEN);
 		__entry->oom_score_adj = task->signal->oom_score_adj;
 	),
 
diff --git a/include/trace/events/osnoise.h b/include/trace/events/osnoise.h
index 3f4273623801..2cf047bb9fb7 100644
--- a/include/trace/events/osnoise.h
+++ b/include/trace/events/osnoise.h
@@ -116,7 +116,7 @@ TRACE_EVENT(thread_noise,
 	),
 
 	TP_fast_assign(
-		memcpy(__entry->comm, t->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, t, TASK_COMM_LEN);
 		__entry->pid = t->pid;
 		__entry->start = start;
 		__entry->duration = duration;
diff --git a/include/trace/events/sched.h b/include/trace/events/sched.h
index 535860581f15..afb24e9dac91 100644
--- a/include/trace/events/sched.h
+++ b/include/trace/events/sched.h
@@ -152,7 +152,7 @@ DECLARE_EVENT_CLASS(sched_wakeup_template,
 	),
 
 	TP_fast_assign(
-		memcpy(__entry->comm, p->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, p, TASK_COMM_LEN);
 		__entry->pid		= p->pid;
 		__entry->prio		= p->prio; /* XXX SCHED_DEADLINE */
 		__entry->target_cpu	= task_cpu(p);
@@ -237,11 +237,11 @@ TRACE_EVENT(sched_switch,
 	),
 
 	TP_fast_assign(
-		memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->prev_comm, prev, TASK_COMM_LEN);
 		__entry->prev_pid	= prev->pid;
 		__entry->prev_prio	= prev->prio;
 		__entry->prev_state	= __trace_sched_switch_state(preempt, prev_state, prev);
-		memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->next_comm, next, TASK_COMM_LEN);
 		__entry->next_pid	= next->pid;
 		__entry->next_prio	= next->prio;
 		/* XXX SCHED_DEADLINE */
@@ -346,7 +346,7 @@ TRACE_EVENT(sched_process_exit,
 	),
 
 	TP_fast_assign(
-		memcpy(__entry->comm, p->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, p, TASK_COMM_LEN);
 		__entry->pid		= p->pid;
 		__entry->prio		= p->prio; /* XXX SCHED_DEADLINE */
 		__entry->group_dead	= group_dead;
@@ -787,7 +787,7 @@ TRACE_EVENT(sched_skip_cpuset_numa,
 	),
 
 	TP_fast_assign(
-		memcpy(__entry->comm, tsk->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, tsk, TASK_COMM_LEN);
 		__entry->pid		 = task_pid_nr(tsk);
 		__entry->tgid		 = task_tgid_nr(tsk);
 		__entry->ngid		 = task_numa_group_id(tsk);
diff --git a/include/trace/events/signal.h b/include/trace/events/signal.h
index 1db7e4b07c01..8fffe6d9bdcc 100644
--- a/include/trace/events/signal.h
+++ b/include/trace/events/signal.h
@@ -67,7 +67,7 @@ TRACE_EVENT(signal_generate,
 	TP_fast_assign(
 		__entry->sig	= sig;
 		TP_STORE_SIGINFO(__entry, info);
-		memcpy(__entry->comm, task->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, task, TASK_COMM_LEN);
 		__entry->pid	= task->pid;
 		__entry->group	= group;
 		__entry->result	= result;
diff --git a/include/trace/events/task.h b/include/trace/events/task.h
index b9a129eb54d9..62b7df4d22d0 100644
--- a/include/trace/events/task.h
+++ b/include/trace/events/task.h
@@ -21,7 +21,7 @@ TRACE_EVENT(task_newtask,
 
 	TP_fast_assign(
 		__entry->pid = task->pid;
-		memcpy(__entry->comm, task->comm, TASK_COMM_LEN);
+		copy_task_comm(__entry->comm, task, TASK_COMM_LEN);
 		__entry->clone_flags = clone_flags;
 		__entry->oom_score_adj = task->signal->oom_score_adj;
 	),
@@ -46,8 +46,9 @@ TRACE_EVENT(task_rename,
 
 	TP_fast_assign(
 		__entry->pid = task->pid;
-		memcpy(entry->oldcomm, task->comm, TASK_COMM_LEN);
-		strscpy(entry->newcomm, comm, TASK_COMM_LEN);
+		copy_task_comm(entry->oldcomm, task, TASK_COMM_LEN);
+		memcpy(entry->newcomm, comm, TASK_COMM_LEN);
+		entry->newcomm[TASK_COMM_LEN - 1] = '\0';
 		__entry->oom_score_adj = task->signal->oom_score_adj;
 	),
 
diff --git a/kernel/printk/nbcon.c b/kernel/printk/nbcon.c
index 4b03b019cd5e..f4c5ae8a3530 100644
--- a/kernel/printk/nbcon.c
+++ b/kernel/printk/nbcon.c
@@ -952,7 +952,7 @@ static void wctxt_load_execution_ctx(struct nbcon_write_context *wctxt,
 {
 	wctxt->cpu = pmsg->cpu;
 	wctxt->pid = pmsg->pid;
-	memcpy(wctxt->comm, pmsg->comm, sizeof(wctxt->comm));
+	copy_task_comm(wctxt->comm, pmsg, sizeof(wctxt->comm));
 	static_assert(sizeof(wctxt->comm) == sizeof(pmsg->comm));
 }
 #else
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index 019b11de262c..ec0c6734bbe1 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -2255,7 +2255,7 @@ static void pmsg_load_execution_ctx(struct printk_message *pmsg,
 {
 	pmsg->cpu = printk_info_get_cpu(info);
 	pmsg->pid = printk_info_get_pid(info);
-	memcpy(pmsg->comm, info->comm, sizeof(pmsg->comm));
+	copy_task_comm(pmsg->comm, info, sizeof(pmsg->comm));
 	static_assert(sizeof(pmsg->comm) == sizeof(info->comm));
 }
 #else

-- 
2.55.0


^ permalink raw reply related

* [PATCH v4 1/6] treewide: Get rid of get_task_comm()
From: André Almeida @ 2026-07-17 13:54 UTC (permalink / raw)
  To: Peter Zijlstra, Juri Lelli, Vincent Guittot, Steven Rostedt,
	Christian Brauner, Kees Cook, Shuah Khan, willy,
	mathieu.desnoyers, David Laight, Linus Torvalds, akpm,
	Yafang Shao, andrii.nakryiko, arnaldo.melo, Petr Mladek
  Cc: linux-kernel, kernel-dev, linux-mm, linux-api, André Almeida
In-Reply-To: <20260717-tonyk-long_name-v4-0-1fedfc870d21@igalia.com>

Since commit 4cc0473d7754 ("get rid of __get_task_comm()"),
get_task_comm() does just a redundant check for the buffer size and call
strscpy_pad(). Replace get_task_comm() calls with strscpy_pad(), that will
do the right thing if the buffers sizes doesn't match: zero-pad if it's
bigger, and truncate if it's smaller.

Link: https://lore.kernel.org/lkml/CAHk-=wi5c=_-FBGo_88CowJd_F-Gi6Ud9d=TALm65ReN7YjrMw@mail.gmail.com/
Signed-off-by: André Almeida <andrealmeid@igalia.com>
---
Changes from v1:
 - Fix for security/ipe/audit.c and net/netfilter/nf_tables_api.c
---
 drivers/connector/cn_proc.c                        |  2 +-
 drivers/dma-buf/sw_sync.c                          |  2 +-
 drivers/gpu/drm/amd/amdgpu/amdgpu_amdkfd_fence.c   |  2 +-
 drivers/gpu/drm/amd/amdgpu/amdgpu_eviction_fence.c |  2 +-
 drivers/gpu/drm/amd/amdgpu/amdgpu_ras.c            |  2 +-
 drivers/gpu/drm/amd/amdgpu/amdgpu_userq_fence.c    |  2 +-
 drivers/gpu/drm/amd/amdgpu/amdgpu_vm.c             |  4 ++--
 drivers/gpu/drm/amd/amdgpu/amdgpu_vram_mgr.c       |  2 +-
 drivers/gpu/drm/lima/lima_ctx.c                    |  2 +-
 drivers/gpu/drm/panfrost/panfrost_gem.c            |  2 +-
 drivers/gpu/drm/panthor/panthor_gem.c              |  2 +-
 drivers/gpu/drm/panthor/panthor_sched.c            |  2 +-
 drivers/gpu/drm/virtio/virtgpu_ioctl.c             |  2 +-
 drivers/hwtracing/stm/core.c                       |  2 +-
 drivers/tty/tty_audit.c                            |  2 +-
 fs/binfmt_elf.c                                    |  2 +-
 fs/binfmt_elf_fdpic.c                              |  2 +-
 fs/proc/array.c                                    |  2 +-
 include/linux/sched.h                              | 19 -------------------
 kernel/audit.c                                     |  6 ++++--
 kernel/auditsc.c                                   |  6 ++++--
 kernel/printk/printk.c                             |  2 +-
 kernel/sys.c                                       |  2 +-
 net/bluetooth/hci_sock.c                           |  2 +-
 net/netfilter/nf_tables_api.c                      |  4 +++-
 security/integrity/integrity_audit.c               |  3 ++-
 security/ipe/audit.c                               |  3 ++-
 security/landlock/domain.c                         |  2 +-
 security/lsm_audit.c                               |  7 ++++---
 29 files changed, 42 insertions(+), 52 deletions(-)

diff --git a/drivers/connector/cn_proc.c b/drivers/connector/cn_proc.c
index 0056ab81fbc3..c78243ed3c2a 100644
--- a/drivers/connector/cn_proc.c
+++ b/drivers/connector/cn_proc.c
@@ -278,7 +278,7 @@ void proc_comm_connector(struct task_struct *task)
 	ev->what = PROC_EVENT_COMM;
 	ev->event_data.comm.process_pid  = task->pid;
 	ev->event_data.comm.process_tgid = task->tgid;
-	get_task_comm(ev->event_data.comm.comm, task);
+	strscpy_pad(ev->event_data.comm.comm, task->comm);
 
 	memcpy(&msg->id, &cn_proc_event_id, sizeof(msg->id));
 	msg->ack = 0; /* not used */
diff --git a/drivers/dma-buf/sw_sync.c b/drivers/dma-buf/sw_sync.c
index 8df20b0218a9..d501657ad801 100644
--- a/drivers/dma-buf/sw_sync.c
+++ b/drivers/dma-buf/sw_sync.c
@@ -312,7 +312,7 @@ static int sw_sync_debugfs_open(struct inode *inode, struct file *file)
 	struct sync_timeline *obj;
 	char task_comm[TASK_COMM_LEN];
 
-	get_task_comm(task_comm, current);
+	strscpy_pad(task_comm, current->comm);
 
 	obj = sync_timeline_create(task_comm);
 	if (!obj)
diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_amdkfd_fence.c b/drivers/gpu/drm/amd/amdgpu/amdgpu_amdkfd_fence.c
index 6a364357522b..13c8857e4ffb 100644
--- a/drivers/gpu/drm/amd/amdgpu/amdgpu_amdkfd_fence.c
+++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_amdkfd_fence.c
@@ -74,7 +74,7 @@ struct amdgpu_amdkfd_fence *amdgpu_amdkfd_fence_create(u64 context,
 	/* This reference gets released in amdkfd_fence_release */
 	mmgrab(mm);
 	fence->mm = mm;
-	get_task_comm(fence->timeline_name, current);
+	strscpy_pad(fence->timeline_name, current->comm);
 	spin_lock_init(&fence->lock);
 	fence->svm_bo = svm_bo;
 	fence->context_id = context_id;
diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_eviction_fence.c b/drivers/gpu/drm/amd/amdgpu/amdgpu_eviction_fence.c
index f6b7522c3c82..046243e3a3cd 100644
--- a/drivers/gpu/drm/amd/amdgpu/amdgpu_eviction_fence.c
+++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_eviction_fence.c
@@ -128,7 +128,7 @@ int amdgpu_evf_mgr_rearm(struct amdgpu_eviction_fence_mgr *evf_mgr,
 		return -ENOMEM;
 
 	ev_fence->evf_mgr = evf_mgr;
-	get_task_comm(ev_fence->timeline_name, current);
+	strscpy_pad(ev_fence->timeline_name, current->comm);
 	spin_lock_init(&ev_fence->lock);
 	dma_fence_init64(&ev_fence->base, &amdgpu_eviction_fence_ops,
 			 &ev_fence->lock, evf_mgr->ev_fence_ctx,
diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_ras.c b/drivers/gpu/drm/amd/amdgpu/amdgpu_ras.c
index 764cd4950408..e60a470b57ec 100644
--- a/drivers/gpu/drm/amd/amdgpu/amdgpu_ras.c
+++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_ras.c
@@ -4445,7 +4445,7 @@ int amdgpu_ras_init(struct amdgpu_device *adev)
 	}
 
 	con->init_task_pid = task_pid_nr(current);
-	get_task_comm(con->init_task_comm, current);
+	strscpy_pad(con->init_task_comm, current->comm);
 
 	mutex_init(&con->critical_region_lock);
 	INIT_LIST_HEAD(&con->critical_region_head);
diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_userq_fence.c b/drivers/gpu/drm/amd/amdgpu/amdgpu_userq_fence.c
index f74ad378e407..f41857473641 100644
--- a/drivers/gpu/drm/amd/amdgpu/amdgpu_userq_fence.c
+++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_userq_fence.c
@@ -85,7 +85,7 @@ int amdgpu_userq_fence_driver_alloc(struct amdgpu_device *adev,
 
 	fence_drv->adev = adev;
 	fence_drv->context = dma_fence_context_alloc(1);
-	get_task_comm(fence_drv->timeline_name, current);
+	strscpy_pad(fence_drv->timeline_name, current->comm);
 
 	*fence_drv_req = fence_drv;
 
diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_vm.c b/drivers/gpu/drm/amd/amdgpu/amdgpu_vm.c
index bb99b7c3a010..84c0b310e9c1 100644
--- a/drivers/gpu/drm/amd/amdgpu/amdgpu_vm.c
+++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_vm.c
@@ -2542,10 +2542,10 @@ void amdgpu_vm_set_task_info(struct amdgpu_vm *vm)
 		return;
 
 	vm->task_info->task.pid = current->pid;
-	get_task_comm(vm->task_info->task.comm, current);
+	strscpy_pad(vm->task_info->task.comm, current->comm);
 
 	vm->task_info->tgid = current->tgid;
-	get_task_comm(vm->task_info->process_name, current->group_leader);
+	strscpy_pad(vm->task_info->process_name, current->group_leader->comm);
 }
 
 /**
diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_vram_mgr.c b/drivers/gpu/drm/amd/amdgpu/amdgpu_vram_mgr.c
index 2a241a5b12c4..f8ce59d8587a 100644
--- a/drivers/gpu/drm/amd/amdgpu/amdgpu_vram_mgr.c
+++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_vram_mgr.c
@@ -563,7 +563,7 @@ static int amdgpu_vram_mgr_new(struct ttm_resource_manager *man,
 	}
 
 	vres->task.pid = task_pid_nr(current);
-	get_task_comm(vres->task.comm, current);
+	strscpy_pad(vres->task.comm, current->comm);
 	list_add_tail(&vres->vres_node, &mgr->allocated_vres_list);
 
 	if (bo->flags & AMDGPU_GEM_CREATE_VRAM_CONTIGUOUS && adjust_dcc_size) {
diff --git a/drivers/gpu/drm/lima/lima_ctx.c b/drivers/gpu/drm/lima/lima_ctx.c
index 68ede7a725e2..e8c5c3601bf1 100644
--- a/drivers/gpu/drm/lima/lima_ctx.c
+++ b/drivers/gpu/drm/lima/lima_ctx.c
@@ -29,7 +29,7 @@ int lima_ctx_create(struct lima_device *dev, struct lima_ctx_mgr *mgr, u32 *id)
 		goto err_out0;
 
 	ctx->pid = task_pid_nr(current);
-	get_task_comm(ctx->pname, current);
+	strscpy_pad(ctx->pname, current->comm);
 
 	return 0;
 
diff --git a/drivers/gpu/drm/panfrost/panfrost_gem.c b/drivers/gpu/drm/panfrost/panfrost_gem.c
index 3a7fce428898..11936c4d3573 100644
--- a/drivers/gpu/drm/panfrost/panfrost_gem.c
+++ b/drivers/gpu/drm/panfrost/panfrost_gem.c
@@ -36,7 +36,7 @@ static void panfrost_gem_debugfs_bo_add(struct panfrost_device *pfdev,
 					struct panfrost_gem_object *bo)
 {
 	bo->debugfs.creator.tgid = current->tgid;
-	get_task_comm(bo->debugfs.creator.process_name, current->group_leader);
+	strscpy_pad(bo->debugfs.creator.process_name, current->group_leader->comm);
 
 	mutex_lock(&pfdev->debugfs.gems_lock);
 	list_add_tail(&bo->debugfs.node, &pfdev->debugfs.gems_list);
diff --git a/drivers/gpu/drm/panthor/panthor_gem.c b/drivers/gpu/drm/panthor/panthor_gem.c
index a1e2eb1ca7bb..ebb3664e4303 100644
--- a/drivers/gpu/drm/panthor/panthor_gem.c
+++ b/drivers/gpu/drm/panthor/panthor_gem.c
@@ -54,7 +54,7 @@ static void panthor_gem_debugfs_bo_add(struct panthor_gem_object *bo)
 						    struct panthor_device, base);
 
 	bo->debugfs.creator.tgid = current->tgid;
-	get_task_comm(bo->debugfs.creator.process_name, current->group_leader);
+	strscpy_pad(bo->debugfs.creator.process_name, current->group_leader->comm);
 
 	mutex_lock(&ptdev->gems.lock);
 	list_add_tail(&bo->debugfs.node, &ptdev->gems.node);
diff --git a/drivers/gpu/drm/panthor/panthor_sched.c b/drivers/gpu/drm/panthor/panthor_sched.c
index 298b046c95ed..81c4659c0d7b 100644
--- a/drivers/gpu/drm/panthor/panthor_sched.c
+++ b/drivers/gpu/drm/panthor/panthor_sched.c
@@ -3617,7 +3617,7 @@ static void group_init_task_info(struct panthor_group *group)
 	struct task_struct *task = current->group_leader;
 
 	group->task_info.pid = task->pid;
-	get_task_comm(group->task_info.comm, task);
+	strscpy_pad(group->task_info.comm, task->comm);
 }
 
 static void add_group_kbo_sizes(struct panthor_device *ptdev,
diff --git a/drivers/gpu/drm/virtio/virtgpu_ioctl.c b/drivers/gpu/drm/virtio/virtgpu_ioctl.c
index 3d8e4ccdb7c1..b10a0e1cb1ed 100644
--- a/drivers/gpu/drm/virtio/virtgpu_ioctl.c
+++ b/drivers/gpu/drm/virtio/virtgpu_ioctl.c
@@ -50,7 +50,7 @@ static void virtio_gpu_create_context_locked(struct virtio_gpu_device *vgdev,
 	} else {
 		char dbgname[TASK_COMM_LEN];
 
-		get_task_comm(dbgname, current);
+		strscpy_pad(dbgname, current->comm);
 		virtio_gpu_cmd_context_create(vgdev, vfpriv->ctx_id,
 					      vfpriv->context_init, strlen(dbgname),
 					      dbgname);
diff --git a/drivers/hwtracing/stm/core.c b/drivers/hwtracing/stm/core.c
index f48c6a8a0654..c7715439964e 100644
--- a/drivers/hwtracing/stm/core.c
+++ b/drivers/hwtracing/stm/core.c
@@ -634,7 +634,7 @@ static ssize_t stm_char_write(struct file *file, const char __user *buf,
 		char comm[sizeof(current->comm)];
 		char *ids[] = { comm, "default", NULL };
 
-		get_task_comm(comm, current);
+		strscpy_pad(comm, current->comm);
 
 		err = stm_assign_first_policy(stmf->stm, &stmf->output, ids, 1);
 		/*
diff --git a/drivers/tty/tty_audit.c b/drivers/tty/tty_audit.c
index d014af6ab060..d514a81d0a5c 100644
--- a/drivers/tty/tty_audit.c
+++ b/drivers/tty/tty_audit.c
@@ -77,7 +77,7 @@ static void tty_audit_log(const char *description, dev_t dev,
 	audit_log_format(ab, "%s pid=%u uid=%u auid=%u ses=%u major=%d minor=%d comm=",
 			 description, pid, uid, loginuid, sessionid,
 			 MAJOR(dev), MINOR(dev));
-	get_task_comm(name, current);
+	strscpy_pad(name, current->comm);
 	audit_log_untrustedstring(ab, name);
 	audit_log_format(ab, " data=");
 	audit_log_n_hex(ab, data, size);
diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
index 16a56b6b3f6c..d25922460b63 100644
--- a/fs/binfmt_elf.c
+++ b/fs/binfmt_elf.c
@@ -1557,7 +1557,7 @@ static int fill_psinfo(struct elf_prpsinfo *psinfo, struct task_struct *p,
 	SET_UID(psinfo->pr_uid, from_kuid_munged(cred->user_ns, cred->uid));
 	SET_GID(psinfo->pr_gid, from_kgid_munged(cred->user_ns, cred->gid));
 	rcu_read_unlock();
-	get_task_comm(psinfo->pr_fname, p);
+	strscpy_pad(psinfo->pr_fname, p->comm);
 
 	return 0;
 }
diff --git a/fs/binfmt_elf_fdpic.c b/fs/binfmt_elf_fdpic.c
index 7e3108489c83..c4d4e59ff34d 100644
--- a/fs/binfmt_elf_fdpic.c
+++ b/fs/binfmt_elf_fdpic.c
@@ -1371,7 +1371,7 @@ static int fill_psinfo(struct elf_prpsinfo *psinfo, struct task_struct *p,
 	SET_UID(psinfo->pr_uid, from_kuid_munged(cred->user_ns, cred->uid));
 	SET_GID(psinfo->pr_gid, from_kgid_munged(cred->user_ns, cred->gid));
 	rcu_read_unlock();
-	get_task_comm(psinfo->pr_fname, p);
+	strscpy_pad(psinfo->pr_fname, p->comm);
 
 	return 0;
 }
diff --git a/fs/proc/array.c b/fs/proc/array.c
index 479ea8cb4ef4..905d910b598f 100644
--- a/fs/proc/array.c
+++ b/fs/proc/array.c
@@ -110,7 +110,7 @@ void proc_task_name(struct seq_file *m, struct task_struct *p, bool escape)
 	else if (p->flags & PF_KTHREAD)
 		get_kthread_comm(tcomm, sizeof(tcomm), p);
 	else
-		get_task_comm(tcomm, p);
+		strscpy_pad(tcomm, p->comm);
 
 	if (escape)
 		seq_escape_str(m, tcomm, ESCAPE_SPACE | ESCAPE_SPECIAL, "\n\\");
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 373bcc0598d1..2714c81bc708 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -2021,25 +2021,6 @@ extern void __set_task_comm(struct task_struct *tsk, const char *from, bool exec
 	__set_task_comm(tsk, from, false);		\
 })
 
-/*
- * - Why not use task_lock()?
- *   User space can randomly change their names anyway, so locking for readers
- *   doesn't make sense. For writers, locking is probably necessary, as a race
- *   condition could lead to long-term mixed results.
- *   The strscpy_pad() in __set_task_comm() can ensure that the task comm is
- *   always NUL-terminated and zero-padded. Therefore the race condition between
- *   reader and writer is not an issue.
- *
- * - BUILD_BUG_ON() can help prevent the buf from being truncated.
- *   Since the callers don't perform any return value checks, this safeguard is
- *   necessary.
- */
-#define get_task_comm(buf, tsk) ({			\
-	BUILD_BUG_ON(sizeof(buf) < TASK_COMM_LEN);	\
-	strscpy_pad(buf, (tsk)->comm);			\
-	buf;						\
-})
-
 static __always_inline void scheduler_ipi(void)
 {
 	/*
diff --git a/kernel/audit.c b/kernel/audit.c
index 562476937fa7..1f0b44d4e5c7 100644
--- a/kernel/audit.c
+++ b/kernel/audit.c
@@ -1667,7 +1667,8 @@ static void audit_log_multicast(int group, const char *op, int err)
 	audit_put_tty(tty);
 	audit_log_task_context(ab); /* subj= */
 	audit_log_format(ab, " comm=");
-	audit_log_untrustedstring(ab, get_task_comm(comm, current));
+	strscpy_pad(comm, current->comm);
+	audit_log_untrustedstring(ab, comm);
 	audit_log_d_path_exe(ab, current->mm); /* exe= */
 	audit_log_format(ab, " nl-mcgrp=%d op=%s res=%d", group, op, !err);
 	audit_log_end(ab);
@@ -2476,7 +2477,8 @@ void audit_log_task_info(struct audit_buffer *ab)
 			 audit_get_sessionid(current));
 	audit_put_tty(tty);
 	audit_log_format(ab, " comm=");
-	audit_log_untrustedstring(ab, get_task_comm(comm, current));
+	strscpy_pad(comm, current->comm);
+	audit_log_untrustedstring(ab, comm);
 	audit_log_d_path_exe(ab, current->mm);
 	audit_log_task_context(ab);
 }
diff --git a/kernel/auditsc.c b/kernel/auditsc.c
index 6610e667c728..1d930f169856 100644
--- a/kernel/auditsc.c
+++ b/kernel/auditsc.c
@@ -2877,7 +2877,8 @@ void __audit_log_nfcfg(const char *name, u8 af, unsigned int nentries,
 	audit_log_format(ab, " pid=%u", task_tgid_nr(current));
 	audit_log_task_context(ab); /* subj= */
 	audit_log_format(ab, " comm=");
-	audit_log_untrustedstring(ab, get_task_comm(comm, current));
+	strscpy_pad(comm, current->comm);
+	audit_log_untrustedstring(ab, comm);
 	audit_log_end(ab);
 }
 EXPORT_SYMBOL_GPL(__audit_log_nfcfg);
@@ -2900,7 +2901,8 @@ static void audit_log_task(struct audit_buffer *ab)
 			 sessionid);
 	audit_log_task_context(ab);
 	audit_log_format(ab, " pid=%d comm=", task_tgid_nr(current));
-	audit_log_untrustedstring(ab, get_task_comm(comm, current));
+	strscpy_pad(comm, current->comm);
+	audit_log_untrustedstring(ab, comm);
 	audit_log_d_path_exe(ab, current->mm);
 }
 
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index 2fe9a963c823..019b11de262c 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -2247,7 +2247,7 @@ static u16 printk_sprint(char *text, u16 size, int facility,
 static void printk_store_execution_ctx(struct printk_info *info)
 {
 	info->caller_id2 = printk_caller_id2();
-	get_task_comm(info->comm, current);
+	strscpy_pad(info->comm, current->comm);
 }
 
 static void pmsg_load_execution_ctx(struct printk_message *pmsg,
diff --git a/kernel/sys.c b/kernel/sys.c
index df69bd71de03..74c22d6dded0 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -2609,7 +2609,7 @@ SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
 		proc_comm_connector(me);
 		break;
 	case PR_GET_NAME:
-		get_task_comm(comm, me);
+		strscpy_pad(comm, me->comm);
 		if (copy_to_user((char __user *)arg2, comm, sizeof(comm)))
 			return -EFAULT;
 		break;
diff --git a/net/bluetooth/hci_sock.c b/net/bluetooth/hci_sock.c
index 070ca388f9ac..4e66cb2d9549 100644
--- a/net/bluetooth/hci_sock.c
+++ b/net/bluetooth/hci_sock.c
@@ -104,7 +104,7 @@ static bool hci_sock_gen_cookie(struct sock *sk)
 			id = 0xffffffff;
 
 		hci_pi(sk)->cookie = id;
-		get_task_comm(hci_pi(sk)->comm, current);
+		strscpy_pad(hci_pi(sk)->comm, current->comm);
 		return true;
 	}
 
diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index a9eaf9455c77..c5555eed1b62 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -9718,9 +9718,11 @@ static int nf_tables_fill_gen_info(struct sk_buff *skb, struct net *net,
 	if (!nlh)
 		goto nla_put_failure;
 
+	strscpy_pad(buf, current->comm);
+
 	if (nla_put_be32(skb, NFTA_GEN_ID, htonl(nft_base_seq(net))) ||
 	    nla_put_be32(skb, NFTA_GEN_PROC_PID, htonl(task_pid_nr(current))) ||
-	    nla_put_string(skb, NFTA_GEN_PROC_NAME, get_task_comm(buf, current)))
+	    nla_put_string(skb, NFTA_GEN_PROC_NAME, buf))
 		goto nla_put_failure;
 
 	nlmsg_end(skb, nlh);
diff --git a/security/integrity/integrity_audit.c b/security/integrity/integrity_audit.c
index d8d9e5ff1cd2..98060060929d 100644
--- a/security/integrity/integrity_audit.c
+++ b/security/integrity/integrity_audit.c
@@ -54,7 +54,8 @@ void integrity_audit_message(int audit_msgno, struct inode *inode,
 			 audit_get_sessionid(current));
 	audit_log_task_context(ab);
 	audit_log_format(ab, " op=%s cause=%s comm=", op, cause);
-	audit_log_untrustedstring(ab, get_task_comm(name, current));
+	strscpy_pad(name, current->comm);
+	audit_log_untrustedstring(ab, name);
 	if (fname) {
 		audit_log_format(ab, " name=");
 		audit_log_untrustedstring(ab, fname);
diff --git a/security/ipe/audit.c b/security/ipe/audit.c
index 93fb59fbddd6..90a6acfb7cdf 100644
--- a/security/ipe/audit.c
+++ b/security/ipe/audit.c
@@ -145,7 +145,8 @@ void ipe_audit_match(const struct ipe_eval_ctx *const ctx,
 	audit_log_format(ab, "ipe_op=%s ipe_hook=%s enforcing=%d pid=%d comm=",
 			 op, audit_hook_names[ctx->hook], READ_ONCE(enforce),
 			 task_tgid_nr(current));
-	audit_log_untrustedstring(ab, get_task_comm(comm, current));
+	strscpy_pad(comm, current->comm);
+	audit_log_untrustedstring(ab, comm);
 
 	if (ctx->file) {
 		audit_log_d_path(ab, " path=", &ctx->file->f_path);
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 9a8355fccd26..b2fc7f722d69 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -102,7 +102,7 @@ static struct landlock_details *get_current_details(void)
 	memcpy(details->exe_path, path_str, path_size);
 	details->pid = get_pid(task_tgid(current));
 	details->uid = from_kuid(&init_user_ns, current_uid());
-	get_task_comm(details->comm, current);
+	strscpy_pad(details->comm, current->comm);
 	return details;
 }
 
diff --git a/security/lsm_audit.c b/security/lsm_audit.c
index 737f5a263a8f..a587ffecd985 100644
--- a/security/lsm_audit.c
+++ b/security/lsm_audit.c
@@ -276,8 +276,8 @@ void audit_log_lsm_data(struct audit_buffer *ab,
 			if (pid) {
 				char tskcomm[sizeof(tsk->comm)];
 				audit_log_format(ab, " opid=%d ocomm=", pid);
-				audit_log_untrustedstring(ab,
-				    get_task_comm(tskcomm, tsk));
+				strscpy_pad(tskcomm, tsk->comm);
+				audit_log_untrustedstring(ab, tskcomm);
 			}
 		}
 		break;
@@ -417,7 +417,8 @@ static void dump_common_audit_data(struct audit_buffer *ab,
 	char comm[sizeof(current->comm)];
 
 	audit_log_format(ab, " pid=%d comm=", task_tgid_nr(current));
-	audit_log_untrustedstring(ab, get_task_comm(comm, current));
+	strscpy_pad(comm, current->comm);
+	audit_log_untrustedstring(ab, comm);
 	audit_log_lsm_data(ab, a);
 }
 

-- 
2.55.0


^ permalink raw reply related

* [PATCH v4 0/6] sched: Add support for long task name
From: André Almeida @ 2026-07-17 13:54 UTC (permalink / raw)
  To: Peter Zijlstra, Juri Lelli, Vincent Guittot, Steven Rostedt,
	Christian Brauner, Kees Cook, Shuah Khan, willy,
	mathieu.desnoyers, David Laight, Linus Torvalds, akpm,
	Yafang Shao, andrii.nakryiko, arnaldo.melo, Petr Mladek
  Cc: linux-kernel, kernel-dev, linux-mm, linux-api, André Almeida

* Use case

When debugging and tracing complex programs with hundreds of threads, 16 bytes
long thread names are not enough anymore. cmd_line can show a lot of
characters, but it's not affected by pthread_setname_np() or
prctl(PR_SET_NAME), so let's give the same love kthreads got with commit
6b59808bfe48 ("workqueue: Show the latest workqueue name in 
/proc/PID/{comm,stat,status}"). This patchset creates a new
PR_{SET,GET}_EXT_NAME that supports 64 bytes long names.

It also introduces a new function copy_task_comm() that ensures that the string
is always NUL-terminated despite of mismatching sizes of buffers. We can't just
use strscpy() because it proved to give some overhead [0] in tracing.

* Patchset

Patch 1 and 2 do some prep work in order to avoid buffer overflows around
the kernel, now that current->comm is bigger. It also make sure that if
the destination buffer is smaller than TASK_COMM_EXT_LEN, it will
be NUL-terminated.

Patch 3 adds a KUnit for the new function copy_task_comm()

Patch 4 sets current->comm length to TASK_COMM_EXT_LEN and take care of
making sure that current userspace APIs gets only TASK_COMM_LEN.

Patch 5 creates new prctl() to set and get all the TASK_COMM_EXT_LEN bytes.

Patch 6 adapts the existing selftest for this new interface.

* Testing

selftests/prctl/set-process-name.c survives this patchset, and it was extended
to the new interface. KUnit test was modified to support copy_task_comm().

I ran the same benchmark as at [0], and no significant change was found.

* Changes

Since v3:
 - Simplify Get rid of get_task_comm() commit
 - Simplify copy_task_comm(): just do a memcpy + NUL char at the end
 - Link to v3: https://patch.msgid.link/20260612-tonyk-long_name-v3-0-7989b66e8a99@igalia.com

Since v2:
 - Add a custom function copy_task_comm() that uses memcpy when possible and
 fallback to strscpy(). It always ensures that the string in NUL-terminated
 - Add KUnit test for the new function
 - Link to v2: https://patch.msgid.link/20260524-tonyk-long_name-v2-0-332f6bd041c4@igalia.com

Since v1:
 - Replace new strtostr() with strscpy()
 - Don't replace memcpy in tools/
 - Link to v1: https://patch.msgid.link/20260517-tonyk-long_name-v1-0-3c282eaa91e2@igalia.com

[0] https://lore.kernel.org/lkml/20260526190625.3f4aca0a@gandalf.local.home/

---
André Almeida (6):
      treewide: Get rid of get_task_comm()
      treewide: Replace memcpy(..., current->comm) with copy_task_comm()
      lib/string_kunit: Add test for copy_task_comm()
      sched: Extend task command name with TASK_COMM_EXT_LEN
      prctl: Add support for long user thread names
      selftests: prctl: Add test for long thread names

 drivers/connector/cn_proc.c                        |  2 +-
 drivers/dma-buf/sw_sync.c                          |  2 +-
 drivers/gpu/drm/amd/amdgpu/amdgpu_amdkfd_fence.c   |  2 +-
 drivers/gpu/drm/amd/amdgpu/amdgpu_eviction_fence.c |  2 +-
 drivers/gpu/drm/amd/amdgpu/amdgpu_ras.c            |  2 +-
 drivers/gpu/drm/amd/amdgpu/amdgpu_userq_fence.c    |  2 +-
 drivers/gpu/drm/amd/amdgpu/amdgpu_vm.c             |  4 +--
 drivers/gpu/drm/amd/amdgpu/amdgpu_vram_mgr.c       |  2 +-
 drivers/gpu/drm/lima/lima_ctx.c                    |  2 +-
 drivers/gpu/drm/panfrost/panfrost_gem.c            |  2 +-
 drivers/gpu/drm/panthor/panthor_gem.c              |  2 +-
 drivers/gpu/drm/panthor/panthor_sched.c            |  2 +-
 drivers/gpu/drm/virtio/virtgpu_ioctl.c             |  2 +-
 drivers/hwtracing/stm/core.c                       |  2 +-
 drivers/tty/tty_audit.c                            |  2 +-
 fs/binfmt_elf.c                                    |  2 +-
 fs/binfmt_elf_fdpic.c                              |  2 +-
 fs/proc/array.c                                    |  2 +-
 include/linux/coredump.h                           |  2 +-
 include/linux/sched.h                              | 29 +++++++----------
 include/linux/tracepoint.h                         |  4 +--
 include/trace/events/block.h                       | 10 +++---
 include/trace/events/coredump.h                    |  2 +-
 include/trace/events/f2fs.h                        |  4 +--
 include/trace/events/oom.h                         |  2 +-
 include/trace/events/osnoise.h                     |  2 +-
 include/trace/events/sched.h                       | 10 +++---
 include/trace/events/signal.h                      |  2 +-
 include/trace/events/task.h                        |  7 ++--
 include/uapi/linux/prctl.h                         |  3 ++
 kernel/audit.c                                     |  6 ++--
 kernel/auditsc.c                                   |  6 ++--
 kernel/printk/nbcon.c                              |  2 +-
 kernel/printk/printk.c                             |  4 +--
 kernel/sys.c                                       | 23 ++++++++++---
 lib/tests/string_kunit.c                           | 38 ++++++++++++++++++++++
 net/bluetooth/hci_sock.c                           |  2 +-
 net/netfilter/nf_tables_api.c                      |  4 ++-
 security/integrity/integrity_audit.c               |  3 +-
 security/ipe/audit.c                               |  3 +-
 security/landlock/domain.c                         |  2 +-
 security/lsm_audit.c                               |  7 ++--
 tools/testing/selftests/prctl/set-process-name.c   | 36 ++++++++++++++++++++
 43 files changed, 172 insertions(+), 79 deletions(-)
---
base-commit: 481ed5dd3ed7136f627b8ec372ba39f5b2e7d27f
change-id: 20260516-tonyk-long_name-b9f345aeb041

Best regards,
--  
André Almeida <andrealmeid@igalia.com>


^ permalink raw reply

* Re: [PATCH 0/3] elf: load the main program from AT_EXECFD
From: Christian Brauner @ 2026-07-17  9:53 UTC (permalink / raw)
  To: Florian Weimer
  Cc: Christian Brauner, Carlos O'Donell, libc-alpha, linux-api,
	linux-kernel
In-Reply-To: <874ihyypvp.fsf@oldenburg.str.redhat.com>

> > At first I was very confused about this proposal but I think I
> > understand what you are after now. Your point is that we shouldn't just
> > fix argv like in my proposal but actually even fix the exe file and
> > remap.
> 
> Not sure about remap.  But I think to turn this into an it-just-works
> solution, we need to fix /proc/self/exe and the auxiliary vector.

Yes.

> 
> > So I think doing this purely in userspace isn't doable. The restore
> > parts can fix everything except the exe link. PR_SET_MM_MAP requires
> > capabilities in the relevant user namespace for the exe file. That makes
> > it pretty useless for us. And dropping that capability requirement isn't
> > feasible, I think. LSMs and audit trust the exe link, so an uncapped
> > exe file would let any process masquerade as an arbitrary executable.
> 
> Is the latter really a problem?

I remember that I spoke out against this about 5 years ago when I merged
CAP_CHECKPOINT_RESTORE. I think changing it is something we can try but
it would take a while.

Extending binfmt_misc to do it as an option when registering a binfmt
handler is way easier imho (and simpler).

> > Everything else (the entire saved auxv, the start/end_code/data, brk and
> > stack markers) is validated but requires no capability at all. So an
> > unprivileged ld.so can already repair /proc/<pid>/auxv and the
> > stat/statm code accounting, but never /proc/self/exe.
> 
> It's good for us if AT_SECURE does not need protecting.  We would like
> to use a fake 1 value in our test suite.
> 
> > Unmapping the first copy is also not really feasible. Even with
> > privilege making this work would be very ugly: The kernel's
> > replace_mm_exe_file() refuses with -EBUSY while any vma still maps the
> > old exe file. From my research, CRIU works around exactly this by
> > copying its restorer blob into an anonymous mapping before it unmaps the
> > old address space.
> 
> Unmapping the first copy was only my idea to make this work with the
> existing kernel facilities.  It's not required for the binfmt_misc case
> and most /usr/bin/ld.so scenarios, and actually drives up complexity
> considerably.

I mean, it is kinda moving into CRIU territory, so yes, doing it like
that will be complex. :)

> > So I think the exe link should be fixed up at exec time.
> > begin_new_exec() sets mm->exe_file to bprm->file which after the
> > binfmt_misc handoff is the interpreter. But bprm->executable is the file
> > the kernel access-checked and kept open for AT_EXECFD. We have it right
> > there. This is the file that would_dump() uses for it's decision and
> > binfmt_misc's 'C' flag derive credentials from.
> >
> > We simply need an extension to binfmt_misc that sets mm->exe_file to
> > bprm->executable. Then it is correct from the start and there's no
> > window and no privilege question and existing 'O'/'C' users (qemu-user
> > registrations) are unaffected. It then raises AT_FLAGS_PRESERVE_ARGV.
> >
> > On the userspace side, ld.so now sees this AT_* flag and in response
> > issues one uncapped PR_SET_MM_MAP (that's available completely
> > unprivileged) to retarget AT_PHDR/AT_ENTRY/AT_BASE and drop the stale
> > AT_EXECFD from saved_auxv.
> >
> > With both in place, attaching gdb to a dispatched process is fully
> > correct and /proc/self/exe-based self-location works. As a bonus the
> > program file gets the same exe_file write-denial a directly executed
> > binary has. Today it is ld.so that gets pinned and the running program's
> > file stays writable while its text is mapped.
> >
> > After this, only uninteresting differences should be left. This would be
> > a follow-up series I'm happy to do. Does that sound reasonable?
> 
> The proposal looks quite straightforward.  It's a shame that it does not

Ok, I'll work on that once the basic AT_EXECFD stuff is done.

> fix the /usr/bin/ld.so case.  Is there anything we can do to address

You mean the case where it's called in userspace. Yeah, that's a bit
more tricky.

> that?
> 
> I wouldn't mind if we had a system call that triggered the kernel ELF
> loader for the main executable.

Ok, certainly another future patch series to consider. Fwiw, if you have
some far out ideas like that (kernel related) you should always feel
free to just file an issue or a pull request at:

https://github.com/uapi-group/kernel-features/

They appear on this website:

https://uapi-group.org/kernel-features/

and we have multiple people (not just me) that regularly pick items from
this list and implement them. This is a good way to avoid ideas just
being forgotten or invisible.


^ permalink raw reply

* Re: [PATCH 0/3] elf: load the main program from AT_EXECFD
From: Florian Weimer @ 2026-07-17  8:36 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Carlos O'Donell, libc-alpha, linux-api, linux-kernel
In-Reply-To: <20260717-heilpflanzen-mondschein-dackel-bb9dc1dbe965@brauner>

Summary for kernel list: We are looking for ways to make executables not
loaded by the kernel more compatible with the rest of the system.  That
includes proper /proc/self/exe and auxv values.  The discussion was
triggered by Christian's binfmt_misc patch, which happens to share many
of the problems when /usr/bin/ld.so is used to load programs (instead of
the kernel).

* Christian Brauner:

> On 2026-07-16 17:37:21+02:00, Florian Weimer wrote:
>> * Christian Brauner:
>> 
>> >> What does /proc/self/exe look like for such processes?  Does GDB work?
>> >
>> > Right, I checked that.
>> >
>> > /proc/self/exe is ld.so. The kernel exec'd ld.so, so it names ld.so
>> > whether the program arrived via AT_EXECFD, via --program-fd, or via a
>> > plain "ld.so PROG" command line.
>> 
>> Yeah, and that causes problems with binaries that try to be relocatable.
>> We don't have a very convenient way to get the correct path in those
>> scenarios, so a lot of code uses /proc/self/exe instead.
>> 
>> I'm wondering if we can use the checkpoint-restore facilities (the
>> restore parts) to fix this: load ld.so another time, transfer control to
>> it, and instruct it to unmap the first copy and then invoke the
>> necessary prctls to make the process look exactly like a directly
>> invoked process.
>
> At first I was very confused about this proposal but I think I
> understand what you are after now. Your point is that we shouldn't just
> fix argv like in my proposal but actually even fix the exe file and
> remap.

Not sure about remap.  But I think to turn this into an it-just-works
solution, we need to fix /proc/self/exe and the auxiliary vector.

> So I think doing this purely in userspace isn't doable. The restore
> parts can fix everything except the exe link. PR_SET_MM_MAP requires
> capabilities in the relevant user namespace for the exe file. That makes
> it pretty useless for us. And dropping that capability requirement isn't
> feasible, I think. LSMs and audit trust the exe link, so an uncapped
> exe file would let any process masquerade as an arbitrary executable.

Is the latter really a problem?

Today, it's possible to stop a process that is SUID on disk before it
runs any code: 

$ ls -l /proc/163710/exe
lrwxrwxrwx. 1 fweimer fweimer 0 Jul 17 10:11 /proc/163710/exe -> /usr/bin/su

(gdb) info thread
  Id   Target Id           Frame 
* 1    process 163710 "su" 0x00007f5ae2f24e40 in _start ()
   from /lib64/ld-linux-x86-64.so.2

This is with kernel.yama.ptrace_scope=1.  I assume this gives me full
control over a process that is nominally running /usr/bin/su.  Even
AT_SECURE is set to 1:

Breakpoint 2, main (argc=1, argv=0x7ffd77cfb2e8) at login-utils/su.c:5
5	{
(gdb) print __libc_enable_secure
$1 = 1
(gdb) print (int) getuid ()
$2 = 1000
(gdb) print (int) geteuid ()
$3 = 1000
(gdb) print (long) getauxval(23)
$4 = 1

Of course, the AT_SECURE transition did not actually happen, and the
process is running with the original user privileges.

> Everything else (the entire saved auxv, the start/end_code/data, brk and
> stack markers) is validated but requires no capability at all. So an
> unprivileged ld.so can already repair /proc/<pid>/auxv and the
> stat/statm code accounting, but never /proc/self/exe.

It's good for us if AT_SECURE does not need protecting.  We would like
to use a fake 1 value in our test suite.

> Unmapping the first copy is also not really feasible. Even with
> privilege making this work would be very ugly: The kernel's
> replace_mm_exe_file() refuses with -EBUSY while any vma still maps the
> old exe file. From my research, CRIU works around exactly this by
> copying its restorer blob into an anonymous mapping before it unmaps the
> old address space.

Unmapping the first copy was only my idea to make this work with the
existing kernel facilities.  It's not required for the binfmt_misc case
and most /usr/bin/ld.so scenarios, and actually drives up complexity
considerably.

> So I think the exe link should be fixed up at exec time.
> begin_new_exec() sets mm->exe_file to bprm->file which after the
> binfmt_misc handoff is the interpreter. But bprm->executable is the file
> the kernel access-checked and kept open for AT_EXECFD. We have it right
> there. This is the file that would_dump() uses for it's decision and
> binfmt_misc's 'C' flag derive credentials from.
>
> We simply need an extension to binfmt_misc that sets mm->exe_file to
> bprm->executable. Then it is correct from the start and there's no
> window and no privilege question and existing 'O'/'C' users (qemu-user
> registrations) are unaffected. It then raises AT_FLAGS_PRESERVE_ARGV.
>
> On the userspace side, ld.so now sees this AT_* flag and in response
> issues one uncapped PR_SET_MM_MAP (that's available completely
> unprivileged) to retarget AT_PHDR/AT_ENTRY/AT_BASE and drop the stale
> AT_EXECFD from saved_auxv.
>
> With both in place, attaching gdb to a dispatched process is fully
> correct and /proc/self/exe-based self-location works. As a bonus the
> program file gets the same exe_file write-denial a directly executed
> binary has. Today it is ld.so that gets pinned and the running program's
> file stays writable while its text is mapped.
>
> After this, only uninteresting differences should be left. This would be
> a follow-up series I'm happy to do. Does that sound reasonable?

The proposal looks quite straightforward.  It's a shame that it does not
fix the /usr/bin/ld.so case.  Is there anything we can do to address
that?

I wouldn't mind if we had a system call that triggered the kernel ELF
loader for the main executable.

Thanks,
Florian


^ 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


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