On Tue, Sep 22, 2026 at 07:58:21PM -0400, Gregory Price wrote:
> 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 28 cases ahead of a planned walker refactor. All cases check syscall
> results and mapping contents, but they deliberately have different review
> contracts.

Yikes that's a lot! :)

>
> This commit was originally 28 individual commits, but was squashed for
> the initial RFC to limit noise during initial review. The question is
> what set of self-tests should retain.
>
> The eight user-visible contract tests verify:
>
>   - MADV_COLD does not populate a PTE hole;
>   - COLD and PAGEOUT reject a locked VMA;
>   - full-range PAGEOUT swaps a PMD-mapped THP without corrupting it;
>   - MADV_COLD does not fault in a swapped PTE;
>   - PAGEOUT preserves an unevictable base page;
>   - authorized file PAGEOUT evicts a clean file page;
>   - unauthorized shared-file PAGEOUT leaves the page resident; and
>   - private-file PAGEOUT reclaims a COW page but preserves a file page.
>
> The eighteen implementation-detail tests record the current folio and
> page-table behavior:
>
>   - full and partial MADV_COLD on a PTE-mapped THP;
>   - full, repeated and partial MADV_COLD on PMD-mapped THPs;
>   - MADV_COLD on the shared zero page and huge zero page;
>   - partial MADV_COLD on a shared PMD-mapped THP;
>   - partial MADV_COLD on pinned PMD- and PTE-mapped THPs;
>   - parallel partial MADV_COLD split attempts;
>   - PAGEOUT on an unevictable THP;
>   - MADV_COLD on active base-page and huge-PMD folios;
>   - partial PAGEOUT on PMD- and PTE-mapped THPs;
>   - partial and full MADV_COLD on shared PTE-mapped THPs; and
>   - PAGEOUT filtering of a PMD-sized protected file folio.
>
> These tests intentionally assert splitting, PMD/PTE mapping shape, folio
> flags or folio sharing. They may need adjustment after a valid kernel
> implementation change. Their purpose here is to detect unintended changes
> during the refactor.
>
> Two additional stress tests use implementation-specific setup while keeping
> contract-level assertions:
>
>   - MADV_COLD on a folio still queued in another CPU's LRU batch
>   - MADV_PAGEOUT during NUMA migration of unevictable shmem THPs.
>
> The stress tests require only successful calls and preserved contents.
> They do not assert which internal path won. Tests requiring root, swap,
> THP allocation, multiple CPUs or NUMA nodes report a skip when their
> prerequisites are unavailable.

I think in general we want to avoid putting stress tests as part of the
selftests in general? Or at least if they might take a long time to run or
excessive memory usage, etc.

Though that might be benchmarks, as we have the THP stress tests so maybe it's
OK?

>
> Assisted-by: LLM
> Signed-off-by: Gregory Price (Meta) <[email protected]>

In any case I really do think this should be split out into smaller parts, this
is a _gigantic_ change :)

Since you list a whole bunch of different test areas and you're exercising
different things you should be able to split this out logically across those I
think?

I like the idea of establishing some sort of invariants first before
implementing changes though in general.

> ---
>  tools/testing/selftests/mm/Makefile        |    2 +
>  tools/testing/selftests/mm/ksft_madvise.sh |    4 +
>  tools/testing/selftests/mm/madvise.c       | 1673 ++++++++++++++++++++
>  tools/testing/selftests/mm/run_vmtests.sh  |    5 +
>  tools/testing/selftests/mm/vm_util.c       |    4 +-
>  tools/testing/selftests/mm/vm_util.h       |    6 +
>  6 files changed, 1692 insertions(+), 2 deletions(-)
>  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 beacc0f873049..8912aa85ca273 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))
> @@ -155,6 +156,7 @@ TEST_PROGS += ksft_kmemleak_dedup.sh
>  TEST_PROGS += ksft_ksm.sh
>  TEST_PROGS += ksft_ksm_numa.sh
>  TEST_PROGS += ksft_madv_guard.sh
> +TEST_PROGS += ksft_madvise.sh
>  TEST_PROGS += ksft_madv_populate.sh
>  TEST_PROGS += ksft_memfd_secret.sh
>  TEST_PROGS += ksft_memory_failure.sh
> diff --git a/tools/testing/selftests/mm/ksft_madvise.sh 
> b/tools/testing/selftests/mm/ksft_madvise.sh
> new file mode 100755
> index 0000000000000..29cbbca800f2c
> --- /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 0000000000000..abde0b1c8a42e
> --- /dev/null
> +++ b/tools/testing/selftests/mm/madvise.c
> @@ -0,0 +1,1673 @@
> +// SPDX-License-Identifier: GPL-2.0
> +#define _GNU_SOURCE
> +
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <pthread.h>
> +#include <sched.h>
> +#include <stdbool.h>
> +#include <stdatomic.h>
> +#include <stdint.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <linux/mman.h>
> +#include <linux/mempolicy.h>
> +#include <sys/ipc.h>
> +#include <sys/mman.h>
> +#include <sys/shm.h>
> +#include <sys/stat.h>
> +#include <sys/syscall.h>
> +#include <sys/uio.h>
> +#include <sys/wait.h>
> +#include <unistd.h>
> +
> +#include "kselftest.h"
> +#include "vm_util.h"
> +
> +#ifndef MADV_COLLAPSE
> +#define MADV_COLLAPSE 25
> +#endif
> +
> +#define NR_CONCURRENT_THREADS 8
> +#define NR_CONCURRENT_THPS 8
> +#define NR_CONCURRENT_ROUNDS 400
> +#define NR_MIGRATION_RACE_THPS 16
> +#define NR_MIGRATION_RACE_ROUNDS 20
> +#define NUMA_MASK_BITS 1024
> +#define NUMA_MASK_LONGS (NUMA_MASK_BITS / (8 * sizeof(unsigned long)))
> +
> +static pthread_barrier_t concurrent_start_barrier;
> +static pthread_barrier_t concurrent_done_barrier;
> +static atomic_int concurrent_worker_errno;
> +static char *concurrent_area;
> +static size_t pmd_size;
> +
> +#if defined(SYS_get_mempolicy) && defined(SYS_mbind) && \
> +     defined(SYS_migrate_pages)
> +struct migration_pageout_data {
> +     pthread_barrier_t start_barrier;
> +     atomic_bool stop;
> +     atomic_int calls;
> +     atomic_int error;
> +     char *mapping;
> +     size_t size;
> +};
> +
> +static bool find_two_memory_nodes(unsigned long *mask, int *node1, int 
> *node2)
> +{
> +     int node;
> +
> +     if (syscall(SYS_get_mempolicy, NULL, mask, NUMA_MASK_BITS + 1, NULL,
> +                 MPOL_F_MEMS_ALLOWED))
> +             return false;
> +
> +     *node1 = *node2 = -1;
> +     for (node = 0; node < NUMA_MASK_BITS; node++) {
> +             if (!(mask[node / (8 * sizeof(*mask))] &
> +                   (1UL << (node % (8 * sizeof(*mask))))))
> +                     continue;
> +             if (*node1 < 0) {
> +                     *node1 = node;
> +             } else {
> +                     *node2 = node;
> +                     return true;
> +             }
> +     }
> +
> +     return false;
> +}
> +
> +static void numa_mask_set(unsigned long *mask, int node)
> +{
> +     mask[node / (8 * sizeof(*mask))] |=
> +             1UL << (node % (8 * sizeof(*mask)));
> +}
> +
> +static void *madvise_pageout_worker(void *arg)
> +{
> +     struct migration_pageout_data *data = arg;
> +
> +     pthread_barrier_wait(&data->start_barrier);
> +     while (!atomic_load_explicit(&data->stop, memory_order_relaxed)) {
> +             if (madvise(data->mapping, data->size, MADV_PAGEOUT) &&
> +                 errno != EAGAIN) {
> +                     atomic_store(&data->error, errno);
> +                     break;
> +             }
> +             atomic_fetch_add(&data->calls, 1);
> +     }
> +
> +     return NULL;
> +}
> +
> +static int migrate_between_nodes(unsigned long *old_nodes,
> +                              unsigned long *new_nodes)
> +{
> +     int i;
> +
> +     for (i = 0; i < NR_MIGRATION_RACE_ROUNDS; i++) {
> +             unsigned long *tmp;
> +
> +             if (syscall(SYS_migrate_pages, 0, NUMA_MASK_BITS + 1,
> +                         old_nodes, new_nodes) < 0)
> +                     return errno;
> +             tmp = old_nodes;
> +             old_nodes = new_nodes;
> +             new_nodes = tmp;
> +     }
> +
> +     return 0;
> +}
> +
> +static int race_pageout_with_migration(char *mapping, size_t size,
> +                                    unsigned long *mask1,
> +                                    unsigned long *mask2)
> +{
> +     struct migration_pageout_data data = {
> +             .mapping = mapping,
> +             .size = size,
> +     };
> +     pthread_t thread;
> +     int ret;
> +
> +     ret = pthread_barrier_init(&data.start_barrier, NULL, 2);
> +     if (ret)
> +             return ret;
> +     ret = pthread_create(&thread, NULL, madvise_pageout_worker, &data);
> +     if (ret) {
> +             pthread_barrier_destroy(&data.start_barrier);
> +             return ret;
> +     }
> +     pthread_barrier_wait(&data.start_barrier);
> +
> +     ret = migrate_between_nodes(mask1, mask2);
> +     atomic_store(&data.stop, true);
> +     pthread_join(thread, NULL);
> +     pthread_barrier_destroy(&data.start_barrier);
> +
> +     if (ret)
> +             return ret;
> +     if (atomic_load(&data.error))
> +             return atomic_load(&data.error);
> +     return atomic_load(&data.calls) ? 0 : EIO;
> +}
> +#endif
> +
> +static char *map_aligned_pages(size_t size)
> +{
> +     char *mapping, *aligned;
> +
> +     mapping = mmap(NULL, size + pmd_size, PROT_READ | PROT_WRITE,
> +                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +     if (mapping == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap");
> +
> +     aligned = (char *)(((uintptr_t)mapping + pmd_size - 1) &
> +                        ~(pmd_size - 1));
> +     if (aligned != mapping)
> +             munmap(mapping, aligned - mapping);
> +     if (aligned + size < mapping + size + pmd_size)
> +             munmap(aligned + size,
> +                    mapping + size + pmd_size - (aligned + size));
> +
> +     memset(aligned, 1, size);
> +     return aligned;
> +}
> +
> +/* MADV_COLLAPSE may fail transiently with EAGAIN. */
> +static bool collapse_all(char *mapping, size_t size, int nr_hpages)
> +{
> +     int ret, retry;
> +
> +     for (retry = 0; retry < 10; retry++) {
> +             ret = madvise(mapping, size, MADV_COLLAPSE);
> +             if (!ret) {
> +                     if (check_huge_anon(mapping, size, nr_hpages, pmd_size))
> +                             return true;
> +             } else if (errno != EAGAIN) {
> +                     return false;
> +             }
> +             usleep(10000);
> +     }
> +
> +     return false;
> +}
> +
> +static bool collapse_shmem(char *mapping, size_t size, int nr_hpages)
> +{
> +     int ret, retry;
> +
> +     for (retry = 0; retry < 10; retry++) {
> +             ret = madvise(mapping, size, MADV_COLLAPSE);
> +             if (!ret) {
> +                     if (check_huge_shmem(mapping, size, nr_hpages, 
> pmd_size))
> +                             return true;
> +             } else if (errno != EAGAIN) {
> +                     return false;
> +             }
> +             usleep(10000);
> +     }
> +
> +     return false;
> +}
> +
> +static char *map_unevictable_shmem(size_t size, int *shmid)
> +{
> +     char *reservation, *mapping, *aligned;
> +
> +     reservation = mmap(NULL, size + pmd_size, PROT_NONE,
> +                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +     if (reservation == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap reservation");
> +     aligned = (char *)(((uintptr_t)reservation + pmd_size - 1) &
> +                        ~(pmd_size - 1));
> +     munmap(reservation, size + pmd_size);
> +
> +     *shmid = shmget(IPC_PRIVATE, size, IPC_CREAT | 0600);
> +     if (*shmid < 0)
> +             ksft_exit_fail_perror("shmget");
> +     if (shmctl(*shmid, SHM_LOCK, NULL)) {
> +             shmctl(*shmid, IPC_RMID, NULL);
> +             ksft_test_result_skip("could not lock a shmem segment\n");
> +             return MAP_FAILED;
> +     }
> +     mapping = shmat(*shmid, aligned, 0);
> +     if (mapping == (void *)-1)
> +             ksft_exit_fail_perror("shmat");
> +     if (mapping != aligned)
> +             ksft_exit_fail_msg("shmat did not honor the aligned address\n");
> +
> +     return mapping;
> +}
> +
> +static void unmap_unevictable_shmem(char *mapping, int shmid)
> +{
> +     shmctl(shmid, SHM_UNLOCK, NULL);
> +     shmdt(mapping);
> +     shmctl(shmid, IPC_RMID, NULL);
> +}
> +
> +static char *map_aligned_file(int fd, size_t size, int flags)
> +{
> +     char *mapping, *aligned;
> +
> +     mapping = mmap(NULL, size + pmd_size, PROT_NONE,
> +                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +     if (mapping == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap reservation");
> +     aligned = (char *)(((uintptr_t)mapping + pmd_size - 1) &
> +                        ~(pmd_size - 1));
> +     if (mmap(aligned, size, PROT_READ | PROT_WRITE, flags | MAP_FIXED,
> +              fd, 0) == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap file");
> +     if (aligned != mapping)
> +             munmap(mapping, aligned - mapping);
> +     if (aligned + size < mapping + size + pmd_size)
> +             munmap(aligned + size,
> +                    mapping + size + pmd_size - (aligned + size));
> +
> +     return aligned;
> +}
> +
> +static void split_pmd_mapping(char *mapping)
> +{
> +     const size_t page_size = getpagesize();
> +
> +     if (mprotect(mapping + page_size, page_size, PROT_READ) ||
> +         mprotect(mapping + page_size, page_size, PROT_READ | PROT_WRITE))
> +             ksft_exit_fail_perror("mprotect");
> +     if (!check_large_folios(mapping, pmd_size, 1, pmd_size))
> +             ksft_exit_fail_msg("mprotect split the physical THP\n");
> +}
> +
> +static void check_memory(char *mapping, size_t size)
> +{
> +     size_t offset;
> +
> +     for (offset = 0; offset < size; offset += getpagesize())
> +             if (mapping[offset] != 1)
> +                     ksft_exit_fail_msg("memory changed at offset %zu\n", 
> offset);
> +}
> +
> +/*
> + * MADV_COLD on a full PTE-mapped THP must preserve the folio, while an
> + * operation on only half of it must split the folio. Neither operation may
> + * alter the mapping contents.
> + */
> +static void test_pte_mapped_madvise_cold(void)
> +{
> +     char *mapping = map_aligned_pages(pmd_size);
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +
> +     split_pmd_mapping(mapping);
> +     if (madvise(mapping, pmd_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (!check_large_folios(mapping, pmd_size, 1, pmd_size))
> +             ksft_exit_fail_msg("full MADV_COLD split a large folio\n");
> +
> +     if (madvise(mapping, pmd_size / 2, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (!check_large_folios(mapping, pmd_size, 0, pmd_size))
> +             ksft_exit_fail_msg("partial MADV_COLD left a large folio\n");
> +     check_memory(mapping, pmd_size);
> +
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("MADV_COLD handles a PTE-mapped THP\n");
> +}
> +
> +/*
> + * A PTE walk must skip holes without populating them or overlooking the
> + * present pages on either side.
> + */
> +static void test_madvise_cold_pte_hole(void)
> +{
> +     const size_t page_size = getpagesize();
> +     const size_t size = 3 * page_size;
> +     char *mapping;
> +     int pagemap_fd;
> +
> +     mapping = mmap(NULL, size, PROT_READ | PROT_WRITE,
> +                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +     if (mapping == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap");
> +     mapping[0] = 1;
> +     mapping[2 * page_size] = 1;
> +
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     if (pagemap_fd < 0)
> +             ksft_exit_fail_perror("open pagemap");
> +     if (pagemap_is_populated(pagemap_fd, mapping + page_size))
> +             ksft_exit_fail_msg("PTE hole was populated before MADV_COLD\n");
> +
> +     if (madvise(mapping, size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (mapping[0] != 1 || mapping[2 * page_size] != 1)
> +             ksft_exit_fail_msg("MADV_COLD changed populated pages\n");
> +     if (pagemap_is_populated(pagemap_fd, mapping + page_size))
> +             ksft_exit_fail_msg("MADV_COLD populated a PTE hole\n");
> +
> +     close(pagemap_fd);
> +     munmap(mapping, size);
> +     ksft_test_result_pass("MADV_COLD skips PTE holes\n");
> +}
> +
> +/*
> + * A read fault on private anonymous memory may install the shared zero page.
> + * MADV_COLD must ignore that special PTE without replacing it or changing 
> the
> + * mapping contents.
> + */
> +static void test_madvise_cold_zero_page(void)
> +{
> +     const size_t page_size = getpagesize();
> +     char *mapping;
> +     int pagemap_fd;
> +
> +     mapping = mmap(NULL, page_size, PROT_READ,
> +                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +     if (mapping == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap");
> +     if (mapping[0])
> +             ksft_exit_fail_msg("anonymous mapping is not zero-filled\n");
> +
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     if (pagemap_fd < 0)
> +             ksft_exit_fail_perror("open pagemap");
> +     if (!pagemap_is_populated(pagemap_fd, mapping))
> +             ksft_exit_fail_msg("zero page is not populated\n");
> +
> +     if (madvise(mapping, page_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (mapping[0] || !pagemap_is_populated(pagemap_fd, mapping))
> +             ksft_exit_fail_msg("MADV_COLD changed the zero-page mapping\n");
> +
> +     close(pagemap_fd);
> +     munmap(mapping, page_size);
> +     ksft_test_result_pass("MADV_COLD skips the shared zero page\n");
> +}
> +
> +/*
> + * A read fault may map the shared huge zero page with a PMD. MADV_COLD must
> + * ignore that special PMD without replacing it or changing the mapping.
> + */
> +static void test_madvise_cold_huge_zero_page(void)
> +{
> +     char *reservation, *mapping;
> +     unsigned long pfn;
> +     uint64_t flags;
> +     int pagemap_fd, kpageflags_fd;
> +
> +     if (geteuid()) {
> +             ksft_test_result_skip("requires root to read page flags\n");
> +             return;
> +     }
> +     reservation = mmap(NULL, 2 * pmd_size, PROT_NONE,
> +                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +     if (reservation == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap reservation");
> +     mapping = (char *)(((uintptr_t)reservation + pmd_size - 1) &
> +                        ~(pmd_size - 1));
> +     if (mmap(mapping, pmd_size, PROT_READ,
> +              MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap huge zero page");
> +     if (mapping != reservation)
> +             munmap(reservation, mapping - reservation);
> +     if (mapping + pmd_size < reservation + 2 * pmd_size)
> +             munmap(mapping + pmd_size,
> +                    reservation + 2 * pmd_size - (mapping + pmd_size));
> +     if (madvise(mapping, pmd_size, MADV_HUGEPAGE))
> +             ksft_exit_fail_perror("MADV_HUGEPAGE");
> +     if (mapping[0])
> +             ksft_exit_fail_msg("anonymous mapping is not zero-filled\n");
> +
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
> +     if (pagemap_fd < 0 || kpageflags_fd < 0)
> +             ksft_exit_fail_perror("open page flags");
> +     pfn = pagemap_get_pfn(pagemap_fd, mapping);
> +     if (pfn == -1ul || pageflags_get(pfn, kpageflags_fd, &flags) ||
> +         !(flags & KPF_ZERO_PAGE)) {
> +             close(kpageflags_fd);
> +             close(pagemap_fd);
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("huge zero page is not available\n");
> +             return;
> +     }
> +
> +     if (madvise(mapping, pmd_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (mapping[0] || pagemap_get_pfn(pagemap_fd, mapping) != pfn)
> +             ksft_exit_fail_msg("MADV_COLD changed the huge-zero-page 
> mapping\n");
> +
> +     close(kpageflags_fd);
> +     close(pagemap_fd);
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("MADV_COLD skips the huge zero page\n");
> +}
> +
> +/*
> + * Neither hint is valid for a locked VMA: reclaiming it would violate the
> + * mlock contract, and merely aging it would serve no purpose.
> + */
> +static void test_madvise_lru_locked_vma(void)
> +{
> +     const size_t page_size = getpagesize();
> +     char *mapping;
> +
> +     mapping = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
> +                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +     if (mapping == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap");
> +     mapping[0] = 1;
> +     if (mlock(mapping, page_size)) {
> +             munmap(mapping, page_size);
> +             ksft_test_result_skip("could not lock a page\n");
> +             return;
> +     }
> +
> +     errno = 0;
> +     if (!madvise(mapping, page_size, MADV_COLD) || errno != EINVAL)
> +             ksft_exit_fail_msg("MADV_COLD accepted a locked VMA\n");
> +     errno = 0;
> +     if (!madvise(mapping, page_size, MADV_PAGEOUT) || errno != EINVAL)
> +             ksft_exit_fail_msg("MADV_PAGEOUT accepted a locked VMA\n");
> +     if (mapping[0] != 1)
> +             ksft_exit_fail_msg("madvise changed locked memory\n");
> +
> +     munlock(mapping, page_size);
> +     munmap(mapping, page_size);
> +     ksft_test_result_pass("COLD and PAGEOUT reject a locked VMA\n");
> +}
> +
> +/*
> + * A full-range MADV_COLD operates directly on a huge PMD. Assert that aging
> + * the mapping preserves both the physical THP and its contents.
> + */
> +static void test_full_pmd_madvise_cold(void)
> +{
> +     char *mapping = map_aligned_pages(pmd_size);
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +     if (madvise(mapping, pmd_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (!check_large_folios(mapping, pmd_size, 1, pmd_size))
> +             ksft_exit_fail_msg("full MADV_COLD split a PMD-mapped THP\n");
> +     check_memory(mapping, pmd_size);
> +
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("full MADV_COLD preserves a PMD-mapped THP\n");
> +}
> +
> +/*
> + * MADV_COLD is idempotent. A second request must handle an already-old huge
> + * PMD without splitting the THP or changing its contents.
> + */
> +static void test_repeated_pmd_madvise_cold(void)
> +{
> +     char *mapping = map_aligned_pages(pmd_size);
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +     if (madvise(mapping, pmd_size, MADV_COLD) ||
> +         madvise(mapping, pmd_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (!check_large_folios(mapping, pmd_size, 1, pmd_size))
> +             ksft_exit_fail_msg("repeated MADV_COLD split a PMD-mapped 
> THP\n");
> +     check_memory(mapping, pmd_size);
> +
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("repeated MADV_COLD preserves a PMD-mapped 
> THP\n");
> +}
> +
> +/*
> + * MADV_COLD on half of a PMD-mapped THP must split the folio so the
> + * unadvised half is not aged as part of the THP.
> + */
> +static void test_partial_pmd_madvise_cold(void)
> +{
> +     char *mapping = map_aligned_pages(pmd_size);
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +     if (madvise(mapping, pmd_size / 2, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (!check_large_folios(mapping, pmd_size, 0, pmd_size))
> +             ksft_exit_fail_msg("partial MADV_COLD left a PMD-mapped THP\n");
> +     check_memory(mapping, pmd_size);
> +
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("partial MADV_COLD splits a PMD-mapped THP\n");
> +}
> +
> +static void pageout_half_thp(char *mapping)
> +{
> +     if (madvise(mapping, pmd_size / 2, MADV_PAGEOUT))
> +             ksft_exit_fail_perror("MADV_PAGEOUT");
> +     if (!check_large_folios(mapping, pmd_size, 0, pmd_size))
> +             ksft_exit_fail_msg("partial MADV_PAGEOUT left a large folio\n");
> +     check_memory(mapping, pmd_size);
> +}
> +
> +/*
> + * Full-range MADV_PAGEOUT can reclaim a PMD-mapped THP directly. Assert that
> + * the mapping is swapped without corrupting the folio contents.
> + */
> +static void test_full_pmd_madvise_pageout(void)
> +{
> +     char *mapping = map_aligned_pages(pmd_size);
> +     int pagemap_fd, retry;
> +     bool swapped = false;
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     if (pagemap_fd < 0)
> +             ksft_exit_fail_perror("open pagemap");
> +
> +     for (retry = 0; retry < 100; retry++) {
> +             if (madvise(mapping, pmd_size, MADV_PAGEOUT))
> +                     ksft_exit_fail_perror("MADV_PAGEOUT");
> +             if (pagemap_is_swapped(pagemap_fd, mapping)) {
> +                     swapped = true;
> +                     break;
> +             }
> +             usleep(10000);
> +     }
> +     if (swapped)
> +             check_memory(mapping, pmd_size);
> +
> +     close(pagemap_fd);
> +     munmap(mapping, pmd_size);
> +     if (!swapped) {
> +             ksft_test_result_skip("MADV_PAGEOUT did not swap the THP\n");
> +             return;
> +     }
> +     ksft_test_result_pass("full MADV_PAGEOUT swaps a PMD-mapped THP\n");
> +}
> +
> +/*
> + * A swapped PTE is non-present but not empty. MADV_COLD must skip the entry
> + * without faulting the page back in or changing its contents.
> + */
> +static void test_madvise_cold_swapped_pte(void)
> +{
> +     const size_t page_size = getpagesize();
> +     char *mapping = map_aligned_pages(page_size);
> +     int pagemap_fd, retry;
> +     bool swapped = false;
> +
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     if (pagemap_fd < 0)
> +             ksft_exit_fail_perror("open pagemap");
> +     for (retry = 0; retry < 100; retry++) {
> +             if (madvise(mapping, page_size, MADV_PAGEOUT))
> +                     ksft_exit_fail_perror("MADV_PAGEOUT");
> +             if (pagemap_is_swapped(pagemap_fd, mapping)) {
> +                     swapped = true;
> +                     break;
> +             }
> +             usleep(10000);
> +     }
> +     if (!swapped) {
> +             close(pagemap_fd);
> +             munmap(mapping, page_size);
> +             ksft_test_result_skip("MADV_PAGEOUT did not swap the page\n");
> +             return;
> +     }
> +
> +     if (madvise(mapping, page_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (!pagemap_is_swapped(pagemap_fd, mapping))
> +             ksft_exit_fail_msg("MADV_COLD faulted in a swapped PTE\n");
> +     if (mapping[0] != 1)
> +             ksft_exit_fail_msg("swapped page contents changed\n");
> +
> +     close(pagemap_fd);
> +     munmap(mapping, page_size);
> +     ksft_test_result_pass("MADV_COLD skips a swapped PTE\n");
> +}
> +
> +/*
> + * vmsplice() retains a reference to a THP in a pipe, preventing a partial
> + * MADV_COLD from splitting it. The advice must leave the folio intact while
> + * pinned, then split it normally after the pipe releases the reference.
> + */
> +static void test_partial_pinned_madvise_cold(bool pte_mapped)
> +{
> +     const size_t page_size = getpagesize();
> +     char *mapping = map_aligned_pages(pmd_size);
> +     struct iovec iov = {
> +             .iov_base = mapping,
> +             .iov_len = page_size,
> +     };
> +     char *buffer;
> +     int pipefd[2];
> +     int retry;
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +     if (pte_mapped)
> +             split_pmd_mapping(mapping);
> +     if (pipe(pipefd))
> +             ksft_exit_fail_perror("pipe");
> +     if (vmsplice(pipefd[1], &iov, 1, SPLICE_F_GIFT) != page_size) {
> +             close(pipefd[0]);
> +             close(pipefd[1]);
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("vmsplice could not retain a THP page\n");
> +             return;
> +     }
> +
> +     if (madvise(mapping, pmd_size / 2, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (!check_large_folios(mapping, pmd_size, 1, pmd_size))
> +             ksft_exit_fail_msg("MADV_COLD split a pinned THP\n");
> +
> +     buffer = malloc(page_size);
> +     if (!buffer)
> +             ksft_exit_fail_perror("malloc");
> +     if (read(pipefd[0], buffer, page_size) != page_size)
> +             ksft_exit_fail_perror("read pipe");
> +     free(buffer);
> +     close(pipefd[0]);
> +     close(pipefd[1]);
> +
> +     for (retry = 0; retry < 10; retry++) {
> +             if (madvise(mapping, pmd_size / 2, MADV_COLD))
> +                     ksft_exit_fail_perror("MADV_COLD");
> +             if (check_large_folios(mapping, pmd_size, 0, pmd_size))
> +                     break;
> +             usleep(10000);
> +     }
> +     if (retry == 10)
> +             ksft_exit_fail_msg("MADV_COLD did not split an unpinned THP\n");
> +     check_memory(mapping, pmd_size);
> +
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("partial MADV_COLD skips a pinned %s THP\n",
> +                           pte_mapped ? "PTE-mapped" : "PMD-mapped");
> +}
> +
> +static void test_partial_pinned_pmd_madvise_cold(void)
> +{
> +     test_partial_pinned_madvise_cold(false);
> +}
> +
> +/*
> + * Pinning a PTE-mapped THP also prevents a partial MADV_COLD from splitting
> + * it. Once the pipe releases the reference, a serial retry must split it.
> + */
> +static void test_partial_pinned_pte_madvise_cold(void)
> +{
> +     test_partial_pinned_madvise_cold(true);
> +}
> +
> +static void *madvise_cold_worker(void *unused)
> +{
> +     int round, i;
> +
> +     (void)unused;
> +     for (round = 0; round < NR_CONCURRENT_ROUNDS; round++) {
> +             pthread_barrier_wait(&concurrent_start_barrier);
> +             for (i = 0; i < NR_CONCURRENT_THPS; i++) {
> +                     if (madvise(concurrent_area + i * pmd_size,
> +                                 pmd_size / 2, MADV_COLD))
> +                             atomic_store(&concurrent_worker_errno, errno);
> +             }
> +             pthread_barrier_wait(&concurrent_done_barrier);
> +     }
> +
> +     return NULL;
> +}
> +
> +/*
> + * Concurrent partial MADV_COLD calls deliberately contend for each THP lock.
> + * Alternate PMD- and PTE-mapped rounds to exercise both split paths. A 
> caller
> + * that loses the trylock race may skip that folio, so retry serially before
> + * asserting the stable interface: all calls succeed, the THPs remain
> + * splittable, and their contents are unchanged.
> + */
> +static void test_concurrent_partial_madvise_cold(void)
> +{
> +     const size_t size = NR_CONCURRENT_THPS * pmd_size;
> +     pthread_t threads[NR_CONCURRENT_THREADS];
> +     int round, i;
> +
> +     concurrent_area = map_aligned_pages(size);
> +     if (!collapse_all(concurrent_area, size, NR_CONCURRENT_THPS)) {
> +             munmap(concurrent_area, size);
> +             ksft_test_result_skip("could not allocate PMD-sized THPs\n");
> +             return;
> +     }
> +     if (pthread_barrier_init(&concurrent_start_barrier, NULL,
> +                              NR_CONCURRENT_THREADS + 1) ||
> +         pthread_barrier_init(&concurrent_done_barrier, NULL,
> +                              NR_CONCURRENT_THREADS + 1))
> +             ksft_exit_fail_msg("pthread_barrier_init failed\n");
> +     for (i = 0; i < NR_CONCURRENT_THREADS; i++)
> +             if (pthread_create(&threads[i], NULL, madvise_cold_worker, 
> NULL))
> +                     ksft_exit_fail_msg("pthread_create failed\n");
> +
> +     for (round = 0; round < NR_CONCURRENT_ROUNDS; round++) {
> +             if (!collapse_all(concurrent_area, size, NR_CONCURRENT_THPS))
> +                     ksft_exit_fail_msg("round %d: failed to form PMD 
> THPs\n",
> +                                        round);
> +             if (round & 1)
> +                     for (i = 0; i < NR_CONCURRENT_THPS; i++)
> +                             split_pmd_mapping(concurrent_area + i * 
> pmd_size);
> +             pthread_barrier_wait(&concurrent_start_barrier);
> +             pthread_barrier_wait(&concurrent_done_barrier);
> +
> +             if (atomic_load(&concurrent_worker_errno)) {
> +                     errno = atomic_load(&concurrent_worker_errno);
> +                     ksft_exit_fail_perror("MADV_COLD");
> +             }
> +             for (i = 0; i < NR_CONCURRENT_THPS; i++) {
> +                     if (madvise(concurrent_area + i * pmd_size,
> +                                 pmd_size / 2, MADV_COLD))
> +                             ksft_exit_fail_perror("MADV_COLD retry");
> +             }
> +             if (!check_large_folios(concurrent_area, size, 0, pmd_size))
> +                     ksft_exit_fail_msg("round %d: PMD THP remained\n", 
> round);
> +     }
> +     check_memory(concurrent_area, size);
> +
> +     for (i = 0; i < NR_CONCURRENT_THREADS; i++)
> +             pthread_join(threads[i], NULL);
> +     pthread_barrier_destroy(&concurrent_done_barrier);
> +     pthread_barrier_destroy(&concurrent_start_barrier);
> +     munmap(concurrent_area, size);
> +     ksft_test_result_pass("concurrent partial MADV_COLD preserves 
> memory\n");
> +}
> +
> +/*
> + * SHM_LOCK makes a shmem folio unevictable without setting VM_LOCKED on this
> + * VMA. MADV_PAGEOUT must put an isolated folio back on its LRU rather than
> + * reclaiming it.
> + */
> +static void test_madvise_pageout_unevictable(void)
> +{
> +     const size_t page_size = getpagesize();
> +     unsigned long pfn;
> +     uint64_t flags;
> +     char *mapping;
> +     int pagemap_fd, kpageflags_fd;
> +     int shmid;
> +
> +     if (geteuid()) {
> +             ksft_test_result_skip("requires root to read page flags\n");
> +             return;
> +     }
> +     shmid = shmget(IPC_PRIVATE, page_size, IPC_CREAT | 0600);
> +     if (shmid < 0)
> +             ksft_exit_fail_perror("shmget");
> +     if (shmctl(shmid, SHM_LOCK, NULL)) {
> +             shmctl(shmid, IPC_RMID, NULL);
> +             ksft_test_result_skip("could not lock a shmem segment\n");
> +             return;
> +     }
> +     mapping = shmat(shmid, NULL, 0);
> +     if (mapping == (void *)-1)
> +             ksft_exit_fail_perror("shmat");
> +     mapping[0] = 1;
> +
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
> +     if (pagemap_fd < 0 || kpageflags_fd < 0)
> +             ksft_exit_fail_perror("open page flags");
> +
> +     /* Drain the LRU add batch so the locked mapping becomes unevictable. */
> +     if (madvise(mapping, page_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     pfn = pagemap_get_pfn(pagemap_fd, mapping);
> +     if (pfn == -1ul || pageflags_get(pfn, kpageflags_fd, &flags) ||
> +         !(flags & KPF_UNEVICTABLE))
> +             ksft_exit_fail_msg("SHM_LOCK page is not unevictable\n");
> +
> +     if (madvise(mapping, page_size, MADV_PAGEOUT))
> +             ksft_exit_fail_perror("MADV_PAGEOUT");
> +     if (!pagemap_is_populated(pagemap_fd, mapping) || mapping[0] != 1)
> +             ksft_exit_fail_msg("MADV_PAGEOUT reclaimed an unevictable 
> page\n");
> +
> +     close(kpageflags_fd);
> +     close(pagemap_fd);
> +     shmctl(shmid, SHM_UNLOCK, NULL);
> +     shmdt(mapping);
> +     shmctl(shmid, IPC_RMID, NULL);
> +     ksft_test_result_pass("MADV_PAGEOUT preserves an unevictable page\n");
> +}
> +
> +/*
> + * Exercise the unevictable PAGEOUT path at PMD granularity. SHM_LOCK keeps
> + * the shmem THP resident without marking its VMA VM_LOCKED.
> + */
> +static void test_madvise_pageout_unevictable_thp(void)
> +{
> +     char *mapping;
> +     int pagemap_fd, shmid;
> +
> +     if (geteuid()) {
> +             ksft_test_result_skip("requires root to lock a shmem 
> segment\n");
> +             return;
> +     }
> +     mapping = map_unevictable_shmem(pmd_size, &shmid);
> +     if (mapping == MAP_FAILED)
> +             return;
> +     memset(mapping, 1, pmd_size);
> +     if (!collapse_shmem(mapping, pmd_size, 1)) {
> +             unmap_unevictable_shmem(mapping, shmid);
> +             ksft_test_result_skip("could not allocate an unevictable 
> THP\n");
> +             return;
> +     }
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     if (pagemap_fd < 0)
> +             ksft_exit_fail_perror("open pagemap");
> +
> +     if (madvise(mapping, pmd_size, MADV_PAGEOUT))
> +             ksft_exit_fail_perror("MADV_PAGEOUT");
> +     if (!pagemap_is_populated(pagemap_fd, mapping) ||
> +         !check_huge_shmem(mapping, pmd_size, 1, pmd_size))
> +             ksft_exit_fail_msg("MADV_PAGEOUT reclaimed an unevictable 
> THP\n");
> +     check_memory(mapping, pmd_size);
> +
> +     close(pagemap_fd);
> +     unmap_unevictable_shmem(mapping, shmid);
> +     ksft_test_result_pass("MADV_PAGEOUT preserves an unevictable THP\n");
> +}
> +
> +/*
> + * migrate_pages() isolates folios from the LRU before replacing their page
> + * table entries. Race that interval against MADV_PAGEOUT on PMD-mapped THPs.
> + * Either operation may skip a folio owned by the other, but repeated calls
> + * must not report an error or corrupt the mapping.
> + */
> +static void test_madvise_pageout_migration(void)
> +{
> +#if defined(SYS_get_mempolicy) && defined(SYS_mbind) && \
> +     defined(SYS_migrate_pages)
> +     unsigned long allowed[NUMA_MASK_LONGS] = {};
> +     unsigned long mask1[NUMA_MASK_LONGS] = {};
> +     unsigned long mask2[NUMA_MASK_LONGS] = {};
> +     const size_t size = NR_MIGRATION_RACE_THPS * pmd_size;
> +     char *mapping;
> +     int node1, node2;
> +     int error;
> +     int shmid;
> +
> +     if (geteuid()) {
> +             ksft_test_result_skip("requires root to lock a shmem 
> segment\n");
> +             return;
> +     }
> +     if (!find_two_memory_nodes(allowed, &node1, &node2)) {
> +             ksft_test_result_skip("requires two allowed NUMA memory 
> nodes\n");
> +             return;
> +     }
> +
> +     numa_mask_set(mask1, node1);
> +     numa_mask_set(mask2, node2);
> +     mapping = map_unevictable_shmem(size, &shmid);
> +     if (mapping == MAP_FAILED)
> +             return;
> +     if (syscall(SYS_mbind, mapping, size, MPOL_BIND, mask1,
> +                 NUMA_MASK_BITS + 1, 0)) {
> +             unmap_unevictable_shmem(mapping, shmid);
> +             ksft_test_result_skip("could not bind shmem to a NUMA node\n");
> +             return;
> +     }
> +     memset(mapping, 1, size);
> +     if (!collapse_shmem(mapping, size, NR_MIGRATION_RACE_THPS)) {
> +             unmap_unevictable_shmem(mapping, shmid);
> +             ksft_test_result_skip("could not allocate unevictable shmem 
> THPs\n");
> +             return;
> +     }
> +
> +     error = race_pageout_with_migration(mapping, size, mask1, mask2);
> +     if (error == ENOSYS) {
> +             unmap_unevictable_shmem(mapping, shmid);
> +             ksft_test_result_skip("NUMA migration is unavailable\n");
> +             return;
> +     }
> +     if (error) {
> +             errno = error;
> +             ksft_exit_fail_perror("MADV_PAGEOUT/migrate_pages race");
> +     }
> +     check_memory(mapping, size);
> +
> +     unmap_unevictable_shmem(mapping, shmid);
> +     ksft_test_result_pass("MADV_PAGEOUT races NUMA migration safely\n");
> +#else
> +     ksft_test_result_skip("NUMA migration system calls are unavailable\n");
> +#endif
> +}
> +
> +static bool activate_folio(char *mapping, char *drain, unsigned long pfn,
> +                        int kpageflags_fd)
> +{
> +     struct iovec local = { .iov_base = drain, .iov_len = 1 };
> +     struct iovec remote = { .iov_base = mapping, .iov_len = 1 };
> +     uint64_t flags;
> +     int retry;
> +
> +     for (retry = 0; retry < 10; retry++) {
> +             if (process_vm_readv(getpid(), &local, 1, &remote, 1, 0) != 1)
> +                     ksft_exit_fail_perror("process_vm_readv");
> +             /* Drain the activation batch without advising the target 
> folio. */
> +             if (madvise(drain, getpagesize(), MADV_COLD))
> +                     ksft_exit_fail_perror("MADV_COLD");
> +             if (pageflags_get(pfn, kpageflags_fd, &flags))
> +                     ksft_exit_fail_perror("read kpageflags");
> +             if (flags & KPF_ACTIVE)
> +                     return true;
> +     }
> +
> +     return false;
> +}
> +
> +/*
> + * Repeated GUP accesses promote an inactive folio on the traditional LRU.
> + * MADV_COLD must deactivate that folio while preserving its contents.
> + */
> +static void test_madvise_cold_active_folio(void)
> +{
> +     const size_t page_size = getpagesize();
> +     char *mapping = map_aligned_pages(page_size);
> +     char *drain = map_aligned_pages(page_size);
> +     cpu_set_t old_mask, mask;
> +     int pagemap_fd, kpageflags_fd;
> +     unsigned long pfn;
> +     uint64_t flags;
> +     int cpu;
> +
> +     if (geteuid()) {
> +             munmap(drain, page_size);
> +             munmap(mapping, page_size);
> +             ksft_test_result_skip("requires root to read page flags\n");
> +             return;
> +     }
> +     if (sched_getaffinity(0, sizeof(old_mask), &old_mask))
> +             ksft_exit_fail_perror("sched_getaffinity");
> +     cpu = sched_getcpu();
> +     if (cpu < 0)
> +             ksft_exit_fail_perror("sched_getcpu");
> +     CPU_ZERO(&mask);
> +     CPU_SET(cpu, &mask);
> +     if (sched_setaffinity(0, sizeof(mask), &mask))
> +             ksft_exit_fail_perror("sched_setaffinity");
> +
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
> +     if (pagemap_fd < 0 || kpageflags_fd < 0)
> +             ksft_exit_fail_perror("open page flags");
> +     pfn = pagemap_get_pfn(pagemap_fd, mapping);
> +     if (pfn == -1ul)
> +             ksft_exit_fail_msg("could not read page PFN\n");
> +
> +     if (!activate_folio(mapping, drain, pfn, kpageflags_fd)) {
> +             close(kpageflags_fd);
> +             close(pagemap_fd);
> +             sched_setaffinity(0, sizeof(old_mask), &old_mask);
> +             munmap(drain, page_size);
> +             munmap(mapping, page_size);
> +             ksft_test_result_skip("could not activate a folio\n");
> +             return;
> +     }
> +
> +     if (madvise(mapping, page_size, MADV_COLD) ||
> +         madvise(drain, page_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (pageflags_get(pfn, kpageflags_fd, &flags))
> +             ksft_exit_fail_perror("read kpageflags");
> +     if ((flags & KPF_ACTIVE) || mapping[0] != 1)
> +             ksft_exit_fail_msg("MADV_COLD did not deactivate the folio\n");
> +
> +     close(kpageflags_fd);
> +     close(pagemap_fd);
> +     if (sched_setaffinity(0, sizeof(old_mask), &old_mask))
> +             ksft_exit_fail_perror("restore affinity");
> +     munmap(drain, page_size);
> +     munmap(mapping, page_size);
> +     ksft_test_result_pass("MADV_COLD deactivates an active folio\n");
> +}
> +
> +struct remote_fault_data {
> +     char *mapping;
> +     pthread_barrier_t barrier;
> +     int cpu;
> +     int error;
> +};
> +
> +static void *remote_fault_worker(void *arg)
> +{
> +     struct remote_fault_data *data = arg;
> +     cpu_set_t mask;
> +
> +     CPU_ZERO(&mask);
> +     CPU_SET(data->cpu, &mask);
> +     if (sched_setaffinity(0, sizeof(mask), &mask))
> +             data->error = errno;
> +     else
> +             data->mapping[0] = 1;
> +     pthread_barrier_wait(&data->barrier);
> +     pthread_barrier_wait(&data->barrier);
> +     return NULL;
> +}
> +
> +/*
> + * A folio faulted on another CPU can remain in that CPU's pending LRU batch.
> + * MADV_COLD drains only the calling CPU and must safely skip the non-LRU
> + * folio without changing the mapping.
> + */
> +static void test_madvise_cold_remote_lru_batch(void)
> +{
> +     const size_t page_size = getpagesize();
> +     struct remote_fault_data data = { .cpu = -1 };
> +     char *mapping;
> +     cpu_set_t old_mask, mask;
> +     pthread_t thread;
> +     unsigned long pfn;
> +     uint64_t flags;
> +     int pagemap_fd, kpageflags_fd;
> +     int cpu, main_cpu = -1;
> +
> +     if (geteuid()) {
> +             ksft_test_result_skip("requires root to read page flags\n");
> +             return;
> +     }
> +     if (sched_getaffinity(0, sizeof(old_mask), &old_mask))
> +             ksft_exit_fail_perror("sched_getaffinity");
> +     for (cpu = 0; cpu < CPU_SETSIZE; cpu++) {
> +             if (!CPU_ISSET(cpu, &old_mask))
> +                     continue;
> +             if (main_cpu < 0) {
> +                     main_cpu = cpu;
> +             } else {
> +                     data.cpu = cpu;
> +                     break;
> +             }
> +     }
> +     if (main_cpu < 0 || data.cpu < 0) {
> +             ksft_test_result_skip("requires two CPUs\n");
> +             return;
> +     }
> +     CPU_ZERO(&mask);
> +     CPU_SET(main_cpu, &mask);
> +     if (sched_setaffinity(0, sizeof(mask), &mask))
> +             ksft_exit_fail_perror("sched_setaffinity");
> +
> +     mapping = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
> +                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +     if (mapping == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap");
> +     data.mapping = mapping;
> +     if (pthread_barrier_init(&data.barrier, NULL, 2) ||
> +         pthread_create(&thread, NULL, remote_fault_worker, &data))
> +             ksft_exit_fail_msg("could not start fault worker\n");
> +     pthread_barrier_wait(&data.barrier);
> +     if (data.error) {
> +             errno = data.error;
> +             ksft_exit_fail_perror("worker sched_setaffinity");
> +     }
> +
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
> +     if (pagemap_fd < 0 || kpageflags_fd < 0)
> +             ksft_exit_fail_perror("open page flags");
> +     pfn = pagemap_get_pfn(pagemap_fd, mapping);
> +     if (pfn == -1ul || pageflags_get(pfn, kpageflags_fd, &flags))
> +             ksft_exit_fail_msg("could not read page flags\n");
> +     if (flags & KPF_LRU) {
> +             pthread_barrier_wait(&data.barrier);
> +             pthread_join(thread, NULL);
> +             pthread_barrier_destroy(&data.barrier);
> +             close(kpageflags_fd);
> +             close(pagemap_fd);
> +             sched_setaffinity(0, sizeof(old_mask), &old_mask);
> +             munmap(mapping, page_size);
> +             ksft_test_result_skip("remote LRU batch was already drained\n");
> +             return;
> +     }
> +
> +     if (madvise(mapping, page_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (mapping[0] != 1 || !pagemap_is_populated(pagemap_fd, mapping))
> +             ksft_exit_fail_msg("MADV_COLD changed a pending-LRU page\n");
> +
> +     pthread_barrier_wait(&data.barrier);
> +     pthread_join(thread, NULL);
> +     pthread_barrier_destroy(&data.barrier);
> +     close(kpageflags_fd);
> +     close(pagemap_fd);
> +     if (sched_setaffinity(0, sizeof(old_mask), &old_mask))
> +             ksft_exit_fail_perror("restore affinity");
> +     munmap(mapping, page_size);
> +     ksft_test_result_pass("MADV_COLD skips a remote pending-LRU page\n");
> +}
> +
> +/*
> + * Exercise the same active-folio transition through a huge PMD, where the
> + * page-table aging operation differs from the PTE implementation.
> + */
> +static void test_pmd_madvise_cold_active_folio(void)
> +{
> +     const size_t page_size = getpagesize();
> +     char *mapping;
> +     char *drain = map_aligned_pages(page_size);
> +     cpu_set_t old_mask, mask;
> +     int pagemap_fd, kpageflags_fd;
> +     unsigned long pfn;
> +     uint64_t flags;
> +     int cpu, fd, retry;
> +     bool active = false;
> +
> +     if (geteuid()) {
> +             munmap(drain, page_size);
> +             ksft_test_result_skip("requires root to read page flags\n");
> +             return;
> +     }
> +     fd = memfd_create("madvise-active-thp", 0);
> +     if (fd < 0)
> +             ksft_exit_fail_perror("memfd_create");
> +     if (ftruncate(fd, pmd_size))
> +             ksft_exit_fail_perror("ftruncate");
> +     mapping = map_aligned_file(fd, pmd_size, MAP_SHARED);
> +     memset(mapping, 1, pmd_size);
> +     if (msync(mapping, pmd_size, MS_SYNC) ||
> +         !collapse_shmem(mapping, pmd_size, 1)) {
> +             munmap(drain, page_size);
> +             munmap(mapping, pmd_size);
> +             close(fd);
> +             ksft_test_result_skip("could not allocate a PMD-sized shmem 
> THP\n");
> +             return;
> +     }
> +     if (sched_getaffinity(0, sizeof(old_mask), &old_mask))
> +             ksft_exit_fail_perror("sched_getaffinity");
> +     cpu = sched_getcpu();
> +     if (cpu < 0)
> +             ksft_exit_fail_perror("sched_getcpu");
> +     CPU_ZERO(&mask);
> +     CPU_SET(cpu, &mask);
> +     if (sched_setaffinity(0, sizeof(mask), &mask))
> +             ksft_exit_fail_perror("sched_setaffinity");
> +
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
> +     if (pagemap_fd < 0 || kpageflags_fd < 0)
> +             ksft_exit_fail_perror("open page flags");
> +     pfn = pagemap_get_pfn(pagemap_fd, mapping);
> +     if (pfn == -1ul)
> +             ksft_exit_fail_msg("could not read THP PFN\n");
> +
> +     for (retry = 0; retry < 10; retry++) {
> +             if (pread(fd, drain, 1, 0) != 1)
> +                     ksft_exit_fail_perror("pread");
> +             if (madvise(drain, page_size, MADV_COLD))
> +                     ksft_exit_fail_perror("MADV_COLD");
> +             if (pageflags_get(pfn, kpageflags_fd, &flags))
> +                     ksft_exit_fail_perror("read kpageflags");
> +             if (flags & KPF_ACTIVE) {
> +                     active = true;
> +                     break;
> +             }
> +     }
> +     if (!active) {
> +             close(kpageflags_fd);
> +             close(pagemap_fd);
> +             sched_setaffinity(0, sizeof(old_mask), &old_mask);
> +             munmap(drain, page_size);
> +             munmap(mapping, pmd_size);
> +             close(fd);
> +             ksft_test_result_skip("could not activate a THP\n");
> +             return;
> +     }
> +
> +     if (madvise(mapping, pmd_size, MADV_COLD) ||
> +         madvise(drain, page_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (pageflags_get(pfn, kpageflags_fd, &flags))
> +             ksft_exit_fail_perror("read kpageflags");
> +     if (flags & KPF_ACTIVE)
> +             ksft_exit_fail_msg("MADV_COLD did not deactivate the THP\n");
> +     if (!check_huge_shmem(mapping, pmd_size, 1, pmd_size))
> +             ksft_exit_fail_msg("MADV_COLD split an active THP\n");
> +     check_memory(mapping, pmd_size);
> +
> +     close(kpageflags_fd);
> +     close(pagemap_fd);
> +     if (sched_setaffinity(0, sizeof(old_mask), &old_mask))
> +             ksft_exit_fail_perror("restore affinity");
> +     munmap(drain, page_size);
> +     munmap(mapping, pmd_size);
> +     close(fd);
> +     ksft_test_result_pass("MADV_COLD deactivates an active THP\n");
> +}
> +
> +/*
> + * MADV_PAGEOUT on half of a PMD-mapped THP must split the folio before
> + * reclaim so the unadvised half is not reclaimed as part of the THP.
> + */
> +static void test_partial_pmd_madvise_pageout(void)
> +{
> +     char *mapping = map_aligned_pages(pmd_size);
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +     pageout_half_thp(mapping);
> +
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("partial MADV_PAGEOUT splits a PMD-mapped THP\n");
> +}
> +
> +/*
> + * mprotect() replaces the huge PMD with PTEs without splitting the physical
> + * THP. MADV_PAGEOUT on half of that PTE-mapped THP must split the folio.
> + */
> +static void test_partial_pte_madvise_pageout(void)
> +{
> +     char *mapping = map_aligned_pages(pmd_size);
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +     split_pmd_mapping(mapping);
> +     pageout_half_thp(mapping);
> +
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("partial MADV_PAGEOUT splits a PTE-mapped THP\n");
> +}
> +
> +static pid_t fork_waiting_child(int pipefd[2])
> +{
> +     pid_t pid;
> +
> +     if (pipe(pipefd))
> +             ksft_exit_fail_perror("pipe");
> +     pid = fork();
> +     if (pid < 0)
> +             ksft_exit_fail_perror("fork");
> +     if (!pid) {
> +             char byte;
> +
> +             close(pipefd[1]);
> +             while (read(pipefd[0], &byte, 1) < 0 && errno == EINTR)
> +                     ;
> +             _exit(0);
> +     }
> +     close(pipefd[0]);
> +     return pid;
> +}
> +
> +/*
> + * fork() gives the THP another mapping, then mprotect() PTE-maps it in the
> + * parent. Assert that partial MADV_COLD leaves the shared physical THP and
> + * its contents intact.
> + */
> +static void test_shared_pte_mapped_madvise_cold(void)
> +{
> +     char *mapping = map_aligned_pages(pmd_size);
> +     int pipefd[2], status;
> +     pid_t pid;
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +     pid = fork_waiting_child(pipefd);
> +
> +     split_pmd_mapping(mapping);
> +     if (madvise(mapping, pmd_size / 2, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (!check_large_folios(mapping, pmd_size, 1, pmd_size))
> +             ksft_exit_fail_msg("MADV_COLD split a shared large folio\n");
> +     check_memory(mapping, pmd_size);
> +
> +     close(pipefd[1]);
> +     if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ||
> +         WEXITSTATUS(status))
> +             ksft_exit_fail_msg("child process failed\n");
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("MADV_COLD skips a shared PTE-mapped THP\n");
> +}
> +
> +/*
> + * A full-range PTE walk reaches the mapcount check rather than the partial
> + * folio split check. A second mapping must still prevent MADV_COLD from
> + * operating on the shared physical THP.
> + */
> +static void test_full_shared_pte_mapped_madvise_cold(void)
> +{
> +     char *mapping = map_aligned_pages(pmd_size);
> +     int pipefd[2], status;
> +     pid_t pid;
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +     pid = fork_waiting_child(pipefd);
> +
> +     split_pmd_mapping(mapping);
> +     if (madvise(mapping, pmd_size, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (!check_large_folios(mapping, pmd_size, 1, pmd_size))
> +             ksft_exit_fail_msg("MADV_COLD split a shared large folio\n");
> +     check_memory(mapping, pmd_size);
> +
> +     close(pipefd[1]);
> +     if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ||
> +         WEXITSTATUS(status))
> +             ksft_exit_fail_msg("child process failed\n");
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("full MADV_COLD skips a shared PTE-mapped THP\n");
> +}
> +
> +/*
> + * fork() gives a PMD-mapped THP another mapping. Assert that partial
> + * MADV_COLD leaves the shared physical THP and its contents intact.
> + */
> +static void test_shared_pmd_madvise_cold(void)
> +{
> +     char *mapping = map_aligned_pages(pmd_size);
> +     int pipefd[2], status;
> +     pid_t pid;
> +
> +     if (!collapse_all(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             ksft_test_result_skip("could not allocate a PMD-sized THP\n");
> +             return;
> +     }
> +     pid = fork_waiting_child(pipefd);
> +
> +     if (madvise(mapping, pmd_size / 2, MADV_COLD))
> +             ksft_exit_fail_perror("MADV_COLD");
> +     if (!check_large_folios(mapping, pmd_size, 1, pmd_size))
> +             ksft_exit_fail_msg("MADV_COLD split a shared PMD-mapped THP\n");
> +     check_memory(mapping, pmd_size);
> +
> +     close(pipefd[1]);
> +     if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ||
> +         WEXITSTATUS(status))
> +             ksft_exit_fail_msg("child process failed\n");
> +     munmap(mapping, pmd_size);
> +     ksft_test_result_pass("MADV_COLD skips a shared PMD-mapped THP\n");
> +}
> +
> +static char *map_readonly_file(size_t size, int flags, int *fd)
> +{
> +     char template[] = "/tmp/madvise-pageout-XXXXXX";
> +     char *mapping;
> +
> +     *fd = mkstemp(template);
> +     if (*fd < 0)
> +             ksft_exit_fail_perror("mkstemp");
> +     if (unlink(template) || ftruncate(*fd, size) || fchmod(*fd, 0400))
> +             ksft_exit_fail_perror("prepare file");
> +     mapping = mmap(NULL, size, PROT_READ | PROT_WRITE, flags, *fd, 0);
> +     if (mapping == MAP_FAILED)
> +             ksft_exit_fail_perror("mmap");
> +
> +     return mapping;
> +}
> +
> +static bool pageout_until_evicted(char *mapping, size_t size, int pagemap_fd)
> +{
> +     int retry;
> +
> +     for (retry = 0; retry < 100; retry++) {
> +             if (madvise(mapping, size, MADV_PAGEOUT))
> +                     ksft_exit_fail_perror("MADV_PAGEOUT");
> +             if (!pagemap_is_populated(pagemap_fd, mapping))
> +                     return true;
> +             usleep(10000);
> +     }
> +     return false;
> +}
> +
> +/*
> + * The owner of a file may page out its clean page-cache pages. Assert that
> + * MADV_PAGEOUT removes the populated PTE and preserves the file contents.
> + */
> +static void test_pageout_file(void)
> +{
> +     const size_t page_size = getpagesize();
> +     char *mapping;
> +     int pagemap_fd;
> +     bool evicted;
> +     int fd;
> +
> +     mapping = map_readonly_file(page_size, MAP_PRIVATE, &fd);
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     if (pagemap_fd < 0)
> +             ksft_exit_fail_perror("open pagemap");
> +     if (mapping[0])
> +             ksft_exit_fail_msg("new file is not zero-filled\n");
> +
> +     evicted = pageout_until_evicted(mapping, page_size, pagemap_fd);
> +     if (mapping[0])
> +             ksft_exit_fail_msg("file mapping contents changed\n");
> +
> +     close(pagemap_fd);
> +     munmap(mapping, page_size);
> +     close(fd);
> +     if (!evicted) {
> +             ksft_test_result_skip("MADV_PAGEOUT did not evict the file 
> page\n");
> +             return;
> +     }
> +     ksft_test_result_pass("MADV_PAGEOUT evicts an authorized file page\n");
> +}
> +
> +static int pageout_shared_file_without_permission(char *mapping,
> +                                               size_t page_size)
> +{
> +     int pagemap_fd;
> +
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     if (pagemap_fd < 0)
> +             return KSFT_FAIL;
> +     if (setgid(65534) || setuid(65534))
> +             return KSFT_FAIL;
> +     if (mapping[0] || !pagemap_is_populated(pagemap_fd, mapping))
> +             return KSFT_FAIL;
> +     if (madvise(mapping, page_size, MADV_PAGEOUT))
> +             return KSFT_FAIL;
> +     if (!pagemap_is_populated(pagemap_fd, mapping) || mapping[0])
> +             return KSFT_FAIL;
> +     return KSFT_PASS;
> +}
> +
> +/*
> + * A caller without file write permission may not page out a shared file
> + * mapping. Assert that MADV_PAGEOUT succeeds without evicting its file page.
> + */
> +static void test_pageout_unauthorized_shared_file(void)
> +{
> +     const size_t page_size = getpagesize();
> +     char *mapping;
> +     int fd, status;
> +     pid_t pid;
> +
> +     if (geteuid()) {
> +             ksft_test_result_skip("requires root to change credentials\n");
> +             return;
> +     }
> +
> +     mapping = map_readonly_file(page_size, MAP_SHARED, &fd);
> +
> +     pid = fork();
> +     if (pid < 0)
> +             ksft_exit_fail_perror("fork");
> +     if (!pid)
> +             _exit(pageout_shared_file_without_permission(mapping, 
> page_size));
> +
> +     if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ||
> +         WEXITSTATUS(status) != KSFT_PASS)
> +             ksft_exit_fail_msg("unauthorized child evicted a shared file 
> page\n");
> +     munmap(mapping, page_size);
> +     close(fd);
> +     ksft_test_result_pass("MADV_PAGEOUT skips an unauthorized shared 
> file\n");
> +}
> +
> +/*
> + * An unauthorized private mapping may page out anonymous COW pages, but not
> + * its file folios. Assert that partial MADV_PAGEOUT filters a large shmem
> + * folio before attempting to split or reclaim it.
> + */
> +static void test_pageout_anon_only_large_folio(void)
> +{
> +     char *mapping, *shared;
> +     gid_t old_egid;
> +     int fd;
> +
> +     if (geteuid()) {
> +             ksft_test_result_skip("requires root to change credentials\n");
> +             return;
> +     }
> +     fd = memfd_create("madvise-pageout-large", 0);
> +     if (fd < 0)
> +             ksft_exit_fail_perror("memfd_create");
> +     if (ftruncate(fd, pmd_size))
> +             ksft_exit_fail_perror("ftruncate");
> +     shared = map_aligned_file(fd, pmd_size, MAP_SHARED);
> +     memset(shared, 1, pmd_size);
> +     if (msync(shared, pmd_size, MS_SYNC))
> +             ksft_exit_fail_perror("msync");
> +     munmap(shared, pmd_size);
> +
> +     mapping = map_aligned_file(fd, pmd_size, MAP_PRIVATE);
> +     if (!collapse_shmem(mapping, pmd_size, 1)) {
> +             munmap(mapping, pmd_size);
> +             close(fd);
> +             ksft_test_result_skip("could not allocate a PMD-sized shmem 
> THP\n");
> +             return;
> +     }
> +     if (fchmod(fd, 0400))
> +             ksft_exit_fail_perror("fchmod");
> +
> +     /* Retain saved uid 0 so this process can restore its credentials. */
> +     old_egid = getegid();
> +     if (setegid(65534) || seteuid(65534))
> +             ksft_exit_fail_perror("drop privileges");
> +     if (madvise(mapping, pmd_size / 2, MADV_PAGEOUT))
> +             ksft_exit_fail_perror("MADV_PAGEOUT");
> +     if (seteuid(0) || setegid(old_egid))
> +             ksft_exit_fail_perror("restore privileges");
> +
> +     if (!check_huge_shmem(mapping, pmd_size, 1, pmd_size) ||
> +         mapping[0] != 1 || mapping[pmd_size - 1] != 1)
> +             ksft_exit_fail_msg("MADV_PAGEOUT changed a protected large 
> folio\n");
> +     munmap(mapping, pmd_size);
> +     close(fd);
> +     ksft_test_result_pass("MADV_PAGEOUT filters a protected large folio\n");
> +}
> +
> +/*
> + * A private file mapping can contain both anonymous COW and file-backed
> + * pages. As a caller without file write permission, assert that PAGEOUT 
> swaps
> + * the COW page but leaves the file-backed page resident.
> + */
> +static void test_pageout_anon_only(void)
> +{
> +     const size_t page_size = getpagesize();
> +     char *mapping;
> +     int pagemap_fd;
> +     bool swapped;
> +     int fd;
> +
> +     if (geteuid()) {
> +             ksft_test_result_skip("requires root to change credentials\n");
> +             return;
> +     }
> +
> +     mapping = map_readonly_file(2 * page_size, MAP_PRIVATE, &fd);
> +     pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +     if (pagemap_fd < 0)
> +             ksft_exit_fail_perror("open pagemap");
> +
> +     if (setgid(65534) || setuid(65534))
> +             ksft_exit_fail_perror("drop privileges");
> +     if (mapping[0] || mapping[page_size])
> +             ksft_exit_fail_msg("new file is not zero-filled\n");
> +     mapping[0] = 1;
> +     if (madvise(mapping, 2 * page_size, MADV_PAGEOUT))
> +             ksft_exit_fail_perror("MADV_PAGEOUT");
> +     if (!pagemap_is_populated(pagemap_fd, mapping + page_size))
> +             ksft_exit_fail_msg("MADV_PAGEOUT evicted a protected file 
> page\n");
> +     swapped = pagemap_is_swapped(pagemap_fd, mapping);
> +     if (mapping[0] != 1 || mapping[page_size])
> +             ksft_exit_fail_msg("private file mapping contents changed\n");
> +
> +     close(pagemap_fd);
> +     munmap(mapping, 2 * page_size);
> +     close(fd);
> +     if (!swapped) {
> +             ksft_test_result_skip("MADV_PAGEOUT did not swap the COW 
> page\n");
> +             return;
> +     }
> +     ksft_test_result_pass("MADV_PAGEOUT filters private file pages\n");
> +}
> +
> +int main(void)
> +{
> +     pmd_size = read_pmd_pagesize();
> +
> +     ksft_print_header();
> +     ksft_set_plan(28);
> +     if (!pmd_size)
> +             ksft_exit_skip("PMD-sized THPs are not supported\n");
> +
> +     test_full_pmd_madvise_cold();
> +     test_repeated_pmd_madvise_cold();
> +     test_partial_pmd_madvise_cold();
> +     test_pte_mapped_madvise_cold();
> +     test_madvise_cold_pte_hole();
> +     test_madvise_cold_zero_page();
> +     test_madvise_cold_huge_zero_page();
> +     test_madvise_lru_locked_vma();
> +     test_shared_pmd_madvise_cold();
> +     test_full_pmd_madvise_pageout();
> +     test_madvise_cold_swapped_pte();
> +     test_partial_pinned_pmd_madvise_cold();
> +     test_partial_pinned_pte_madvise_cold();
> +     test_concurrent_partial_madvise_cold();
> +     test_madvise_pageout_unevictable();
> +     test_madvise_pageout_unevictable_thp();
> +     test_madvise_pageout_migration();
> +     test_madvise_cold_active_folio();
> +     test_madvise_cold_remote_lru_batch();
> +     test_pmd_madvise_cold_active_folio();
> +     test_partial_pmd_madvise_pageout();
> +     test_partial_pte_madvise_pageout();
> +     test_shared_pte_mapped_madvise_cold();
> +     test_full_shared_pte_mapped_madvise_cold();
> +     test_pageout_file();
> +     test_pageout_unauthorized_shared_file();
> +     test_pageout_anon_only_large_folio();
> +     test_pageout_anon_only();
> +     ksft_finished();
> +}
> diff --git a/tools/testing/selftests/mm/run_vmtests.sh 
> b/tools/testing/selftests/mm/run_vmtests.sh
> index 6990485b1a9da..bc136ead10f91 100755
> --- a/tools/testing/selftests/mm/run_vmtests.sh
> +++ b/tools/testing/selftests/mm/run_vmtests.sh
> @@ -51,6 +51,8 @@ separated by spaces:
>       hmm smoke tests
>  - madv_guard
>       test madvise(2) MADV_GUARD_INSTALL and MADV_GUARD_REMOVE options
> +- madvise
> +     test MADV_COLD and MADV_PAGEOUT
>  - madv_populate
>       test memadvise(2) MADV_POPULATE_{READ,WRITE} options
>  - memfd_secret
> @@ -327,6 +329,9 @@ CATEGORY="hmm" run_test bash ./test_hmm.sh smoke
>  # MADV_GUARD_INSTALL and MADV_GUARD_REMOVE tests
>  CATEGORY="madv_guard" run_test ./guard-regions
>
> +# MADV_COLD and MADV_PAGEOUT tests
> +CATEGORY="madvise" run_test ./madvise
> +
>  # MADV_POPULATE_READ and MADV_POPULATE_WRITE tests
>  CATEGORY="madv_populate" run_test ./madv_populate
>
> diff --git a/tools/testing/selftests/mm/vm_util.c 
> b/tools/testing/selftests/mm/vm_util.c
> index 31d331c1c4521..af0a7f65bfd9f 100644
> --- a/tools/testing/selftests/mm/vm_util.c
> +++ b/tools/testing/selftests/mm/vm_util.c
> @@ -373,8 +373,8 @@ static bool __check_pmd_huge(void *addr, char *pattern, 
> int nr_hpages,
>       return thp == (nr_hpages * (hpage_size >> 10));
>  }
>
> -static bool check_large_folios(void *addr, size_t len, int nr_hpages,
> -             uint64_t hpage_size)
> +bool check_large_folios(void *addr, size_t len, int nr_hpages,
> +                     uint64_t hpage_size)
>  {
>       int order = 0, pagesize = getpagesize();
>       unsigned int nr_pages = hpage_size / pagesize;
> diff --git a/tools/testing/selftests/mm/vm_util.h 
> b/tools/testing/selftests/mm/vm_util.h
> index 072a6c756c517..33ae68d721df9 100644
> --- a/tools/testing/selftests/mm/vm_util.h
> +++ b/tools/testing/selftests/mm/vm_util.h
> @@ -22,10 +22,14 @@
>  #define PM_SWAP                       BIT_ULL(62)
>  #define PM_PRESENT                    BIT_ULL(63)
>
> +#define KPF_LRU                       BIT_ULL(5)
> +#define KPF_ACTIVE                    BIT_ULL(6)
>  #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)
> +#define KPF_ZERO_PAGE                 BIT_ULL(24)
>  /*
>   * Ignore the checkpatch warning, we must read from x but don't want to do
>   * anything with it in order to trigger a read page fault. We therefore must 
> use
> @@ -97,6 +101,8 @@ unsigned long rss_anon(void);
>  bool check_huge_anon(void *addr, size_t len, int nr_hpages, uint64_t 
> hpage_size);
>  bool check_huge_file(void *addr, size_t len, int nr_hpages, uint64_t 
> hpage_size);
>  bool check_huge_shmem(void *addr, size_t len, int nr_hpages, uint64_t 
> hpage_size);
> +bool check_large_folios(void *addr, size_t len, int nr_hpages,
> +                     uint64_t hpage_size);
>  int64_t allocate_transhuge(void *ptr, int pagemap_fd);
>  int pageflags_get(unsigned long pfn, int kpageflags_fd, uint64_t *flags);
>  int gather_folio_orders(char *vaddr_start, size_t len,
> --
> 2.53.0-Meta
>

--
Cheers, Lorenzo

Reply via email to