[PATCH v3 4/7] selftests/mm: check that MADV_COLD and MADV_PAGEOUT preserve shared memory

From: Gregory Price (Meta)

Date: Mon Oct 19 2026 - 15:01:23 EST


Both hints are non-destructive, but the tests so far only check that for
private anonymous memory. Shared memory reaches reclaim differently:

Test the following:
- a dirty shared file page must be written back or left resident,
- shared anonymous memory is paged out through shmem
- a SHM_LOCK'd segment is unevictable without its VMA being VM_LOCKED.

MADV_PAGEOUT unmaps a shmem page before it tries to swap it out
so none of these needs swap.

The SHM_LOCK test confirms the page is unevictable and skips otherwise.

Signed-off-by: Gregory Price (Meta) <gourry@xxxxxxxxxx>
---
v3: Dropped Sarthak's Reviewed-by from v2 5/8: added the fixture check
for hidden PFNs and create_temp_file(), and the shared anonymous
test now fails rather than skips when the page is not unmapped.
---
.../testing/selftests/mm/madv_cold_pageout.c | 177 ++++++++++++++++++
tools/testing/selftests/mm/vm_util.h | 1 +
2 files changed, 178 insertions(+)

diff --git a/tools/testing/selftests/mm/madv_cold_pageout.c b/tools/testing/selftests/mm/madv_cold_pageout.c
index 0874726ec141..7d92f0ecbd27 100644
--- a/tools/testing/selftests/mm/madv_cold_pageout.c
+++ b/tools/testing/selftests/mm/madv_cold_pageout.c
@@ -7,7 +7,11 @@
#include <fcntl.h>
#include <sched.h>
#include <stdbool.h>
+#include <stdint.h>
+#include <sys/ipc.h>
#include <sys/mman.h>
+#include <sys/shm.h>
+#include <sys/stat.h>
#include <unistd.h>

#include "kselftest_harness.h"
@@ -44,19 +48,47 @@ static void pin_to_current_cpu(void)
CPU_FREE(mask);
}

+/* Whether pagemap shows PFNs, which it only does with CAP_SYS_ADMIN. */
+static bool pagemap_shows_pfns(struct __test_metadata *_metadata,
+ int pagemap_fd)
+{
+ unsigned long pfn;
+ char *page;
+
+ page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(page, MAP_FAILED);
+ page[0] = 1;
+ pfn = pagemap_get_pfn(pagemap_fd, page);
+ ASSERT_EQ(munmap(page, page_size), 0);
+
+ return pfn && pfn != -1UL;
+}
+
FIXTURE(madv_cold_pageout)
{
int pagemap_fd;
+ /* Needs root, and is -1 unless pagemap also shows PFNs. */
+ int kpageflags_fd;
};

FIXTURE_SETUP(madv_cold_pageout)
{
self->pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
ASSERT_GE(self->pagemap_fd, 0);
+ self->kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
+ /* Page flags are no use without the PFNs to look them up by. */
+ if (self->kpageflags_fd >= 0 &&
+ !pagemap_shows_pfns(_metadata, self->pagemap_fd)) {
+ close(self->kpageflags_fd);
+ self->kpageflags_fd = -1;
+ }
}

FIXTURE_TEARDOWN(madv_cold_pageout)
{
+ if (self->kpageflags_fd >= 0)
+ close(self->kpageflags_fd);
close(self->pagemap_fd);
}

@@ -76,6 +108,58 @@ static bool pageout_until_swapped(char *mapping, size_t size, int pagemap_fd)
return false;
}

+/* Page out the range until its first page is unmapped, or madvise() fails. */
+static bool pageout_until_unmapped(char *mapping, size_t size, int pagemap_fd)
+{
+ int retry;
+
+ for (retry = 0; retry < PAGEOUT_RETRIES; retry++) {
+ if (madvise(mapping, size, MADV_PAGEOUT))
+ return false;
+ if (!pagemap_is_populated(pagemap_fd, mapping))
+ return true;
+ usleep(10000);
+ }
+
+ return false;
+}
+
+/* Create an unlinked, zero-filled temporary file, or return -1. */
+static int create_temp_file(size_t size)
+{
+ char template[] = "/tmp/madv_cold_pageout-XXXXXX";
+ int fd;
+
+ fd = mkstemp(template);
+ if (fd < 0)
+ return -1;
+ unlink(template);
+ if (ftruncate(fd, size)) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+/* Map an unlinked, zero-filled temporary file with the given permissions. */
+static char *map_temp_file(struct __test_metadata *_metadata, size_t size,
+ int flags, mode_t mode)
+{
+ char *mapping;
+ int fd;
+
+ fd = create_temp_file(size);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fchmod(fd, mode), 0);
+
+ mapping = mmap(NULL, size, PROT_READ | PROT_WRITE, flags, fd, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+ close(fd);
+
+ return mapping;
+}
+
/*
* Both hints are best effort, so these tests check what userspace can rely
* on: which mappings are accepted, which pages must never be reclaimed, and
@@ -275,6 +359,99 @@ TEST_F(madv_cold_pageout, pageout_skips_swapped_pte)
check_skips_swapped_pte(_metadata, self->pagemap_fd, MADV_PAGEOUT);
}

+/* MADV_COLD must not discard a dirty page of a shared file mapping. */
+TEST_F(madv_cold_pageout, cold_preserves_dirty_shared_file_page)
+{
+ char *mapping;
+
+ mapping = map_temp_file(_metadata, page_size, MAP_SHARED, 0600);
+ mapping[0] = 1;
+
+ ASSERT_EQ(madvise(mapping, page_size, MADV_COLD), 0);
+ ASSERT_EQ(mapping[0], 1);
+
+ ASSERT_EQ(munmap(mapping, page_size), 0);
+}
+
+/*
+ * MADV_PAGEOUT may write a dirty shared file page back or leave it resident,
+ * but must not discard it.
+ */
+TEST_F(madv_cold_pageout, pageout_preserves_dirty_shared_file_page)
+{
+ char *mapping;
+
+ mapping = map_temp_file(_metadata, page_size, MAP_SHARED, 0600);
+ mapping[0] = 1;
+
+ if (!pageout_until_unmapped(mapping, page_size, self->pagemap_fd))
+ SKIP(return, "MADV_PAGEOUT did not unmap the page");
+ ASSERT_EQ(mapping[0], 1);
+
+ ASSERT_EQ(munmap(mapping, page_size), 0);
+}
+
+/* MADV_PAGEOUT must not lose the contents of shared anonymous memory. */
+TEST_F(madv_cold_pageout, pageout_preserves_shared_anon_page)
+{
+ char *mapping;
+
+ mapping = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+ mapping[0] = 1;
+
+ /* MADV_PAGEOUT unmaps a shmem page even when it cannot swap it out. */
+ ASSERT_TRUE(pageout_until_unmapped(mapping, page_size,
+ self->pagemap_fd));
+ ASSERT_EQ(mapping[0], 1);
+
+ ASSERT_EQ(munmap(mapping, page_size), 0);
+}
+
+/*
+ * SHM_LOCK makes shmem pages unevictable without setting VM_LOCKED on the
+ * VMA, so MADV_PAGEOUT accepts the range but must not reclaim the page.
+ */
+TEST_F(madv_cold_pageout, pageout_skips_unevictable_page)
+{
+ unsigned long pfn;
+ uint64_t flags;
+ char *mapping;
+ int shmid;
+
+ if (self->kpageflags_fd < 0)
+ SKIP(return, "cannot read page flags");
+ shmid = shmget(IPC_PRIVATE, page_size, IPC_CREAT | 0600);
+ ASSERT_GE(shmid, 0);
+ if (shmctl(shmid, SHM_LOCK, NULL)) {
+ shmctl(shmid, IPC_RMID, NULL);
+ SKIP(return, "could not lock a shmem segment");
+ }
+ mapping = shmat(shmid, NULL, 0);
+ /* The segment is destroyed once it is detached. */
+ shmctl(shmid, IPC_RMID, NULL);
+ ASSERT_NE(mapping, (void *)-1);
+ mapping[0] = 1;
+
+ /*
+ * MADV_COLD drains this CPU's LRU add batch, which makes the page
+ * unevictable.
+ */
+ ASSERT_EQ(madvise(mapping, page_size, MADV_COLD), 0);
+ pfn = pagemap_get_pfn(self->pagemap_fd, mapping);
+ ASSERT_NE(pfn, -1UL);
+ ASSERT_EQ(pageflags_get(pfn, self->kpageflags_fd, &flags), 0);
+ if (!(flags & KPF_UNEVICTABLE))
+ SKIP(return, "could not make the page unevictable");
+
+ ASSERT_EQ(madvise(mapping, page_size, MADV_PAGEOUT), 0);
+ ASSERT_TRUE(pagemap_is_populated(self->pagemap_fd, mapping));
+ ASSERT_EQ(mapping[0], 1);
+
+ ASSERT_EQ(shmdt(mapping), 0);
+}
+
/* Swap out an anonymous page, to see whether swap works at all. */
static bool probe_swap(int pagemap_fd)
{
diff --git a/tools/testing/selftests/mm/vm_util.h b/tools/testing/selftests/mm/vm_util.h
index b5d59729d432..95898e045bac 100644
--- a/tools/testing/selftests/mm/vm_util.h
+++ b/tools/testing/selftests/mm/vm_util.h
@@ -24,6 +24,7 @@

#define KPF_COMPOUND_HEAD BIT_ULL(15)
#define KPF_COMPOUND_TAIL BIT_ULL(16)
+#define KPF_UNEVICTABLE BIT_ULL(18)
#define KPF_HWPOISON BIT_ULL(19)
#define KPF_THP BIT_ULL(22)
/*
--
2.56.0