* [PATCH 1/8] selftests/mm: add MADV_COLD and MADV_PAGEOUT contract tests
2026-10-14 17:57 [PATCH 0/8] selftests/mm: add MADV_COLD and MADV_PAGEOUT contract tests Gregory Price (Meta)
@ 2026-10-14 17:57 ` Gregory Price (Meta)
2026-10-14 17:57 ` [PATCH 2/8] selftests/mm: check that MADV_PAGEOUT skips holes and swapped pages Gregory Price (Meta)
` (6 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Gregory Price (Meta) @ 2026-10-14 17:57 UTC (permalink / raw)
To: linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
MADV_COLD and MADV_PAGEOUT share a page-table walker covering ordinary
PTEs, PTE-mapped large folios and huge PMDs. Existing selftests provide
little coverage of its range, folio-state and permission decisions.
Add a kselftest harness test for both hints, starting with the most basic
behavior userspace relies on: MADV_COLD neither populates a PTE hole nor
faults a swapped page back in, and neither hint may be applied to a locked
VMA.
The swapped-page test skips when swap is unavailable.
Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
---
tools/testing/selftests/mm/Makefile | 2 +
tools/testing/selftests/mm/ksft_madvise.sh | 4 +
tools/testing/selftests/mm/madvise.c | 134 +++++++++++++++++++++
tools/testing/selftests/mm/run_vmtests.sh | 5 +
4 files changed, 145 insertions(+)
create mode 100755 tools/testing/selftests/mm/ksft_madvise.sh
create mode 100644 tools/testing/selftests/mm/madvise.c
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index beacc0f87304..07d61110eb24 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -69,6 +69,7 @@ TEST_GEN_FILES += hugetlb-shm
TEST_GEN_FILES += hugetlb-soft-offline
TEST_GEN_FILES += khugepaged
TEST_GEN_FILES += madv_populate
+TEST_GEN_FILES += madvise
TEST_GEN_FILES += map_fixed_noreplace
TEST_GEN_FILES += map_populate
ifneq (,$(filter $(ARCH),arm64 riscv riscv64 x86 x86_64 loongarch32 loongarch64))
@@ -156,6 +157,7 @@ TEST_PROGS += ksft_ksm.sh
TEST_PROGS += ksft_ksm_numa.sh
TEST_PROGS += ksft_madv_guard.sh
TEST_PROGS += ksft_madv_populate.sh
+TEST_PROGS += ksft_madvise.sh
TEST_PROGS += ksft_memfd_secret.sh
TEST_PROGS += ksft_memory_failure.sh
TEST_PROGS += ksft_migration.sh
diff --git a/tools/testing/selftests/mm/ksft_madvise.sh b/tools/testing/selftests/mm/ksft_madvise.sh
new file mode 100755
index 000000000000..29cbbca800f2
--- /dev/null
+++ b/tools/testing/selftests/mm/ksft_madvise.sh
@@ -0,0 +1,4 @@
+#!/bin/sh -e
+# SPDX-License-Identifier: GPL-2.0
+
+./run_vmtests.sh -t madvise
diff --git a/tools/testing/selftests/mm/madvise.c b/tools/testing/selftests/mm/madvise.c
new file mode 100644
index 000000000000..bb4efb92e6da
--- /dev/null
+++ b/tools/testing/selftests/mm/madvise.c
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * MADV_COLD and MADV_PAGEOUT tests.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "vm_util.h"
+
+/* Reclaim can fail transiently, so retry for up to a second. */
+#define PAGEOUT_RETRIES 100
+
+static size_t page_size;
+
+FIXTURE(madvise)
+{
+ int pagemap_fd;
+};
+
+FIXTURE_SETUP(madvise)
+{
+ page_size = getpagesize();
+
+ self->pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+ ASSERT_GE(self->pagemap_fd, 0);
+}
+
+FIXTURE_TEARDOWN(madvise)
+{
+ close(self->pagemap_fd);
+}
+
+/* Page out the range until its first page is swapped. */
+static bool pageout_until_swapped(struct __test_metadata *_metadata,
+ char *mapping, size_t size, int pagemap_fd)
+{
+ int retry;
+
+ for (retry = 0; retry < PAGEOUT_RETRIES; retry++) {
+ ASSERT_EQ(madvise(mapping, size, MADV_PAGEOUT), 0);
+ if (pagemap_is_swapped(pagemap_fd, mapping))
+ return true;
+ usleep(10000);
+ }
+
+ return false;
+}
+
+/*
+ * 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
+ * that neither hint changes memory contents or populates the range. A test
+ * that needs a page to be reclaimed first checks that reclaim works at all,
+ * and skips if it does not.
+ */
+
+/* Neither hint may populate a hole between two populated pages. */
+static void check_skips_pte_hole(struct __test_metadata *_metadata,
+ int pagemap_fd, int advice)
+{
+ char *mapping;
+
+ 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_FALSE(pagemap_is_populated(pagemap_fd, mapping + page_size));
+
+ ASSERT_EQ(madvise(mapping, 3 * page_size, advice), 0);
+ ASSERT_FALSE(pagemap_is_populated(pagemap_fd, mapping + page_size));
+ ASSERT_EQ(mapping[0], 1);
+ ASSERT_EQ(mapping[2 * page_size], 1);
+
+ ASSERT_EQ(munmap(mapping, 3 * page_size), 0);
+}
+
+TEST_F(madvise, cold_skips_pte_hole)
+{
+ check_skips_pte_hole(_metadata, self->pagemap_fd, MADV_COLD);
+}
+
+/* Neither hint may be applied to locked memory. */
+TEST_F(madvise, locked_vma_is_rejected)
+{
+ char *mapping;
+
+ mapping = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+ mapping[0] = 1;
+ if (mlock(mapping, page_size))
+ SKIP(return, "could not lock a page");
+
+ ASSERT_EQ(madvise(mapping, page_size, MADV_COLD), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(madvise(mapping, page_size, MADV_PAGEOUT), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(mapping[0], 1);
+
+ ASSERT_EQ(munmap(mapping, 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)
+{
+ char *mapping;
+
+ mapping = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+ mapping[0] = 1;
+ if (!pageout_until_swapped(_metadata, mapping, page_size, pagemap_fd))
+ SKIP(return, "MADV_PAGEOUT did not swap the page");
+
+ ASSERT_EQ(madvise(mapping, page_size, advice), 0);
+ ASSERT_TRUE(pagemap_is_swapped(pagemap_fd, mapping));
+ ASSERT_EQ(mapping[0], 1);
+
+ ASSERT_EQ(munmap(mapping, page_size), 0);
+}
+
+TEST_F(madvise, cold_skips_swapped_pte)
+{
+ check_skips_swapped_pte(_metadata, self->pagemap_fd, MADV_COLD);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index a1b45a3dedae..0e6a24659d12 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -53,6 +53,8 @@ separated by spaces:
test madvise(2) MADV_GUARD_INSTALL and MADV_GUARD_REMOVE options
- madv_populate
test memadvise(2) MADV_POPULATE_{READ,WRITE} options
+- madvise
+ test madvise(2) MADV_COLD and MADV_PAGEOUT
- memfd_secret
test memfd_secret(2)
- process_mrelease
@@ -330,6 +332,9 @@ CATEGORY="madv_guard" run_test ./guard-regions
# MADV_POPULATE_READ and MADV_POPULATE_WRITE tests
CATEGORY="madv_populate" run_test ./madv_populate
+# MADV_COLD and MADV_PAGEOUT tests
+CATEGORY="madvise" run_test ./madvise
+
# PROCESS_MADV test
CATEGORY="process_madv" run_test ./process_madv
--
2.56.0
^ permalink raw reply [flat|nested] 16+ messages in thread* [PATCH 2/8] selftests/mm: check that MADV_PAGEOUT skips holes and swapped pages
2026-10-14 17:57 [PATCH 0/8] selftests/mm: add MADV_COLD and MADV_PAGEOUT contract tests Gregory Price (Meta)
2026-10-14 17:57 ` [PATCH 1/8] " Gregory Price (Meta)
@ 2026-10-14 17:57 ` Gregory Price (Meta)
2026-10-14 17:57 ` [PATCH 3/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT range handling Gregory Price (Meta)
` (5 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Gregory Price (Meta) @ 2026-10-14 17:57 UTC (permalink / raw)
To: linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
The contract tests check that MADV_COLD neither populates a PTE hole nor
faults a swapped page back in. MADV_PAGEOUT makes the same promises, so
check them for it too.
Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
---
tools/testing/selftests/mm/madvise.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/tools/testing/selftests/mm/madvise.c b/tools/testing/selftests/mm/madvise.c
index bb4efb92e6da..42cc08fe45ae 100644
--- a/tools/testing/selftests/mm/madvise.c
+++ b/tools/testing/selftests/mm/madvise.c
@@ -85,6 +85,11 @@ TEST_F(madvise, cold_skips_pte_hole)
check_skips_pte_hole(_metadata, self->pagemap_fd, MADV_COLD);
}
+TEST_F(madvise, pageout_skips_pte_hole)
+{
+ check_skips_pte_hole(_metadata, self->pagemap_fd, MADV_PAGEOUT);
+}
+
/* Neither hint may be applied to locked memory. */
TEST_F(madvise, locked_vma_is_rejected)
{
@@ -131,4 +136,9 @@ TEST_F(madvise, cold_skips_swapped_pte)
check_skips_swapped_pte(_metadata, self->pagemap_fd, MADV_COLD);
}
+TEST_F(madvise, pageout_skips_swapped_pte)
+{
+ check_skips_swapped_pte(_metadata, self->pagemap_fd, MADV_PAGEOUT);
+}
+
TEST_HARNESS_MAIN
--
2.56.0
^ permalink raw reply [flat|nested] 16+ messages in thread* [PATCH 3/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT range handling
2026-10-14 17:57 [PATCH 0/8] selftests/mm: add MADV_COLD and MADV_PAGEOUT contract tests Gregory Price (Meta)
2026-10-14 17:57 ` [PATCH 1/8] " Gregory Price (Meta)
2026-10-14 17:57 ` [PATCH 2/8] selftests/mm: check that MADV_PAGEOUT skips holes and swapped pages Gregory Price (Meta)
@ 2026-10-14 17:57 ` Gregory Price (Meta)
2026-10-14 21:31 ` [PATCH] fixup! " Gregory Price (Meta)
2026-10-16 12:17 ` [PATCH 3/8] " Sarthak Sharma
2026-10-14 17:57 ` [PATCH 4/8] selftests/mm: check that MADV_COLD and MADV_PAGEOUT reject hugetlb Gregory Price (Meta)
` (4 subsequent siblings)
7 siblings, 2 replies; 16+ messages in thread
From: Gregory Price (Meta) @ 2026-10-14 17:57 UTC (permalink / raw)
To: linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
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 neighbours, after a control page shows that swap works.
Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
---
tools/testing/selftests/mm/madvise.c | 103 +++++++++++++++++++++++++++
1 file changed, 103 insertions(+)
diff --git a/tools/testing/selftests/mm/madvise.c b/tools/testing/selftests/mm/madvise.c
index 42cc08fe45ae..f122154fac05 100644
--- a/tools/testing/selftests/mm/madvise.c
+++ b/tools/testing/selftests/mm/madvise.c
@@ -5,6 +5,7 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
+#include <sched.h>
#include <stdbool.h>
#include <sys/mman.h>
#include <unistd.h>
@@ -51,6 +52,21 @@ static bool pageout_until_swapped(struct __test_metadata *_metadata,
return false;
}
+/*
+ * Keep this task on one CPU, so that draining that CPU's LRU batches drains
+ * the folios this task faulted in.
+ */
+static void pin_to_current_cpu(struct __test_metadata *_metadata)
+{
+ int cpu = sched_getcpu();
+ cpu_set_t mask;
+
+ ASSERT_GE(cpu, 0);
+ CPU_ZERO(&mask);
+ CPU_SET(cpu, &mask);
+ ASSERT_EQ(sched_setaffinity(0, sizeof(mask), &mask), 0);
+}
+
/*
* 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
@@ -111,6 +127,93 @@ TEST_F(madvise, locked_vma_is_rejected)
ASSERT_EQ(munmap(mapping, page_size), 0);
}
+/* A zero-length range is accepted. */
+TEST_F(madvise, 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(madvise, 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(madvise, unmapped_gap_fails_after_advising)
+{
+ char *mapping, *control;
+ bool swapped = false;
+ int retry;
+
+ pin_to_current_cpu(_metadata);
+
+ /* Make sure MADV_PAGEOUT can swap at all before relying on it. */
+ control = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(control, MAP_FAILED);
+ control[0] = 1;
+ if (!pageout_until_swapped(_metadata, control, page_size,
+ self->pagemap_fd))
+ SKIP(return, "MADV_PAGEOUT did not swap the page");
+
+ 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(mapping[0], 1);
+ ASSERT_EQ(mapping[2 * page_size], 1);
+
+ 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);
+ ASSERT_EQ(munmap(control, 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
^ permalink raw reply [flat|nested] 16+ messages in thread* [PATCH] fixup! selftests/mm: check MADV_COLD and MADV_PAGEOUT range handling
2026-10-14 17:57 ` [PATCH 3/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT range handling Gregory Price (Meta)
@ 2026-10-14 21:31 ` Gregory Price (Meta)
2026-10-16 12:17 ` [PATCH 3/8] " Sarthak Sharma
1 sibling, 0 replies; 16+ messages in thread
From: Gregory Price (Meta) @ 2026-10-14 21:31 UTC (permalink / raw)
To: linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
Sashiko fixup for patch 3/8
This is largely an overly aggressive fix for systems with >1024 cpus,
but it's simple enough and can just be squashed.
sched_getcpu() can return a CPU number at or above CPU_SETSIZE on large
systems. CPU_SET() ignores such a number, so the fixed-size mask stays
empty, sched_setaffinity() fails with EINVAL, and every test that pins
itself fails. Size the mask to the CPU number with CPU_ALLOC() instead.
Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
---
tools/testing/selftests/mm/madvise.c | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/mm/madvise.c b/tools/testing/selftests/mm/madvise.c
index d59c03ec8191..3f361bcf1039 100644
--- a/tools/testing/selftests/mm/madvise.c
+++ b/tools/testing/selftests/mm/madvise.c
@@ -224,12 +224,18 @@ static bool pageout_until_evicted(struct __test_metadata *_metadata,
static void pin_to_current_cpu(struct __test_metadata *_metadata)
{
int cpu = sched_getcpu();
- cpu_set_t mask;
+ cpu_set_t *mask;
+ size_t size;
ASSERT_GE(cpu, 0);
- CPU_ZERO(&mask);
- CPU_SET(cpu, &mask);
- ASSERT_EQ(sched_setaffinity(0, sizeof(mask), &mask), 0);
+ /* CPU numbers can exceed CPU_SETSIZE, so size the mask to fit. */
+ mask = CPU_ALLOC(cpu + 1);
+ ASSERT_NE(mask, NULL);
+ size = CPU_ALLOC_SIZE(cpu + 1);
+ CPU_ZERO_S(size, mask);
+ CPU_SET_S(cpu, size, mask);
+ ASSERT_EQ(sched_setaffinity(0, size, mask), 0);
+ CPU_FREE(mask);
}
static bool page_is_present(int pagemap_fd, char *addr)
--
2.56.0
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH 3/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT range handling
2026-10-14 17:57 ` [PATCH 3/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT range handling Gregory Price (Meta)
2026-10-14 21:31 ` [PATCH] fixup! " Gregory Price (Meta)
@ 2026-10-16 12:17 ` Sarthak Sharma
2026-10-16 13:22 ` Gregory Price
1 sibling, 1 reply; 16+ messages in thread
From: Sarthak Sharma @ 2026-10-16 12:17 UTC (permalink / raw)
To: Gregory Price (Meta), linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
Hi Gregory!
On 10/14/26 11:27 PM, Gregory Price (Meta) wrote:
> 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 neighbours, after a control page shows that swap works.
>
> Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
> ---
[...]
> +/*
> + * 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(madvise, unmapped_gap_fails_after_advising)
> +{
> + char *mapping, *control;
> + bool swapped = false;
> + int retry;
> +
> + pin_to_current_cpu(_metadata);
> +
> + /* Make sure MADV_PAGEOUT can swap at all before relying on it. */
> + control = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
> + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> + ASSERT_NE(control, MAP_FAILED);
> + control[0] = 1;
> + if (!pageout_until_swapped(_metadata, control, page_size,
> + self->pagemap_fd))
> + SKIP(return, "MADV_PAGEOUT did not swap the page");
This test is checking for both MADV_COLD and MADV_PAGEOUT. So if we skip
here because we couldn't pageout, then we don't test for MADV_COLD
behaviour at all. Do we want it this way, or should the tests for both
of these be separate?
> +
> + 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(mapping[0], 1);
> + ASSERT_EQ(mapping[2 * page_size], 1);
> +
> + 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);
> + ASSERT_EQ(munmap(control, 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)
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH 3/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT range handling
2026-10-16 12:17 ` [PATCH 3/8] " Sarthak Sharma
@ 2026-10-16 13:22 ` Gregory Price
2026-10-16 14:07 ` Gregory Price
0 siblings, 1 reply; 16+ messages in thread
From: Gregory Price @ 2026-10-16 13:22 UTC (permalink / raw)
To: Sarthak Sharma
Cc: linux-kselftest, linux-mm, linux-kernel, kernel-team, akpm,
david, ljs, liam, vbabka, rppt, surenb, mhocko, shuah
On Fri, Oct 16, 2026 at 05:47:57PM +0530, Sarthak Sharma wrote:
> > +/*
> > + * 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(madvise, unmapped_gap_fails_after_advising)
> > +{
> > + char *mapping, *control;
> > + bool swapped = false;
> > + int retry;
> > +
> > + pin_to_current_cpu(_metadata);
> > +
> > + /* Make sure MADV_PAGEOUT can swap at all before relying on it. */
> > + control = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
> > + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> > + ASSERT_NE(control, MAP_FAILED);
> > + control[0] = 1;
> > + if (!pageout_until_swapped(_metadata, control, page_size,
> > + self->pagemap_fd))
> > + SKIP(return, "MADV_PAGEOUT did not swap the page");
>
> This test is checking for both MADV_COLD and MADV_PAGEOUT. So if we skip
> here because we couldn't pageout, then we don't test for MADV_COLD
> behaviour at all. Do we want it this way, or should the tests for both
> of these be separate?
>
If swap is off, the behavior of COLD and PAGEOUT look pretty much the
same, and we can't actually test them meaningfully.
We check that madvise(COLD) returns -1, but that's also largely
meaningless if this initial pageout_until_swapped() call fails.
At least that's my thought. If you think I should split it, I can.
~Gregory
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH 3/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT range handling
2026-10-16 13:22 ` Gregory Price
@ 2026-10-16 14:07 ` Gregory Price
0 siblings, 0 replies; 16+ messages in thread
From: Gregory Price @ 2026-10-16 14:07 UTC (permalink / raw)
To: Sarthak Sharma
Cc: linux-kselftest, linux-mm, linux-kernel, kernel-team, akpm,
david, ljs, liam, vbabka, rppt, surenb, mhocko, shuah
On Fri, Oct 16, 2026 at 09:22:54AM -0400, Gregory Price wrote:
> On Fri, Oct 16, 2026 at 05:47:57PM +0530, Sarthak Sharma wrote:
> > > +/*
> > > + * 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(madvise, unmapped_gap_fails_after_advising)
> > > +{
> > > + char *mapping, *control;
> > > + bool swapped = false;
> > > + int retry;
> > > +
> > > + pin_to_current_cpu(_metadata);
> > > +
> > > + /* Make sure MADV_PAGEOUT can swap at all before relying on it. */
> > > + control = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
> > > + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> > > + ASSERT_NE(control, MAP_FAILED);
> > > + control[0] = 1;
> > > + if (!pageout_until_swapped(_metadata, control, page_size,
> > > + self->pagemap_fd))
> > > + SKIP(return, "MADV_PAGEOUT did not swap the page");
> >
> > This test is checking for both MADV_COLD and MADV_PAGEOUT. So if we skip
> > here because we couldn't pageout, then we don't test for MADV_COLD
> > behaviour at all. Do we want it this way, or should the tests for both
> > of these be separate?
> >
>
> If swap is off, the behavior of COLD and PAGEOUT look pretty much the
> same, and we can't actually test them meaningfully.
>
> We check that madvise(COLD) returns -1, but that's also largely
> meaningless if this initial pageout_until_swapped() call fails.
>
> At least that's my thought. If you think I should split it, I can.
>
on second look, i will just move the cold checks above the rest, since
they're mostly checking failure paths. those are mostly unchanging
regardless of swap status.
so basically no need to split, just a mild reorder.
~Gregory
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH 4/8] selftests/mm: check that MADV_COLD and MADV_PAGEOUT reject hugetlb
2026-10-14 17:57 [PATCH 0/8] selftests/mm: add MADV_COLD and MADV_PAGEOUT contract tests Gregory Price (Meta)
` (2 preceding siblings ...)
2026-10-14 17:57 ` [PATCH 3/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT range handling Gregory Price (Meta)
@ 2026-10-14 17:57 ` Gregory Price (Meta)
2026-10-14 17:57 ` [PATCH 5/8] selftests/mm: check that MADV_COLD and MADV_PAGEOUT preserve shared memory Gregory Price (Meta)
` (3 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Gregory Price (Meta) @ 2026-10-14 17:57 UTC (permalink / raw)
To: linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
Neither hint may be applied to hugetlb memory, so madvise(2) must fail
with EINVAL for a hugetlb VMA. Locked VMAs are already checked here and
PFNMAP VMAs by the pfnmap test.
Check the hugetlb case for both hints.
Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
---
tools/testing/selftests/mm/madvise.c | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/tools/testing/selftests/mm/madvise.c b/tools/testing/selftests/mm/madvise.c
index f122154fac05..bd78f63f1314 100644
--- a/tools/testing/selftests/mm/madvise.c
+++ b/tools/testing/selftests/mm/madvise.c
@@ -127,6 +127,29 @@ TEST_F(madvise, locked_vma_is_rejected)
ASSERT_EQ(munmap(mapping, page_size), 0);
}
+/* Neither hint may be applied to hugetlb memory. */
+TEST_F(madvise, hugetlb_vma_is_rejected)
+{
+ size_t size = default_huge_page_size();
+ char *mapping;
+
+ if (!size)
+ SKIP(return, "hugetlb is not supported");
+ mapping = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
+ if (mapping == MAP_FAILED)
+ SKIP(return, "no hugetlb pages are available");
+ mapping[0] = 1;
+
+ ASSERT_EQ(madvise(mapping, size, MADV_COLD), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(madvise(mapping, size, MADV_PAGEOUT), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(mapping[0], 1);
+
+ ASSERT_EQ(munmap(mapping, size), 0);
+}
+
/* A zero-length range is accepted. */
TEST_F(madvise, zero_length_is_accepted)
{
--
2.56.0
^ permalink raw reply [flat|nested] 16+ messages in thread* [PATCH 5/8] selftests/mm: check that MADV_COLD and MADV_PAGEOUT preserve shared memory
2026-10-14 17:57 [PATCH 0/8] selftests/mm: add MADV_COLD and MADV_PAGEOUT contract tests Gregory Price (Meta)
` (3 preceding siblings ...)
2026-10-14 17:57 ` [PATCH 4/8] selftests/mm: check that MADV_COLD and MADV_PAGEOUT reject hugetlb Gregory Price (Meta)
@ 2026-10-14 17:57 ` Gregory Price (Meta)
2026-10-14 17:57 ` [PATCH 6/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT on partly advised THPs Gregory Price (Meta)
` (2 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Gregory Price (Meta) @ 2026-10-14 17:57 UTC (permalink / raw)
To: linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
Both hints are non-destructive, but the tests so far only check that for
private anonymous memory. Shared memory reaches reclaim through different
paths: a dirty shared file page must be written back or left resident,
shared anonymous memory is paged out through shmem, and a SHM_LOCK'd
segment is unevictable without its VMA being VM_LOCKED.
Check that MADV_COLD and MADV_PAGEOUT preserve a dirty shared file page,
that MADV_PAGEOUT preserves a shared anonymous page, and that it leaves a
SHM_LOCK'd page resident. The SHM_LOCK test needs root to confirm through
/proc/kpageflags that the page is unevictable, and skips if it is not.
Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
---
tools/testing/selftests/mm/madvise.c | 138 +++++++++++++++++++++++++++
tools/testing/selftests/mm/vm_util.h | 1 +
2 files changed, 139 insertions(+)
diff --git a/tools/testing/selftests/mm/madvise.c b/tools/testing/selftests/mm/madvise.c
index bd78f63f1314..0851f5428e3d 100644
--- a/tools/testing/selftests/mm/madvise.c
+++ b/tools/testing/selftests/mm/madvise.c
@@ -7,7 +7,10 @@
#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 <unistd.h>
#include "kselftest_harness.h"
@@ -21,6 +24,8 @@ static size_t page_size;
FIXTURE(madvise)
{
int pagemap_fd;
+ /* Opening it needs CAP_SYS_ADMIN, -1 otherwise. */
+ int kpageflags_fd;
};
FIXTURE_SETUP(madvise)
@@ -29,10 +34,13 @@ FIXTURE_SETUP(madvise)
self->pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
ASSERT_GE(self->pagemap_fd, 0);
+ self->kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
}
FIXTURE_TEARDOWN(madvise)
{
+ if (self->kpageflags_fd >= 0)
+ close(self->kpageflags_fd);
close(self->pagemap_fd);
}
@@ -52,6 +60,22 @@ static bool pageout_until_swapped(struct __test_metadata *_metadata,
return false;
}
+/* Page out the range until its first page is no longer mapped. */
+static bool pageout_until_evicted(struct __test_metadata *_metadata,
+ char *mapping, size_t size, int pagemap_fd)
+{
+ int retry;
+
+ for (retry = 0; retry < PAGEOUT_RETRIES; retry++) {
+ ASSERT_EQ(madvise(mapping, size, MADV_PAGEOUT), 0);
+ if (!pagemap_is_populated(pagemap_fd, mapping))
+ return true;
+ usleep(10000);
+ }
+
+ return false;
+}
+
/*
* Keep this task on one CPU, so that draining that CPU's LRU batches drains
* the folios this task faulted in.
@@ -67,6 +91,25 @@ static void pin_to_current_cpu(struct __test_metadata *_metadata)
ASSERT_EQ(sched_setaffinity(0, sizeof(mask), &mask), 0);
}
+/* Map a shared mapping of an unlinked, zero-filled temporary file. */
+static char *map_temp_file(struct __test_metadata *_metadata, size_t size)
+{
+ char template[] = "/tmp/madvise-pageout-XXXXXX";
+ char *mapping;
+ int fd;
+
+ fd = mkstemp(template);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(unlink(template), 0);
+ ASSERT_EQ(ftruncate(fd, size), 0);
+
+ mapping = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, 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
@@ -267,4 +310,99 @@ TEST_F(madvise, 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(madvise, cold_preserves_dirty_shared_file_page)
+{
+ char *mapping;
+
+ mapping = map_temp_file(_metadata, page_size);
+ 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(madvise, pageout_preserves_dirty_shared_file_page)
+{
+ char *mapping;
+
+ mapping = map_temp_file(_metadata, page_size);
+ mapping[0] = 1;
+
+ if (!pageout_until_evicted(_metadata, 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(madvise, 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;
+
+ if (!pageout_until_evicted(_metadata, mapping, page_size,
+ self->pagemap_fd))
+ SKIP(return, "MADV_PAGEOUT did not evict the page");
+ 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(madvise, pageout_skips_unevictable_page)
+{
+ unsigned long pfn;
+ uint64_t flags;
+ char *mapping;
+ int shmid;
+
+ if (self->kpageflags_fd < 0)
+ SKIP(return, "requires root to 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);
+ pin_to_current_cpu(_metadata);
+ 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);
+}
+
TEST_HARNESS_MAIN
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
^ permalink raw reply [flat|nested] 16+ messages in thread* [PATCH 6/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT on partly advised THPs
2026-10-14 17:57 [PATCH 0/8] selftests/mm: add MADV_COLD and MADV_PAGEOUT contract tests Gregory Price (Meta)
` (4 preceding siblings ...)
2026-10-14 17:57 ` [PATCH 5/8] selftests/mm: check that MADV_COLD and MADV_PAGEOUT preserve shared memory Gregory Price (Meta)
@ 2026-10-14 17:57 ` Gregory Price (Meta)
2026-10-14 17:57 ` [PATCH 7/8] selftests/mm: check MADV_PAGEOUT file permissions Gregory Price (Meta)
2026-10-14 17:57 ` [PATCH 8/8] selftests/mm: check that unauthorized MADV_PAGEOUT skips large file folios Gregory Price (Meta)
7 siblings, 0 replies; 16+ messages in thread
From: Gregory Price (Meta) @ 2026-10-14 17:57 UTC (permalink / raw)
To: linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
Paging out a THP that the advised range fully covers must not corrupt it.
A THP that the range only partly covers must be split before reclaim, or
skipped, so that no page outside the range is reclaimed. MADV_COLD takes
the same split path.
Check that a whole PMD-mapped THP is swapped intact, that pages outside
the range stay resident after paging out a base page or half of a PMD- or
PTE-mapped THP, and that MADV_COLD on half of either THP preserves its
contents. Tests skip when PMD-sized THPs cannot be allocated, and with
/proc/kpageflags the PTE-mapped cases also confirm the folio stayed large.
Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
---
tools/testing/selftests/mm/madvise.c | 234 +++++++++++++++++++++++++++
1 file changed, 234 insertions(+)
diff --git a/tools/testing/selftests/mm/madvise.c b/tools/testing/selftests/mm/madvise.c
index 0851f5428e3d..8822991eae4a 100644
--- a/tools/testing/selftests/mm/madvise.c
+++ b/tools/testing/selftests/mm/madvise.c
@@ -8,6 +8,7 @@
#include <sched.h>
#include <stdbool.h>
#include <stdint.h>
+#include <string.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/shm.h>
@@ -16,10 +17,15 @@
#include "kselftest_harness.h"
#include "vm_util.h"
+#ifndef MADV_COLLAPSE
+#define MADV_COLLAPSE 25
+#endif
+
/* Reclaim can fail transiently, so retry for up to a second. */
#define PAGEOUT_RETRIES 100
static size_t page_size;
+static size_t pmd_size;
FIXTURE(madvise)
{
@@ -31,6 +37,7 @@ FIXTURE(madvise)
FIXTURE_SETUP(madvise)
{
page_size = getpagesize();
+ pmd_size = read_pmd_pagesize();
self->pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
ASSERT_GE(self->pagemap_fd, 0);
@@ -44,6 +51,111 @@ FIXTURE_TEARDOWN(madvise)
close(self->pagemap_fd);
}
+/*
+ * Reserve a PMD-aligned range, so that memory mapped over it with MAP_FIXED
+ * can be backed by PMD-mapped THPs.
+ */
+static char *reserve_aligned(struct __test_metadata *_metadata, size_t size)
+{
+ char *reservation, *aligned;
+
+ reservation = mmap(NULL, size + pmd_size, PROT_NONE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(reservation, MAP_FAILED);
+
+ aligned = (char *)(((uintptr_t)reservation + pmd_size - 1) &
+ ~(pmd_size - 1));
+ if (aligned != reservation)
+ ASSERT_EQ(munmap(reservation, aligned - reservation), 0);
+ ASSERT_EQ(munmap(aligned + size, reservation + pmd_size - aligned), 0);
+
+ return aligned;
+}
+
+/* Map PMD-aligned anonymous memory filled with 1s. */
+static char *map_aligned_pages(struct __test_metadata *_metadata, size_t size)
+{
+ char *mapping = reserve_aligned(_metadata, size);
+
+ ASSERT_EQ(mmap(mapping, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0),
+ mapping);
+ memset(mapping, 1, size);
+
+ return mapping;
+}
+
+/*
+ * Collapse a PMD-aligned range into PMD-mapped anonymous THPs. MADV_COLLAPSE
+ * may fail transiently with EAGAIN.
+ */
+static bool collapse(char *mapping, size_t size)
+{
+ int nr_hpages = size / pmd_size;
+ int retry;
+
+ for (retry = 0; retry < 10; retry++) {
+ if (madvise(mapping, size, MADV_COLLAPSE)) {
+ if (errno != EAGAIN)
+ return false;
+ } else if (check_huge_anon(mapping, size, nr_hpages,
+ pmd_size)) {
+ return true;
+ }
+ usleep(10000);
+ }
+
+ return false;
+}
+
+/* Map a PMD-mapped anonymous THP filled with 1s, or return NULL. */
+static char *map_thp(struct __test_metadata *_metadata)
+{
+ char *mapping;
+
+ if (!pmd_size)
+ return NULL;
+ mapping = map_aligned_pages(_metadata, pmd_size);
+ if (collapse(mapping, pmd_size))
+ return mapping;
+ ASSERT_EQ(munmap(mapping, pmd_size), 0);
+ return NULL;
+}
+
+/*
+ * Changing the protection of one page PTE-maps a THP. When /proc/kpageflags
+ * is readable, also check that the folio is still large.
+ */
+static void pte_map_thp(struct __test_metadata *_metadata, char *mapping,
+ int pagemap_fd, int kpageflags_fd)
+{
+ unsigned long pfn;
+ uint64_t flags;
+
+ ASSERT_EQ(mprotect(mapping + page_size, page_size, PROT_READ), 0);
+ ASSERT_EQ(mprotect(mapping + page_size, page_size,
+ PROT_READ | PROT_WRITE), 0);
+ ASSERT_TRUE(check_huge_anon(mapping, pmd_size, 0, pmd_size));
+ if (kpageflags_fd < 0)
+ return;
+
+ pfn = pagemap_get_pfn(pagemap_fd, mapping);
+ ASSERT_NE(pfn, -1UL);
+ ASSERT_EQ(pageflags_get(pfn, kpageflags_fd, &flags), 0);
+ ASSERT_TRUE(flags & KPF_COMPOUND_HEAD);
+}
+
+static void assert_filled(struct __test_metadata *_metadata,
+ const char *mapping, size_t size)
+{
+ size_t offset;
+
+ for (offset = 0; offset < size; offset += page_size) {
+ ASSERT_EQ(mapping[offset], 1)
+ TH_LOG("memory changed at offset %zu", offset);
+ }
+}
+
/* Page out the range until its first page is swapped. */
static bool pageout_until_swapped(struct __test_metadata *_metadata,
char *mapping, size_t size, int pagemap_fd)
@@ -91,6 +203,11 @@ static void pin_to_current_cpu(struct __test_metadata *_metadata)
ASSERT_EQ(sched_setaffinity(0, sizeof(mask), &mask), 0);
}
+static bool page_is_present(int pagemap_fd, char *addr)
+{
+ return pagemap_get_entry(pagemap_fd, addr) & PM_PRESENT;
+}
+
/* Map a shared mapping of an unlinked, zero-filled temporary file. */
static char *map_temp_file(struct __test_metadata *_metadata, size_t size)
{
@@ -405,4 +522,121 @@ TEST_F(madvise, pageout_skips_unevictable_page)
ASSERT_EQ(shmdt(mapping), 0);
}
+/* MADV_PAGEOUT of a whole THP must swap it without corrupting it. */
+TEST_F(madvise, pageout_swaps_thp)
+{
+ char *mapping;
+
+ mapping = map_thp(_metadata);
+ if (!mapping)
+ SKIP(return, "could not allocate a PMD-sized THP");
+
+ if (!pageout_until_swapped(_metadata, mapping, pmd_size,
+ self->pagemap_fd))
+ SKIP(return, "MADV_PAGEOUT did not swap the THP");
+ assert_filled(_metadata, mapping, pmd_size);
+
+ ASSERT_EQ(munmap(mapping, pmd_size), 0);
+}
+
+/*
+ * MADV_PAGEOUT must not reclaim pages outside the advised range. kswapd or
+ * page migration could also unmap them, but neither should happen when the
+ * selftests run by themselves.
+ */
+TEST_F(madvise, pageout_stays_within_range)
+{
+ char *mapping;
+
+ mapping = mmap(NULL, 3 * page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+ memset(mapping, 1, 3 * page_size);
+ if (!pageout_until_swapped(_metadata, mapping + page_size, page_size,
+ self->pagemap_fd))
+ SKIP(return, "MADV_PAGEOUT did not swap the page");
+
+ ASSERT_TRUE(page_is_present(self->pagemap_fd, mapping));
+ ASSERT_TRUE(page_is_present(self->pagemap_fd, mapping + 2 * page_size));
+ assert_filled(_metadata, mapping, 3 * page_size);
+
+ ASSERT_EQ(munmap(mapping, 3 * page_size), 0);
+}
+
+/*
+ * MADV_PAGEOUT must not reclaim the unadvised half of a THP: the THP must be
+ * split before reclaim, or skipped. See pageout_stays_within_range about
+ * other unmapping.
+ */
+static void pageout_half_thp(struct __test_metadata *_metadata, char *mapping,
+ int pagemap_fd)
+{
+ size_t offset;
+
+ if (!pageout_until_swapped(_metadata, mapping, pmd_size / 2,
+ pagemap_fd))
+ SKIP(return, "MADV_PAGEOUT did not swap the advised half");
+
+ for (offset = pmd_size / 2; offset < pmd_size; offset += page_size)
+ ASSERT_TRUE(page_is_present(pagemap_fd, mapping + offset));
+ assert_filled(_metadata, mapping, pmd_size);
+}
+
+TEST_F(madvise, pageout_stays_within_pmd_mapped_thp)
+{
+ char *mapping;
+
+ mapping = map_thp(_metadata);
+ if (!mapping)
+ SKIP(return, "could not allocate a PMD-sized THP");
+
+ pageout_half_thp(_metadata, mapping, self->pagemap_fd);
+
+ ASSERT_EQ(munmap(mapping, pmd_size), 0);
+}
+
+TEST_F(madvise, pageout_stays_within_pte_mapped_thp)
+{
+ char *mapping;
+
+ mapping = map_thp(_metadata);
+ if (!mapping)
+ SKIP(return, "could not allocate a PMD-sized THP");
+ pte_map_thp(_metadata, mapping, self->pagemap_fd, self->kpageflags_fd);
+
+ pageout_half_thp(_metadata, mapping, self->pagemap_fd);
+
+ ASSERT_EQ(munmap(mapping, pmd_size), 0);
+}
+
+/* MADV_COLD on part of a THP may split it, but must not change it. */
+TEST_F(madvise, cold_preserves_half_of_pmd_mapped_thp)
+{
+ char *mapping;
+
+ mapping = map_thp(_metadata);
+ if (!mapping)
+ SKIP(return, "could not allocate a PMD-sized THP");
+
+ ASSERT_EQ(madvise(mapping, pmd_size / 2, MADV_COLD), 0);
+ assert_filled(_metadata, mapping, pmd_size);
+
+ ASSERT_EQ(munmap(mapping, pmd_size), 0);
+}
+
+TEST_F(madvise, cold_preserves_half_of_pte_mapped_thp)
+{
+ char *mapping;
+
+ mapping = map_thp(_metadata);
+ if (!mapping)
+ SKIP(return, "could not allocate a PMD-sized THP");
+ pte_map_thp(_metadata, mapping, self->pagemap_fd, self->kpageflags_fd);
+
+ ASSERT_EQ(madvise(mapping, pmd_size / 2, MADV_COLD), 0);
+ assert_filled(_metadata, mapping, pmd_size);
+
+ ASSERT_EQ(munmap(mapping, pmd_size), 0);
+}
+
TEST_HARNESS_MAIN
--
2.56.0
^ permalink raw reply [flat|nested] 16+ messages in thread* [PATCH 7/8] selftests/mm: check MADV_PAGEOUT file permissions
2026-10-14 17:57 [PATCH 0/8] selftests/mm: add MADV_COLD and MADV_PAGEOUT contract tests Gregory Price (Meta)
` (5 preceding siblings ...)
2026-10-14 17:57 ` [PATCH 6/8] selftests/mm: check MADV_COLD and MADV_PAGEOUT on partly advised THPs Gregory Price (Meta)
@ 2026-10-14 17:57 ` Gregory Price (Meta)
2026-10-16 12:18 ` Sarthak Sharma
2026-10-14 17:57 ` [PATCH 8/8] selftests/mm: check that unauthorized MADV_PAGEOUT skips large file folios Gregory Price (Meta)
7 siblings, 1 reply; 16+ messages in thread
From: Gregory Price (Meta) @ 2026-10-14 17:57 UTC (permalink / raw)
To: linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
MADV_PAGEOUT only pages out file pages for a caller that owns the file or
could open it for writing, per can_do_file_pageout(). Any other caller
gets nothing from a shared file mapping, and only the anonymous COW pages
from a private one.
Check that the owner of a read-only file can evict a clean page of it,
once a writable file page shows that file pages can be paged out at all.
That test runs as an unprivileged owner even when started as root, so
that capabilities cannot stand in for ownership.
Also check that an unauthorized caller leaves a shared file page resident,
and that it pages out the COW page of a private mapping but not the file
page next to it. These need root to change credentials, and skip if that
fails.
Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
---
tools/testing/selftests/mm/madvise.c | 105 +++++++++++++++++++++++++--
1 file changed, 100 insertions(+), 5 deletions(-)
diff --git a/tools/testing/selftests/mm/madvise.c b/tools/testing/selftests/mm/madvise.c
index 8822991eae4a..74202fed2d04 100644
--- a/tools/testing/selftests/mm/madvise.c
+++ b/tools/testing/selftests/mm/madvise.c
@@ -5,6 +5,7 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
+#include <grp.h>
#include <sched.h>
#include <stdbool.h>
#include <stdint.h>
@@ -12,6 +13,7 @@
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/shm.h>
+#include <sys/stat.h>
#include <unistd.h>
#include "kselftest_harness.h"
@@ -208,8 +210,9 @@ static bool page_is_present(int pagemap_fd, char *addr)
return pagemap_get_entry(pagemap_fd, addr) & PM_PRESENT;
}
-/* Map a shared mapping of an unlinked, zero-filled temporary file. */
-static char *map_temp_file(struct __test_metadata *_metadata, size_t size)
+/* 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 template[] = "/tmp/madvise-pageout-XXXXXX";
char *mapping;
@@ -219,14 +222,21 @@ static char *map_temp_file(struct __test_metadata *_metadata, size_t size)
ASSERT_GE(fd, 0);
ASSERT_EQ(unlink(template), 0);
ASSERT_EQ(ftruncate(fd, size), 0);
+ ASSERT_EQ(fchmod(fd, mode), 0);
- mapping = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ mapping = mmap(NULL, size, PROT_READ | PROT_WRITE, flags, fd, 0);
ASSERT_NE(mapping, MAP_FAILED);
close(fd);
return mapping;
}
+/* Become an unprivileged user without capabilities. */
+static bool drop_privileges(void)
+{
+ return !setgroups(0, NULL) && !setgid(65534) && !setuid(65534);
+}
+
/*
* 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
@@ -432,7 +442,7 @@ TEST_F(madvise, cold_preserves_dirty_shared_file_page)
{
char *mapping;
- mapping = map_temp_file(_metadata, page_size);
+ mapping = map_temp_file(_metadata, page_size, MAP_SHARED, 0600);
mapping[0] = 1;
ASSERT_EQ(madvise(mapping, page_size, MADV_COLD), 0);
@@ -449,7 +459,7 @@ TEST_F(madvise, pageout_preserves_dirty_shared_file_page)
{
char *mapping;
- mapping = map_temp_file(_metadata, page_size);
+ mapping = map_temp_file(_metadata, page_size, MAP_SHARED, 0600);
mapping[0] = 1;
if (!pageout_until_evicted(_metadata, mapping, page_size,
@@ -639,4 +649,89 @@ TEST_F(madvise, cold_preserves_half_of_pte_mapped_thp)
ASSERT_EQ(munmap(mapping, pmd_size), 0);
}
+/*
+ * The owner of a file may page out its clean page-cache pages. Run as an
+ * unprivileged owner, so that capabilities cannot stand in for ownership.
+ */
+TEST_F(madvise, pageout_evicts_owned_file_page)
+{
+ char *mapping, *control;
+
+ if (!geteuid() && !drop_privileges())
+ SKIP(return, "could not drop privileges");
+ pin_to_current_cpu(_metadata);
+
+ /* Make sure a writable file page can be paged out at all. */
+ control = map_temp_file(_metadata, page_size, MAP_PRIVATE, 0600);
+ ASSERT_EQ(control[0], 0);
+ if (!pageout_until_evicted(_metadata, control, page_size,
+ self->pagemap_fd))
+ SKIP(return, "MADV_PAGEOUT did not evict a file page");
+
+ mapping = map_temp_file(_metadata, page_size, MAP_PRIVATE, 0400);
+ ASSERT_EQ(mapping[0], 0);
+ ASSERT_TRUE(pageout_until_evicted(_metadata, mapping, page_size,
+ self->pagemap_fd));
+ ASSERT_EQ(mapping[0], 0);
+
+ ASSERT_EQ(munmap(mapping, page_size), 0);
+ ASSERT_EQ(munmap(control, page_size), 0);
+}
+
+/*
+ * A caller that neither owns nor may write a file must not page out its
+ * shared mappings. MADV_PAGEOUT succeeds but leaves the page resident.
+ */
+TEST_F(madvise, pageout_skips_unauthorized_shared_file)
+{
+ char *mapping;
+
+ if (geteuid())
+ SKIP(return, "requires root to change credentials");
+ mapping = map_temp_file(_metadata, page_size, MAP_SHARED, 0400);
+ if (!drop_privileges())
+ SKIP(return, "could not drop privileges");
+
+ ASSERT_EQ(mapping[0], 0);
+ ASSERT_TRUE(pagemap_is_populated(self->pagemap_fd, mapping));
+ ASSERT_EQ(madvise(mapping, page_size, MADV_PAGEOUT), 0);
+ ASSERT_TRUE(pagemap_is_populated(self->pagemap_fd, mapping));
+ ASSERT_EQ(mapping[0], 0);
+
+ ASSERT_EQ(munmap(mapping, page_size), 0);
+}
+
+/*
+ * A caller that neither owns nor may write a file may page out the anonymous
+ * COW pages of a private mapping of it, but must leave the file pages
+ * resident.
+ */
+TEST_F(madvise, pageout_private_file_reclaims_only_cow)
+{
+ char *mapping;
+ bool swapped;
+
+ if (geteuid())
+ SKIP(return, "requires root to change credentials");
+ mapping = map_temp_file(_metadata, 2 * page_size, MAP_PRIVATE,
+ 0400);
+ if (!drop_privileges())
+ SKIP(return, "could not drop privileges");
+
+ ASSERT_EQ(mapping[0], 0);
+ ASSERT_EQ(mapping[page_size], 0);
+ mapping[0] = 1;
+
+ ASSERT_EQ(madvise(mapping, 2 * page_size, MADV_PAGEOUT), 0);
+ ASSERT_TRUE(pagemap_is_populated(self->pagemap_fd,
+ mapping + page_size));
+ swapped = pagemap_is_swapped(self->pagemap_fd, mapping);
+ ASSERT_EQ(mapping[0], 1);
+ ASSERT_EQ(mapping[page_size], 0);
+ if (!swapped)
+ SKIP(return, "MADV_PAGEOUT did not swap the COW page");
+
+ ASSERT_EQ(munmap(mapping, 2 * page_size), 0);
+}
+
TEST_HARNESS_MAIN
--
2.56.0
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH 7/8] selftests/mm: check MADV_PAGEOUT file permissions
2026-10-14 17:57 ` [PATCH 7/8] selftests/mm: check MADV_PAGEOUT file permissions Gregory Price (Meta)
@ 2026-10-16 12:18 ` Sarthak Sharma
2026-10-16 13:48 ` Gregory Price
0 siblings, 1 reply; 16+ messages in thread
From: Sarthak Sharma @ 2026-10-16 12:18 UTC (permalink / raw)
To: Gregory Price (Meta), linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
Hi Gregory!
On 10/14/26 11:27 PM, Gregory Price (Meta) wrote:
> MADV_PAGEOUT only pages out file pages for a caller that owns the file or
> could open it for writing, per can_do_file_pageout(). Any other caller
> gets nothing from a shared file mapping, and only the anonymous COW pages
> from a private one.
>
> Check that the owner of a read-only file can evict a clean page of it,
> once a writable file page shows that file pages can be paged out at all.
> That test runs as an unprivileged owner even when started as root, so
> that capabilities cannot stand in for ownership.
>
> Also check that an unauthorized caller leaves a shared file page resident,
> and that it pages out the COW page of a private mapping but not the file
> page next to it. These need root to change credentials, and skip if that
> fails.
>
> Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
> ---
[...]
>
> +/*
> + * The owner of a file may page out its clean page-cache pages. Run as an
> + * unprivileged owner, so that capabilities cannot stand in for ownership.
> + */
> +TEST_F(madvise, pageout_evicts_owned_file_page)
> +{
> + char *mapping, *control;
> +
> + if (!geteuid() && !drop_privileges())
> + SKIP(return, "could not drop privileges");
> + pin_to_current_cpu(_metadata);
Is there a reason why we are pinning to the current CPU here and not in
the next two tests?
IIUC (please let me know if I am wrong here), without pinning, say in
the test pageout_private_file_reclaims_only_cow:
[...]
> + * A caller that neither owns nor may write a file may page out the anonymous
> + * COW pages of a private mapping of it, but must leave the file pages
> + * resident.
> + */
> +TEST_F(madvise, pageout_private_file_reclaims_only_cow)
> +{
> + char *mapping;
> + bool swapped;
> +
> + if (geteuid())
> + SKIP(return, "requires root to change credentials");
> + mapping = map_temp_file(_metadata, 2 * page_size, MAP_PRIVATE,
> + 0400);
> + if (!drop_privileges())
> + SKIP(return, "could not drop privileges");
> +
> + ASSERT_EQ(mapping[0], 0);
> + ASSERT_EQ(mapping[page_size], 0);
> + mapping[0] = 1;
If both pages are on CPU 1's per CPU LRU batch here and then hte task
migrates to CPU 2 here
> +
> + ASSERT_EQ(madvise(mapping, 2 * page_size, MADV_PAGEOUT), 0);
Then this can cause lru_add_drain() on CPU 2, and therefore both pages
do not reach the main LRU list. Thus they won't be reclaimed and the
test can skip here since the CoW page wasn't swapped.
> + ASSERT_TRUE(pagemap_is_populated(self->pagemap_fd,
> + mapping + page_size));
> + swapped = pagemap_is_swapped(self->pagemap_fd, mapping);
> + ASSERT_EQ(mapping[0], 1);
> + ASSERT_EQ(mapping[page_size], 0);
> + if (!swapped)
> + SKIP(return, "MADV_PAGEOUT did not swap the COW page");
Also, here we skip if we couldn't swap out the CoW page. Won't it make
more sense to check with pageout_until_swapped() first if pages can be
skipped, and then fail here if the CoW page wasn't skipped after all the
retries, similar to how some other tests implement it?
> +
> + ASSERT_EQ(munmap(mapping, 2 * page_size), 0);
> +}
> +
> TEST_HARNESS_MAIN
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH 7/8] selftests/mm: check MADV_PAGEOUT file permissions
2026-10-16 12:18 ` Sarthak Sharma
@ 2026-10-16 13:48 ` Gregory Price
0 siblings, 0 replies; 16+ messages in thread
From: Gregory Price @ 2026-10-16 13:48 UTC (permalink / raw)
To: Sarthak Sharma
Cc: linux-kselftest, linux-mm, linux-kernel, kernel-team, akpm,
david, ljs, liam, vbabka, rppt, surenb, mhocko, shuah
On Fri, Oct 16, 2026 at 05:48:11PM +0530, Sarthak Sharma wrote:
> > +/*
> > + * The owner of a file may page out its clean page-cache pages. Run as an
> > + * unprivileged owner, so that capabilities cannot stand in for ownership.
> > + */
> > +TEST_F(madvise, pageout_evicts_owned_file_page)
> > +{
> > + char *mapping, *control;
> > +
> > + if (!geteuid() && !drop_privileges())
> > + SKIP(return, "could not drop privileges");
> > + pin_to_current_cpu(_metadata);
>
> Is there a reason why we are pinning to the current CPU here and not in
> the next two tests?
Oversight on my part. I added it where i got a failure, but didn't
carry it forward to the other tests.
> > + ASSERT_EQ(mapping[0], 0);
> > + ASSERT_EQ(mapping[page_size], 0);
> > + mapping[0] = 1;
>
> If both pages are on CPU 1's per CPU LRU batch here and then hte task
> migrates to CPU 2 here
>
> > +
> > + ASSERT_EQ(madvise(mapping, 2 * page_size, MADV_PAGEOUT), 0);
>
> Then this can cause lru_add_drain() on CPU 2, and therefore both pages
> do not reach the main LRU list. Thus they won't be reclaimed and the
> test can skip here since the CoW page wasn't swapped.
I think there are other corner cases here as well. So yes i'll set up
thei pinning - probably worth it to just pin the entire suite of tests.
My original set had a bunch of this tested more explicitly, but this is
really implementation details that are not worth the additional test
complexity. I think we can just rid ourselves of that and just pin
every test and make sure it does what we expect in that case.
>
> > + ASSERT_TRUE(pagemap_is_populated(self->pagemap_fd,
> > + mapping + page_size));
> > + swapped = pagemap_is_swapped(self->pagemap_fd, mapping);
> > + ASSERT_EQ(mapping[0], 1);
> > + ASSERT_EQ(mapping[page_size], 0);
> > + if (!swapped)
> > + SKIP(return, "MADV_PAGEOUT did not swap the COW page");
>
> Also, here we skip if we couldn't swap out the CoW page. Won't it make
> more sense to check with pageout_until_swapped() first if pages can be
> skipped, and then fail here if the CoW page wasn't skipped after all the
> retries, similar to how some other tests implement it?
>
Ah yeah good catch. I'll adjust like this:
/* Ensure pageout-swap actually works first with a control page */
control[0] = 1;
if (!pageout_until_swapped(_metadata, control, page_size,
self->pagemap_fd))
SKIP(return, "MADV_PAGEOUT did not swap the control page");
/* Run the actual test */
ASSERT_EQ(mapping[0], 0);
ASSERT_EQ(mapping[page_size], 0);
mapping[0] = 1;
ASSERT_TRUE(pageout_until_swapped(_metadata, mapping, 2 * page_size,
self->pagemap_fd));
ASSERT_TRUE(pagemap_is_populated(self->pagemap_fd,
mapping + page_size));
ASSERT_EQ(mapping[0], 1);
ASSERT_EQ(mapping[page_size], 0);
~Gregory
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH 8/8] selftests/mm: check that unauthorized MADV_PAGEOUT skips large file folios
2026-10-14 17:57 [PATCH 0/8] selftests/mm: add MADV_COLD and MADV_PAGEOUT contract tests Gregory Price (Meta)
` (6 preceding siblings ...)
2026-10-14 17:57 ` [PATCH 7/8] selftests/mm: check MADV_PAGEOUT file permissions Gregory Price (Meta)
@ 2026-10-14 17:57 ` Gregory Price (Meta)
2026-10-14 21:26 ` Gregory Price
7 siblings, 1 reply; 16+ messages in thread
From: Gregory Price (Meta) @ 2026-10-14 17:57 UTC (permalink / raw)
To: linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
A caller that neither owns nor may write a file may page out the COW pages
of a private mapping of it, but not its file pages. When the advised range
only partly covers a large file folio, the folio must be filtered out
before it is split or reclaimed, on separate paths for PMD- and PTE-mapped
folios.
Check that partial MADV_PAGEOUT by such a caller leaves every page of a
PMD-sized shmem folio resident, both when a PMD maps it and when a mapping
of half of it maps it with PTEs.
Signed-off-by: Gregory Price (Meta) <gourry@gourry.net>
---
tools/testing/selftests/mm/madvise.c | 121 +++++++++++++++++++++++++--
1 file changed, 115 insertions(+), 6 deletions(-)
diff --git a/tools/testing/selftests/mm/madvise.c b/tools/testing/selftests/mm/madvise.c
index 74202fed2d04..d59c03ec8191 100644
--- a/tools/testing/selftests/mm/madvise.c
+++ b/tools/testing/selftests/mm/madvise.c
@@ -88,10 +88,11 @@ static char *map_aligned_pages(struct __test_metadata *_metadata, size_t size)
}
/*
- * Collapse a PMD-aligned range into PMD-mapped anonymous THPs. MADV_COLLAPSE
- * may fail transiently with EAGAIN.
+ * Collapse a PMD-aligned range into PMD-mapped THPs, and confirm the result
+ * with check_huge(). MADV_COLLAPSE may fail transiently with EAGAIN.
*/
-static bool collapse(char *mapping, size_t size)
+static bool collapse(char *mapping, size_t size,
+ bool (*check_huge)(void *, size_t, int, uint64_t))
{
int nr_hpages = size / pmd_size;
int retry;
@@ -100,8 +101,7 @@ static bool collapse(char *mapping, size_t size)
if (madvise(mapping, size, MADV_COLLAPSE)) {
if (errno != EAGAIN)
return false;
- } else if (check_huge_anon(mapping, size, nr_hpages,
- pmd_size)) {
+ } else if (check_huge(mapping, size, nr_hpages, pmd_size)) {
return true;
}
usleep(10000);
@@ -118,7 +118,7 @@ static char *map_thp(struct __test_metadata *_metadata)
if (!pmd_size)
return NULL;
mapping = map_aligned_pages(_metadata, pmd_size);
- if (collapse(mapping, pmd_size))
+ if (collapse(mapping, pmd_size, check_huge_anon))
return mapping;
ASSERT_EQ(munmap(mapping, pmd_size), 0);
return NULL;
@@ -147,6 +147,33 @@ static void pte_map_thp(struct __test_metadata *_metadata, char *mapping,
ASSERT_TRUE(flags & KPF_COMPOUND_HEAD);
}
+static char *map_aligned_file(struct __test_metadata *_metadata, int fd,
+ size_t size, int flags)
+{
+ char *mapping = reserve_aligned(_metadata, size);
+
+ ASSERT_EQ(mmap(mapping, size, PROT_READ | PROT_WRITE, flags | MAP_FIXED,
+ fd, 0), mapping);
+
+ return mapping;
+}
+
+/* Create a PMD-sized shmem file filled with 1s. */
+static int create_shmem_file(struct __test_metadata *_metadata)
+{
+ char *shared;
+ int fd;
+
+ fd = memfd_create("madvise-pageout", 0);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(ftruncate(fd, pmd_size), 0);
+ shared = map_aligned_file(_metadata, fd, pmd_size, MAP_SHARED);
+ memset(shared, 1, pmd_size);
+ ASSERT_EQ(munmap(shared, pmd_size), 0);
+
+ return fd;
+}
+
static void assert_filled(struct __test_metadata *_metadata,
const char *mapping, size_t size)
{
@@ -734,4 +761,86 @@ TEST_F(madvise, pageout_private_file_reclaims_only_cow)
ASSERT_EQ(munmap(mapping, 2 * page_size), 0);
}
+/*
+ * An unauthorized caller must not page out a large file folio of a private
+ * mapping, even when the advised range only partly covers it. See
+ * pageout_stays_within_range about other unmapping.
+ */
+TEST_F(madvise, pageout_private_file_skips_large_file_folio)
+{
+ char *mapping;
+ size_t offset;
+ int fd;
+
+ if (!pmd_size)
+ SKIP(return, "PMD-sized THPs are not supported");
+ if (geteuid())
+ SKIP(return, "requires root to change credentials");
+ fd = create_shmem_file(_metadata);
+ mapping = map_aligned_file(_metadata, fd, pmd_size, MAP_PRIVATE);
+ if (!collapse(mapping, pmd_size, check_huge_file))
+ SKIP(return, "could not allocate a PMD-sized shmem THP");
+ ASSERT_EQ(fchmod(fd, 0400), 0);
+ if (!drop_privileges())
+ SKIP(return, "could not drop privileges");
+
+ ASSERT_EQ(madvise(mapping, pmd_size / 2, MADV_PAGEOUT), 0);
+ for (offset = 0; offset < pmd_size; offset += page_size)
+ ASSERT_TRUE(page_is_present(self->pagemap_fd,
+ mapping + offset));
+ assert_filled(_metadata, mapping, pmd_size);
+
+ ASSERT_EQ(munmap(mapping, pmd_size), 0);
+ close(fd);
+}
+
+/*
+ * An unauthorized caller must not page out a large file folio that a private
+ * mapping maps with PTEs, even when the advised range covers only part of it.
+ * See pageout_stays_within_range about other unmapping.
+ */
+TEST_F(madvise, pageout_private_file_skips_pte_mapped_file_folio)
+{
+ const size_t size = pmd_size / 2;
+ char *mapping, *shared;
+ unsigned long pfn;
+ uint64_t flags;
+ size_t offset;
+ int fd;
+
+ if (!pmd_size)
+ SKIP(return, "PMD-sized THPs are not supported");
+ if (geteuid())
+ SKIP(return, "requires root to change credentials");
+ if (self->kpageflags_fd < 0)
+ SKIP(return, "requires root to read page flags");
+ fd = create_shmem_file(_metadata);
+ shared = map_aligned_file(_metadata, fd, pmd_size, MAP_SHARED);
+ if (!collapse(shared, pmd_size, check_huge_file))
+ SKIP(return, "could not allocate a PMD-sized shmem THP");
+ ASSERT_EQ(munmap(shared, pmd_size), 0);
+
+ /* Mapping half of the folio leaves it mapped by PTEs. */
+ mapping = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+ assert_filled(_metadata, mapping, size);
+ 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_COMPOUND_HEAD))
+ SKIP(return, "the shmem THP was split");
+ ASSERT_EQ(fchmod(fd, 0400), 0);
+ if (!drop_privileges())
+ SKIP(return, "could not drop privileges");
+
+ ASSERT_EQ(madvise(mapping, size / 2, MADV_PAGEOUT), 0);
+ for (offset = 0; offset < size; offset += page_size)
+ ASSERT_TRUE(page_is_present(self->pagemap_fd,
+ mapping + offset));
+ assert_filled(_metadata, mapping, size);
+
+ ASSERT_EQ(munmap(mapping, size), 0);
+ close(fd);
+}
+
TEST_HARNESS_MAIN
--
2.56.0
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH 8/8] selftests/mm: check that unauthorized MADV_PAGEOUT skips large file folios
2026-10-14 17:57 ` [PATCH 8/8] selftests/mm: check that unauthorized MADV_PAGEOUT skips large file folios Gregory Price (Meta)
@ 2026-10-14 21:26 ` Gregory Price
0 siblings, 0 replies; 16+ messages in thread
From: Gregory Price @ 2026-10-14 21:26 UTC (permalink / raw)
To: linux-kselftest, linux-mm
Cc: linux-kernel, kernel-team, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, shuah
On Wed, Oct 14, 2026 at 01:57:56PM -0400, Gregory Price (Meta) wrote:
> A caller that neither owns nor may write a file may page out the COW pages
> of a private mapping of it, but not its file pages. When the advised range
> only partly covers a large file folio, the folio must be filtered out
> before it is split or reclaimed, on separate paths for PMD- and PTE-mapped
> folios.
>
> Check that partial MADV_PAGEOUT by such a caller leaves every page of a
> PMD-sized shmem folio resident, both when a PMD maps it and when a mapping
> of half of it maps it with PTEs.
>
... snip ...
> + ASSERT_EQ(madvise(mapping, pmd_size / 2, MADV_PAGEOUT), 0);
> + for (offset = 0; offset < pmd_size; offset += page_size)
> + ASSERT_TRUE(page_is_present(self->pagemap_fd,
> + mapping + offset));
> + assert_filled(_metadata, mapping, pmd_size);
Sashiko reported:
The commit message states that the folio must be filtered out before it is
split or reclaimed. Since page_is_present() only confirms the pages were not
reclaimed, should this test also verify that the underlying folio was not
split by the partial-range madvise?
I'm fairly sure this is a false-positive. Splitting a file folio unmaps it
entirely, so the page_is_present assert would fail.
~Gregory
^ permalink raw reply [flat|nested] 16+ messages in thread