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, ®) || 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
next prev 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.