Linux userland API discussions
 help / color / mirror / Atom feed
From: Li Chen <me@linux.beauty>
To: Christian Brauner <brauner@kernel.org>,
	Kees Cook <kees@kernel.org>,
	Alexander Viro <viro@zeniv.linux.org.uk>
Cc: linux-fsdevel@vger.kernel.org, linux-api@vger.kernel.org,
	linux-kernel@vger.kernel.org, linux-mm@kvack.org,
	linux-arch@vger.kernel.org, linux-doc@vger.kernel.org,
	linux-kselftest@vger.kernel.org, x86@kernel.org,
	Arnd Bergmann <arnd@arndb.de>, Andy Lutomirski <luto@kernel.org>,
	Thomas Gleixner <tglx@kernel.org>, Ingo Molnar <mingo@redhat.com>,
	Borislav Petkov <bp@alien8.de>,
	Dave Hansen <dave.hansen@linux.intel.com>,
	"H. Peter Anvin" <hpa@zytor.com>, Jan Kara <jack@suse.cz>,
	Jonathan Corbet <corbet@lwn.net>,
	Shuah Khan <skhan@linuxfoundation.org>, Li Chen <me@linux.beauty>
Subject: [RFC PATCH v1 13/13] selftests/exec: cover spawn template basics
Date: Thu, 28 May 2026 17:52:34 +0800	[thread overview]
Message-ID: <20260528095235.2491226-14-me@linux.beauty> (raw)
In-Reply-To: <20260528095235.2491226-1-me@linux.beauty>

Add exec selftests for the spawn_template ABI. Cover basic spawning,
relative path rejection, execfd execute-permission checks, default fd
closing, close-range actions using newfd -1, and stale path rejection
after executable metadata changes.

Also cover atomic path replacement while a template fd for an old path is
still alive. The old template must reject the changed path with ESTALE, and
a new template for the same path must execute the replacement.

Signed-off-by: Li Chen <me@linux.beauty>
---
 MAINTAINERS                                   |   1 +
 tools/testing/selftests/exec/Makefile         |   1 +
 tools/testing/selftests/exec/spawn_template.c | 997 ++++++++++++++++++
 3 files changed, 999 insertions(+)
 create mode 100644 tools/testing/selftests/exec/spawn_template.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 3e737097940f9..77b3da32b4d2a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9747,6 +9747,7 @@ F:	include/uapi/linux/spawn_template.h
 F:	kernel/fork.c
 F:	mm/vma_exec.c
 F:	tools/testing/selftests/exec/
+F:	tools/testing/selftests/exec/spawn_template.c
 N:	asm/elf.h
 N:	binfmt
 
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index 45a3cfc435cfd..cf39fe916b9ba 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -20,6 +20,7 @@ TEST_FILES := Makefile
 TEST_GEN_PROGS += recursion-depth
 TEST_GEN_PROGS += null-argv
 TEST_GEN_PROGS += check-exec
+TEST_GEN_PROGS += spawn_template
 
 EXTRA_CLEAN := $(OUTPUT)/subdir.moved $(OUTPUT)/execveat.moved $(OUTPUT)/xxxxx*	\
 	       $(OUTPUT)/S_I*.test
diff --git a/tools/testing/selftests/exec/spawn_template.c b/tools/testing/selftests/exec/spawn_template.c
new file mode 100644
index 0000000000000..26708143ac9dc
--- /dev/null
+++ b/tools/testing/selftests/exec/spawn_template.c
@@ -0,0 +1,997 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <linux/spawn_template.h>
+
+#include "kselftest.h"
+
+#ifndef __NR_spawn_template_create
+#define __NR_spawn_template_create 472
+#endif
+
+#ifndef __NR_spawn_template_spawn
+#define __NR_spawn_template_spawn 473
+#endif
+
+#define SPAWN_TEMPLATE_MISSING_SYSCALL_ERRNO	38
+#define SPAWN_TEMPLATE_KERNEL_NSIG		64
+#define SPAWN_TEMPLATE_KERNEL_SIGSET_WORDS	\
+	(SPAWN_TEMPLATE_KERNEL_NSIG / (8 * sizeof(unsigned long)))
+
+static const char *true_path;
+static char self_path[PATH_MAX];
+
+struct spawn_template_kernel_sigset {
+	unsigned long sig[SPAWN_TEMPLATE_KERNEL_SIGSET_WORDS];
+};
+
+static void spawn_template_kernel_sigempty(struct spawn_template_kernel_sigset *set)
+{
+	memset(set, 0, sizeof(*set));
+}
+
+static void spawn_template_kernel_sigadd(struct spawn_template_kernel_sigset *set,
+					 int sig)
+{
+	sig--;
+	set->sig[sig / (8 * sizeof(unsigned long))] |=
+		1UL << (sig % (8 * sizeof(unsigned long)));
+}
+
+static int read_fd_string(int fd, const char *expected)
+{
+	char buf[128];
+	ssize_t nread;
+
+	nread = read(fd, buf, sizeof(buf) - 1);
+	if (nread < 0)
+		return -errno;
+
+	buf[nread] = '\0';
+	return strcmp(buf, expected) ? -EINVAL : 0;
+}
+
+static int write_file(const char *path, const char *data, mode_t mode)
+{
+	size_t left = strlen(data);
+	const char *p = data;
+	int fd;
+	int ret = 0;
+
+	fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, mode);
+	if (fd < 0)
+		return -errno;
+
+	while (left) {
+		ssize_t written = write(fd, p, left);
+
+		if (written < 0) {
+			ret = -errno;
+			break;
+		}
+		left -= written;
+		p += written;
+	}
+
+	close(fd);
+	return ret;
+}
+
+static int create_template_path(const char *path)
+{
+	struct spawn_template_create_args args = {
+		.flags = SPAWN_TEMPLATE_CREATE_CLOEXEC,
+		.execfd = -1,
+		.filename = (uintptr_t)path,
+	};
+
+	return syscall(__NR_spawn_template_create, &args, sizeof(args));
+}
+
+static int create_template_fd(int execfd)
+{
+	struct spawn_template_create_args args = {
+		.flags = SPAWN_TEMPLATE_CREATE_CLOEXEC,
+		.execfd = execfd,
+	};
+
+	return syscall(__NR_spawn_template_create, &args, sizeof(args));
+}
+
+static int spawn_template_start(int template_fd, char *const argv[],
+				struct spawn_template_action *actions,
+				unsigned int actions_len,
+				unsigned long long flags, pid_t *pid_out,
+				int *pidfd_out)
+{
+	char *const envp[] = { "PATH=/usr/bin:/bin", NULL };
+	struct spawn_template_spawn_args args = {
+		.flags = flags,
+		.argv = (uintptr_t)argv,
+		.envp = (uintptr_t)envp,
+		.actions = (uintptr_t)actions,
+		.actions_len = actions_len,
+	};
+	int pidfd = -1;
+	pid_t pid;
+	int ret;
+
+	args.pidfd = (uintptr_t)&pidfd;
+
+	pid = syscall(__NR_spawn_template_spawn, template_fd, &args,
+		      sizeof(args));
+	if (pid < 0) {
+		ret = -errno;
+		if (pidfd >= 0) {
+			siginfo_t info;
+
+			waitid(P_PIDFD, pidfd, &info, WEXITED);
+			close(pidfd);
+		}
+		return ret;
+	}
+
+	*pid_out = pid;
+	*pidfd_out = pidfd;
+	return 0;
+}
+
+static int spawn_template(int template_fd, char *const argv[],
+			  struct spawn_template_action *actions,
+			  unsigned int actions_len, unsigned long long flags)
+{
+	siginfo_t info = {};
+	int pidfd;
+	pid_t pid;
+	int ret;
+
+	ret = spawn_template_start(template_fd, argv, actions, actions_len, flags,
+				   &pid, &pidfd);
+	if (ret)
+		return ret;
+	(void)pid;
+
+	ret = waitid(P_PIDFD, pidfd, &info, WEXITED);
+	if (ret < 0) {
+		ret = -errno;
+		goto out_close_pidfd;
+	}
+
+	if (info.si_code != CLD_EXITED) {
+		ret = -EINVAL;
+		goto out_close_pidfd;
+	}
+
+	ret = info.si_status;
+
+out_close_pidfd:
+	if (pidfd >= 0)
+		close(pidfd);
+	return ret;
+}
+
+static const char *find_true(void)
+{
+	static const char * const paths[] = {
+		"/usr/bin/true",
+		"/bin/true",
+	};
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(paths); i++) {
+		if (access(paths[i], X_OK) == 0)
+			return paths[i];
+	}
+	return NULL;
+}
+
+static int copy_file(const char *src, const char *dst)
+{
+	char buf[8192];
+	ssize_t nread;
+	int infd;
+	int outfd;
+	int ret = 0;
+
+	infd = open(src, O_RDONLY | O_CLOEXEC);
+	if (infd < 0)
+		return -errno;
+
+	outfd = open(dst, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0700);
+	if (outfd < 0) {
+		ret = -errno;
+		goto out_close_in;
+	}
+
+	while ((nread = read(infd, buf, sizeof(buf))) > 0) {
+		char *p = buf;
+		ssize_t left = nread;
+
+		while (left > 0) {
+			ssize_t written = write(outfd, p, left);
+
+			if (written < 0) {
+				ret = -errno;
+				goto out_close_out;
+			}
+			left -= written;
+			p += written;
+		}
+	}
+	if (nread < 0)
+		ret = -errno;
+
+out_close_out:
+	close(outfd);
+out_close_in:
+	close(infd);
+	return ret;
+}
+
+static int test_basic_spawn(void)
+{
+	char *const argv[] = { (char *)true_path, NULL };
+	int template_fd;
+	int ret;
+
+	template_fd = create_template_path(true_path);
+	if (template_fd < 0)
+		return -errno;
+
+	ret = spawn_template(template_fd, argv, NULL, 0, 0);
+	close(template_fd);
+	return ret;
+}
+
+static int test_relative_path_rejected(void)
+{
+	int template_fd;
+
+	template_fd = create_template_path("true");
+	if (template_fd >= 0) {
+		close(template_fd);
+		return -EINVAL;
+	}
+
+	return errno == EINVAL ? 0 : -errno;
+}
+
+static int test_execfd_requires_execute(void)
+{
+	char path[] = "/tmp/spawn-template-noexec-XXXXXX";
+	int template_fd;
+	int fd;
+	int ret = 0;
+
+	fd = mkstemp(path);
+	if (fd < 0)
+		return -errno;
+
+	if (fchmod(fd, 0600)) {
+		ret = -errno;
+		goto out;
+	}
+
+	template_fd = create_template_fd(fd);
+	if (template_fd >= 0) {
+		close(template_fd);
+		ret = -EINVAL;
+		goto out;
+	}
+
+	ret = errno == EACCES ? 0 : -errno;
+
+out:
+	close(fd);
+	unlink(path);
+	return ret;
+}
+
+static int test_default_closes_extra_fds(void)
+{
+	char fdarg[32];
+	char *const argv[] = {
+		self_path,
+		"--check-fd-closed",
+		fdarg,
+		NULL,
+	};
+	int template_fd;
+	int extra_fd;
+	int ret;
+
+	extra_fd = open("/dev/null", O_RDONLY);
+	if (extra_fd < 0)
+		return -errno;
+
+	snprintf(fdarg, sizeof(fdarg), "%d", extra_fd);
+
+	template_fd = create_template_path(self_path);
+	if (template_fd < 0) {
+		ret = -errno;
+		goto out_close_extra;
+	}
+
+	ret = spawn_template(template_fd, argv, NULL, 0, 0);
+	close(template_fd);
+
+out_close_extra:
+	close(extra_fd);
+	return ret;
+}
+
+static int test_close_range_max_action(void)
+{
+	char fdarg[32];
+	char *const argv[] = {
+		self_path,
+		"--check-fd-closed",
+		fdarg,
+		NULL,
+	};
+	struct spawn_template_action action = {
+		.type = SPAWN_TEMPLATE_ACTION_CLOSE_RANGE,
+		.fd = -1,
+		.newfd = -1,
+	};
+	int template_fd;
+	int extra_fd;
+	int ret;
+
+	extra_fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
+	if (extra_fd < 0)
+		return -errno;
+
+	action.fd = extra_fd;
+	snprintf(fdarg, sizeof(fdarg), "%d", extra_fd);
+
+	template_fd = create_template_path(self_path);
+	if (template_fd < 0) {
+		ret = -errno;
+		goto out_close_extra;
+	}
+
+	ret = spawn_template(template_fd, argv, &action, 1,
+			     SPAWN_TEMPLATE_SPAWN_INHERIT_FDS);
+	close(template_fd);
+
+out_close_extra:
+	close(extra_fd);
+	return ret;
+}
+
+static int test_dup2_stdio_actions(void)
+{
+	char *const argv[] = { self_path, "--write-stdio", NULL };
+	struct spawn_template_action actions[2];
+	char out_buf[32];
+	char err_buf[32];
+	int out_pipe[2];
+	int err_pipe[2];
+	int template_fd;
+	int ret = 0;
+
+	if (pipe2(out_pipe, O_CLOEXEC))
+		return -errno;
+	if (pipe2(err_pipe, O_CLOEXEC)) {
+		ret = -errno;
+		goto out_close_out_pipe;
+	}
+
+	actions[0] = (struct spawn_template_action) {
+		.type = SPAWN_TEMPLATE_ACTION_DUP2,
+		.fd = out_pipe[1],
+		.newfd = STDOUT_FILENO,
+	};
+	actions[1] = (struct spawn_template_action) {
+		.type = SPAWN_TEMPLATE_ACTION_DUP2,
+		.fd = err_pipe[1],
+		.newfd = STDERR_FILENO,
+	};
+
+	template_fd = create_template_path(self_path);
+	if (template_fd < 0) {
+		ret = -errno;
+		goto out_close_err_pipe;
+	}
+
+	ret = spawn_template(template_fd, argv, actions, ARRAY_SIZE(actions), 0);
+	close(template_fd);
+	if (ret)
+		goto out_close_err_pipe;
+
+	close(out_pipe[1]);
+	out_pipe[1] = -1;
+	close(err_pipe[1]);
+	err_pipe[1] = -1;
+
+	memset(out_buf, 0, sizeof(out_buf));
+	memset(err_buf, 0, sizeof(err_buf));
+	if (read(out_pipe[0], out_buf, sizeof(out_buf) - 1) < 0) {
+		ret = -errno;
+		goto out_close_err_pipe;
+	}
+	if (read(err_pipe[0], err_buf, sizeof(err_buf) - 1) < 0) {
+		ret = -errno;
+		goto out_close_err_pipe;
+	}
+	if (strcmp(out_buf, "stdout-token\n") ||
+	    strcmp(err_buf, "stderr-token\n"))
+		ret = -EINVAL;
+
+out_close_err_pipe:
+	if (err_pipe[1] >= 0)
+		close(err_pipe[1]);
+	close(err_pipe[0]);
+out_close_out_pipe:
+	if (out_pipe[1] >= 0)
+		close(out_pipe[1]);
+	close(out_pipe[0]);
+	return ret;
+}
+
+static int test_open_action_stdin(void)
+{
+	char dir[] = "/tmp/spawn-template-open-XXXXXX";
+	char path[PATH_MAX];
+	char *const argv[] = {
+		self_path,
+		"--check-fd-content",
+		"0",
+		"open-action-token\n",
+		NULL,
+	};
+	struct spawn_template_open open_arg = {
+		.path = (uintptr_t)path,
+		.how = {
+			.flags = O_RDONLY,
+		},
+	};
+	struct spawn_template_action action = {
+		.type = SPAWN_TEMPLATE_ACTION_OPEN,
+		.fd = AT_FDCWD,
+		.newfd = STDIN_FILENO,
+		.arg = (uintptr_t)&open_arg,
+	};
+	int template_fd;
+	int ret;
+
+	if (!mkdtemp(dir))
+		return -errno;
+
+	snprintf(path, sizeof(path), "%s/input", dir);
+	ret = write_file(path, "open-action-token\n", 0600);
+	if (ret)
+		goto out_unlink;
+
+	template_fd = create_template_path(self_path);
+	if (template_fd < 0) {
+		ret = -errno;
+		goto out_unlink;
+	}
+
+	ret = spawn_template(template_fd, argv, &action, 1, 0);
+	close(template_fd);
+
+out_unlink:
+	unlink(path);
+	rmdir(dir);
+	return ret;
+}
+
+static int test_fchdir_action(void)
+{
+	char dir[] = "/tmp/spawn-template-fchdir-XXXXXX";
+	char resolved[PATH_MAX];
+	char *const argv[] = {
+		self_path,
+		"--check-cwd",
+		resolved,
+		NULL,
+	};
+	struct spawn_template_action action = {
+		.type = SPAWN_TEMPLATE_ACTION_FCHDIR,
+	};
+	int template_fd;
+	int dirfd;
+	int ret;
+
+	if (!mkdtemp(dir))
+		return -errno;
+	if (!realpath(dir, resolved)) {
+		ret = -errno;
+		goto out_rmdir;
+	}
+
+	dirfd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	if (dirfd < 0) {
+		ret = -errno;
+		goto out_rmdir;
+	}
+	action.fd = dirfd;
+
+	template_fd = create_template_path(self_path);
+	if (template_fd < 0) {
+		ret = -errno;
+		goto out_close_dirfd;
+	}
+
+	ret = spawn_template(template_fd, argv, &action, 1, 0);
+	close(template_fd);
+
+out_close_dirfd:
+	close(dirfd);
+out_rmdir:
+	rmdir(dir);
+	return ret;
+}
+
+static int test_sigmask_action(void)
+{
+	char sigarg[16];
+	char *const argv[] = {
+		self_path,
+		"--check-sigmask",
+		sigarg,
+		NULL,
+	};
+	struct spawn_template_kernel_sigset mask;
+	struct spawn_template_sigset sigset_arg = {
+		.sigset = (uintptr_t)&mask,
+		.sigsetsize = sizeof(mask),
+	};
+	struct spawn_template_action action = {
+		.type = SPAWN_TEMPLATE_ACTION_SIGMASK,
+		.arg = (uintptr_t)&sigset_arg,
+	};
+	int template_fd;
+	int ret;
+
+	spawn_template_kernel_sigempty(&mask);
+	spawn_template_kernel_sigadd(&mask, SIGUSR1);
+	snprintf(sigarg, sizeof(sigarg), "%d", SIGUSR1);
+
+	template_fd = create_template_path(self_path);
+	if (template_fd < 0)
+		return -errno;
+
+	ret = spawn_template(template_fd, argv, &action, 1, 0);
+	close(template_fd);
+	return ret;
+}
+
+static int test_sigdefault_action(void)
+{
+	char sigarg[16];
+	char *const argv[] = {
+		self_path,
+		"--check-sigdefault",
+		sigarg,
+		NULL,
+	};
+	struct spawn_template_kernel_sigset mask;
+	struct sigaction old_sa;
+	struct sigaction ignore_sa = {
+		.sa_handler = SIG_IGN,
+	};
+	struct spawn_template_sigset sigset_arg = {
+		.sigset = (uintptr_t)&mask,
+		.sigsetsize = sizeof(mask),
+	};
+	struct spawn_template_action action = {
+		.type = SPAWN_TEMPLATE_ACTION_SIGDEFAULT,
+		.arg = (uintptr_t)&sigset_arg,
+	};
+	int template_fd;
+	int ret;
+
+	spawn_template_kernel_sigempty(&mask);
+	spawn_template_kernel_sigadd(&mask, SIGUSR1);
+	snprintf(sigarg, sizeof(sigarg), "%d", SIGUSR1);
+
+	if (sigaction(SIGUSR1, &ignore_sa, &old_sa))
+		return -errno;
+
+	template_fd = create_template_path(self_path);
+	if (template_fd < 0) {
+		ret = -errno;
+		goto out_restore_signal;
+	}
+
+	ret = spawn_template(template_fd, argv, &action, 1, 0);
+	close(template_fd);
+
+out_restore_signal:
+	sigaction(SIGUSR1, &old_sa, NULL);
+	return ret;
+}
+
+static int test_inherit_fds_flag(void)
+{
+	char fdarg[32];
+	char *const argv[] = {
+		self_path,
+		"--check-fd-open",
+		fdarg,
+		NULL,
+	};
+	int template_fd;
+	int extra_fd;
+	int ret;
+
+	extra_fd = open("/dev/null", O_RDONLY);
+	if (extra_fd < 0)
+		return -errno;
+	snprintf(fdarg, sizeof(fdarg), "%d", extra_fd);
+
+	template_fd = create_template_path(self_path);
+	if (template_fd < 0) {
+		ret = -errno;
+		goto out_close_extra;
+	}
+
+	ret = spawn_template(template_fd, argv, NULL, 0,
+			     SPAWN_TEMPLATE_SPAWN_INHERIT_FDS);
+	close(template_fd);
+
+out_close_extra:
+	close(extra_fd);
+	return ret;
+}
+
+static int test_pidfd_waitid(void)
+{
+	char *const argv[] = { (char *)true_path, NULL };
+	siginfo_t info = {};
+	int template_fd;
+	int pidfd;
+	pid_t pid;
+	int ret;
+
+	template_fd = create_template_path(true_path);
+	if (template_fd < 0)
+		return -errno;
+
+	ret = spawn_template_start(template_fd, argv, NULL, 0, 0, &pid, &pidfd);
+	close(template_fd);
+	if (ret)
+		return ret;
+
+	ret = waitid(P_PIDFD, pidfd, &info, WEXITED);
+	if (ret < 0) {
+		ret = -errno;
+		waitpid(pid, NULL, 0);
+		goto out_close_pidfd;
+	}
+	if (info.si_code != CLD_EXITED || info.si_status)
+		ret = -EINVAL;
+
+out_close_pidfd:
+	close(pidfd);
+	return ret;
+}
+
+static int test_create_actions_rejected(void)
+{
+	struct spawn_template_action action = {
+		.type = SPAWN_TEMPLATE_ACTION_CLOSE,
+		.fd = STDIN_FILENO,
+	};
+	struct spawn_template_create_args args = {
+		.flags = SPAWN_TEMPLATE_CREATE_CLOEXEC,
+		.execfd = -1,
+		.filename = (uintptr_t)true_path,
+		.actions = (uintptr_t)&action,
+		.actions_len = 1,
+	};
+	int template_fd;
+
+	template_fd = syscall(__NR_spawn_template_create, &args, sizeof(args));
+	if (template_fd >= 0) {
+		close(template_fd);
+		return -EINVAL;
+	}
+
+	return errno == EINVAL ? 0 : -errno;
+}
+
+static int test_script_template_unsupported(void)
+{
+	char dir[] = "/tmp/spawn-template-script-XXXXXX";
+	char path[PATH_MAX];
+	int template_fd;
+	int ret;
+
+	if (!mkdtemp(dir))
+		return -errno;
+
+	snprintf(path, sizeof(path), "%s/script", dir);
+	ret = write_file(path, "#!/bin/sh\nexit 0\n", 0700);
+	if (ret)
+		goto out_unlink;
+
+	template_fd = create_template_path(path);
+	if (template_fd >= 0) {
+		close(template_fd);
+		ret = -EINVAL;
+		goto out_unlink;
+	}
+	ret = errno == ENOEXEC ? 0 : -errno;
+
+out_unlink:
+	unlink(path);
+	rmdir(dir);
+	return ret;
+}
+
+static int test_deny_write_while_template_alive(void)
+{
+	char dir[] = "/tmp/spawn-template-deny-write-XXXXXX";
+	char path[PATH_MAX];
+	int template_fd;
+	int write_fd;
+	int ret = 0;
+
+	if (!mkdtemp(dir))
+		return -errno;
+
+	snprintf(path, sizeof(path), "%s/copy", dir);
+	ret = copy_file(self_path, path);
+	if (ret)
+		goto out_unlink;
+
+	template_fd = create_template_path(path);
+	if (template_fd < 0) {
+		ret = -errno;
+		goto out_unlink;
+	}
+
+	write_fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC);
+	if (write_fd >= 0) {
+		close(write_fd);
+		ret = -EINVAL;
+	} else {
+		ret = errno == ETXTBSY ? 0 : -errno;
+	}
+
+	close(template_fd);
+out_unlink:
+	unlink(path);
+	rmdir(dir);
+	return ret;
+}
+
+static int test_stale_path_rejected(void)
+{
+	char dir[] = "/tmp/spawn-template-stale-XXXXXX";
+	char path[PATH_MAX];
+	char *const argv[] = { path, "--exit-zero", NULL };
+	int template_fd;
+	int ret = 0;
+
+	if (!mkdtemp(dir))
+		return -errno;
+
+	snprintf(path, sizeof(path), "%s/copy", dir);
+	ret = copy_file(self_path, path);
+	if (ret)
+		goto out_unlink;
+
+	template_fd = create_template_path(path);
+	if (template_fd < 0) {
+		ret = -errno;
+		goto out_unlink;
+	}
+
+	if (chmod(path, 0600)) {
+		ret = -errno;
+		goto out_close_template;
+	}
+
+	ret = spawn_template(template_fd, argv, NULL, 0, 0);
+	if (ret >= 0)
+		ret = -EINVAL;
+	else
+		ret = ret == -ESTALE ? 0 : ret;
+
+out_close_template:
+	close(template_fd);
+out_unlink:
+	unlink(path);
+	rmdir(dir);
+	return ret;
+}
+
+static int test_path_replacement_allows_tool_update(void)
+{
+	char dir[] = "/tmp/spawn-template-update-XXXXXX";
+	char path[PATH_MAX];
+	char new_path[PATH_MAX];
+	char *const argv[] = { path, "--exit-zero", NULL };
+	int new_template_fd = -1;
+	int template_fd = -1;
+	int ret;
+
+	if (!mkdtemp(dir))
+		return -errno;
+
+	snprintf(path, sizeof(path), "%s/tool", dir);
+	snprintf(new_path, sizeof(new_path), "%s/tool.new", dir);
+	ret = copy_file(self_path, path);
+	if (ret)
+		goto out;
+	ret = copy_file(self_path, new_path);
+	if (ret)
+		goto out;
+
+	template_fd = create_template_path(path);
+	if (template_fd < 0) {
+		ret = -errno;
+		goto out;
+	}
+
+	if (rename(new_path, path)) {
+		ret = -errno;
+		goto out;
+	}
+
+	ret = spawn_template(template_fd, argv, NULL, 0, 0);
+	if (ret != -ESTALE) {
+		ret = ret < 0 ? ret : -EINVAL;
+		goto out;
+	}
+
+	new_template_fd = create_template_path(path);
+	if (new_template_fd < 0) {
+		ret = -errno;
+		goto out;
+	}
+
+	ret = spawn_template(new_template_fd, argv, NULL, 0, 0);
+
+out:
+	if (new_template_fd >= 0)
+		close(new_template_fd);
+	if (template_fd >= 0)
+		close(template_fd);
+	unlink(new_path);
+	unlink(path);
+	rmdir(dir);
+	return ret;
+}
+
+static void run_test(const char *name, int (*fn)(void))
+{
+	int ret = fn();
+
+	if (!ret)
+		ksft_test_result_pass("%s\n", name);
+	else
+		ksft_test_result_fail("%s failed: %s (%d)\n",
+				      name, strerror(-ret), -ret);
+}
+
+static void check_syscall_available(void)
+{
+	int template_fd;
+
+	template_fd = create_template_path(true_path);
+	if (template_fd >= 0) {
+		close(template_fd);
+		return;
+	}
+
+	if (errno == SPAWN_TEMPLATE_MISSING_SYSCALL_ERRNO)
+		ksft_exit_skip("spawn_template syscalls are not available\n");
+
+	ksft_exit_fail_msg("spawn_template_create failed: %s (%d)\n",
+			   strerror(errno), errno);
+}
+
+int main(int argc, char **argv)
+{
+	ssize_t len;
+
+	if (argc == 2 && !strcmp(argv[1], "--exit-zero"))
+		return 0;
+
+	if (argc == 3 && !strcmp(argv[1], "--check-fd-closed")) {
+		int fd = atoi(argv[2]);
+
+		return fcntl(fd, F_GETFD) < 0 && errno == EBADF ? 0 : 1;
+	}
+
+	if (argc == 3 && !strcmp(argv[1], "--check-fd-open")) {
+		int fd = atoi(argv[2]);
+
+		return fcntl(fd, F_GETFD) >= 0 ? 0 : 1;
+	}
+
+	if (argc == 4 && !strcmp(argv[1], "--check-fd-content"))
+		return read_fd_string(atoi(argv[2]), argv[3]) ? 1 : 0;
+
+	if (argc == 3 && !strcmp(argv[1], "--check-cwd")) {
+		char cwd[PATH_MAX];
+
+		if (!getcwd(cwd, sizeof(cwd)))
+			return 1;
+		return strcmp(cwd, argv[2]) ? 1 : 0;
+	}
+
+	if (argc == 3 && !strcmp(argv[1], "--check-sigmask")) {
+		sigset_t mask;
+		int sig = atoi(argv[2]);
+
+		if (sigprocmask(SIG_BLOCK, NULL, &mask))
+			return 1;
+		return sigismember(&mask, sig) == 1 ? 0 : 1;
+	}
+
+	if (argc == 3 && !strcmp(argv[1], "--check-sigdefault")) {
+		struct sigaction sa;
+		int sig = atoi(argv[2]);
+
+		if (sigaction(sig, NULL, &sa))
+			return 1;
+		return sa.sa_handler == SIG_DFL ? 0 : 1;
+	}
+
+	if (argc == 2 && !strcmp(argv[1], "--write-stdio")) {
+		if (write(STDOUT_FILENO, "stdout-token\n", 13) != 13)
+			return 1;
+		if (write(STDERR_FILENO, "stderr-token\n", 13) != 13)
+			return 1;
+		return 0;
+	}
+
+	true_path = find_true();
+	if (!true_path)
+		ksft_exit_skip("could not find true executable\n");
+
+	len = readlink("/proc/self/exe", self_path, sizeof(self_path) - 1);
+	if (len < 0)
+		ksft_exit_fail_msg("readlink(/proc/self/exe) failed: %s\n",
+				   strerror(errno));
+	self_path[len] = '\0';
+
+	check_syscall_available();
+
+	ksft_print_header();
+	ksft_set_plan(17);
+
+	run_test("basic spawn", test_basic_spawn);
+	run_test("relative path rejected", test_relative_path_rejected);
+	run_test("execfd execute permission checked",
+		 test_execfd_requires_execute);
+	run_test("default fd close", test_default_closes_extra_fds);
+	run_test("close_range action max fd", test_close_range_max_action);
+	run_test("dup2 stdio actions", test_dup2_stdio_actions);
+	run_test("open action stdin", test_open_action_stdin);
+	run_test("fchdir action", test_fchdir_action);
+	run_test("sigmask action", test_sigmask_action);
+	run_test("sigdefault action", test_sigdefault_action);
+	run_test("inherit fds flag", test_inherit_fds_flag);
+	run_test("pidfd waitid", test_pidfd_waitid);
+	run_test("create-time actions rejected", test_create_actions_rejected);
+	run_test("script template unsupported", test_script_template_unsupported);
+	run_test("deny write while template alive",
+		 test_deny_write_while_template_alive);
+	run_test("stale path rejected", test_stale_path_rejected);
+	run_test("path replacement allows tool update",
+		 test_path_replacement_allows_tool_update);
+
+	ksft_finished();
+}
-- 
2.52.0


  parent reply	other threads:[~2026-05-28  9:59 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-28  9:52 [RFC PATCH v1 00/13] exec: add spawn templates for repeated executable startup Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 01/13] exec: factor argument setup out of do_execveat_common() Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 02/13] exec: add an internal helper for opened executables Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 03/13] file: expose helpers for in-kernel fd actions Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 04/13] exec: add spawn template UAPI definitions Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 05/13] exec: add spawn template file descriptors Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 06/13] exec: add spawn_template_spawn() Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 07/13] exec: validate spawn template executable identity Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 08/13] binfmt_elf: cache ELF metadata for spawn templates Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 09/13] Documentation: describe " Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 10/13] exec: require absolute paths for path-created templates Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 11/13] exec: let close-range actions target the max fd Li Chen
2026-05-28  9:52 ` [RFC PATCH v1 12/13] syscalls: add generic spawn template entries Li Chen
2026-05-28  9:52 ` Li Chen [this message]
2026-05-28 11:02 ` [RFC PATCH v1 00/13] exec: add spawn templates for repeated executable startup Christian Brauner
2026-06-01  2:47   ` Li Chen
2026-06-01 19:55   ` Kees Cook
2026-05-28 12:55 ` Mateusz Guzik
2026-06-01 15:11   ` Li Chen
2026-05-28 18:27 ` Andy Lutomirski

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260528095235.2491226-14-me@linux.beauty \
    --to=me@linux.beauty \
    --cc=arnd@arndb.de \
    --cc=bp@alien8.de \
    --cc=brauner@kernel.org \
    --cc=corbet@lwn.net \
    --cc=dave.hansen@linux.intel.com \
    --cc=hpa@zytor.com \
    --cc=jack@suse.cz \
    --cc=kees@kernel.org \
    --cc=linux-api@vger.kernel.org \
    --cc=linux-arch@vger.kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=luto@kernel.org \
    --cc=mingo@redhat.com \
    --cc=skhan@linuxfoundation.org \
    --cc=tglx@kernel.org \
    --cc=viro@zeniv.linux.org.uk \
    --cc=x86@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox