All Virtuozzo development lists (kernel + QEMU)
 help / color / mirror / Atom feed
From: Konstantin Khorenko <khorenko@virtuozzo.com>
To: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>,
	Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>,
	Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Cc: devel@openvz.org
Subject: [Devel] [PATCH hci-8.1 6/7] selftests/mm: check who can still reach the old mm of a preserving exec
Date: Wed, 14 Oct 2026 19:15:04 +0200	[thread overview]
Message-ID: <20261014-b4-prep-khorenko-vstor-149847-5-doexec-selftests-v1-6-b23b454691db@virtuozzo.com> (raw)
In-Reply-To: <20261014-b4-prep-khorenko-vstor-149847-5-doexec-selftests-v1-0-b23b454691db@virtuozzo.com>

- 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, &reg) || 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, &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

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

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 ` Konstantin Khorenko [this message]
2026-10-14 17:15 ` [Devel] [PATCH hci-8.1 7/7] selftests/mm: say why a doexec case fails Konstantin Khorenko

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261014-b4-prep-khorenko-vstor-149847-5-doexec-selftests-v1-6-b23b454691db@virtuozzo.com \
    --to=khorenko@virtuozzo.com \
    --cc=andrey.drobyshev@virtuozzo.com \
    --cc=devel@openvz.org \
    --cc=ptikhomirov@virtuozzo.com \
    --cc=vasileios.almpanis@virtuozzo.com \
    /path/to/YOUR_REPLY

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

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