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 2/7] selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack
Date: Wed, 14 Oct 2026 19:15:00 +0200	[thread overview]
Message-ID: <20261014-b4-prep-khorenko-vstor-149847-5-doexec-selftests-v1-2-b23b454691db@virtuozzo.com> (raw)
In-Reply-To: <20261014-b4-prep-khorenko-vstor-149847-5-doexec-selftests-v1-0-b23b454691db@virtuozzo.com>

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

  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 ` Konstantin Khorenko [this message]
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

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-2-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.