All Virtuozzo development lists (kernel + QEMU)
 help / color / mirror / Atom feed
From: Konstantin Khorenko <khorenko@virtuozzo.com>
To: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>,
	Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>,
	Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Cc: devel@openvz.org
Subject: [Devel] [PATCH hci-8.1 6/7] selftests/mm: check that a setuid exec refuses preserved memory
Date: Wed, 14 Oct 2026 19:12:25 +0200	[thread overview]
Message-ID: <20261014-b4-prep-khorenko-vstor-149847-3-doexec-oracle-fixes-v1-6-e34cbe0d1440@virtuozzo.com> (raw)
In-Reply-To: <20261014-b4-prep-khorenko-vstor-149847-3-doexec-oracle-fixes-v1-0-e34cbe0d1440@virtuozzo.com>

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@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

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

  parent reply	other threads:[~2026-10-14 17:14 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-14 17:12 [Devel] [PATCH hci-8.1 0/7] mm, exec: fix the MADV_DOEXEC code we carry from Oracle Konstantin Khorenko
2026-10-14 17:12 ` [Devel] [PATCH hci-8.1 1/7] oracle/mm: take a file reference when preserving a file-backed VMA Konstantin Khorenko
2026-10-14 17:12 ` [Devel] [PATCH hci-8.1 2/7] oracle/mm: refuse MADV_DOEXEC on special mappings Konstantin Khorenko
2026-10-14 17:12 ` [Devel] [PATCH hci-8.1 3/7] oracle/mm: keep a MADV_DOEXEC range from becoming executable Konstantin Khorenko
2026-10-14 17:12 ` [Devel] [PATCH hci-8.1 4/7] oracle/exec, elf: look for the opt-in note in every PT_NOTE segment Konstantin Khorenko
2026-10-14 17:12 ` [Devel] [PATCH hci-8.1 5/7] exec: refuse to preserve memory across a privilege-changing exec Konstantin Khorenko
2026-10-14 17:12 ` Konstantin Khorenko [this message]
2026-10-14 17:12 ` [Devel] [PATCH hci-8.1 7/7] selftests/mm: check MADV_DOEXEC on /dev/zero and exec permission Konstantin Khorenko

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=20261014-b4-prep-khorenko-vstor-149847-3-doexec-oracle-fixes-v1-6-e34cbe0d1440@virtuozzo.com \
    --to=khorenko@virtuozzo.com \
    --cc=andrey.drobyshev@virtuozzo.com \
    --cc=devel@openvz.org \
    --cc=ptikhomirov@virtuozzo.com \
    --cc=vasileios.almpanis@virtuozzo.com \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.