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
next prev 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox