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 --- 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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