[Devel] [PATCH hci-8.1 6/7] selftests/mm: check that a setuid exec refuses preserved memory

Konstantin Khorenko khorenko at virtuozzo.com
Wed Oct 14 20:12:25 MSK 2026


Copy the test binary, make it setuid root and exec it from an
unprivileged child with a MADV_DOEXEC range: exec must fail with EPERM
and leave the mapping intact, and must succeed once the range is
unmarked with MADV_DONTEXEC. The case needs root and a directory that
is neither private nor mounted nosuid; the current directory, /var/tmp
and /tmp are tried in that order and the case is skipped if none fits.
The copy goes into a new directory that others can enter but not list,
and run setuid it does nothing but report its euid.

https://virtuozzo.atlassian.net/browse/VSTOR-150232

Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko at virtuozzo.com>
---
 tools/testing/selftests/mm/doexec.c | 121 ++++++++++++++++++++++++++++++++++--
 1 file changed, 117 insertions(+), 4 deletions(-)

diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 33529aa41367b..dfa3da2c43934 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -3,6 +3,7 @@
 #define _GNU_SOURCE
 #include <errno.h>
 #include <fcntl.h>
+#include <grp.h>
 #include <linux/vhost.h>
 #include <poll.h>
 #include <pthread.h>
@@ -17,6 +18,8 @@
 #include <sys/ioctl.h>
 #include <sys/mman.h>
 #include <sys/resource.h>
+#include <sys/sendfile.h>
+#include <sys/stat.h>
 #include <sys/syscall.h>
 #include <sys/wait.h>
 #include <unistd.h>
@@ -27,6 +30,9 @@
 #ifndef MADV_DOEXEC
 #define MADV_DOEXEC 201
 #endif
+#ifndef MADV_DONTEXEC
+#define MADV_DONTEXEC 202
+#endif
 #ifndef MADV_COLLAPSE
 #define MADV_COLLAPSE 25
 #endif
@@ -283,7 +289,8 @@ static int before_exec(int kind)
 	return KSFT_FAIL;
 }
 
-static int check_exec(char *ram, size_t size, size_t stride, bool reject)
+static int exec_path(const char *exe, char *ram, size_t size, size_t stride,
+		     bool reject)
 {
 	char addr_arg[32], size_arg[32], stride_arg[32];
 
@@ -291,11 +298,16 @@ static int check_exec(char *ram, size_t size, size_t stride, bool reject)
 		 (unsigned long long)(uintptr_t)ram);
 	snprintf(size_arg, sizeof(size_arg), "%zu", size);
 	snprintf(stride_arg, sizeof(stride_arg), "%zu", stride);
-	execl("/proc/self/exe", "doexec", reject ? "--reject" : "--check", addr_arg, size_arg,
+	execl(exe, "doexec", reject ? "--reject" : "--check", addr_arg, size_arg,
 	      stride_arg, NULL);
 	return errno;
 }
 
+static int check_exec(char *ram, size_t size, size_t stride, bool reject)
+{
+	return exec_path("/proc/self/exe", ram, size, stride, reject);
+}
+
 static char *preserved_ram(size_t size)
 {
 	char *ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
@@ -397,6 +409,98 @@ static int vhost_owner_exec(void)
 	return KSFT_FAIL;
 }
 
+static void remove_setuid_copy(char *path)
+{
+	unlink(path);
+	*strrchr(path, '/') = '\0';
+	rmdir(path);
+}
+
+static int make_setuid_copy(const char *dir, char *path, size_t len)
+{
+	ssize_t copied = -1;
+	int in, out;
+	size_t n;
+
+	snprintf(path, len, "%s/doexec-setuid.XXXXXX", dir);
+	/* Others may run the copy but cannot find it. */
+	if (!mkdtemp(path))
+		return -1;
+	if (chmod(path, 0711)) {
+		rmdir(path);
+		return -1;
+	}
+	n = strlen(path);
+	snprintf(path + n, len - n, "/doexec.XXXXXX");
+	out = mkstemp(path);
+	in = open("/proc/self/exe", O_RDONLY);
+	if (in >= 0 && out >= 0)
+		while ((copied = sendfile(out, in, NULL, 1 << 20)) > 0)
+			;
+	if (in >= 0)
+		close(in);
+	if (out >= 0 && !copied && !fchmod(out, S_ISUID | 0755)) {
+		close(out);
+		return 0;
+	}
+	if (out >= 0)
+		close(out);
+	remove_setuid_copy(path);
+	return -1;
+}
+
+static int unprivileged_exec(const char *path, bool preserve)
+{
+	size_t size = sysconf(_SC_PAGESIZE);
+	pid_t pid = fork();
+	char *ram;
+	int err;
+
+	if (pid < 0)
+		return KSFT_FAIL;
+	if (pid)
+		return wait_child(pid);
+	if (setgroups(0, NULL) || setresgid(65534, 65534, 65534) ||
+	    setresuid(65534, 65534, 65534))
+		_exit(KSFT_SKIP);
+	if (!preserve) {
+		execl(path, "doexec", "--euid", NULL);
+		_exit(KSFT_SKIP);
+	}
+	ram = preserved_ram(size);
+	if (!ram)
+		_exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+	err = exec_path(path, ram, size, 1, true);
+	/* Exec must return EPERM with the old mapping intact. */
+	if (err != EPERM || check_bytes(ram, size, 0x31))
+		_exit(KSFT_FAIL);
+	if (madvise(ram, size, MADV_DONTEXEC))
+		_exit(KSFT_FAIL);
+	execl(path, "doexec", "--euid", NULL);
+	_exit(KSFT_FAIL);
+}
+
+/* The ELF opt-in must not pull memory into a more privileged image. */
+static int setuid_exec(void)
+{
+	static const char * const dirs[] = { ".", "/var/tmp", "/tmp" };
+	char path[96];
+	int i, ret = KSFT_SKIP;
+
+	if (geteuid())
+		return KSFT_SKIP;
+	for (i = 0; i < 3 && ret == KSFT_SKIP; i++) {
+		if (make_setuid_copy(dirs[i], path, sizeof(path)))
+			continue;
+		/* A nosuid mount or a private directory makes the copy useless. */
+		ret = unprivileged_exec(path, false);
+		if (ret == KSFT_PASS)
+			ret = unprivileged_exec(path, true);
+		remove_setuid_copy(path);
+	}
+	return ret;
+}
+
 static void *exec_thread(void *arg)
 {
 	check_exec(arg, sysconf(_SC_PAGESIZE), 1, false);
@@ -723,6 +827,10 @@ int main(int argc, char **argv)
 	pid_t pid;
 	int kind, ret;
 
+	/* The setuid copy only reports that it runs as root. */
+	if (geteuid() != getuid())
+		return argc == 2 && !strcmp(argv[1], "--euid") && !geteuid() ?
+		       KSFT_PASS : KSFT_FAIL;
 	if (argc == 7 && !strcmp(argv[1], "--exec"))
 		return after_exec(atoi(argv[2]),
 			(char *)(uintptr_t)strtoull(argv[3], NULL, 10),
@@ -732,6 +840,8 @@ int main(int argc, char **argv)
 			strtoull(argv[3], NULL, 10), atoi(argv[4]), atoi(argv[5]));
 	if (argc == 5 && !strcmp(argv[1], "--reject"))
 		return KSFT_FAIL;
+	if (argc == 2 && !strcmp(argv[1], "--euid"))
+		return geteuid() ? KSFT_SKIP : KSFT_PASS;
 	if (argc == 5 && !strcmp(argv[1], "--proc"))
 		return proc_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
 				 strtoull(argv[3], NULL, 10), atoi(argv[4]));
@@ -751,7 +861,7 @@ int main(int argc, char **argv)
 		return KSFT_PASS;
 	}
 	ksft_print_header();
-	ksft_set_plan(NR_TESTS + 8);
+	ksft_set_plan(NR_TESTS + 9);
 	for (kind = 0; kind < NR_TESTS; kind++) {
 		fflush(stdout);
 		pid = fork();
@@ -774,7 +884,7 @@ int main(int argc, char **argv)
 		ksft_test_result_skip("pre-existing COW: DOEXEC unavailable\n");
 	else
 		ksft_test_result(ret == KSFT_PASS, "pre-existing COW preserved\n");
-	for (kind = 0; kind < 7; kind++) {
+	for (kind = 0; kind < 8; kind++) {
 		static const char * const extra_names[] = {
 			"CLONE_VM: recoverable EBUSY and retry",
 			"own threads: preserving exec allowed",
@@ -783,6 +893,7 @@ int main(int argc, char **argv)
 			"non-leader thread: preserving exec allowed",
 			"proc readers: repeated preserving exec allowed",
 			"concurrent CLONE_VM: shared memory is not stolen",
+			"setuid exec: preserving exec refused",
 		};
 
 		fflush(stdout);
@@ -802,6 +913,8 @@ int main(int argc, char **argv)
 				_exit(proc_readers_exec());
 			case 6:
 				_exit(clone_exec_race());
+			case 7:
+				_exit(setuid_exec());
 			}
 		}
 		ret = pid < 0 ? KSFT_FAIL : wait_child(pid);

-- 
2.43.0



More information about the Devel mailing list