* [Devel] [PATCH hci-8.1 0/7] selftests/mm: broader MADV_DOEXEC coverage
@ 2026-10-14 17:14 Konstantin Khorenko
2026-10-14 17:14 ` [Devel] [PATCH hci-8.1 1/7] selftests/mm: cover the MADV_DOEXEC interface and VMA attributes Konstantin Khorenko
` (6 more replies)
0 siblings, 7 replies; 8+ messages in thread
From: Konstantin Khorenko @ 2026-10-14 17:14 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
Selftests for MADV_DOEXEC beyond the cases the fixes need: the sysctl
and the refused mappings, partial and attribute-carrying ranges (mlock,
DONTFORK, WIPEONFORK, userfaultfd, memory policy), the opt-in note
(missing, malformed, not in the last PT_NOTE), a preserved range under
the new stack, large, swapped and partly marked THP ranges, an exec from
a vfork() child, VM_MAYEXEC read from smaps and given back to the
copies, and a /proc/pid/mem write and a vhost kthread that keep the old
mm across the exec. Failing cases say why. Applies on top of "mm: allow
MADV_DOEXEC on VFIO BAR mappings".
All cases pass on release and debug kernels with the whole stack.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
---
Konstantin Khorenko (7):
selftests/mm: cover the MADV_DOEXEC interface and VMA attributes
selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack
selftests/mm: cover large, swapped and partly marked THP ranges
selftests/mm: check that MADV_DOEXEC refuses an exec from a vfork() child
selftests/mm: check VM_MAYEXEC of MADV_DOEXEC ranges in smaps
selftests/mm: check who can still reach the old mm of a preserving exec
selftests/mm: say why a doexec case fails
tools/testing/selftests/mm/doexec.c | 1456 ++++++++++++++++++++++++++++++-----
1 file changed, 1283 insertions(+), 173 deletions(-)
---
base-commit: 569abd49ca6b0b102f2e5b995a15459b53a8a19f
change-id: 20261014-b4-prep-khorenko-vstor-149847-5-doexec-selftests-496ac17b9688
Best regards,
--
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [Devel] [PATCH hci-8.1 1/7] selftests/mm: cover the MADV_DOEXEC interface and VMA attributes
2026-10-14 17:14 [Devel] [PATCH hci-8.1 0/7] selftests/mm: broader MADV_DOEXEC coverage Konstantin Khorenko
@ 2026-10-14 17:14 ` Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 2/7] selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack Konstantin Khorenko
` (5 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Konstantin Khorenko @ 2026-10-14 17:14 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
Add cases for what madvise accepts and what the copy of a preserved VMA
keeps:
- /proc/sys/vm/madv_doexec_flag reports MADV_DOEXEC, and a kernel
without the sysctl does not accept MADV_DOEXEC either.
- Shared anonymous, private file, executable, MAP_GROWSDOWN and stack
mappings are refused with EINVAL, and so is a hugetlb mapping when
huge pages are available.
- A range unmarked with MADV_DONTEXEC (twice, the second a no-op) is
not preserved.
- Marking the middle page of a mapping preserves that page only.
- An mlocked range is preserved without VM_LOCKED.
- MADV_DONTFORK and MADV_WIPEONFORK carry over, so a fork in the new
image leaves the range out or wipes it.
- A userfaultfd registration does not carry over.
- The memory policy of the range carries over.
A flag expected to disappear is checked in smaps before the exec as
well, so that a failed lookup cannot pass for its absence. EINVAL from
MADV_DOEXEC fails a case on a 64-bit kernel with the madv_doexec_flag
sysctl, in the existing cases too; the cases skip only when something
they need is missing: the feature, free huge pages, the mlock limit,
userfaultfd, mbind. A failing case prints what failed, and the
children running the cases, old and new, exit with exit(), so that the
message reaches a pipe.
New cases go to a table, and the checks run in the new image to a
second one, so that main() stays small.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 424 +++++++++++++++++++++++++++++++++---
1 file changed, 392 insertions(+), 32 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index a89fe2f067df5..850b498e3d1dc 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -1,10 +1,12 @@
// SPDX-License-Identifier: GPL-2.0-only
-/* Verify that exec preserves the memory backing long-term DMA pins. */
+/* Verify what MADV_DOEXEC preserves across exec, long-term DMA pins first. */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
+#include <linux/mempolicy.h>
+#include <linux/userfaultfd.h>
#include <linux/vfio.h>
#include <linux/vhost.h>
#include <poll.h>
@@ -25,12 +27,16 @@
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
+#include <sys/utsname.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../../../../mm/gup_test.h"
#include "../kselftest.h"
+#ifndef UFFD_USER_MODE_ONLY
+#define UFFD_USER_MODE_ONLY 1
+#endif
#ifndef MADV_DOEXEC
#define MADV_DOEXEC 201
#endif
@@ -40,6 +46,9 @@
#ifndef MADV_COLLAPSE
#define MADV_COLLAPSE 25
#endif
+#ifndef MADV_WIPEONFORK
+#define MADV_WIPEONFORK 18
+#endif
/* The downstream ELF loader requires this opt-in in a PT_NOTE segment. */
asm(".pushsection .note.doexec,\"a\",%note\n"
@@ -117,6 +126,38 @@ static int check_bytes(char *ram, size_t size, unsigned char value)
return KSFT_PASS;
}
+/*
+ * EINVAL from MADV_DOEXEC means no feature only on a kernel without the
+ * sysctl, or on a 32-bit one, which has no VM_EXEC_KEEP.
+ */
+static int unsupported(void)
+{
+ int err = errno;
+ struct utsname uts;
+
+ if (err == EINVAL &&
+ (access("/proc/sys/vm/madv_doexec_flag", F_OK) ||
+ (!uname(&uts) && !strstr(uts.machine, "64") &&
+ strcmp(uts.machine, "s390x") && strcmp(uts.machine, "alpha"))))
+ return KSFT_SKIP;
+ ksft_print_msg("MADV_DOEXEC: %s\n", strerror(err));
+ fflush(stdout);
+ errno = err;
+ return KSFT_FAIL;
+}
+
+static int fail(const char *what)
+{
+ ksft_print_msg("%s\n", what);
+ fflush(stdout);
+ return KSFT_FAIL;
+}
+
+static int check_contents(char *ram, size_t size, unsigned char value)
+{
+ return check_bytes(ram, size, value) ? fail("contents lost") : KSFT_PASS;
+}
+
static int after_cow_exec(char *ram, size_t size, pid_t sibling, int pipe_fd)
{
char ready = 1;
@@ -146,7 +187,7 @@ static int before_cow_exec(void)
return KSFT_FAIL;
memset(ram, 0x31, size);
if (madvise(ram, size, MADV_DOEXEC))
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
if (pipe(pipe_fd))
return KSFT_FAIL;
pid = fork();
@@ -272,10 +313,8 @@ static int before_exec(int kind)
return KSFT_SKIP;
if (kind == LOCKED && mlock(ram, size))
return KSFT_SKIP;
- if (madvise(ram, size, MADV_DOEXEC)) {
- ksft_print_msg("MADV_DOEXEC: %s\n", strerror(errno));
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
- }
+ if (madvise(ram, size, MADV_DOEXEC))
+ return unsupported();
pin = (struct pin_longterm_test) {
.addr = (uintptr_t)ram,
.size = size,
@@ -358,7 +397,7 @@ static int shared_mm_exec(bool own_thread)
pid_t pid;
if (!ram)
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
if (pipe(pipe_fd))
return KSFT_FAIL;
if (own_thread) {
@@ -403,7 +442,7 @@ static int vhost_owner_exec(void)
return KSFT_SKIP;
ram = preserved_ram(size);
if (!ram)
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
if (ioctl(fd, VHOST_SET_OWNER))
return KSFT_SKIP;
if (ioctl(fd, VHOST_RESET_OWNER))
@@ -473,7 +512,7 @@ static int unprivileged_exec(const char *path, bool preserve)
}
ram = preserved_ram(size);
if (!ram)
- _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ _exit(unsupported());
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))
@@ -520,7 +559,7 @@ static int devzero_exec(void)
return KSFT_FAIL;
memset(ram, 0x31, size);
if (madvise(ram, size, MADV_DOEXEC))
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
check_exec(ram, size, 1, false);
return KSFT_FAIL;
}
@@ -558,7 +597,7 @@ static int noexec_dontexec(void)
goto out;
}
if (madvise(ram, size, MADV_DOEXEC)) {
- ret = errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ ret = unsupported();
goto out;
}
if (!madvise(ram, size, MADV_DONTEXEC) && exec_refused(ram, size))
@@ -577,7 +616,7 @@ static int doexec_noexec(void)
int ret;
if (!ram)
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
if (!exec_refused(ram, size) || madvise(ram, size, MADV_DONTEXEC) ||
mprotect(ram, size, PROT_READ | PROT_EXEC))
return KSFT_FAIL;
@@ -631,7 +670,7 @@ static int devmem_refused(void)
int fd;
if (!preserved_ram(size))
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
/* Without FMODE_WRITE, do_mmap() drops VM_SHARED from the mapping. */
fd = open("/dev/mem", O_RDWR);
if (fd < 0)
@@ -681,8 +720,10 @@ static int vfio_bar_exec(void)
uintptr_t aligned;
int dev;
- if (!bdf || !preserved_ram(page))
+ if (!bdf)
return KSFT_SKIP;
+ if (!preserved_ram(page))
+ return unsupported();
dev = vfio_device_fd(bdf);
if (dev < 0 || ioctl(dev, VFIO_DEVICE_GET_REGION_INFO, &info) ||
!(info.flags & VFIO_REGION_INFO_FLAG_MMAP) || info.size < page) {
@@ -725,7 +766,7 @@ static int nonleader_exec(void)
pthread_t thread;
if (!ram)
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
if (pthread_create(&thread, NULL, exec_thread, ram))
return KSFT_FAIL;
pthread_join(thread, NULL);
@@ -773,7 +814,7 @@ static int proc_readers_exec(void)
close(ready[0]);
close(go[1]);
if (!ram)
- _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ _exit(unsupported());
if (write(ready[1], "x", 1) != 1 || read(go[0], buffer, 1) != 1)
_exit(KSFT_FAIL);
close(ready[1]);
@@ -910,7 +951,7 @@ static int clone_exec_race(void)
close(report[0]);
race.ram = preserved_ram(size);
if (!race.ram)
- _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ _exit(unsupported());
if (pthread_create(&thread, NULL, clone_race_thread, &race))
_exit(KSFT_FAIL);
while (atomic_load(&race.rounds) < 4)
@@ -985,7 +1026,7 @@ static int allocation_failures(void)
for (i = 0; i < size; i += stride)
ram[i] = 0x31;
if (madvise(ram, size, MADV_DOEXEC))
- _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ _exit(unsupported());
/* Keep the path lookup, which /proc fails with ENOENT, out of the sweep. */
exe = open("/proc/self/exe", O_RDONLY | O_CLOEXEC);
fd = open("/proc/self/fail-nth", O_WRONLY | O_CLOEXEC);
@@ -1034,6 +1075,308 @@ static int allocation_failures(void)
return early && fatal ? KSFT_PASS : KSFT_SKIP;
}
+static void exec_mode(const char *exe, const char *mode, void *addr, size_t size)
+{
+ char addr_arg[32], size_arg[32];
+
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)addr);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ execl(exe, "doexec", mode, addr_arg, size_arg, NULL);
+ ksft_print_msg("exec: %s\n", strerror(errno));
+ fflush(stdout);
+}
+
+static bool range_mapped(char *addr, size_t size)
+{
+ return !msync(addr, size, MS_ASYNC);
+}
+
+static int sysctl_flag(void)
+{
+ FILE *file = fopen("/proc/sys/vm/madv_doexec_flag", "r");
+ int flag = -1;
+
+ if (!file) {
+ if (access("/proc/sys/vm", F_OK) || !preserved_ram(sysconf(_SC_PAGESIZE)))
+ return KSFT_SKIP;
+ return fail("MADV_DOEXEC works without the sysctl");
+ }
+ if (fscanf(file, "%d", &flag) != 1)
+ flag = -1;
+ fclose(file);
+ return flag == MADV_DOEXEC ? KSFT_PASS : fail("unexpected madv_doexec_flag");
+}
+
+static bool doexec_refused(char *addr, size_t size, const char *what)
+{
+ if (addr == MAP_FAILED) {
+ ksft_print_msg("%s: mmap: %s\n", what, strerror(errno));
+ /* A security policy may forbid the mapping itself. */
+ return errno == EACCES || errno == EPERM;
+ }
+ if (madvise(addr, size, MADV_DOEXEC) && errno == EINVAL)
+ return true;
+ ksft_print_msg("%s not refused\n", what);
+ return false;
+}
+
+/* Apart from VFIO PFN maps, only private non-stack anonymous memory can be marked. */
+static int refused_mappings(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ int fd = open("/proc/self/exe", O_RDONLY);
+ char stack_var = 0;
+ bool ok = true;
+
+ if (!preserved_ram(size))
+ return unsupported();
+ ok &= doexec_refused(mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_ANONYMOUS, -1, 0), size, "shared anon");
+ ok &= doexec_refused(mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0), size,
+ "private file");
+ ok &= doexec_refused(mmap(NULL, size, PROT_READ | PROT_EXEC,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0), size, "executable");
+ ok &= doexec_refused(mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN, -1, 0), size,
+ "MAP_GROWSDOWN");
+ ok &= doexec_refused((char *)((uintptr_t)&stack_var & ~(size - 1)), size, "stack");
+ close(fd);
+ return ok ? KSFT_PASS : KSFT_FAIL;
+}
+
+static int hugetlb_refused(void)
+{
+ size_t size = 2ul << 20;
+ char *addr;
+
+ if (!preserved_ram(sysconf(_SC_PAGESIZE)))
+ return unsupported();
+ addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
+ if (addr == MAP_FAILED)
+ return KSFT_SKIP;
+ return doexec_refused(addr, size, "hugetlb") ? KSFT_PASS : KSFT_FAIL;
+}
+
+static int unmarked_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+
+ if (!ram)
+ return unsupported();
+ if (madvise(ram, size, MADV_DONTEXEC) || madvise(ram, size, MADV_DONTEXEC))
+ return fail("MADV_DONTEXEC failed");
+ exec_mode("/proc/self/exe", "--absent", ram, size);
+ return KSFT_FAIL;
+}
+
+static int after_absent(char *ram, size_t size)
+{
+ return range_mapped(ram, size) ? fail("range still mapped") : KSFT_PASS;
+}
+
+/* Marking the middle page splits the VMA; only that page carries over. */
+static int partial_exec(void)
+{
+ size_t page = sysconf(_SC_PAGESIZE);
+ char *ram = mmap(NULL, 3 * page, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+ if (ram == MAP_FAILED)
+ return KSFT_FAIL;
+ memset(ram, 0x31, 3 * page);
+ if (madvise(ram + page, page, MADV_DOEXEC))
+ return unsupported();
+ exec_mode("/proc/self/exe", "--partial", ram, 3 * page);
+ return KSFT_FAIL;
+}
+
+static int after_partial(char *ram, size_t size)
+{
+ size_t page = size / 3;
+
+ if (range_mapped(ram, page) || range_mapped(ram + 2 * page, page))
+ return fail("unmarked page preserved");
+ if (!range_mapped(ram + page, page))
+ return fail("marked page not preserved");
+ return check_contents(ram + page, page, 0x31);
+}
+
+/* The flag is checked before exec too, so that its absence after is meaningful. */
+static int exec_with_flag(char *ram, size_t size, const char *mode, const char *flag)
+{
+ if (!smaps_has_flag(ram, flag))
+ return fail("flag missing before exec");
+ exec_mode("/proc/self/exe", mode, ram, size);
+ return KSFT_FAIL;
+}
+
+static int flagged_exec(const char *mode, int advice, const char *flag)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+
+ if (!ram)
+ return unsupported();
+ if (madvise(ram, size, advice))
+ return fail("madvise failed");
+ return exec_with_flag(ram, size, mode, flag);
+}
+
+static int locked_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+
+ if (!ram)
+ return unsupported();
+ if (mlock(ram, size))
+ return KSFT_SKIP;
+ return exec_with_flag(ram, size, "--unlocked", " lo ");
+}
+
+static int after_unlocked(char *ram, size_t size)
+{
+ if (smaps_has_flag(ram, " lo "))
+ return fail("VM_LOCKED preserved");
+ return check_contents(ram, size, 0x31);
+}
+
+static int dontfork_exec(void)
+{
+ return flagged_exec("--dontfork", MADV_DONTFORK, " dc ");
+}
+
+static int wipeonfork_exec(void)
+{
+ return flagged_exec("--wipeonfork", MADV_WIPEONFORK, " wf ");
+}
+
+/* The fork flags carry over: a child of the new image sees what they say. */
+static int after_fork_flag(char *ram, size_t size, const char *flag, bool wipe)
+{
+ pid_t pid;
+
+ if (!smaps_has_flag(ram, flag))
+ return fail("fork flag lost");
+ if (check_bytes(ram, size, 0x31))
+ return fail("contents lost");
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ if (wipe)
+ _exit(range_mapped(ram, size) ? check_bytes(ram, size, 0) : KSFT_FAIL);
+ _exit(range_mapped(ram, size) ? KSFT_FAIL : KSFT_PASS);
+ }
+ return wait_child(pid);
+}
+
+static int after_dontfork(char *ram, size_t size)
+{
+ return after_fork_flag(ram, size, " dc ", false);
+}
+
+static int after_wipeonfork(char *ram, size_t size)
+{
+ return after_fork_flag(ram, size, " wf ", true);
+}
+
+/* The userfaultfd context belongs to the old mm and must not carry over. */
+static int uffd_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ struct uffdio_api api = { .api = UFFD_API };
+ struct uffdio_register reg = {
+ .mode = UFFDIO_REGISTER_MODE_MISSING,
+ };
+ int uffd;
+
+ if (!ram)
+ return unsupported();
+ uffd = syscall(__NR_userfaultfd, O_CLOEXEC | UFFD_USER_MODE_ONLY);
+ if (uffd < 0 || ioctl(uffd, UFFDIO_API, &api))
+ return KSFT_SKIP;
+ reg.range.start = (uintptr_t)ram;
+ reg.range.len = size;
+ if (ioctl(uffd, UFFDIO_REGISTER, ®))
+ return KSFT_SKIP;
+ if (!smaps_has_flag(ram, " um "))
+ return fail("userfaultfd registration not in smaps");
+ exec_mode("/proc/self/exe", "--no-uffd", ram, size);
+ return KSFT_FAIL;
+}
+
+static int after_no_uffd(char *ram, size_t size)
+{
+ if (smaps_has_flag(ram, " um "))
+ return fail("userfaultfd registration preserved");
+ return check_contents(ram, size, 0x31);
+}
+
+/* vma_dup_policy() carries the memory policy of the range over. */
+static int mempolicy_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ unsigned long nodes = 1;
+
+ if (!ram)
+ return unsupported();
+ if (syscall(__NR_mbind, ram, size, MPOL_PREFERRED, &nodes, 2, 0))
+ return KSFT_SKIP;
+ exec_mode("/proc/self/exe", "--mempolicy", ram, size);
+ return KSFT_FAIL;
+}
+
+static int after_mempolicy(char *ram, size_t size)
+{
+ unsigned long nodes[16] = { 0 };
+ int mode = -1;
+
+ if (syscall(__NR_get_mempolicy, &mode, nodes, 8 * sizeof(nodes), ram, MPOL_F_ADDR))
+ return fail("get_mempolicy failed");
+ if (mode != MPOL_PREFERRED || nodes[0] != 1)
+ return fail("memory policy lost");
+ return check_contents(ram, size, 0x31);
+}
+
+struct after_mode {
+ const char *mode;
+ int (*fn)(char *ram, size_t size);
+};
+
+static const struct after_mode after_modes[] = {
+ { "--absent", after_absent },
+ { "--partial", after_partial },
+ { "--unlocked", after_unlocked },
+ { "--dontfork", after_dontfork },
+ { "--wipeonfork", after_wipeonfork },
+ { "--no-uffd", after_no_uffd },
+ { "--mempolicy", after_mempolicy },
+};
+
+struct more_case {
+ const char *name;
+ int (*fn)(void);
+};
+
+static const struct more_case more_cases[] = {
+ { "madv_doexec_flag sysctl matches MADV_DOEXEC", sysctl_flag },
+ { "shared, file, executable and stack mappings refused", refused_mappings },
+ { "hugetlb mapping refused", hugetlb_refused },
+ { "MADV_DONTEXEC: range not preserved", unmarked_exec },
+ { "partially marked VMA: only the marked part preserved", partial_exec },
+ { "mlocked range: preserved unlocked", locked_exec },
+ { "MADV_DONTFORK range: preserved, not given to a child", dontfork_exec },
+ { "MADV_WIPEONFORK range: preserved, wiped in a child", wipeonfork_exec },
+ { "userfaultfd registration: not preserved", uffd_exec },
+ { "memory policy: preserved", mempolicy_exec },
+};
+
int main(int argc, char **argv)
{
pid_t pid;
@@ -1052,6 +1395,10 @@ 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;
+ for (kind = 0; argc == 4 && kind < (int)ARRAY_SIZE(after_modes); kind++)
+ if (!strcmp(argv[1], after_modes[kind].mode))
+ return after_modes[kind].fn((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
+ strtoull(argv[3], NULL, 10));
if (argc == 2 && !strcmp(argv[1], "--euid"))
return geteuid() ? KSFT_SKIP : KSFT_PASS;
if (argc == 4 && !strcmp(argv[1], "--pfnmap"))
@@ -1070,18 +1417,20 @@ int main(int argc, char **argv)
write(fd, "0", 1);
close(fd);
}
+ if (!range_mapped(ram, size))
+ return fail("range not preserved");
for (i = 0; i < size; i += stride)
if (ram[i] != 0x31)
return KSFT_FAIL;
return KSFT_PASS;
}
ksft_print_header();
- ksft_set_plan(NR_TESTS + 13);
+ ksft_set_plan(NR_TESTS + 13 + ARRAY_SIZE(more_cases));
for (kind = 0; kind < NR_TESTS; kind++) {
fflush(stdout);
pid = fork();
if (!pid)
- _exit(before_exec(kind));
+ exit(before_exec(kind));
ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
if (ret == KSFT_SKIP)
ksft_test_result_skip("%s: DOEXEC, GUP test or mapping unavailable\n",
@@ -1093,7 +1442,7 @@ int main(int argc, char **argv)
fflush(stdout);
pid = fork();
if (!pid)
- _exit(before_cow_exec());
+ exit(before_cow_exec());
ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
if (ret == KSFT_SKIP)
ksft_test_result_skip("pre-existing COW: DOEXEC unavailable\n");
@@ -1121,27 +1470,27 @@ int main(int argc, char **argv)
switch (kind) {
case 0:
case 1:
- _exit(shared_mm_exec(kind));
+ exit(shared_mm_exec(kind));
case 2:
- _exit(vhost_owner_exec());
+ exit(vhost_owner_exec());
case 3:
- _exit(allocation_failures());
+ exit(allocation_failures());
case 4:
- _exit(nonleader_exec());
+ exit(nonleader_exec());
case 5:
- _exit(proc_readers_exec());
+ exit(proc_readers_exec());
case 6:
- _exit(clone_exec_race());
+ exit(clone_exec_race());
case 7:
- _exit(setuid_exec());
+ exit(setuid_exec());
case 8:
- _exit(devzero_exec());
+ exit(devzero_exec());
case 9:
- _exit(doexec_noexec());
+ exit(doexec_noexec());
case 10:
- _exit(devmem_refused());
+ exit(devmem_refused());
case 11:
- _exit(vfio_bar_exec());
+ exit(vfio_bar_exec());
}
}
ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
@@ -1150,5 +1499,16 @@ int main(int argc, char **argv)
else
ksft_test_result(ret == KSFT_PASS, "%s\n", extra_names[kind]);
}
+ for (kind = 0; kind < (int)ARRAY_SIZE(more_cases); kind++) {
+ fflush(stdout);
+ pid = fork();
+ if (!pid)
+ exit(more_cases[kind].fn());
+ ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
+ if (ret == KSFT_SKIP)
+ ksft_test_result_skip("%s: unavailable\n", more_cases[kind].name);
+ else
+ ksft_test_result(ret == KSFT_PASS, "%s\n", more_cases[kind].name);
+ }
ksft_finished();
}
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [Devel] [PATCH hci-8.1 2/7] selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack
2026-10-14 17:14 [Devel] [PATCH hci-8.1 0/7] selftests/mm: broader MADV_DOEXEC coverage Konstantin Khorenko
2026-10-14 17:14 ` [Devel] [PATCH hci-8.1 1/7] selftests/mm: cover the MADV_DOEXEC interface and VMA attributes Konstantin Khorenko
@ 2026-10-14 17:15 ` Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 3/7] selftests/mm: cover large, swapped and partly marked THP ranges Konstantin Khorenko
` (4 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Konstantin Khorenko @ 2026-10-14 17:15 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
Add cases for the exec side:
- A copy of the test binary whose note type is changed carries no
opt-in, and the marked range is absent in the new image, while an
unchanged copy run the same way keeps it.
- A copy whose note has the opt-in type but another name, in the last
PT_NOTE segment, fails exec with ENOEXEC before the point of no
return, and the marked range is still there. The copy is started in a
mode that fails if the exec goes through. The same note in another
segment is skipped: the exec goes through without the memory.
- A copy whose PT_NOTE holding the opt-in comes before another PT_NOTE
in the program headers still gets the memory: the loader has to look
at every note segment, not only the last one. The test binary also
carries an 8-aligned note, which ld.bfd and lld never put in the
PT_NOTE of the 4-aligned opt-in, so there are two to reorder.
- With ADDR_NO_RANDOMIZE the new stack starts at STACK_TOP and grows by
128K below the argument pages, plus the stack guard gap; a range
preserved there makes setup_arg_pages() fail the exec, which kills
the task with SIGSEGV. The place is computed from the size of the
environment, and the same exec with the page far below the stack must
succeed first. x86_64 only, as the address comes from its STACK_TOP;
skipped when randomization is off, since the page then falls on the
stack of the test itself.
The note is found through the PT_NOTE program headers and patched in the
byte order of the machine; a missing note fails the cases. The copies
are written to the first of the current directory, /var/tmp and /tmp
that is not mounted noexec, unlinked and executed through their fd.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 373 +++++++++++++++++++++++++++++++++++-
1 file changed, 370 insertions(+), 3 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 850b498e3d1dc..5eaf752b8413c 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -1,10 +1,12 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Verify what MADV_DOEXEC preserves across exec, long-term DMA pins first. */
#define _GNU_SOURCE
+#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
+#include <link.h>
#include <linux/mempolicy.h>
#include <linux/userfaultfd.h>
#include <linux/vfio.h>
@@ -22,9 +24,11 @@
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/mount.h>
+#include <sys/personality.h>
#include <sys/resource.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
+#include <sys/statvfs.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
#include <sys/utsname.h>
@@ -34,6 +38,9 @@
#include "../../../../mm/gup_test.h"
#include "../kselftest.h"
+#ifndef MAP_FIXED_NOREPLACE
+#define MAP_FIXED_NOREPLACE 0x100000
+#endif
#ifndef UFFD_USER_MODE_ONLY
#define UFFD_USER_MODE_ONLY 1
#endif
@@ -58,6 +65,14 @@ asm(".pushsection .note.doexec,\"a\",%note\n"
".balign 4\n"
".popsection\n");
+/* An 8-aligned note cannot share a PT_NOTE with the 4-aligned ones. */
+asm(".pushsection .note.doexec8,\"a\",%note\n"
+ ".balign 8\n"
+ ".long 4, 0, 1\n"
+ ".asciz \"DOE\"\n"
+ ".balign 8\n"
+ ".popsection\n");
+
enum test_kind {
BASE,
HUGE,
@@ -452,6 +467,8 @@ static int vhost_owner_exec(void)
return KSFT_FAIL;
}
+static const char * const copy_dirs[] = { ".", "/var/tmp", "/tmp" };
+
static void remove_setuid_copy(char *path)
{
unlink(path);
@@ -526,14 +543,13 @@ static int unprivileged_exec(const char *path, bool preserve)
/* 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)))
+ for (i = 0; i < (int)ARRAY_SIZE(copy_dirs) && ret == KSFT_SKIP; i++) {
+ if (make_setuid_copy(copy_dirs[i], path, sizeof(path)))
continue;
/* A nosuid mount or a private directory makes the copy useless. */
ret = unprivileged_exec(path, false);
@@ -1172,6 +1188,18 @@ static int unmarked_exec(void)
return KSFT_FAIL;
}
+static int after_present(char *ram, size_t size)
+{
+ if (!range_mapped(ram, size))
+ return fail("range not preserved");
+ return check_contents(ram, size, 0x31);
+}
+
+static int after_never(char *ram, size_t size)
+{
+ return fail("exec was expected to fail");
+}
+
static int after_absent(char *ram, size_t size)
{
return range_mapped(ram, size) ? fail("range still mapped") : KSFT_PASS;
@@ -1344,6 +1372,338 @@ static int after_mempolicy(char *ram, size_t size)
return check_contents(ram, size, 0x31);
}
+#define DOEXEC_NOTE_TYPE 0x07c1feed
+#define ELF_EHDR ElfW(Ehdr)
+#define ELF_PHDR ElfW(Phdr)
+#define ELF_NHDR ElfW(Nhdr)
+
+static char *read_self(size_t *size)
+{
+ int fd = open("/proc/self/exe", O_RDONLY);
+ char *image = NULL;
+ struct stat st;
+
+ if (fd < 0)
+ return NULL;
+ if (!fstat(fd, &st))
+ image = malloc(st.st_size);
+ if (image && read(fd, image, st.st_size) != st.st_size) {
+ free(image);
+ image = NULL;
+ }
+ close(fd);
+ if (image)
+ *size = st.st_size;
+ return image;
+}
+
+static ELF_NHDR *find_opt_in_note(char *image, size_t size, int *seg)
+{
+ ELF_EHDR *ehdr = (ELF_EHDR *)image;
+ ELF_PHDR *phdr;
+ size_t off, end;
+ int i;
+
+ if (size < sizeof(*ehdr) || memcmp(ehdr->e_ident, ELFMAG, SELFMAG))
+ return NULL;
+ for (i = 0; i < ehdr->e_phnum; i++) {
+ phdr = (ELF_PHDR *)(image + ehdr->e_phoff) + i;
+ if (phdr->p_type != PT_NOTE || phdr->p_offset + phdr->p_filesz > size)
+ continue;
+ end = phdr->p_offset + phdr->p_filesz;
+ for (off = phdr->p_offset; off + sizeof(ELF_NHDR) <= end;) {
+ ELF_NHDR *note = (ELF_NHDR *)(image + off);
+
+ if (note->n_type == DOEXEC_NOTE_TYPE &&
+ note->n_namesz == sizeof("preserved-mem-ok") &&
+ !memcmp(note + 1, "preserved-mem-ok", sizeof("preserved-mem-ok"))) {
+ *seg = i;
+ return note;
+ }
+ off += sizeof(*note) + ((note->n_namesz + 3) & ~3u) +
+ ((note->n_descsz + 3) & ~3u);
+ }
+ }
+ return NULL;
+}
+
+enum note_patch { NOTE_KEEP, NOTE_RETYPE, NOTE_RENAME, NOTE_NOT_LAST, NOTE_RENAME_NOT_LAST };
+
+/* Move the segment holding the opt-in before another PT_NOTE if it is the last. */
+static bool note_not_last(char *image, int seg)
+{
+ ELF_EHDR *ehdr = (ELF_EHDR *)image;
+ ELF_PHDR *phdr = (ELF_PHDR *)(image + ehdr->e_phoff);
+ ELF_PHDR tmp;
+ int i, other = -1;
+
+ for (i = 0; i < ehdr->e_phnum; i++) {
+ if (phdr[i].p_type != PT_NOTE || i == seg)
+ continue;
+ if (i > seg)
+ return true;
+ if (other < 0)
+ other = i;
+ }
+ if (other < 0)
+ return false;
+ tmp = phdr[other];
+ phdr[other] = phdr[seg];
+ phdr[seg] = tmp;
+ return true;
+}
+
+/* Make the segment holding the opt-in the last PT_NOTE. */
+static void note_last(char *image, int seg)
+{
+ ELF_EHDR *ehdr = (ELF_EHDR *)image;
+ ELF_PHDR *phdr = (ELF_PHDR *)(image + ehdr->e_phoff);
+ ELF_PHDR tmp;
+ int i, last = seg;
+
+ for (i = 0; i < ehdr->e_phnum; i++)
+ if (phdr[i].p_type == PT_NOTE)
+ last = i;
+ tmp = phdr[last];
+ phdr[last] = phdr[seg];
+ phdr[seg] = tmp;
+}
+
+/* An fd of a patched copy of this binary; -2: no opt-in note, -3: one PT_NOTE. */
+static int patched_copy(enum note_patch how)
+{
+ char path[64], *image;
+ ELF_NHDR *note;
+ struct statvfs vfs;
+ int fd = -1, i, seg;
+ size_t size;
+
+ image = read_self(&size);
+ if (!image)
+ return -1;
+ note = find_opt_in_note(image, size, &seg);
+ if (!note) {
+ free(image);
+ return -2;
+ }
+ if (how == NOTE_RETYPE)
+ note->n_type = DOEXEC_NOTE_TYPE - 1;
+ else if (how == NOTE_RENAME) {
+ memcpy(note + 1, "preserved-mem-no", 16);
+ note_last(image, seg);
+ } else if ((how == NOTE_NOT_LAST || how == NOTE_RENAME_NOT_LAST) &&
+ !note_not_last(image, seg)) {
+ free(image);
+ return -3;
+ }
+ if (how == NOTE_RENAME_NOT_LAST)
+ memcpy(note + 1, "preserved-mem-no", 16);
+ for (i = 0; i < (int)ARRAY_SIZE(copy_dirs) && fd < 0; i++) {
+ if (statvfs(copy_dirs[i], &vfs) || (vfs.f_flag & ST_NOEXEC))
+ continue;
+ snprintf(path, sizeof(path), "%s/doexec-note.XXXXXX", copy_dirs[i]);
+ fd = mkstemp(path);
+ if (fd < 0)
+ continue;
+ if (write(fd, image, size) != (ssize_t)size || fchmod(fd, 0755)) {
+ close(fd);
+ unlink(path);
+ fd = -1;
+ continue;
+ }
+ close(fd);
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ unlink(path);
+ }
+ free(image);
+ return fd;
+}
+
+static int exec_fd_mode(int exe, const char *mode, char *ram, size_t size)
+{
+ char addr_arg[32], size_arg[32];
+ char *args[] = { "doexec", (char *)mode, addr_arg, size_arg, NULL };
+
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ syscall(__NR_execveat, exe, "", args, environ, AT_EMPTY_PATH);
+ return errno;
+}
+
+/* An unpatched copy run the same way keeps the memory. */
+static int copy_control(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ pid_t pid = fork();
+ char *ram;
+ int exe;
+
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ ram = preserved_ram(size);
+ if (!ram)
+ exit(unsupported());
+ exe = patched_copy(NOTE_KEEP);
+ if (exe == -2)
+ exit(fail("opt-in note not found in the test binary"));
+ if (exe < 0)
+ exit(KSFT_SKIP);
+ exit(exec_fd_mode(exe, "--present", ram, size) == EACCES ? KSFT_SKIP : KSFT_FAIL);
+ }
+ return wait_child(pid);
+}
+
+static int no_note_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram;
+ int exe, err;
+
+ err = copy_control();
+ if (err)
+ return err == KSFT_SKIP ? KSFT_SKIP : fail("unpatched copy lost the memory");
+ ram = preserved_ram(size);
+ if (!ram)
+ return unsupported();
+ exe = patched_copy(NOTE_RETYPE);
+ if (exe == -2)
+ return fail("opt-in note not found in the test binary");
+ if (exe < 0)
+ return KSFT_SKIP;
+ err = exec_fd_mode(exe, "--absent", ram, size);
+ if (err == EACCES)
+ return KSFT_SKIP;
+ ksft_print_msg("exec: %s\n", strerror(err));
+ return KSFT_FAIL;
+}
+
+static int note_not_last_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ int exe, err;
+
+ if (!ram)
+ return unsupported();
+ exe = patched_copy(NOTE_NOT_LAST);
+ if (exe == -2)
+ return fail("opt-in note not found in the test binary");
+ if (exe < 0)
+ return KSFT_SKIP;
+ err = exec_fd_mode(exe, "--present", ram, size);
+ if (err == EACCES)
+ return KSFT_SKIP;
+ ksft_print_msg("exec: %s\n", strerror(err));
+ return KSFT_FAIL;
+}
+
+/* Outside the last PT_NOTE a malformed opt-in is skipped, not refused. */
+static int bad_note_not_last_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ int exe, err;
+
+ if (!ram)
+ return unsupported();
+ exe = patched_copy(NOTE_RENAME_NOT_LAST);
+ if (exe == -2)
+ return fail("opt-in note not found in the test binary");
+ if (exe < 0)
+ return KSFT_SKIP;
+ err = exec_fd_mode(exe, "--absent", ram, size);
+ if (err == EACCES)
+ return KSFT_SKIP;
+ ksft_print_msg("exec: %s\n", strerror(err));
+ return KSFT_FAIL;
+}
+
+static int bad_note_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ int exe, err;
+
+ if (!ram)
+ return unsupported();
+ exe = patched_copy(NOTE_RENAME);
+ if (exe == -2)
+ return fail("opt-in note not found in the test binary");
+ if (exe < 0)
+ return KSFT_SKIP;
+ err = exec_fd_mode(exe, "--never", ram, size);
+ if (err == EACCES)
+ return KSFT_SKIP;
+ if (err != ENOEXEC) {
+ ksft_print_msg("exec: %s\n", strerror(err));
+ return fail("malformed note not refused with ENOEXEC");
+ }
+ return check_contents(ram, size, 0x31);
+}
+
+#if defined(__x86_64__) && !defined(__ILP32__)
+/* With @below pages under the argument pages, exec the page preserved there. */
+static int stack_exec(size_t below)
+{
+ size_t size = sysconf(_SC_PAGESIZE), args = 2 * size;
+ struct rlimit no_core = { 0, 0 };
+ char *top = (char *)((1ul << 47) - size), *ram, **env;
+ int status;
+ pid_t pid;
+
+ for (env = environ; *env; env++)
+ args += strlen(*env) + 1;
+ args = (args + size - 1) & ~(size - 1);
+ pid = fork();
+ if (pid < 0)
+ return -1;
+ if (!pid) {
+ setrlimit(RLIMIT_CORE, &no_core);
+ if (personality(ADDR_NO_RANDOMIZE) < 0)
+ _exit(KSFT_SKIP);
+ ram = mmap(top - args - below * size, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
+ if (ram == MAP_FAILED)
+ _exit(KSFT_SKIP);
+ memset(ram, 0x31, size);
+ if (madvise(ram, size, MADV_DOEXEC))
+ _exit(unsupported());
+ exec_mode("/proc/self/exe", "--present", ram, size);
+ _exit(KSFT_FAIL);
+ }
+ if (waitpid(pid, &status, 0) != pid)
+ return -1;
+ return status;
+}
+
+static int stack_overlap_exec(void)
+{
+ int status;
+
+ /* Far below the stack, past the default guard gap, the same exec works... */
+ status = stack_exec(4096);
+ if (status < 0 || !WIFEXITED(status))
+ return fail("control exec failed");
+ if (WEXITSTATUS(status) != KSFT_PASS)
+ return WEXITSTATUS(status) == KSFT_SKIP ? KSFT_SKIP : fail("control exec failed");
+ /* ...within the 128K the stack is grown by, it kills the task. */
+ status = stack_exec(16);
+ if (status < 0)
+ return KSFT_FAIL;
+ if (WIFEXITED(status))
+ return WEXITSTATUS(status) == KSFT_SKIP ? KSFT_SKIP :
+ fail("exec did not kill the task");
+ return WTERMSIG(status) == SIGSEGV ? KSFT_PASS : fail("killed by another signal");
+}
+#else
+static int stack_overlap_exec(void)
+{
+ return KSFT_SKIP;
+}
+#endif
+
struct after_mode {
const char *mode;
int (*fn)(char *ram, size_t size);
@@ -1351,6 +1711,8 @@ struct after_mode {
static const struct after_mode after_modes[] = {
{ "--absent", after_absent },
+ { "--never", after_never },
+ { "--present", after_present },
{ "--partial", after_partial },
{ "--unlocked", after_unlocked },
{ "--dontfork", after_dontfork },
@@ -1375,6 +1737,11 @@ static const struct more_case more_cases[] = {
{ "MADV_WIPEONFORK range: preserved, wiped in a child", wipeonfork_exec },
{ "userfaultfd registration: not preserved", uffd_exec },
{ "memory policy: preserved", mempolicy_exec },
+ { "binary without the opt-in note: nothing preserved", no_note_exec },
+ { "malformed opt-in note: ENOEXEC, memory intact", bad_note_exec },
+ { "opt-in note not in the last PT_NOTE: preserved", note_not_last_exec },
+ { "malformed opt-in note not in the last PT_NOTE: skipped", bad_note_not_last_exec },
+ { "preserved range under the new stack: exec kills the task", stack_overlap_exec },
};
int main(int argc, char **argv)
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [Devel] [PATCH hci-8.1 3/7] selftests/mm: cover large, swapped and partly marked THP ranges
2026-10-14 17:14 [Devel] [PATCH hci-8.1 0/7] selftests/mm: broader MADV_DOEXEC coverage Konstantin Khorenko
2026-10-14 17:14 ` [Devel] [PATCH hci-8.1 1/7] selftests/mm: cover the MADV_DOEXEC interface and VMA attributes Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 2/7] selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack Konstantin Khorenko
@ 2026-10-14 17:15 ` Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 4/7] selftests/mm: check that MADV_DOEXEC refuses an exec from a vfork() child Konstantin Khorenko
` (3 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Konstantin Khorenko @ 2026-10-14 17:15 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
Add cases for the kinds of memory a preserved range can hold:
- A 1 GB range touched every 2 MB, large enough for vma_dup() to split
the copy into several padata jobs with CONFIG_PADATA.
- A range pushed to swap with MADV_PAGEOUT, so that the copy takes swap
entries and the new image swaps the pages back in. Skipped when none
of the pages reaches swap.
- A THP of which only the first half is marked, which splits its PMD
and makes the copy batch PTEs of a large folio. Every page of the
second half must be absent in the new image. Skipped unless smaps
shows the range mapped by a THP before marking.
Both base page cases tolerate EINVAL from MADV_NOHUGEPAGE, which a
kernel without THP returns.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 132 +++++++++++++++++++++++++++++++++---
1 file changed, 124 insertions(+), 8 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 5eaf752b8413c..ab1d461de5517 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -56,6 +56,9 @@
#ifndef MADV_WIPEONFORK
#define MADV_WIPEONFORK 18
#endif
+#ifndef MADV_PAGEOUT
+#define MADV_PAGEOUT 21
+#endif
/* The downstream ELF loader requires this opt-in in a PT_NOTE segment. */
asm(".pushsection .note.doexec,\"a\",%note\n"
@@ -642,29 +645,35 @@ static int doexec_noexec(void)
return ret;
}
-static bool smaps_has_flag(char *addr, const char *flag)
+/* Copy the line of the smaps entry of the VMA at @addr that starts with @prefix. */
+static bool smaps_line(char *addr, const char *prefix, char *line, size_t len)
{
FILE *file = fopen("/proc/self/smaps", "r");
- char line[256], want[32];
bool in_vma = false, found = false;
+ char want[32];
if (!file)
return false;
snprintf(want, sizeof(want), "%lx-", (unsigned long)addr);
- while (fgets(line, sizeof(line), file)) {
+ while (!found && fgets(line, len, file)) {
char *space = strchr(line, ' ');
- if (!space)
- continue;
- if (memchr(line, '-', space - line) && !memchr(line, ':', space - line))
+ if (space && memchr(line, '-', space - line) && !memchr(line, ':', space - line))
in_vma = !strncmp(line, want, strlen(want));
- else if (in_vma && !strncmp(line, "VmFlags:", 8))
- found = strstr(line, flag) != NULL;
+ else if (in_vma)
+ found = !strncmp(line, prefix, strlen(prefix));
}
fclose(file);
return found;
}
+static bool smaps_has_flag(char *addr, const char *flag)
+{
+ char line[512];
+
+ return smaps_line(addr, "VmFlags:", line, sizeof(line)) && strstr(line, flag);
+}
+
static int after_pfnmap_exec(char *bar, size_t size)
{
size_t page = sysconf(_SC_PAGESIZE), i;
@@ -1704,6 +1713,109 @@ static int stack_overlap_exec(void)
}
#endif
+/* Large enough for vma_dup() to split the copy between several threads. */
+static int sparse_exec(void)
+{
+ const size_t size = 1ul << 30, stride = 2ul << 20;
+ char *ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
+ size_t i;
+
+ if (ram == MAP_FAILED)
+ return KSFT_SKIP;
+ /* EINVAL: no THP support, the range is mapped with base pages anyway. */
+ if (madvise(ram, size, MADV_NOHUGEPAGE) && errno != EINVAL)
+ return KSFT_FAIL;
+ for (i = 0; i < size; i += stride)
+ ram[i] = 0x31;
+ if (madvise(ram, size, MADV_DOEXEC))
+ return unsupported();
+ check_exec(ram, size, stride, false);
+ return KSFT_FAIL;
+}
+
+static bool page_swapped(char *addr)
+{
+ size_t page = sysconf(_SC_PAGESIZE);
+ int fd = open("/proc/self/pagemap", O_RDONLY);
+ uint64_t entry = 0;
+
+ if (fd < 0)
+ return false;
+ if (pread(fd, &entry, sizeof(entry), (uintptr_t)addr / page * sizeof(entry)) !=
+ sizeof(entry))
+ entry = 0;
+ close(fd);
+ return entry & (1ull << 62);
+}
+
+/* Swap entries are copied, and the new image swaps the pages back in. */
+static int swapped_exec(void)
+{
+ size_t page = sysconf(_SC_PAGESIZE), size = 64 * page, i;
+ char *ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+ if (ram == MAP_FAILED ||
+ (madvise(ram, size, MADV_NOHUGEPAGE) && errno != EINVAL))
+ return KSFT_FAIL;
+ memset(ram, 0x31, size);
+ if (madvise(ram, size, MADV_PAGEOUT))
+ return KSFT_SKIP;
+ for (i = 0; i < size && !page_swapped(ram + i); i += page)
+ ;
+ if (i == size)
+ return KSFT_SKIP;
+ if (madvise(ram, size, MADV_DOEXEC))
+ return unsupported();
+ check_exec(ram, size, page, false);
+ return KSFT_FAIL;
+}
+
+static unsigned long smaps_kb(char *addr, const char *field)
+{
+ char line[256];
+
+ if (!smaps_line(addr, field, line, sizeof(line)))
+ return 0;
+ return strtoul(line + strlen(field), NULL, 10);
+}
+
+/* Marking half of a THP splits its PMD; the PTE-mapped half carries over. */
+static int thp_half_exec(void)
+{
+ size_t size = huge_page_size();
+ char *area, *ram;
+
+ if (!size)
+ return KSFT_SKIP;
+ area = mmap(NULL, 2 * size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (area == MAP_FAILED)
+ return KSFT_FAIL;
+ ram = (char *)(((uintptr_t)area + size - 1) & ~(size - 1));
+ if (madvise(ram, size, MADV_HUGEPAGE))
+ return KSFT_SKIP;
+ memset(ram, 0x31, size);
+ madvise(ram, size, MADV_COLLAPSE);
+ if (smaps_kb(ram, "AnonHugePages:") < size >> 10)
+ return KSFT_SKIP;
+ if (madvise(ram, size / 2, MADV_DOEXEC))
+ return unsupported();
+ exec_mode("/proc/self/exe", "--thp-half", ram, size);
+ return KSFT_FAIL;
+}
+
+static int after_thp_half(char *ram, size_t size)
+{
+ size_t page = sysconf(_SC_PAGESIZE), off;
+
+ for (off = size / 2; off < size; off += page)
+ if (range_mapped(ram + off, page))
+ return fail("unmarked half preserved");
+ return check_contents(ram, size / 2, 0x31);
+}
+
struct after_mode {
const char *mode;
int (*fn)(char *ram, size_t size);
@@ -1719,6 +1831,7 @@ static const struct after_mode after_modes[] = {
{ "--wipeonfork", after_wipeonfork },
{ "--no-uffd", after_no_uffd },
{ "--mempolicy", after_mempolicy },
+ { "--thp-half", after_thp_half },
};
struct more_case {
@@ -1742,6 +1855,9 @@ static const struct more_case more_cases[] = {
{ "opt-in note not in the last PT_NOTE: preserved", note_not_last_exec },
{ "malformed opt-in note not in the last PT_NOTE: skipped", bad_note_not_last_exec },
{ "preserved range under the new stack: exec kills the task", stack_overlap_exec },
+ { "1 GB sparse range: preserved", sparse_exec },
+ { "swapped out pages: preserved", swapped_exec },
+ { "half of a THP: preserved", thp_half_exec },
};
int main(int argc, char **argv)
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [Devel] [PATCH hci-8.1 4/7] selftests/mm: check that MADV_DOEXEC refuses an exec from a vfork() child
2026-10-14 17:14 [Devel] [PATCH hci-8.1 0/7] selftests/mm: broader MADV_DOEXEC coverage Konstantin Khorenko
` (2 preceding siblings ...)
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 3/7] selftests/mm: cover large, swapped and partly marked THP ranges Konstantin Khorenko
@ 2026-10-14 17:15 ` Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 5/7] selftests/mm: check VM_MAYEXEC of MADV_DOEXEC ranges in smaps Konstantin Khorenko
` (2 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Konstantin Khorenko @ 2026-10-14 17:15 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
A vfork() parent shares the mm with the child and would be woken in
exec_mmap() before the preserved ranges are copied, so the exec is
refused with EBUSY. Check the errno in the child and that the parent's
marked memory is intact afterwards.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index ab1d461de5517..01e786a4cb75c 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -1839,6 +1839,33 @@ struct more_case {
int (*fn)(void);
};
+/* A vfork() parent shares the mm and survives the exec: refused. */
+static int vfork_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ char addr_arg[32], size_arg[32];
+ char *args[] = { "doexec", "--check", addr_arg, size_arg, "1", NULL };
+ enum { REFUSED = 10 };
+ pid_t pid;
+
+ if (!ram)
+ return unsupported();
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ pid = vfork();
+ if (!pid) {
+ execv("/proc/self/exe", args);
+ _exit(errno == EBUSY ? REFUSED : KSFT_FAIL);
+ }
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (wait_child(pid) != REFUSED)
+ return fail("preserving exec from a vfork() child not refused");
+ return check_bytes(ram, size, 0x31) ? KSFT_FAIL : KSFT_PASS;
+}
+
static const struct more_case more_cases[] = {
{ "madv_doexec_flag sysctl matches MADV_DOEXEC", sysctl_flag },
{ "shared, file, executable and stack mappings refused", refused_mappings },
@@ -1858,6 +1885,7 @@ static const struct more_case more_cases[] = {
{ "1 GB sparse range: preserved", sparse_exec },
{ "swapped out pages: preserved", swapped_exec },
{ "half of a THP: preserved", thp_half_exec },
+ { "vfork() child: preserving exec refused", vfork_exec },
};
int main(int argc, char **argv)
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [Devel] [PATCH hci-8.1 5/7] selftests/mm: check VM_MAYEXEC of MADV_DOEXEC ranges in smaps
2026-10-14 17:14 [Devel] [PATCH hci-8.1 0/7] selftests/mm: broader MADV_DOEXEC coverage Konstantin Khorenko
` (3 preceding siblings ...)
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 4/7] selftests/mm: check that MADV_DOEXEC refuses an exec from a vfork() child Konstantin Khorenko
@ 2026-10-14 17:15 ` Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 6/7] selftests/mm: check who can still reach the old mm of a preserving exec Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 7/7] selftests/mm: say why a doexec case fails Konstantin Khorenko
6 siblings, 0 replies; 8+ messages in thread
From: Konstantin Khorenko @ 2026-10-14 17:15 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
An EACCES from mprotect(PROT_EXEC) does not prove that VM_MAYEXEC is
clear: an LSM or MDWE refuses it the same way. Read the flag from
VmFlags as well. Also check that a fork child and the new image get
VM_MAYEXEC back, as the mark is dropped from their copies.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 94 +++++++++++++++++++++++++------------
1 file changed, 64 insertions(+), 30 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 01e786a4cb75c..89862e0b6a4dc 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -583,9 +583,43 @@ static int devzero_exec(void)
return KSFT_FAIL;
}
+/* Copy the line of the smaps entry of the VMA at @addr that starts with @prefix. */
+static bool smaps_line(char *addr, const char *prefix, char *line, size_t len)
+{
+ FILE *file = fopen("/proc/self/smaps", "r");
+ bool in_vma = false, found = false;
+ char want[32];
+
+ if (!file)
+ return false;
+ snprintf(want, sizeof(want), "%lx-", (unsigned long)addr);
+ while (!found && fgets(line, len, file)) {
+ char *space = strchr(line, ' ');
+
+ if (space && memchr(line, '-', space - line) && !memchr(line, ':', space - line))
+ in_vma = !strncmp(line, want, strlen(want));
+ else if (in_vma)
+ found = !strncmp(line, prefix, strlen(prefix));
+ }
+ fclose(file);
+ return found;
+}
+
+static bool smaps_has_flag(char *addr, const char *flag)
+{
+ char line[512];
+
+ return smaps_line(addr, "VmFlags:", line, sizeof(line)) && strstr(line, flag);
+}
+
+/* VM_MAYEXEC ("me") is clear, so mprotect(PROT_EXEC) fails. */
static bool exec_refused(char *ram, size_t size)
{
- return mprotect(ram, size, PROT_READ | PROT_EXEC) && errno == EACCES;
+ char line[512];
+
+ return smaps_line(ram, "VmFlags:", line, sizeof(line)) &&
+ !strstr(line, " me ") &&
+ mprotect(ram, size, PROT_READ | PROT_EXEC) && errno == EACCES;
}
/* MADV_DONTEXEC must not grant more than mmap() did on a noexec mount. */
@@ -645,35 +679,6 @@ static int doexec_noexec(void)
return ret;
}
-/* Copy the line of the smaps entry of the VMA at @addr that starts with @prefix. */
-static bool smaps_line(char *addr, const char *prefix, char *line, size_t len)
-{
- FILE *file = fopen("/proc/self/smaps", "r");
- bool in_vma = false, found = false;
- char want[32];
-
- if (!file)
- return false;
- snprintf(want, sizeof(want), "%lx-", (unsigned long)addr);
- while (!found && fgets(line, len, file)) {
- char *space = strchr(line, ' ');
-
- if (space && memchr(line, '-', space - line) && !memchr(line, ':', space - line))
- in_vma = !strncmp(line, want, strlen(want));
- else if (in_vma)
- found = !strncmp(line, prefix, strlen(prefix));
- }
- fclose(file);
- return found;
-}
-
-static bool smaps_has_flag(char *addr, const char *flag)
-{
- char line[512];
-
- return smaps_line(addr, "VmFlags:", line, sizeof(line)) && strstr(line, flag);
-}
-
static int after_pfnmap_exec(char *bar, size_t size)
{
size_t page = sysconf(_SC_PAGESIZE), i;
@@ -1274,6 +1279,33 @@ static int locked_exec(void)
return exec_with_flag(ram, size, "--unlocked", " lo ");
}
+/* Dropping the mark in a copy gives VM_MAYEXEC back. */
+static int mayexec_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ pid_t pid;
+
+ if (!ram)
+ return unsupported();
+ if (!exec_refused(ram, size))
+ return fail("VM_MAYEXEC kept by MADV_DOEXEC");
+ pid = fork();
+ if (!pid)
+ _exit(smaps_has_flag(ram, " me ") ? KSFT_PASS : KSFT_FAIL);
+ if (pid < 0 || wait_child(pid) != KSFT_PASS)
+ return fail("VM_MAYEXEC not back in a fork child");
+ exec_mode("/proc/self/exe", "--mayexec", ram, size);
+ return KSFT_FAIL;
+}
+
+static int after_mayexec(char *ram, size_t size)
+{
+ if (!smaps_has_flag(ram, " me "))
+ return fail("VM_MAYEXEC not back after exec");
+ return check_contents(ram, size, 0x31);
+}
+
static int after_unlocked(char *ram, size_t size)
{
if (smaps_has_flag(ram, " lo "))
@@ -1832,6 +1864,7 @@ static const struct after_mode after_modes[] = {
{ "--no-uffd", after_no_uffd },
{ "--mempolicy", after_mempolicy },
{ "--thp-half", after_thp_half },
+ { "--mayexec", after_mayexec },
};
struct more_case {
@@ -1886,6 +1919,7 @@ static const struct more_case more_cases[] = {
{ "swapped out pages: preserved", swapped_exec },
{ "half of a THP: preserved", thp_half_exec },
{ "vfork() child: preserving exec refused", vfork_exec },
+ { "exec permission back in a fork child and the new image", mayexec_exec },
};
int main(int argc, char **argv)
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [Devel] [PATCH hci-8.1 6/7] selftests/mm: check who can still reach the old mm of a preserving exec
2026-10-14 17:14 [Devel] [PATCH hci-8.1 0/7] selftests/mm: broader MADV_DOEXEC coverage Konstantin Khorenko
` (4 preceding siblings ...)
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 5/7] selftests/mm: check VM_MAYEXEC of MADV_DOEXEC ranges in smaps Konstantin Khorenko
@ 2026-10-14 17:15 ` Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 7/7] selftests/mm: say why a doexec case fails Konstantin Khorenko
6 siblings, 0 replies; 8+ messages in thread
From: Konstantin Khorenko @ 2026-10-14 17:15 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
- A /proc/pid/mem write that took the mm before the exec and stalls on
a userfaultfd source until after it must land in the old mm, not in
the pages handed to the new image. Needs userfaultfd for kernel
faults, so CAP_SYS_PTRACE or vm.unprivileged_userfaultfd.
- A vhost worker in kthread mode runs on the owner's mm through
kthread_use_mm() and survives the exec: the exec fails with EBUSY
until the owner is reset.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 156 ++++++++++++++++++++++++++++++++++++
1 file changed, 156 insertions(+)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 89862e0b6a4dc..1b33bc489eb71 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -1867,6 +1867,157 @@ static const struct after_mode after_modes[] = {
{ "--mayexec", after_mayexec },
};
+/* The new image tells the parent it runs, then checks @ram once told to. */
+static int after_old_mm(char *ram, size_t size, int done, int check)
+{
+ char c;
+
+ if (write(done, "x", 1) != 1 || read(check, &c, 1) != 1)
+ return KSFT_FAIL;
+ if (check_bytes(ram, size, 0x31))
+ return fail("a write through the old mm reached the new image");
+ return KSFT_PASS;
+}
+
+struct mem_write {
+ int fd;
+ char *src;
+ char *dst;
+ size_t size;
+ ssize_t ret;
+};
+
+static void *mem_writer(void *arg)
+{
+ struct mem_write *w = arg;
+
+ w->ret = pwrite(w->fd, w->src, w->size, (off_t)(uintptr_t)w->dst);
+ return NULL;
+}
+
+/* A /proc/pid/mem write stalled across the exec must land in the old mm. */
+static int old_mm_write_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ struct uffdio_api api = { .api = UFFD_API };
+ struct uffdio_register reg = { .mode = UFFDIO_REGISTER_MODE_MISSING };
+ struct uffdio_copy copy = { .len = size };
+ struct mem_write w = { .fd = -1, .size = size };
+ int to_child[2], to_parent[2], uffd, ret = KSFT_FAIL;
+ bool stalled = false, waited = false, copied;
+ char path[64], *fill, c;
+ struct uffd_msg msg;
+ struct pollfd pfd;
+ pthread_t thread;
+ pid_t pid;
+
+ uffd = syscall(__NR_userfaultfd, O_CLOEXEC);
+ if (uffd < 0)
+ return KSFT_SKIP;
+ w.src = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ fill = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ reg.range.start = (uintptr_t)w.src;
+ reg.range.len = size;
+ if (w.src == MAP_FAILED || fill == MAP_FAILED || ioctl(uffd, UFFDIO_API, &api) ||
+ ioctl(uffd, UFFDIO_REGISTER, ®) || pipe(to_child) || pipe(to_parent))
+ return KSFT_FAIL;
+ memset(fill, 0x77, size);
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ char *ram = preserved_ram(size), args[4][32];
+
+ close(uffd);
+ close(to_child[1]);
+ close(to_parent[0]);
+ if (!ram)
+ _exit(unsupported());
+ if (write(to_parent[1], &ram, sizeof(ram)) != sizeof(ram) ||
+ read(to_child[0], &c, 1) != 1)
+ _exit(KSFT_FAIL);
+ snprintf(args[0], sizeof(args[0]), "%llu", (unsigned long long)(uintptr_t)ram);
+ snprintf(args[1], sizeof(args[1]), "%zu", size);
+ snprintf(args[2], sizeof(args[2]), "%d", to_parent[1]);
+ snprintf(args[3], sizeof(args[3]), "%d", to_child[0]);
+ execl("/proc/self/exe", "doexec", "--old-mm", args[0], args[1], args[2],
+ args[3], NULL);
+ _exit(KSFT_FAIL);
+ }
+ close(to_child[0]);
+ close(to_parent[1]);
+ if (read(to_parent[0], &w.dst, sizeof(w.dst)) != sizeof(w.dst)) {
+ ret = wait_child(pid);
+ waited = true;
+ goto out;
+ }
+ snprintf(path, sizeof(path), "/proc/%d/mem", pid);
+ w.fd = open(path, O_RDWR);
+ if (w.fd < 0) {
+ ret = KSFT_SKIP;
+ goto out;
+ }
+ if (pthread_create(&thread, NULL, mem_writer, &w))
+ goto out;
+ /* The writer holds the child's mm while it faults on its source. */
+ pfd.fd = uffd;
+ pfd.events = POLLIN;
+ if (poll(&pfd, 1, 10000) == 1 && read(uffd, &msg, sizeof(msg)) == sizeof(msg) &&
+ msg.event == UFFD_EVENT_PAGEFAULT && write(to_child[1], "x", 1) == 1)
+ stalled = read(to_parent[0], &c, 1) == 1;
+ copy.dst = (uintptr_t)w.src;
+ copy.src = (uintptr_t)fill;
+ copied = !ioctl(uffd, UFFDIO_COPY, ©);
+ if (!copied)
+ close(uffd); /* releases the writer as well */
+ pthread_join(thread, NULL);
+ if (!copied)
+ ret = fail("UFFDIO_COPY failed");
+ else if (!stalled)
+ ret = fail("could not exec while a /proc/pid/mem write was stalled");
+ else if (w.ret != (ssize_t)size)
+ ret = fail("write through the old mm failed");
+ else if (write(to_child[1], "x", 1) == 1) {
+ ret = wait_child(pid);
+ waited = true;
+ }
+out:
+ if (!waited) {
+ kill(pid, SIGKILL);
+ waitpid(pid, NULL, 0);
+ }
+ return ret;
+}
+
+/* A vhost worker in kthread mode runs on the owner's mm and outlives the exec. */
+static int vhost_kthread_exec(void)
+{
+#ifdef VHOST_SET_FORK_FROM_OWNER
+ size_t size = sysconf(_SC_PAGESIZE);
+ __u8 mode = VHOST_FORK_OWNER_KTHREAD;
+ int fd = open("/dev/vhost-net", O_RDWR), err;
+ char *ram;
+
+ if (fd < 0)
+ return KSFT_SKIP;
+ ram = preserved_ram(size);
+ if (!ram)
+ return unsupported();
+ if (ioctl(fd, VHOST_SET_FORK_FROM_OWNER, &mode) || ioctl(fd, VHOST_SET_OWNER))
+ return KSFT_SKIP;
+ err = check_exec(ram, size, 1, true);
+ if (err != EBUSY || check_bytes(ram, size, 0x31))
+ return fail("preserving exec with a vhost kthread not refused");
+ if (ioctl(fd, VHOST_RESET_OWNER))
+ return KSFT_FAIL;
+ close(fd);
+ check_exec(ram, size, 1, false);
+ return KSFT_FAIL;
+#else
+ return KSFT_SKIP;
+#endif
+}
+
struct more_case {
const char *name;
int (*fn)(void);
@@ -1920,6 +2071,8 @@ static const struct more_case more_cases[] = {
{ "half of a THP: preserved", thp_half_exec },
{ "vfork() child: preserving exec refused", vfork_exec },
{ "exec permission back in a fork child and the new image", mayexec_exec },
+ { "/proc/pid/mem write stalled across exec: lands in the old mm", old_mm_write_exec },
+ { "vhost owner in kthread mode: preserving exec refused", vhost_kthread_exec },
};
int main(int argc, char **argv)
@@ -1949,6 +2102,9 @@ int main(int argc, char **argv)
if (argc == 4 && !strcmp(argv[1], "--pfnmap"))
return after_pfnmap_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
strtoull(argv[3], NULL, 10));
+ if (argc == 6 && !strcmp(argv[1], "--old-mm"))
+ return after_old_mm((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
+ strtoull(argv[3], NULL, 10), atoi(argv[4]), atoi(argv[5]));
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]));
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [Devel] [PATCH hci-8.1 7/7] selftests/mm: say why a doexec case fails
2026-10-14 17:14 [Devel] [PATCH hci-8.1 0/7] selftests/mm: broader MADV_DOEXEC coverage Konstantin Khorenko
` (5 preceding siblings ...)
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 6/7] selftests/mm: check who can still reach the old mm of a preserving exec Konstantin Khorenko
@ 2026-10-14 17:15 ` Konstantin Khorenko
6 siblings, 0 replies; 8+ messages in thread
From: Konstantin Khorenko @ 2026-10-14 17:15 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
Many failure paths of doexec return KSFT_FAIL without a word, so a
failing case shows nothing but "not ok". Print the call that failed
with its errno, or what the check found, before failing.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 223 ++++++++++++++++++++++--------------
1 file changed, 136 insertions(+), 87 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 1b33bc489eb71..f74471abe13e2 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -103,6 +103,8 @@ static int wait_child(pid_t pid)
do {
ret = waitpid(pid, &status, 0);
} while (ret < 0 && errno == EINTR);
+ if (ret >= 0 && WIFSIGNALED(status))
+ ksft_print_msg("child killed by signal %d\n", WTERMSIG(status));
if (ret < 0 || !WIFEXITED(status))
return KSFT_FAIL;
return WEXITSTATUS(status);
@@ -171,6 +173,13 @@ static int fail(const char *what)
return KSFT_FAIL;
}
+static int fail_errno(const char *what)
+{
+ ksft_print_msg("%s: %s\n", what, strerror(errno));
+ fflush(stdout);
+ return KSFT_FAIL;
+}
+
static int check_contents(char *ram, size_t size, unsigned char value)
{
return check_bytes(ram, size, value) ? fail("contents lost") : KSFT_PASS;
@@ -179,14 +188,14 @@ static int check_contents(char *ram, size_t size, unsigned char value)
static int after_cow_exec(char *ram, size_t size, pid_t sibling, int pipe_fd)
{
char ready = 1;
- int ret = check_bytes(ram, size, 0x31);
+ int ret = check_bytes(ram, size, 0x31) ? fail("contents lost") : KSFT_PASS;
memset(ram, 0x42, size);
if (write(pipe_fd, &ready, 1) != 1)
- ret = KSFT_FAIL;
+ ret = fail("lost the COW sibling");
close(pipe_fd);
if (wait_child(sibling))
- ret = KSFT_FAIL;
+ ret = fail("COW sibling saw the new image's writes");
return ret;
}
@@ -202,20 +211,22 @@ static int before_cow_exec(void)
ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ram == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
memset(ram, 0x31, size);
if (madvise(ram, size, MADV_DOEXEC))
return unsupported();
if (pipe(pipe_fd))
- return KSFT_FAIL;
+ return fail_errno("pipe");
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
close(pipe_fd[1]);
if (read(pipe_fd[0], &ready, 1) != 1)
_exit(KSFT_FAIL);
- _exit(check_bytes(ram, size, 0x31));
+ if (check_bytes(ram, size, 0x31))
+ _exit(fail("COW sibling lost its contents"));
+ _exit(KSFT_PASS);
}
close(pipe_fd[0]);
snprintf(addr_arg, sizeof(addr_arg), "%llu",
@@ -225,6 +236,7 @@ static int before_cow_exec(void)
snprintf(fd_arg, sizeof(fd_arg), "%d", pipe_fd[1]);
execl("/proc/self/exe", "doexec", "--cow", addr_arg, size_arg,
pid_arg, fd_arg, NULL);
+ fail_errno("exec");
close(pipe_fd[1]);
wait_child(pid);
return KSFT_FAIL;
@@ -242,8 +254,7 @@ static void exec_stage(int fd, char *ram, size_t size, int kind, int stage)
snprintf(stage_arg, sizeof(stage_arg), "%d", stage);
execl("/proc/self/exe", "doexec", "--exec", fd_arg, addr_arg, size_arg,
kind_arg, stage_arg, NULL);
- ksft_print_msg("exec: %s\n", strerror(errno));
- _exit(KSFT_FAIL);
+ _exit(fail_errno("exec"));
}
static int after_exec(int fd, char *ram, size_t size, int kind, int stage)
@@ -255,33 +266,35 @@ static int after_exec(int fd, char *ram, size_t size, int kind, int stage)
if (check_pin(fd, ram, size, before))
return KSFT_FAIL;
if (kind == PROTECTED && mprotect(ram, size, PROT_READ | PROT_WRITE))
- return KSFT_FAIL;
+ return fail_errno("mprotect");
memset(ram, after, size);
if (check_pin(fd, ram, size, after))
return KSFT_FAIL;
if (stage == 1) {
/* VM_EXEC_KEEP is consumed, so mark again for the second exec. */
if (madvise(ram, size, MADV_DOEXEC))
- return KSFT_FAIL;
+ return fail_errno("MADV_DOEXEC");
if (kind == PROTECTED && mprotect(ram, size, PROT_READ))
- return KSFT_FAIL;
+ return fail_errno("mprotect");
exec_stage(fd, ram, size, kind, 2);
}
/* Fork must copy into the child and retain the parent's pinned pages. */
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
memset(ram, 0x64, size);
_exit(KSFT_PASS);
}
- if (wait_child(pid) || check_pin(fd, ram, size, after))
+ if (wait_child(pid))
+ return fail("fork child of the new image failed");
+ if (check_pin(fd, ram, size, after))
return KSFT_FAIL;
memset(ram, 0x75, size);
if (check_pin(fd, ram, size, 0x75))
return KSFT_FAIL;
- return ioctl(fd, PIN_LONGTERM_TEST_STOP) ? KSFT_FAIL : KSFT_PASS;
+ return ioctl(fd, PIN_LONGTERM_TEST_STOP) ? fail_errno("PIN_LONGTERM_TEST_STOP") : KSFT_PASS;
}
static size_t huge_page_size(void)
@@ -316,7 +329,7 @@ static int before_exec(int kind)
mapping = mmap(NULL, 2 * size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapping == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
aligned = ((uintptr_t)mapping + size - 1) & ~(size - 1);
ram = (char *)aligned;
if (ram != mapping)
@@ -325,7 +338,7 @@ static int before_exec(int kind)
munmap(ram + size, mapping + 2 * size - (ram + size));
if (madvise(ram, size, kind == HUGE ? MADV_HUGEPAGE : MADV_NOHUGEPAGE) &&
(kind == HUGE || errno != EINVAL))
- return KSFT_FAIL;
+ return fail_errno("THP madvise");
memset(ram, 0x31, size);
if (kind == HUGE && madvise(ram, size, MADV_COLLAPSE))
return KSFT_SKIP;
@@ -345,7 +358,7 @@ static int before_exec(int kind)
return errno == ENOTTY ? KSFT_SKIP : KSFT_FAIL;
}
if (kind == PROTECTED && mprotect(ram, size, PROT_READ))
- return KSFT_FAIL;
+ return fail_errno("mprotect");
exec_stage(fd, ram, size, kind, 1);
return KSFT_FAIL;
}
@@ -366,7 +379,13 @@ static int exec_path(const char *exe, char *ram, size_t size, size_t stride,
static int check_exec(char *ram, size_t size, size_t stride, bool reject)
{
- return exec_path("/proc/self/exe", ram, size, stride, reject);
+ int err = exec_path("/proc/self/exe", ram, size, stride, reject);
+
+ if (!reject) {
+ errno = err;
+ fail_errno("exec");
+ }
+ return err;
}
static char *preserved_ram(size_t size)
@@ -417,10 +436,10 @@ static int shared_mm_exec(bool own_thread)
if (!ram)
return unsupported();
if (pipe(pipe_fd))
- return KSFT_FAIL;
+ return fail_errno("pipe");
if (own_thread) {
if (pthread_create(&thread, NULL, blocked_thread, &pipe_fd[0]))
- return KSFT_FAIL;
+ return fail("pthread_create failed");
check_exec(ram, size, 1, false);
write(pipe_fd[1], "x", 1);
pthread_join(thread, NULL);
@@ -429,16 +448,18 @@ static int shared_mm_exec(bool own_thread)
stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
if (stack == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
pid = clone(blocked_clone, stack + 65536, CLONE_VM | SIGCHLD, pipe_fd);
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("clone");
ret = check_exec(ram, size, 1, true);
/* Exec must return EBUSY with the old mapping intact. */
- if (ret != EBUSY || check_bytes(ram, size, 0x31))
- ret = KSFT_FAIL;
- else
- ret = KSFT_PASS;
+ if (ret != EBUSY) {
+ errno = ret;
+ ret = fail_errno("exec with a CLONE_VM task not refused with EBUSY");
+ } else {
+ ret = check_bytes(ram, size, 0x31) ? fail("contents lost") : KSFT_PASS;
+ }
write(pipe_fd[1], "x", 1);
if (wait_child(pid))
ret = KSFT_FAIL;
@@ -464,7 +485,7 @@ static int vhost_owner_exec(void)
if (ioctl(fd, VHOST_SET_OWNER))
return KSFT_SKIP;
if (ioctl(fd, VHOST_RESET_OWNER))
- return KSFT_FAIL;
+ return fail_errno("VHOST_RESET_OWNER");
close(fd);
check_exec(ram, size, 1, false);
return KSFT_FAIL;
@@ -520,7 +541,7 @@ static int unprivileged_exec(const char *path, bool preserve)
int err;
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (pid)
return wait_child(pid);
if (setgroups(0, NULL) || setresgid(65534, 65534, 65534) ||
@@ -535,12 +556,16 @@ static int unprivileged_exec(const char *path, bool preserve)
_exit(unsupported());
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 (err != EPERM) {
+ errno = err;
+ _exit(fail_errno("setuid exec not refused with EPERM"));
+ }
+ if (check_bytes(ram, size, 0x31))
+ _exit(fail("contents lost by a refused setuid exec"));
if (madvise(ram, size, MADV_DONTEXEC))
- _exit(KSFT_FAIL);
+ _exit(fail_errno("MADV_DONTEXEC"));
execl(path, "doexec", "--euid", NULL);
- _exit(KSFT_FAIL);
+ _exit(fail_errno("exec of the setuid copy"));
}
/* The ELF opt-in must not pull memory into a more privileged image. */
@@ -575,7 +600,7 @@ static int devzero_exec(void)
ram = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
close(fd);
if (ram == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
memset(ram, 0x31, size);
if (madvise(ram, size, MADV_DOEXEC))
return unsupported();
@@ -655,6 +680,8 @@ static int noexec_dontexec(void)
}
if (!madvise(ram, size, MADV_DONTEXEC) && exec_refused(ram, size))
ret = KSFT_PASS;
+ else
+ ret = fail("MADV_DONTEXEC made a noexec mapping executable");
out:
umount2(dir, MNT_DETACH);
rmdir(dir);
@@ -670,9 +697,13 @@ static int doexec_noexec(void)
if (!ram)
return unsupported();
- if (!exec_refused(ram, size) || madvise(ram, size, MADV_DONTEXEC) ||
- mprotect(ram, size, PROT_READ | PROT_EXEC))
- return KSFT_FAIL;
+ if (!exec_refused(ram, size))
+ return fail("marked range can be made executable");
+ if (madvise(ram, size, MADV_DONTEXEC) || !smaps_has_flag(ram, " me "))
+ return fail("MADV_DONTEXEC did not give VM_MAYEXEC back");
+ /* SELinux execmem or MDWE may still refuse it. */
+ if (mprotect(ram, size, PROT_READ | PROT_EXEC))
+ return KSFT_SKIP;
ret = noexec_dontexec();
if (ret == KSFT_SKIP)
ksft_print_msg("noexec mount part skipped\n");
@@ -684,11 +715,11 @@ static int after_pfnmap_exec(char *bar, size_t size)
size_t page = sysconf(_SC_PAGESIZE), i;
if (!smaps_has_flag(bar, " pf ") || !smaps_has_flag(bar, " sh "))
- return KSFT_FAIL;
+ return fail("BAR not a shared PFN map after exec");
for (i = 0; i < size / page; i++)
if (__atomic_load_n((uint32_t *)(bar + i * page), __ATOMIC_RELAXED) !=
(0xd0e00000 | i))
- return KSFT_FAIL;
+ return fail("BAR contents changed across exec");
return munmap(bar, size) ? KSFT_FAIL : KSFT_PASS;
}
@@ -709,7 +740,9 @@ static int devmem_refused(void)
close(fd);
if (ram == MAP_FAILED || !smaps_has_flag(ram, " pf "))
return KSFT_SKIP;
- return madvise(ram, size, MADV_DOEXEC) && errno == EINVAL ? KSFT_PASS : KSFT_FAIL;
+ if (madvise(ram, size, MADV_DOEXEC) && errno == EINVAL)
+ return KSFT_PASS;
+ return fail("MADV_DOEXEC on a shared /dev/mem mapping not refused");
}
static int vfio_device_fd(const char *bdf)
@@ -764,12 +797,12 @@ static int vfio_bar_exec(void)
/* Align to the size so that the BAR can be mapped with huge PFN entries. */
area = mmap(NULL, 2 * size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (area == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
aligned = ((uintptr_t)area + size - 1) & ~(size - 1);
bar = mmap((void *)aligned, size, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_FIXED, dev, info.offset);
if (bar == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
for (i = 0; i < size / page; i++)
__atomic_store_n((uint32_t *)(bar + i * page), 0xd0e00000 | i,
__ATOMIC_RELAXED);
@@ -781,7 +814,7 @@ static int vfio_bar_exec(void)
(unsigned long long)(uintptr_t)bar);
snprintf(size_arg, sizeof(size_arg), "%zu", size);
execl("/proc/self/exe", "doexec", "--pfnmap", addr_arg, size_arg, NULL);
- return KSFT_FAIL;
+ return fail_errno("exec");
}
static void *exec_thread(void *arg)
@@ -798,7 +831,7 @@ static int nonleader_exec(void)
if (!ram)
return unsupported();
if (pthread_create(&thread, NULL, exec_thread, ram))
- return KSFT_FAIL;
+ return fail("pthread_create failed");
pthread_join(thread, NULL);
return KSFT_FAIL;
}
@@ -808,18 +841,18 @@ static int proc_exec(char *ram, size_t size, int stage)
char addr_arg[32], size_arg[32], stage_arg[32];
if (check_bytes(ram, size, 0x31))
- return KSFT_FAIL;
+ return fail("contents lost in a repeated exec");
if (stage == 32)
return KSFT_PASS;
if (madvise(ram, size, MADV_DOEXEC))
- return KSFT_FAIL;
+ return fail_errno("MADV_DOEXEC");
snprintf(addr_arg, sizeof(addr_arg), "%llu",
(unsigned long long)(uintptr_t)ram);
snprintf(size_arg, sizeof(size_arg), "%zu", size);
snprintf(stage_arg, sizeof(stage_arg), "%d", stage + 1);
execl("/proc/self/exe", "doexec", "--proc", addr_arg, size_arg,
stage_arg, NULL);
- return KSFT_FAIL;
+ return fail_errno("exec");
}
/* Read both pre-opened and freshly opened proc files during repeated exec. */
@@ -833,10 +866,10 @@ static int proc_readers_exec(void)
int i;
if (pipe(ready) || pipe(go))
- return KSFT_FAIL;
+ return fail_errno("pipe");
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
size_t size = 16 * sysconf(_SC_PAGESIZE);
char *ram = preserved_ram(size);
@@ -874,7 +907,8 @@ static int proc_readers_exec(void)
for (;;) {
waited = waitpid(pid, &status, WNOHANG);
if (waited == pid) {
- ret = WIFEXITED(status) ? WEXITSTATUS(status) : KSFT_FAIL;
+ ret = WIFEXITED(status) ? WEXITSTATUS(status) :
+ fail("repeated exec killed by a signal");
break;
}
if (waited < 0 && errno != EINTR)
@@ -968,13 +1002,14 @@ static int clone_exec_race(void)
pid_t pid;
if (pipe(report))
- return KSFT_FAIL;
+ return fail_errno("pipe");
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
size_t size = sysconf(_SC_PAGESIZE);
struct clone_race race = { .report = report[1] };
+ const char *why = "exec refused with EBUSY every time";
pthread_t thread;
unsigned int attempt;
@@ -983,25 +1018,31 @@ static int clone_exec_race(void)
if (!race.ram)
_exit(unsupported());
if (pthread_create(&thread, NULL, clone_race_thread, &race))
- _exit(KSFT_FAIL);
+ _exit(fail("pthread_create failed"));
while (atomic_load(&race.rounds) < 4)
usleep(1000);
for (attempt = 0; attempt < 500; attempt++) {
- if (check_exec(race.ram, size, 1, false) != EBUSY)
+ errno = exec_path("/proc/self/exe", race.ram, size, 1, false);
+ if (errno != EBUSY) {
+ why = NULL;
+ fail_errno("exec");
break;
- if (check_bytes(race.ram, size, 0x31))
+ }
+ if (check_bytes(race.ram, size, 0x31)) {
+ why = "contents changed while exec was refused";
break;
+ }
usleep(1000);
}
atomic_store(&race.stop, true);
pthread_join(thread, NULL);
- _exit(KSFT_FAIL);
+ _exit(why ? fail(why) : KSFT_FAIL);
}
close(report[1]);
ret = wait_child(pid);
pfd = (struct pollfd) { .fd = report[0], .events = POLLIN | POLLHUP };
if (poll(&pfd, 1, 3000) <= 0 || read(report[0], &failed, 1) != 0)
- ret = KSFT_FAIL;
+ ret = fail("a racing CLONE_VM clone failed");
close(report[0]);
return ret;
}
@@ -1041,7 +1082,7 @@ static int allocation_failures(void)
for (nth = 1; nth <= 256; nth++) {
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
char *ram, number[32];
size_t i;
@@ -1052,7 +1093,7 @@ static int allocation_failures(void)
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ram == MAP_FAILED ||
(madvise(ram, size, MADV_NOHUGEPAGE) && errno != EINVAL))
- _exit(KSFT_FAIL);
+ _exit(fail_errno("mmap or MADV_NOHUGEPAGE"));
for (i = 0; i < size; i += stride)
ram[i] = 0x31;
if (madvise(ram, size, MADV_DOEXEC))
@@ -1062,13 +1103,13 @@ static int allocation_failures(void)
fd = open("/proc/self/fail-nth", O_WRONLY | O_CLOEXEC);
len = snprintf(number, sizeof(number), "%u", nth);
if (exe < 0 || fd < 0 || write(fd, number, len) != len)
- _exit(KSFT_FAIL);
+ _exit(fail_errno("fail-nth setup"));
err = check_exec_fd(exe, ram, size, stride);
/* Disarm before checking mappings or exiting. */
pwrite(fd, "0", 1, 0);
for (i = 0; i < size; i += stride)
if (ram[i] != 0x31)
- _exit(KSFT_FAIL);
+ _exit(fail("contents lost by a failed exec"));
/*
* Failed allocations also surface as E2BIG from
* copy_strings(), and as ELIBBAD or EIO from reading the
@@ -1084,7 +1125,7 @@ static int allocation_failures(void)
waited = waitpid(pid, &status, 0);
} while (waited < 0 && errno == EINTR);
if (waited < 0)
- return KSFT_FAIL;
+ return fail_errno("waitpid");
if (WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV)
fatal++;
else if (WIFEXITED(status) && WEXITSTATUS(status) == 10)
@@ -1141,9 +1182,11 @@ static int sysctl_flag(void)
static bool doexec_refused(char *addr, size_t size, const char *what)
{
if (addr == MAP_FAILED) {
- ksft_print_msg("%s: mmap: %s\n", what, strerror(errno));
/* A security policy may forbid the mapping itself. */
- return errno == EACCES || errno == EPERM;
+ if (errno == EACCES || errno == EPERM)
+ return true;
+ ksft_print_msg("%s: mmap: %s\n", what, strerror(errno));
+ return false;
}
if (madvise(addr, size, MADV_DOEXEC) && errno == EINVAL)
return true;
@@ -1227,7 +1270,7 @@ static int partial_exec(void)
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ram == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
memset(ram, 0x31, 3 * page);
if (madvise(ram + page, page, MADV_DOEXEC))
return unsupported();
@@ -1334,11 +1377,13 @@ static int after_fork_flag(char *ram, size_t size, const char *flag, bool wipe)
return fail("contents lost");
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
- if (wipe)
- _exit(range_mapped(ram, size) ? check_bytes(ram, size, 0) : KSFT_FAIL);
- _exit(range_mapped(ram, size) ? KSFT_FAIL : KSFT_PASS);
+ if (wipe && (!range_mapped(ram, size) || check_bytes(ram, size, 0)))
+ _exit(fail("child of the new image did not get a wiped range"));
+ if (!wipe && range_mapped(ram, size))
+ _exit(fail("child of the new image got a DONTFORK range"));
+ _exit(KSFT_PASS);
}
return wait_child(pid);
}
@@ -1581,7 +1626,7 @@ static int copy_control(void)
int exe;
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
ram = preserved_ram(size);
if (!ram)
@@ -1604,7 +1649,7 @@ static int no_note_exec(void)
err = copy_control();
if (err)
- return err == KSFT_SKIP ? KSFT_SKIP : fail("unpatched copy lost the memory");
+ return err == KSFT_SKIP ? KSFT_SKIP : fail("unpatched copy did not keep the memory");
ram = preserved_ram(size);
if (!ram)
return unsupported();
@@ -1732,7 +1777,7 @@ static int stack_overlap_exec(void)
/* ...within the 128K the stack is grown by, it kills the task. */
status = stack_exec(16);
if (status < 0)
- return KSFT_FAIL;
+ return fail_errno("fork or waitpid");
if (WIFEXITED(status))
return WEXITSTATUS(status) == KSFT_SKIP ? KSFT_SKIP :
fail("exec did not kill the task");
@@ -1757,7 +1802,7 @@ static int sparse_exec(void)
return KSFT_SKIP;
/* EINVAL: no THP support, the range is mapped with base pages anyway. */
if (madvise(ram, size, MADV_NOHUGEPAGE) && errno != EINVAL)
- return KSFT_FAIL;
+ return fail_errno("MADV_NOHUGEPAGE");
for (i = 0; i < size; i += stride)
ram[i] = 0x31;
if (madvise(ram, size, MADV_DOEXEC))
@@ -1790,7 +1835,7 @@ static int swapped_exec(void)
if (ram == MAP_FAILED ||
(madvise(ram, size, MADV_NOHUGEPAGE) && errno != EINVAL))
- return KSFT_FAIL;
+ return fail_errno("mmap or MADV_NOHUGEPAGE");
memset(ram, 0x31, size);
if (madvise(ram, size, MADV_PAGEOUT))
return KSFT_SKIP;
@@ -1824,7 +1869,7 @@ static int thp_half_exec(void)
area = mmap(NULL, 2 * size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (area == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
ram = (char *)(((uintptr_t)area + size - 1) & ~(size - 1));
if (madvise(ram, size, MADV_HUGEPAGE))
return KSFT_SKIP;
@@ -1873,7 +1918,7 @@ static int after_old_mm(char *ram, size_t size, int done, int check)
char c;
if (write(done, "x", 1) != 1 || read(check, &c, 1) != 1)
- return KSFT_FAIL;
+ return fail("lost the parent of the old mm write case");
if (check_bytes(ram, size, 0x31))
return fail("a write through the old mm reached the new image");
return KSFT_PASS;
@@ -1920,11 +1965,11 @@ static int old_mm_write_exec(void)
reg.range.len = size;
if (w.src == MAP_FAILED || fill == MAP_FAILED || ioctl(uffd, UFFDIO_API, &api) ||
ioctl(uffd, UFFDIO_REGISTER, ®) || pipe(to_child) || pipe(to_parent))
- return KSFT_FAIL;
+ return fail_errno("userfaultfd setup");
memset(fill, 0x77, size);
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
char *ram = preserved_ram(size), args[4][32];
@@ -1942,7 +1987,7 @@ static int old_mm_write_exec(void)
snprintf(args[3], sizeof(args[3]), "%d", to_child[0]);
execl("/proc/self/exe", "doexec", "--old-mm", args[0], args[1], args[2],
args[3], NULL);
- _exit(KSFT_FAIL);
+ _exit(fail_errno("exec"));
}
close(to_child[0]);
close(to_parent[1]);
@@ -1957,8 +2002,10 @@ static int old_mm_write_exec(void)
ret = KSFT_SKIP;
goto out;
}
- if (pthread_create(&thread, NULL, mem_writer, &w))
+ if (pthread_create(&thread, NULL, mem_writer, &w)) {
+ ret = fail("pthread_create failed");
goto out;
+ }
/* The writer holds the child's mm while it faults on its source. */
pfd.fd = uffd;
pfd.events = POLLIN;
@@ -2009,7 +2056,7 @@ static int vhost_kthread_exec(void)
if (err != EBUSY || check_bytes(ram, size, 0x31))
return fail("preserving exec with a vhost kthread not refused");
if (ioctl(fd, VHOST_RESET_OWNER))
- return KSFT_FAIL;
+ return fail_errno("VHOST_RESET_OWNER");
close(fd);
check_exec(ram, size, 1, false);
return KSFT_FAIL;
@@ -2044,10 +2091,10 @@ static int vfork_exec(void)
_exit(errno == EBUSY ? REFUSED : KSFT_FAIL);
}
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (wait_child(pid) != REFUSED)
return fail("preserving exec from a vfork() child not refused");
- return check_bytes(ram, size, 0x31) ? KSFT_FAIL : KSFT_PASS;
+ return check_bytes(ram, size, 0x31) ? fail("contents lost by a refused exec") : KSFT_PASS;
}
static const struct more_case more_cases[] = {
@@ -2083,7 +2130,7 @@ int main(int argc, char **argv)
/* 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;
+ KSFT_PASS : fail("setuid copy started in another mode");
if (argc == 7 && !strcmp(argv[1], "--exec"))
return after_exec(atoi(argv[2]),
(char *)(uintptr_t)strtoull(argv[3], NULL, 10),
@@ -2092,7 +2139,7 @@ int main(int argc, char **argv)
return after_cow_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
strtoull(argv[3], NULL, 10), atoi(argv[4]), atoi(argv[5]));
if (argc == 5 && !strcmp(argv[1], "--reject"))
- return KSFT_FAIL;
+ return fail("exec went through instead of failing");
for (kind = 0; argc == 4 && kind < (int)ARRAY_SIZE(after_modes); kind++)
if (!strcmp(argv[1], after_modes[kind].mode))
return after_modes[kind].fn((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
@@ -2121,8 +2168,10 @@ int main(int argc, char **argv)
if (!range_mapped(ram, size))
return fail("range not preserved");
for (i = 0; i < size; i += stride)
- if (ram[i] != 0x31)
- return KSFT_FAIL;
+ if (ram[i] != 0x31) {
+ ksft_print_msg("byte %zu: %#x\n", i, (unsigned char)ram[i]);
+ return fail("preserved contents changed");
+ }
return KSFT_PASS;
}
ksft_print_header();
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-10-14 17:16 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-14 17:14 [Devel] [PATCH hci-8.1 0/7] selftests/mm: broader MADV_DOEXEC coverage Konstantin Khorenko
2026-10-14 17:14 ` [Devel] [PATCH hci-8.1 1/7] selftests/mm: cover the MADV_DOEXEC interface and VMA attributes Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 2/7] selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 3/7] selftests/mm: cover large, swapped and partly marked THP ranges Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 4/7] selftests/mm: check that MADV_DOEXEC refuses an exec from a vfork() child Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 5/7] selftests/mm: check VM_MAYEXEC of MADV_DOEXEC ranges in smaps Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 6/7] selftests/mm: check who can still reach the old mm of a preserving exec Konstantin Khorenko
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 7/7] selftests/mm: say why a doexec case fails Konstantin Khorenko
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.