All Virtuozzo development lists (kernel + QEMU)
 help / color / mirror / Atom feed
From: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
To: Konstantin Khorenko <khorenko@virtuozzo.com>
Cc: devel@openvz.org
Subject: [Devel] [PATCH RFC PATCH hci-8.1 4/4] selftests/mm: test memory preservation across exec
Date: Fri, 09 Oct 2026 17:00:07 +0300	[thread overview]
Message-ID: <20261009-vfio_v1-v1-4-1901cd6c3c74@virtuozzo.com> (raw)
In-Reply-To: <20261009-vfio_v1-v1-0-1901cd6c3c74@virtuozzo.com>

Add an ELF-opted-in MADV_DOEXEC test and wire it into the mm test runner.
Compare CPU writes against retained long-term GUP pins across repeated
exec and a later fork, including base pages, THP, mlock, protected mappings
and fast GUP. Check existing COW mappings separately.

Exercise recoverable rejection and retry with an external CLONE_VM task,
own and non-leader threads, concurrent shared-mm clone creation, proc
inspection during repeated exec, and vhost owner reset before exec. Add
an optional task-local allocation-failure sweep.

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

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 tools/testing/selftests/mm/Makefile       |   1 +
 tools/testing/selftests/mm/doexec.c       | 774 ++++++++++++++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh |   1 +
 3 files changed, 776 insertions(+)

diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index c837b0549c89..7f9efb0e0a38 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -74,6 +74,7 @@ endif
 
 TEST_GEN_FILES = cow
 TEST_GEN_FILES += compaction_test
+TEST_GEN_FILES += doexec
 TEST_GEN_FILES += gup_longterm
 TEST_GEN_FILES += gup_test
 TEST_GEN_FILES += hmm-tests
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
new file mode 100644
index 000000000000..6ec09e868679
--- /dev/null
+++ b/tools/testing/selftests/mm/doexec.c
@@ -0,0 +1,774 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Verify that exec preserves the memory backing long-term DMA pins. */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/vhost.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/resource.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../../../../mm/gup_test.h"
+#include "../kselftest.h"
+
+#ifndef MADV_DOEXEC
+#define MADV_DOEXEC 201
+#endif
+#ifndef MADV_COLLAPSE
+#define MADV_COLLAPSE 25
+#endif
+
+/* The downstream ELF loader requires this opt-in in a PT_NOTE segment. */
+asm(".pushsection .note.doexec,\"a\",@note\n"
+	".balign 4\n"
+	".long 17, 0, 0x07c1feed\n"
+	".asciz \"preserved-mem-ok\"\n"
+	".balign 4\n"
+	".popsection\n");
+
+enum test_kind {
+	BASE,
+	HUGE,
+	LOCKED,
+	PROTECTED,
+	READ_PIN,
+	FAST_PIN,
+	NR_TESTS,
+};
+
+static const char * const names[] = {
+	"base pages",
+	"PMD huge pages",
+	"mlocked pages",
+	"read-only mapping",
+	"read-only pins",
+	"fast GUP pins",
+};
+
+static int wait_child(pid_t pid)
+{
+	int status;
+	pid_t ret;
+
+	do {
+		ret = waitpid(pid, &status, 0);
+	} while (ret < 0 && errno == EINTR);
+	if (ret < 0 || !WIFEXITED(status))
+		return KSFT_FAIL;
+	return WEXITSTATUS(status);
+}
+
+static int check_pin(int fd, char *ram, size_t size, unsigned char value)
+{
+	char *copy = malloc(size);
+	uint64_t addr = (uintptr_t)copy;
+	size_t i;
+	int ret = KSFT_FAIL;
+
+	if (!copy)
+		return ret;
+	if (ioctl(fd, PIN_LONGTERM_TEST_READ, &addr)) {
+		ksft_print_msg("read pin: %s\n", strerror(errno));
+		goto out;
+	}
+	for (i = 0; i < size; i++) {
+		if ((unsigned char)ram[i] != value ||
+		    (unsigned char)copy[i] != value) {
+			ksft_print_msg("CPU/pin mismatch at byte %zu\n", i);
+			goto out;
+		}
+	}
+	ret = KSFT_PASS;
+out:
+	free(copy);
+	return ret;
+}
+
+static int check_bytes(char *ram, size_t size, unsigned char value)
+{
+	size_t i;
+
+	for (i = 0; i < size; i++)
+		if ((unsigned char)ram[i] != value)
+			return KSFT_FAIL;
+	return KSFT_PASS;
+}
+
+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);
+
+	memset(ram, 0x42, size);
+	if (write(pipe_fd, &ready, 1) != 1)
+		ret = KSFT_FAIL;
+	close(pipe_fd);
+	if (wait_child(sibling))
+		ret = KSFT_FAIL;
+	return ret;
+}
+
+/* Pages shared with a separate mm must retain their pre-existing COW state. */
+static int before_cow_exec(void)
+{
+	size_t size = 16 * sysconf(_SC_PAGESIZE);
+	char addr_arg[32], size_arg[32], pid_arg[32], fd_arg[32], ready;
+	int pipe_fd[2];
+	char *ram;
+	pid_t pid;
+
+	ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+		   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	if (ram == MAP_FAILED)
+		return KSFT_FAIL;
+	memset(ram, 0x31, size);
+	if (madvise(ram, size, MADV_DOEXEC))
+		return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+	if (pipe(pipe_fd))
+		return KSFT_FAIL;
+	pid = fork();
+	if (pid < 0)
+		return KSFT_FAIL;
+	if (!pid) {
+		close(pipe_fd[1]);
+		if (read(pipe_fd[0], &ready, 1) != 1)
+			_exit(KSFT_FAIL);
+		_exit(check_bytes(ram, size, 0x31));
+	}
+	close(pipe_fd[0]);
+	snprintf(addr_arg, sizeof(addr_arg), "%llu",
+		 (unsigned long long)(uintptr_t)ram);
+	snprintf(size_arg, sizeof(size_arg), "%zu", size);
+	snprintf(pid_arg, sizeof(pid_arg), "%d", pid);
+	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);
+	close(pipe_fd[1]);
+	wait_child(pid);
+	return KSFT_FAIL;
+}
+
+static void exec_stage(int fd, char *ram, size_t size, int kind, int stage)
+{
+	char fd_arg[32], addr_arg[32], size_arg[32], kind_arg[32], stage_arg[32];
+
+	snprintf(fd_arg, sizeof(fd_arg), "%d", fd);
+	snprintf(addr_arg, sizeof(addr_arg), "%llu",
+		 (unsigned long long)(uintptr_t)ram);
+	snprintf(size_arg, sizeof(size_arg), "%zu", size);
+	snprintf(kind_arg, sizeof(kind_arg), "%d", kind);
+	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);
+}
+
+static int after_exec(int fd, char *ram, size_t size, int kind, int stage)
+{
+	unsigned char before = stage == 1 ? 0x31 : 0x42;
+	unsigned char after = stage == 1 ? 0x42 : 0x53;
+	pid_t pid;
+
+	if (check_pin(fd, ram, size, before))
+		return KSFT_FAIL;
+	if (kind == PROTECTED && mprotect(ram, size, PROT_READ | PROT_WRITE))
+		return KSFT_FAIL;
+	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;
+		if (kind == PROTECTED && mprotect(ram, size, PROT_READ))
+			return KSFT_FAIL;
+		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;
+	if (!pid) {
+		memset(ram, 0x64, size);
+		_exit(KSFT_PASS);
+	}
+	if (wait_child(pid) || 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;
+}
+
+static size_t huge_page_size(void)
+{
+	FILE *file = fopen("/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", "r");
+	size_t size = 0;
+
+	if (file) {
+		if (fscanf(file, "%zu", &size) != 1)
+			size = 0;
+		fclose(file);
+	}
+	return size;
+}
+
+static int before_exec(int kind)
+{
+	size_t size = 16 * sysconf(_SC_PAGESIZE);
+	struct pin_longterm_test pin;
+	char *ram, *mapping;
+	uintptr_t aligned;
+	int fd;
+
+	if (kind == HUGE) {
+		size = huge_page_size();
+		if (!size)
+			return KSFT_SKIP;
+	}
+	fd = open("/sys/kernel/debug/gup_test", O_RDWR);
+	if (fd < 0)
+		return KSFT_SKIP;
+	mapping = mmap(NULL, 2 * size, PROT_READ | PROT_WRITE,
+		       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	if (mapping == MAP_FAILED)
+		return KSFT_FAIL;
+	aligned = ((uintptr_t)mapping + size - 1) & ~(size - 1);
+	ram = (char *)aligned;
+	if (ram != mapping)
+		munmap(mapping, ram - mapping);
+	if (ram + size != mapping + 2 * size)
+		munmap(ram + size, mapping + 2 * size - (ram + size));
+	if (madvise(ram, size, kind == HUGE ? MADV_HUGEPAGE : MADV_NOHUGEPAGE))
+		return KSFT_FAIL;
+	memset(ram, 0x31, size);
+	if (kind == HUGE && madvise(ram, size, MADV_COLLAPSE))
+		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;
+	}
+	pin = (struct pin_longterm_test) {
+		.addr = (uintptr_t)ram,
+		.size = size,
+		.flags = kind == READ_PIN ? 0 : PIN_LONGTERM_TEST_FLAG_USE_WRITE,
+	};
+	if (kind == FAST_PIN)
+		pin.flags |= PIN_LONGTERM_TEST_FLAG_USE_FAST;
+	if (ioctl(fd, PIN_LONGTERM_TEST_START, &pin)) {
+		ksft_print_msg("pin: %s\n", strerror(errno));
+		return errno == ENOTTY ? KSFT_SKIP : KSFT_FAIL;
+	}
+	if (kind == PROTECTED && mprotect(ram, size, PROT_READ))
+		return KSFT_FAIL;
+	exec_stage(fd, ram, size, kind, 1);
+	return KSFT_FAIL;
+}
+
+static int check_exec(char *ram, size_t size, size_t stride, bool reject)
+{
+	char addr_arg[32], size_arg[32], stride_arg[32];
+
+	snprintf(addr_arg, sizeof(addr_arg), "%llu",
+		 (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,
+	      stride_arg, NULL);
+	return errno;
+}
+
+static char *preserved_ram(size_t size)
+{
+	char *ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+			 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+	if (ram == MAP_FAILED)
+		return NULL;
+	memset(ram, 0x31, size);
+	if (madvise(ram, size, MADV_DOEXEC)) {
+		munmap(ram, size);
+		return NULL;
+	}
+	return ram;
+}
+
+static int blocked_task(void *arg)
+{
+	int fd = *(int *)arg;
+	char ready;
+
+	return read(fd, &ready, 1) == 1 ? KSFT_PASS : KSFT_FAIL;
+}
+
+static void *blocked_thread(void *arg)
+{
+	blocked_task(arg);
+	return NULL;
+}
+
+/* Own threads may be killed by exec; a separate CLONE_VM task may not. */
+static int shared_mm_exec(bool own_thread)
+{
+	size_t size = sysconf(_SC_PAGESIZE);
+	char *ram = preserved_ram(size), *stack;
+	int pipe_fd[2], ret;
+	pthread_t thread;
+	pid_t pid;
+
+	if (!ram)
+		return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+	if (pipe(pipe_fd))
+		return KSFT_FAIL;
+	if (own_thread) {
+		if (pthread_create(&thread, NULL, blocked_thread, &pipe_fd[0]))
+			return KSFT_FAIL;
+		check_exec(ram, size, 1, false);
+		write(pipe_fd[1], "x", 1);
+		pthread_join(thread, NULL);
+		return KSFT_FAIL;
+	}
+	stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE,
+		     MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
+	if (stack == MAP_FAILED)
+		return KSFT_FAIL;
+	pid = clone(blocked_task, stack + 65536, CLONE_VM | SIGCHLD, &pipe_fd[0]);
+	if (pid < 0)
+		return KSFT_FAIL;
+	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;
+	write(pipe_fd[1], "x", 1);
+	if (wait_child(pid))
+		ret = KSFT_FAIL;
+	if (ret)
+		return ret;
+	/* Releasing the outside user must allow a retry. */
+	check_exec(ram, size, 1, false);
+	return KSFT_FAIL;
+}
+
+/* The caller must release its backend owner before preserving exec. */
+static int vhost_owner_exec(void)
+{
+	size_t size = sysconf(_SC_PAGESIZE);
+	char *ram;
+	int fd = open("/dev/vhost-net", O_RDWR);
+
+	if (fd < 0)
+		return KSFT_SKIP;
+	ram = preserved_ram(size);
+	if (!ram)
+		return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+	if (ioctl(fd, VHOST_SET_OWNER))
+		return KSFT_SKIP;
+	if (ioctl(fd, VHOST_RESET_OWNER))
+		return KSFT_FAIL;
+	close(fd);
+	check_exec(ram, size, 1, false);
+	return KSFT_FAIL;
+}
+
+static void *exec_thread(void *arg)
+{
+	check_exec(arg, sysconf(_SC_PAGESIZE), 1, false);
+	return NULL;
+}
+
+static int nonleader_exec(void)
+{
+	char *ram = preserved_ram(sysconf(_SC_PAGESIZE));
+	pthread_t thread;
+
+	if (!ram)
+		return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+	if (pthread_create(&thread, NULL, exec_thread, ram))
+		return KSFT_FAIL;
+	pthread_join(thread, NULL);
+	return KSFT_FAIL;
+}
+
+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;
+	if (stage == 32)
+		return KSFT_PASS;
+	if (madvise(ram, size, MADV_DOEXEC))
+		return KSFT_FAIL;
+	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;
+}
+
+/* Read both pre-opened and freshly opened proc files during repeated exec. */
+static int proc_readers_exec(void)
+{
+	static const char * const files[] = { "maps", "smaps_rollup", "pagemap" };
+	int ready[2], go[2], fds[3] = { -1, -1, -1 }, status, ret = KSFT_FAIL;
+	char paths[3][80], buffer[4096];
+	unsigned int round = 0;
+	pid_t pid, waited;
+	int i;
+
+	if (pipe(ready) || pipe(go))
+		return KSFT_FAIL;
+	pid = fork();
+	if (pid < 0)
+		return KSFT_FAIL;
+	if (!pid) {
+		size_t size = 16 * sysconf(_SC_PAGESIZE);
+		char *ram = preserved_ram(size);
+
+		close(ready[0]);
+		close(go[1]);
+		if (!ram)
+			_exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+		if (write(ready[1], "x", 1) != 1 || read(go[0], buffer, 1) != 1)
+			_exit(KSFT_FAIL);
+		close(ready[1]);
+		close(go[0]);
+		_exit(proc_exec(ram, size, 0));
+	}
+	close(ready[1]);
+	close(go[0]);
+	if (read(ready[0], buffer, 1) != 1) {
+		close(ready[0]);
+		close(go[1]);
+		return wait_child(pid);
+	}
+	close(ready[0]);
+	for (i = 0; i < 3; i++) {
+		snprintf(paths[i], sizeof(paths[i]), "/proc/%d/%s", pid, files[i]);
+		fds[i] = open(paths[i], O_RDONLY);
+		if (fds[i] < 0) {
+			ret = KSFT_SKIP;
+			goto stop;
+		}
+	}
+	if (write(go[1], "x", 1) != 1)
+		goto stop;
+	close(go[1]);
+	go[1] = -1;
+	for (;;) {
+		waited = waitpid(pid, &status, WNOHANG);
+		if (waited == pid) {
+			ret = WIFEXITED(status) ? WEXITSTATUS(status) : KSFT_FAIL;
+			break;
+		}
+		if (waited < 0 && errno != EINTR)
+			break;
+		for (i = 0; i < 3; i++) {
+			lseek(fds[i], 0, SEEK_SET);
+			if (read(fds[i], buffer, sizeof(buffer)) < 0 &&
+			    errno != EINTR && errno != ESRCH)
+				goto stop;
+		}
+		/* Keep two old handles while refreshing the third. */
+		i = round++ % 3;
+		close(fds[i]);
+		fds[i] = open(paths[i], O_RDONLY);
+		if (fds[i] < 0) {
+			ret = wait_child(pid);
+			break;
+		}
+	}
+	for (i = 0; i < 3; i++)
+		if (fds[i] >= 0)
+			close(fds[i]);
+	return ret;
+stop:
+	if (go[1] >= 0)
+		close(go[1]);
+	kill(pid, SIGKILL);
+	wait_child(pid);
+	for (i = 0; i < 3; i++)
+		if (fds[i] >= 0)
+			close(fds[i]);
+	return ret;
+}
+
+struct clone_race {
+	char *ram;
+	int report;
+	atomic_bool stop;
+	atomic_uint rounds;
+};
+
+static int clone_race_child(void *arg)
+{
+	struct clone_race *race = arg;
+	unsigned int i;
+
+	for (i = 0; i < 8; i++) {
+		if (__atomic_load_n(race->ram, __ATOMIC_RELAXED) != 0x31) {
+			write(race->report, "x", 1);
+			return KSFT_FAIL;
+		}
+		usleep(1000);
+	}
+	return KSFT_PASS;
+}
+
+static void *clone_race_thread(void *arg)
+{
+	struct clone_race *race = arg;
+	char *stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE,
+			   MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
+	pid_t pid;
+
+	if (stack == MAP_FAILED) {
+		write(race->report, "x", 1);
+		atomic_store(&race->rounds, 4);
+		return NULL;
+	}
+	while (!atomic_load(&race->stop)) {
+		pid = clone(clone_race_child, stack + 65536,
+			    CLONE_VM | SIGCHLD, race);
+		if (pid >= 0) {
+			if (wait_child(pid))
+				write(race->report, "x", 1);
+		} else if (errno != EBUSY && errno != EINTR) {
+			write(race->report, "x", 1);
+		}
+		atomic_fetch_add(&race->rounds, 1);
+		usleep(1000);
+	}
+	munmap(stack, 65536);
+	return NULL;
+}
+
+/* A supervisor collects errors from any shared-mm child surviving exec. */
+static int clone_exec_race(void)
+{
+	int report[2], ret;
+	struct pollfd pfd;
+	char failed;
+	pid_t pid;
+
+	if (pipe(report))
+		return KSFT_FAIL;
+	pid = fork();
+	if (pid < 0)
+		return KSFT_FAIL;
+	if (!pid) {
+		size_t size = sysconf(_SC_PAGESIZE);
+		struct clone_race race = { .report = report[1] };
+		pthread_t thread;
+		unsigned int attempt;
+
+		close(report[0]);
+		race.ram = preserved_ram(size);
+		if (!race.ram)
+			_exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+		if (pthread_create(&thread, NULL, clone_race_thread, &race))
+			_exit(KSFT_FAIL);
+		while (atomic_load(&race.rounds) < 4)
+			usleep(1000);
+		for (attempt = 0; attempt < 500; attempt++) {
+			if (check_exec(race.ram, size, 1, false) != EBUSY)
+				break;
+			if (check_bytes(race.ram, size, 0x31))
+				break;
+			usleep(1000);
+		}
+		atomic_store(&race.stop, true);
+		pthread_join(thread, NULL);
+		_exit(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;
+	close(report[0]);
+	return ret;
+}
+
+/* Opt-in sweep of allocation failures, using only the child's fail-nth. */
+static int allocation_failures(void)
+{
+	const size_t size = 256ul << 20, stride = 2ul << 20;
+	struct rlimit no_core = { 0, 0 };
+	unsigned int early = 0, fatal = 0, success = 0, loader = 0;
+	unsigned int nth;
+	pid_t pid;
+	int status;
+	pid_t waited;
+
+	if (!getenv("DOEXEC_FAULT_INJECTION") || access("/proc/self/fail-nth", W_OK))
+		return KSFT_SKIP;
+	for (nth = 1; nth <= 256; nth++) {
+		pid = fork();
+		if (pid < 0)
+			return KSFT_FAIL;
+		if (!pid) {
+			char *ram, number[32];
+			size_t i;
+			int fd, len, err;
+
+			setrlimit(RLIMIT_CORE, &no_core);
+			ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+				   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+			if (ram == MAP_FAILED || madvise(ram, size, MADV_NOHUGEPAGE))
+				_exit(KSFT_FAIL);
+			for (i = 0; i < size; i += stride)
+				ram[i] = 0x31;
+			if (madvise(ram, size, MADV_DOEXEC))
+				_exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+			fd = open("/proc/self/fail-nth", O_WRONLY | O_CLOEXEC);
+			len = snprintf(number, sizeof(number), "%u", nth);
+			if (fd < 0 || write(fd, number, len) != len)
+				_exit(KSFT_FAIL);
+			err = check_exec(ram, size, stride, false);
+			/* 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(err == ENOMEM || err == EFAULT || err == EAGAIN ? 10 : KSFT_FAIL);
+		}
+		do {
+			waited = waitpid(pid, &status, 0);
+		} while (waited < 0 && errno == EINTR);
+		if (waited < 0)
+			return KSFT_FAIL;
+		if (WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV)
+			fatal++;
+		else if (WIFEXITED(status) && WEXITSTATUS(status) == 10)
+			early++;
+		else if (WIFEXITED(status) && WEXITSTATUS(status) == KSFT_PASS)
+			success++;
+		/* A dynamic loader may exit when a post-exec allocation fails. */
+		else if (WIFEXITED(status) && WEXITSTATUS(status) == 127)
+			loader++;
+		else if (WIFEXITED(status) && WEXITSTATUS(status) == KSFT_SKIP)
+			return KSFT_SKIP;
+		else
+			return KSFT_FAIL;
+	}
+	ksft_print_msg("faults: %u recoverable, %u SIGSEGV, %u success, %u loader exits\n",
+		       early, fatal, success, loader);
+	/* A sweep that injected no recoverable/fatal errors provides no coverage. */
+	return early && fatal ? KSFT_PASS : KSFT_SKIP;
+}
+
+int main(int argc, char **argv)
+{
+	pid_t pid;
+	int kind, ret;
+
+	if (argc == 7 && !strcmp(argv[1], "--exec"))
+		return after_exec(atoi(argv[2]),
+			(char *)(uintptr_t)strtoull(argv[3], NULL, 10),
+			strtoull(argv[4], NULL, 10), atoi(argv[5]), atoi(argv[6]));
+	if (argc == 6 && !strcmp(argv[1], "--cow"))
+		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;
+	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]));
+	if (argc == 5 && !strcmp(argv[1], "--check")) {
+		char *ram = (char *)(uintptr_t)strtoull(argv[2], NULL, 10);
+		size_t size = strtoull(argv[3], NULL, 10);
+		size_t stride = strtoull(argv[4], NULL, 10), i;
+		int fd = open("/proc/self/fail-nth", O_WRONLY);
+
+		if (fd >= 0) {
+			write(fd, "0", 1);
+			close(fd);
+		}
+		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 + 8);
+	for (kind = 0; kind < NR_TESTS; kind++) {
+		fflush(stdout);
+		pid = fork();
+		if (!pid)
+			_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",
+					      names[kind]);
+		else
+			ksft_test_result(ret == KSFT_PASS, "%s: repeated exec and fork\n",
+					 names[kind]);
+	}
+	fflush(stdout);
+	pid = fork();
+	if (!pid)
+		_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");
+	else
+		ksft_test_result(ret == KSFT_PASS, "pre-existing COW preserved\n");
+	for (kind = 0; kind < 7; kind++) {
+		static const char * const extra_names[] = {
+			"CLONE_VM: recoverable EBUSY and retry",
+			"own threads: preserving exec allowed",
+			"vhost owner: reset before preserving exec",
+			"allocation failure sweep (opt-in)",
+			"non-leader thread: preserving exec allowed",
+			"proc readers: repeated preserving exec allowed",
+			"concurrent CLONE_VM: shared memory is not stolen",
+		};
+
+		fflush(stdout);
+		pid = fork();
+		if (!pid) {
+			switch (kind) {
+			case 0:
+			case 1:
+				_exit(shared_mm_exec(kind));
+			case 2:
+				_exit(vhost_owner_exec());
+			case 3:
+				_exit(allocation_failures());
+			case 4:
+				_exit(nonleader_exec());
+			case 5:
+				_exit(proc_readers_exec());
+			case 6:
+				_exit(clone_exec_race());
+			}
+		}
+		ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
+		if (ret == KSFT_SKIP)
+			ksft_test_result_skip("%s: unavailable or disabled\n", extra_names[kind]);
+		else
+			ksft_test_result(ret == KSFT_PASS, "%s\n", extra_names[kind]);
+	}
+	ksft_finished();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 6ac8ba7e2447..9643b39762b8 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -296,6 +296,7 @@ fi
 # Dump pages 0, 19, and 4096, using pin_user_pages:
 CATEGORY="gup_test" run_test ./gup_test -ct -F 0x1 0 19 0x1000
 CATEGORY="gup_test" run_test ./gup_longterm
+CATEGORY="gup_test" run_test ./doexec
 
 CATEGORY="userfaultfd" run_test ./uffd-unit-tests
 uffd_stress_bin=./uffd-stress

-- 
2.47.1

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

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

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-09 14:00 [Devel] [PATCH RFC hci-8.1 0/4] mm: keep DMA-pinned MADV_DOEXEC memory working " Andrey Drobyshev
2026-10-09 14:00 ` [Devel] [PATCH RFC PATCH hci-8.1 1/4] mm: support moving anonymous page tables between address spaces Andrey Drobyshev
2026-10-09 15:02   ` no-reply
2026-10-09 14:00 ` [Devel] [PATCH RFC PATCH hci-8.1 2/4] exec: protect preserved memory against surviving shared-mm tasks Andrey Drobyshev
2026-10-09 14:57   ` no-reply
2026-10-09 14:00 ` [Devel] [PATCH RFC PATCH hci-8.1 3/4] mm: move MADV_DOEXEC memory instead of copying it Andrey Drobyshev
2026-10-09 15:08   ` no-reply
2026-10-09 14:00 ` Andrey Drobyshev [this message]
2026-10-09 15:07   ` [Devel] [PATCH RFC PATCH hci-8.1 4/4] selftests/mm: test memory preservation across exec no-reply

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=20261009-vfio_v1-v1-4-1901cd6c3c74@virtuozzo.com \
    --to=andrey.drobyshev@virtuozzo.com \
    --cc=devel@openvz.org \
    --cc=khorenko@virtuozzo.com \
    /path/to/YOUR_REPLY

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

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