[LTP] [PATCH v2 2/2] syscalls/madvise13: Check MADV_DONTNEED across an unmapped hole

Wenyuan Li 2063309626@qq.com
Wed Nov 4 12:00:21 CET 2026


madvise(2) documents that Linux applies the advice to the mapped parts of
a range containing unmapped pages, while returning ENOMEM for the
request. For a private anonymous mapping, MADV_DONTNEED should therefore
discard the contents of the mapped portions on both sides of a hole.

Existing tests do not cover that combination. madvise02 checks the ENOMEM
return for MADV_NORMAL and MADV_WILLNEED on a shared file mapping whose
final page has been unmapped; it does not check MADV_DONTNEED, and it does
not look at the contents of the mapped portions after the call. madvise03
checks zero-fill-on-demand for MADV_DONTNEED on a fully mapped range.

Use eight pages with distinct nonzero byte patterns and advise pages 2
through 4. Run once with all pages mapped and once with page 3 unmapped.
Check the return value, zero-filled target pages, unchanged data outside
the range, and preservation of the hole. Use mincore() to check the hole
without dereferencing it, not to infer physical-page reclamation.

Allocate a fresh mapping for each case, use the runtime page size, and
release the complete region in cleanup, including the case with a hole.
The test does not require root privileges.

Tested on x86-64, 4096-byte pages, kernel 6.18.54.1: 18 TPASS, 36 TPASS
with -i 2.

Link: https://man7.org/linux/man-pages/man2/madvise.2.html
Signed-off-by: Wenyuan Li <2063309626@qq.com>
---
 runtest/syscalls                              |   1 +
 testcases/kernel/syscalls/madvise/.gitignore  |   1 +
 testcases/kernel/syscalls/madvise/madvise13.c | 154 ++++++++++++++++++
 3 files changed, 156 insertions(+)
 create mode 100644 testcases/kernel/syscalls/madvise/madvise13.c

diff --git a/runtest/syscalls b/runtest/syscalls
index ff3de1b15..48cbdab39 100644
--- a/runtest/syscalls
+++ b/runtest/syscalls
@@ -1095,6 +1095,7 @@ madvise09 madvise09
 madvise10 madvise10
 madvise11 madvise11
 madvise12 madvise12
+madvise13 madvise13
 
 newuname01 newuname01
 
diff --git a/testcases/kernel/syscalls/madvise/.gitignore b/testcases/kernel/syscalls/madvise/.gitignore
index 758e601a9..3e7dbe653 100644
--- a/testcases/kernel/syscalls/madvise/.gitignore
+++ b/testcases/kernel/syscalls/madvise/.gitignore
@@ -9,3 +9,4 @@
 /madvise10
 /madvise11
 /madvise12
+/madvise13
diff --git a/testcases/kernel/syscalls/madvise/madvise13.c b/testcases/kernel/syscalls/madvise/madvise13.c
new file mode 100644
index 000000000..cb4323e5b
--- /dev/null
+++ b/testcases/kernel/syscalls/madvise/madvise13.c
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 Wenyuan Li <2063309626@qq.com>
+ */
+
+/*\
+ * Verify MADV_DONTNEED on a private anonymous range containing an unmapped
+ * page. The call must return ENOMEM but still discard the mapped pages on
+ * both sides of the hole, without changing data outside the requested range.
+ * Also test the same request with the middle page mapped.
+ *
+ * madvise(2), VERSIONS/Linux, specifies that Linux applies the advice to
+ * the mapped parts of a range containing holes, even though it returns
+ * ENOMEM.
+ */
+
+#include <sys/mman.h>
+#include "tst_test.h"
+#include "lapi/mmap.h"
+
+#define PAGES 8
+#define HOLE_PAGE 3
+/* Test-local range, not a madvise(2) advice. */
+#define RANGE_FIRST 2
+#define RANGE_LAST 4
+#define RANGE_PAGES (RANGE_LAST - RANGE_FIRST + 1)
+
+static unsigned char *addr;
+static size_t page_size;
+
+static void cleanup(void)
+{
+	if (addr) {
+		SAFE_MUNMAP(addr, PAGES * page_size);
+		addr = NULL;
+	}
+}
+
+/*
+ * Return 1 if the page is mapped, 0 if mincore() reports ENOMEM (a hole).
+ * Any other mincore() failure is a test library error.
+ */
+static int page_mapped(unsigned int page)
+{
+	unsigned char vec;
+
+	TEST(mincore(addr + page * page_size, page_size, &vec));
+
+	if (TST_RET == -1) {
+		if (TST_ERR == ENOMEM)
+			return 0;
+
+		tst_brk(TBROK | TTERRNO, "mincore() on page %u", page);
+	}
+
+	return 1;
+}
+
+/*
+ * Return the offset of the first byte that differs from expected, or
+ * page_size when the whole page matches.
+ */
+static size_t page_first_mismatch(unsigned int page, unsigned char expected)
+{
+	unsigned char *ptr = addr + page * page_size;
+	size_t i;
+
+	for (i = 0; i < page_size; i++) {
+		if (ptr[i] != expected)
+			return i;
+	}
+
+	return page_size;
+}
+
+static void run(unsigned int hole)
+{
+	unsigned int page;
+	unsigned char expected;
+	size_t off;
+
+	tst_res(TINFO, "MADV_DONTNEED %s a hole", hole ? "across" : "without");
+
+	addr = SAFE_MMAP(NULL, PAGES * page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+	for (page = 0; page < PAGES; page++) {
+		if (!page_mapped(page))
+			tst_brk(TBROK, "Page %u is not mapped after mmap()", page);
+
+		memset(addr + page * page_size, 0x40 + page, page_size);
+
+		off = page_first_mismatch(page, 0x40 + page);
+		if (off != page_size)
+			tst_brk(TBROK, "Page %u byte %zu was not initialized", page, off);
+	}
+
+	if (hole) {
+		SAFE_MUNMAP(addr + HOLE_PAGE * page_size, page_size);
+
+		if (page_mapped(HOLE_PAGE))
+			tst_brk(TBROK, "The hole was not created");
+
+		TST_EXP_FAIL(madvise(addr + RANGE_FIRST * page_size,
+				     RANGE_PAGES * page_size, MADV_DONTNEED), ENOMEM);
+	} else {
+		TST_EXP_PASS(madvise(addr + RANGE_FIRST * page_size,
+				     RANGE_PAGES * page_size, MADV_DONTNEED));
+	}
+
+	for (page = 0; page < PAGES; page++) {
+		if (hole && page == HOLE_PAGE)
+			continue;
+
+		expected = 0x40 + page;
+		if (page >= RANGE_FIRST && page <= RANGE_LAST)
+			expected = 0;
+
+		if (!page_mapped(page)) {
+			tst_res(TFAIL, "Page %u is no longer mapped", page);
+			continue;
+		}
+
+		off = page_first_mismatch(page, expected);
+		if (off == page_size) {
+			tst_res(TPASS, "Page %u contains only %#x", page, expected);
+		} else {
+			tst_res(TFAIL, "Page %u byte %zu: expected %#x, got %#x",
+				page, off, expected,
+				addr[page * page_size + off]);
+		}
+	}
+
+	if (hole) {
+		if (page_mapped(HOLE_PAGE))
+			tst_res(TFAIL, "The hole is mapped after MADV_DONTNEED");
+		else
+			tst_res(TPASS, "The hole remains unmapped");
+	}
+
+	cleanup();
+}
+
+static void setup(void)
+{
+	page_size = SAFE_SYSCONF(_SC_PAGESIZE);
+}
+
+static struct tst_test test = {
+	.test = run,
+	.tcnt = 2,
+	.setup = setup,
+	.cleanup = cleanup,
+};
-- 
2.43.0



More information about the ltp mailing list