[Devel] [PATCH hci-8.1 2/7] selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack

Konstantin Khorenko khorenko at virtuozzo.com
Wed Oct 14 20:15:00 MSK 2026


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 at 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



More information about the Devel mailing list