From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mail.openvz.org (unknown [69.168.225.77]) by lore.virtuozzo.com (Postfix) with ESMTPS id 25565802A5 for ; Wed, 14 Oct 2026 17:13:00 +0000 (UTC) Received: from mail.openvz.org (localhost [127.0.0.1]) by mail.openvz.org (8.14.4/8.14.4) with ESMTP id 69EHBhur001263; Wed, 14 Oct 2026 20:11:44 +0300 DKIM-Filter: OpenDKIM Filter v2.11.0 mail.openvz.org 69EHBhur001263 Authentication-Results: mail.openvz.org; dkim=fail reason="signature verification failed" (2048-bit key) header.d=virtuozzo.com header.i=@virtuozzo.com header.b="afwmN4tV" Received: from mail-wm1-f72.google.com (mail-wm1-f72.google.com [209.85.128.72]) by mail.openvz.org (8.14.4/8.14.4) with ESMTP id 69EHBJ7g001203 (version=TLSv1/SSLv3 cipher=AES128-GCM-SHA256 bits=128 verify=FAIL) for ; Wed, 14 Oct 2026 20:11:19 +0300 DKIM-Filter: OpenDKIM Filter v2.11.0 mail.openvz.org 69EHBJ7g001203 Received: by mail-wm1-f72.google.com with SMTP id 5b1f17b1804b1-4a028616bebso32632785e9.1 for ; Wed, 14 Oct 2026 10:11:19 -0700 (PDT) X-Gm-Message-State: AFq9FYLv8v20fagp9bkgia2rIYbIMqgIIdL9OHsmqW2mKlDMIoRmBJ8c rg15TufFin1091rkSfE9ox9L2TO2/Ipw1/EfQqk47thbfODftTJlze0SNJsr7Y3giG6OZfG9lCF MKEB0no7qylSJ5wb+cGD0rqITA9cSvuX/KSE2s2uLqWLFHcY+tl1keg== X-Gm-Gg: AYBFou36bAZ9IbxSidt5W8YJvGrXsE1OP9YC1H6JKTkbmOkX0Ri01pzIHHdI0s3fBdP 161Bqp7Hyo6Z9B9RC97gTMpP/EnmLEfHSrQWPjtGf6GRyNkI8H4IuobXeRhp2oBqT2YptTTj6u0 +OWlhQ1fDafguQ6BI+jcjNzmaQ5uPhWPQ2r5wFoffh+RTVRyDi2WZcgGeBH4YohGgZFypiyh+r/ GLQmJI4DMFq2mKvlJs6vmdSBI3VMlJUTa4/7etSok4K/DQu6ozoE7CrFhsExUzSsvG41Y9z1WiP YVTik47NNWCyEN9Ags8uIBfANiC2rmpwpn3ugaIM3qGWJuG2TQhx0AbpiTZr10gZqYy6KwhMVjx gNdolTju14Zd9mHGftA== X-Received: by 2002:a05:600c:8218:b0:49f:ffdc:939d with SMTP id 5b1f17b1804b1-4a18e49d9f5mr319832635e9.9.1791997879141; Wed, 14 Oct 2026 10:11:19 -0700 (PDT) X-Received: by 2002:a05:600c:8218:b0:49f:ffdc:939d with SMTP id 5b1f17b1804b1-4a18e49d9f5mr319832025e9.9.1791997878553; Wed, 14 Oct 2026 10:11:18 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1791997878; cv=none; d=google.com; s=arc-20260327; b=tDti+5pk9nMIbS6qJzJHWC8zaUa1rWQ4B7EnPbvRuj8JS15phTEox4Z7VdnlFWPusv 53iGXtGiJJAY0FtnKSrZIv+S4S4FDuQnL6GIczG6DU8deCG3aclyucc1J1JI+5yQiF/6 i3OQ+Z/mw98vzcC5mKnztkl2S87MRzWbHNUuJJq4s5tTJfZwigggmD8rJtESnFhxSfcb toQBnyFEiHhUGw11yWaHYsYNIp0kVFJ9Rezeu1v7BGpYwfkIIh8YeqfivUdaE4bc02Ru x/CN9nbt0U6OAEhkPRdb/y8i2H5hRolSPkECvyJsDqtLSB+OQZvPHCLlL21C/xRQ+2D0 pzPw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20260327; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :mime-version:subject:date:from:dkim-signature; bh=jLe2IfcF28EJsmuLA5L6BffeVwi0JKmkvCwN0sPxjfo=; fh=aQfw1i5a4NQPWqzJLDKkrMTvdRoUn/cswL6yDWdmKfA=; b=OJaLn2UzrALzPFJ8+WZjHK8l7IPv6OxBSEVsLzM6PJFB8B13kLwM/0eQYKkj1HUjtf wXaRfim45SthHzNqt4ma9k6Xj9MOd8x8WOTHHGvWCwa9VKHOf2tFBZEJe9IVc/yndro2 9MIVoYsHZ5mprGa3gVb1L14v9e5esyvsPKy5HcdQNA4CpNYuTSm1ON0krlKJyQUh2SK7 eR4aGhpTD3QKzDz61fUdOizPohfsfN/agQ2s2D/O06mJXa5Q+Cra/dIEYDRflMqJn/kb scoIGF5ulptnVXsHjp9XIPnszky5xbrvB3RVsxYITYEBMB3PBJ+BIBNaZ/5QeY0g/a5m r1SQ==; dara=google.com ARC-Authentication-Results: i=1; mx.google.com; dkim=pass header.i=@virtuozzo.com header.s=relay header.b=afwmN4tV; spf=pass (google.com: domain of khorenko@virtuozzo.com designates 130.117.225.111 as permitted sender) smtp.mailfrom=khorenko@virtuozzo.com; dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=virtuozzo.com Received: from relay.virtuozzo.com (relay.virtuozzo.com. [130.117.225.111]) by mx.google.com with ESMTPS id 5b1f17b1804b1-4a1bd64302bsi624395e9.1.2026.10.14.10.11.18 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 14 Oct 2026 10:11:18 -0700 (PDT) Received-SPF: pass (google.com: domain of khorenko@virtuozzo.com designates 130.117.225.111 as permitted sender) client-ip=130.117.225.111; Authentication-Results: mx.google.com; dkim=pass header.i=@virtuozzo.com header.s=relay header.b=afwmN4tV; spf=pass (google.com: domain of khorenko@virtuozzo.com designates 130.117.225.111 as permitted sender) smtp.mailfrom=khorenko@virtuozzo.com; dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=virtuozzo.com DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=virtuozzo.com; s=relay; h=Message-Id:Content-Type:MIME-Version:Subject:Date :From; bh=jLe2IfcF28EJsmuLA5L6BffeVwi0JKmkvCwN0sPxjfo=; b=afwmN4tVLG/Ef/IFBPz pi+9NegLoa4S4lv6vdS5uKup64Ro+8NngtYCTRidRVxKtAQCFL6umUArTl2SuoCZll6wgYd+yCmRJ LKTYH1DavDRacsJN1OmmP7qVEo6nu4MJlUbuX7gyXShbsKYqrXz2K4Q9flCNjGfgJXqC0X29nOMQM PPQpZX27Gm9Ogh0mevxHsTnCQ9FQjeB4DProHdxD6nUc++erS0Yv6yA79PEluI2h7lF65IT9646CW QswygqHiHkyctV1hBzWa5bxLaxqABDH4Sfb6JVtFNoHAozqeImHXx24k1BLgBN3cgXTRMFsKoTxAE BfHi6YL8ekKKfEg==; Received: from ch-demo-asa.virtuozzo.com ([130.117.225.8] helo=f0.sw.ru) by relay.virtuozzo.com with esmtp (Exim 4.96) (envelope-from ) id 1xH2S8-0081J5-2J; Wed, 14 Oct 2026 19:11:16 +0200 From: Konstantin Khorenko Date: Wed, 14 Oct 2026 19:11:11 +0200 MIME-Version: 1.0 Message-Id: <20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-8-36f1120f9b57@virtuozzo.com> References: <20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com> In-Reply-To: <20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com> To: Vasileios Almpanis , Andrey Drobyshev , Pavel Tikhomirov X-Mailer: b4 0.16.0 X-Developer-Signature: v=1; a=openpgp-sha256; l=26990; i=khorenko@virtuozzo.com; h=from:subject:message-id; bh=6+ssKXChXcgp9XvIQaueNVgXVeACt/Mv0R14mjonRGU=; b=owEBbQKS/ZANAwAKAVGWCkf5YVwqAcsmYgBqz7ezj0Uj/Mo3frjxqNvRtuDw6+TfuBvQtOEej ScOruirpaGJAjMEAAEKAB0WIQRWD23IPcXT0GO9CCxRlgpH+WFcKgUCas+3swAKCRBRlgpH+WFc KhoWEACXw/btRpWIwjZTpg+NLup+voTggjjgSBXAbvaPbGhqw2MOo+6uYAiVE8f/cr7MFRRvsYe /uILeGWUvg/t5X2J52wKv5pGFXz/rxPV1Bo/ZKxTJXOuZiexo4MnsGfIwztTLsNOyTxJaYqUZth XMGD0pDf4TaJGBAQse/8x+l7HjH2GTgftEUFh65ygfldgqamA28aLO/JqeMM+DJAuRlorhF8itU +C58QBiYfZJ/e6xH76Y7U8q0m9egAxYnk4D0vwhbIUcJ0sg+AulUpfsePybRilUyuY8txCB7N/H sg8flBmMzTKQxpERIhl7Hordok277YV1kC6yeeI8xKe62pKbsFSYn6uq2QNKcImucmJNfOIpImY 7+QWTZyKJB/wYGsRQkxvtsfb13UISJXxHS4zSj41sU0KcumEP4eAS3yu8vIAkPXSS0H+fwQoFob 6WAx5w5MPSqr+0qd+xis0a4Y34gxr/hz3xgKOH25ESYYxMoxpgPQ9HqpTLZRwU9zp+wI2dFp+dW DD6FWirzHzjgTlbRgWBurS7mZzbw4xLi/7qyzQ81iFRsbIQto4G9KTr5onUkfBngOymMQD+FQ63 q8hRIozuHEOOis5DFDWkPQYkSV7ylXTjRAWsf+6jIB88SIY5oBftPnjgP9IUZHEmpvRsFsloCjc L97yd1B4lQDrYyQ== X-Developer-Key: i=khorenko@virtuozzo.com; a=openpgp; fpr=560F6DC83DC5D3D063BD082C51960A47F9615C2A X-OZ-Fwd: true Cc: devel@openvz.org Subject: [Devel] [PATCH hci-8.1 8/8] selftests/mm: test memory preservation across exec X-BeenThere: devel@openvz.org X-Mailman-Version: 2.1.12 Precedence: list List-Id: OpenVZ development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Sender: devel-bounces@openvz.org Errors-To: devel-bounces@openvz.org From: Andrey Drobyshev Add doexec, a MADV_DOEXEC test with the ELF opt-in note, and wire it into run_vmtests.sh. The pin cases need CONFIG_GUP_TEST. What it checks: - CPU writes match what a long-term pin held through gup_test reads back, across two execs and a later fork, for base pages, PMD huge pages, mlocked pages, a read-only mapping, read-only pins and fast GUP pins. - A COW mapping shared with a child forked before exec stays intact on both sides. - exec is refused with EBUSY while a CLONE_VM task outside the thread group has the mm, and succeeds once that task is gone. - Own threads, a non-leader thread calling exec, /proc readers during repeated exec and a vhost device whose owner was reset before exec do not block it. - A CLONE_VM clone racing with exec does not end up sharing the preserved memory. - Optionally, a sweep over task-local allocation failures. https://virtuozzo.atlassian.net/browse/VSTOR-150231 Feature: oracle/mm: MADV_DOEXEC madvise() flag Signed-off-by: Andrey Drobyshev [khorenko: exec through an fd opened before fail-nth is armed, as /proc fails the path lookup with ENOENT; accept E2BIG, ELIBBAD and EIO, which exec returns for some failed allocations; report an unexpected child status; add the binary to .gitignore; use %note, which every architecture's assembler accepts; accept EINVAL from MADV_NOHUGEPAGE on kernels without THP; run it in a doexec category of its own; let the blocked CLONE_VM child drop its copy of the pipe write end] Assisted-by: LLM Signed-off-by: Konstantin Khorenko --- tools/testing/selftests/mm/.gitignore | 1 + tools/testing/selftests/mm/Makefile | 1 + tools/testing/selftests/mm/doexec.c | 814 ++++++++++++++++++++++++++++++ tools/testing/selftests/mm/run_vmtests.sh | 4 + 4 files changed, 820 insertions(+) diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore index 8f01f4da1c0de..2bdaebdbb87ea 100644 --- a/tools/testing/selftests/mm/.gitignore +++ b/tools/testing/selftests/mm/.gitignore @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0-only cow +doexec hugepage-mmap hugepage-mremap hugepage-shm diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile index c837b0549c892..7f9efb0e0a386 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 0000000000000..33529aa41367b --- /dev/null +++ b/tools/testing/selftests/mm/doexec.c @@ -0,0 +1,814 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Verify that exec preserves the memory backing long-term DMA pins. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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) && + (kind == HUGE || errno != EINVAL)) + 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; +} + +/* Without its own copy of the write end, the clone ends with its parent. */ +static int blocked_clone(void *arg) +{ + close(((int *)arg)[1]); + return blocked_task(arg); +} + +/* 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_clone, stack + 65536, CLONE_VM | SIGCHLD, pipe_fd); + 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; +} + +static int check_exec_fd(int exe, char *ram, size_t size, size_t stride) +{ + char addr_arg[32], size_arg[32], stride_arg[32]; + char *args[] = { "doexec", "--check", addr_arg, size_arg, stride_arg, NULL }; + + 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); + syscall(__NR_execveat, exe, "", args, environ, AT_EMPTY_PATH); + return errno; +} + +static int unexpected_status(unsigned int nth, int status) +{ + ksft_print_msg("fail-nth %u: wait status %#x\n", nth, status); + return KSFT_FAIL; +} + +/* 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, exe; + + 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) && errno != EINVAL)) + _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); + /* 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); + len = snprintf(number, sizeof(number), "%u", nth); + if (exe < 0 || fd < 0 || write(fd, number, len) != len) + _exit(KSFT_FAIL); + 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); + /* + * Failed allocations also surface as E2BIG from + * copy_strings(), and as ELIBBAD or EIO from reading the + * ELF interpreter. + */ + if (err == ENOMEM || err == EFAULT || err == EAGAIN || + err == E2BIG || err == ELIBBAD || err == EIO) + _exit(10); + ksft_print_msg("fail-nth %u: exec: %s\n", nth, strerror(err)); + _exit(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 unexpected_status(nth, status); + } + 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 6ac8ba7e24471..5604fc8939a33 100755 --- a/tools/testing/selftests/mm/run_vmtests.sh +++ b/tools/testing/selftests/mm/run_vmtests.sh @@ -77,6 +77,8 @@ to the -t argument containing one or more of the following categories test prctl(PR_SET_MDWE, ...) - page_frag test handling of page fragment allocation and freeing +- doexec + test memory preserved across exec with MADV_DOEXEC example: ./run_vmtests.sh -t "hmm mmap ksm" EOF @@ -297,6 +299,8 @@ fi CATEGORY="gup_test" run_test ./gup_test -ct -F 0x1 0 19 0x1000 CATEGORY="gup_test" run_test ./gup_longterm +CATEGORY="doexec" run_test ./doexec + CATEGORY="userfaultfd" run_test ./uffd-unit-tests uffd_stress_bin=./uffd-stress CATEGORY="userfaultfd" run_test ${uffd_stress_bin} anon 20 16 -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel