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