[PATCH v3 2/7] selftests/mm: check MADV_COLD and MADV_PAGEOUT range handling

From: Gregory Price (Meta)

Date: Mon Oct 19 2026 - 14:24:41 EST


madvise(2) accepts a zero-length range, rejects an unaligned start, and
reports an unmapped gap in the range with ENOMEM while still applying the
advice to the mapped pages on either side.

Check each of these for both hints. Around the gap, only MADV_PAGEOUT has
an effect the test can observe, so only MADV_PAGEOUT is checked for
paging out both neighbors, and only if MADV_PAGEOUT can swap at all.
Both hints' ENOMEM is checked first, so it is covered without swap.

Signed-off-by: Gregory Price (Meta) <gourry@xxxxxxxxxx>
---
.../testing/selftests/mm/madv_cold_pageout.c | 80 +++++++++++++++++++
1 file changed, 80 insertions(+)

diff --git a/tools/testing/selftests/mm/madv_cold_pageout.c b/tools/testing/selftests/mm/madv_cold_pageout.c
index 5d451d409937..8cbbfd4c24c4 100644
--- a/tools/testing/selftests/mm/madv_cold_pageout.c
+++ b/tools/testing/selftests/mm/madv_cold_pageout.c
@@ -141,6 +141,86 @@ TEST_F(madv_cold_pageout, locked_vma_is_rejected)
ASSERT_EQ(munmap(mapping, page_size), 0);
}

+/* A zero-length range is accepted. */
+TEST_F(madv_cold_pageout, zero_length_is_accepted)
+{
+ char *mapping;
+
+ mapping = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+
+ ASSERT_EQ(madvise(mapping, 0, MADV_COLD), 0);
+ ASSERT_EQ(madvise(mapping, 0, MADV_PAGEOUT), 0);
+
+ ASSERT_EQ(munmap(mapping, page_size), 0);
+}
+
+/* The start of the range must be page-aligned. */
+TEST_F(madv_cold_pageout, unaligned_start_is_rejected)
+{
+ char *mapping;
+
+ mapping = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+ mapping[0] = 1;
+
+ ASSERT_EQ(madvise(mapping + 1, page_size, MADV_COLD), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(madvise(mapping + 1, page_size, MADV_PAGEOUT), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(mapping[0], 1);
+
+ ASSERT_EQ(munmap(mapping, 2 * page_size), 0);
+}
+
+/*
+ * An unmapped gap in the range makes either hint fail with ENOMEM, after
+ * advising the mapped pages on both sides of the gap. Only MADV_PAGEOUT has
+ * an effect on those pages that the test can observe.
+ */
+TEST_F(madv_cold_pageout, unmapped_gap_fails_after_advising)
+{
+ bool swapped = false;
+ char *mapping;
+ int retry;
+
+ mapping = mmap(NULL, 3 * page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+ mapping[0] = 1;
+ mapping[2 * page_size] = 1;
+ ASSERT_EQ(munmap(mapping + page_size, page_size), 0);
+
+ ASSERT_EQ(madvise(mapping, 3 * page_size, MADV_COLD), -1);
+ ASSERT_EQ(errno, ENOMEM);
+ ASSERT_EQ(madvise(mapping, 3 * page_size, MADV_PAGEOUT), -1);
+ ASSERT_EQ(errno, ENOMEM);
+ ASSERT_EQ(mapping[0], 1);
+ ASSERT_EQ(mapping[2 * page_size], 1);
+
+ if (!can_swap)
+ SKIP(return, "MADV_PAGEOUT cannot swap");
+
+ for (retry = 0; retry < PAGEOUT_RETRIES && !swapped; retry++) {
+ ASSERT_EQ(madvise(mapping, 3 * page_size, MADV_PAGEOUT), -1);
+ ASSERT_EQ(errno, ENOMEM);
+ swapped = pagemap_is_swapped(self->pagemap_fd, mapping) &&
+ pagemap_is_swapped(self->pagemap_fd,
+ mapping + 2 * page_size);
+ if (!swapped)
+ usleep(10000);
+ }
+ ASSERT_TRUE(swapped)
+ TH_LOG("MADV_PAGEOUT did not page out both sides of the gap");
+ ASSERT_EQ(mapping[0], 1);
+ ASSERT_EQ(mapping[2 * page_size], 1);
+
+ ASSERT_EQ(munmap(mapping, page_size), 0);
+ ASSERT_EQ(munmap(mapping + 2 * page_size, page_size), 0);
+}
+
/* Neither hint may fault a swapped page back in. */
static void check_skips_swapped_pte(struct __test_metadata *_metadata,
int pagemap_fd, int advice)
--
2.56.0