Add madv_collapse_range to verify the fix for the spurious -EINVAL
returned by madvise_collapse() when the madvised range contains no
complete PMD-aligned window.

madvise_collapse() rounds the caller range inward to PMD boundaries:

  hstart = (start + ~HPAGE_PMD_MASK) & HPAGE_PMD_MASK  // round up
  hend   =  end   &  HPAGE_PMD_MASK                    // round down

When hstart >= hend there is no PMD window to collapse.  Previously
the final expression computed (hend - hstart) without guarding against
hstart > hend, causing unsigned wrap-around and a spurious -EINVAL.
Both tests expect 0: "no PMD window to collapse" is a successful no-op.

  Test 1 - aligned start (hstart == hend):
    start = 2MiB-aligned, len = PAGE_SIZE
    Both hstart and hend round to the same 2MiB boundary.
    Was already correct; included as a regression reference.

  Test 2 - unaligned start (hstart > hend):
    start = aligned + PAGE_SIZE, len = PAGE_SIZE
    hstart rounds up past the next 2MiB boundary while hend rounds
    down below start.  (hend - hstart) wrapped unsigned, causing the
    final comparison to fail and return -EINVAL instead of 0.

Signed-off-by: Chen Wandun <[email protected]>
---
 tools/testing/selftests/mm/.gitignore         |   1 +
 tools/testing/selftests/mm/Makefile           |   2 +
 .../selftests/mm/ksft_madv_collapse.sh        |   4 +
 .../selftests/mm/madv_collapse_range.c        | 141 ++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh     |   5 +
 5 files changed, 153 insertions(+)
 create mode 100755 tools/testing/selftests/mm/ksft_madv_collapse.sh
 create mode 100644 tools/testing/selftests/mm/madv_collapse_range.c

diff --git a/tools/testing/selftests/mm/.gitignore 
b/tools/testing/selftests/mm/.gitignore
index b0c30c5ee9e3..a24f8c3cf3dc 100644
--- a/tools/testing/selftests/mm/.gitignore
+++ b/tools/testing/selftests/mm/.gitignore
@@ -28,6 +28,7 @@ protection_keys
 protection_keys_32
 protection_keys_64
 madv_populate
+madv_collapse_range
 uffd-stress
 uffd-unit-tests
 uffd-wp-mremap
diff --git a/tools/testing/selftests/mm/Makefile 
b/tools/testing/selftests/mm/Makefile
index cd24596cdd27..758639f8ae8e 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -68,6 +68,7 @@ TEST_GEN_FILES += hugepage-mremap
 TEST_GEN_FILES += hugepage-shm
 TEST_GEN_FILES += hugepage-vmemmap
 TEST_GEN_FILES += khugepaged
+TEST_GEN_FILES += madv_collapse_range
 TEST_GEN_FILES += madv_populate
 TEST_GEN_FILES += map_fixed_noreplace
 TEST_GEN_FILES += map_hugetlb
@@ -153,6 +154,7 @@ TEST_PROGS += ksft_hugetlb.sh
 TEST_PROGS += ksft_hugevm.sh
 TEST_PROGS += ksft_ksm.sh
 TEST_PROGS += ksft_ksm_numa.sh
+TEST_PROGS += ksft_madv_collapse.sh
 TEST_PROGS += ksft_madv_guard.sh
 TEST_PROGS += ksft_madv_populate.sh
 TEST_PROGS += ksft_memfd_secret.sh
diff --git a/tools/testing/selftests/mm/ksft_madv_collapse.sh 
b/tools/testing/selftests/mm/ksft_madv_collapse.sh
new file mode 100755
index 000000000000..0d0b0356cbd0
--- /dev/null
+++ b/tools/testing/selftests/mm/ksft_madv_collapse.sh
@@ -0,0 +1,4 @@
+#!/bin/sh -e
+# SPDX-License-Identifier: GPL-2.0
+
+./run_vmtests.sh -t madv_collapse
diff --git a/tools/testing/selftests/mm/madv_collapse_range.c 
b/tools/testing/selftests/mm/madv_collapse_range.c
new file mode 100644
index 000000000000..11850be80dd8
--- /dev/null
+++ b/tools/testing/selftests/mm/madv_collapse_range.c
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Tests for MADV_COLLAPSE behavior when the madvise range contains no
+ * complete PMD-aligned window (range smaller than 2 MiB).
+ *
+ * madvise_collapse() rounds the caller range inward to PMD boundaries:
+ *
+ *   hstart = (start + ~HPAGE_PMD_MASK) & HPAGE_PMD_MASK  // round up
+ *   hend   =  end   &  HPAGE_PMD_MASK                    // round down
+ *
+ * When hstart >= hend the collapsing loop is not entered.  Previously,
+ * the final return expression computed (hend - hstart) without guarding
+ * against hstart > hend, causing unsigned wrap-around and a spurious
+ * -EINVAL.  Both tests expect 0: "no PMD window to collapse" is a
+ * successful no-op, not an error.
+ *
+ * Test 1: aligned start (hstart == hend):
+ *   start = 2MiB-aligned, len = PAGE_SIZE
+ *   hstart = aligned, hend = aligned  ->  0 == 0  ->  0  (was already correct)
+ *
+ * Test 2: unaligned start (hstart > hend):
+ *   start = aligned + PAGE_SIZE, len = PAGE_SIZE
+ *   hstart = aligned + 2MiB, hend = aligned
+ *   (hend - hstart) wraps unsigned  ->  was -EINVAL, fixed to 0
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <linux/mman.h>
+
+#include "kselftest.h"
+#include "vm_util.h"
+
+#ifndef MADV_COLLAPSE
+#define MADV_COLLAPSE  25
+#endif
+
+static unsigned long   page_size;
+static unsigned long   hpage_size;
+
+/*
+ * Test 1: 2MiB-aligned start, len = PAGE_SIZE.
+ *   hstart == hend  ->  0
+ */
+static void test_subpmd_aligned(char *aligned)
+{
+       int ret;
+
+       ksft_print_msg("[RUN] sub-PMD: 2MiB-aligned start, len=PAGE_SIZE\n");
+       ret = madvise(aligned, page_size, MADV_COLLAPSE);
+       ksft_test_result(ret == 0,
+                        "sub-PMD aligned start returns 0 (ret=%d errno=%d)\n",
+                        ret, ret ? errno : 0);
+}
+
+/*
+ * Test 2: start = aligned + PAGE_SIZE, len = PAGE_SIZE.
+ *   hstart = aligned + hpage_size  >  hend = aligned
+ *   unsigned wrap was -EINVAL; correct answer is 0.
+ */
+static void test_subpmd_unaligned(char *aligned)
+{
+       int ret;
+
+       ksft_print_msg("[RUN] sub-PMD: unaligned start (aligned+PAGE), 
len=PAGE_SIZE\n");
+       ksft_print_msg("      hstart=%p > hend=%p\n",
+                      (void *)(aligned + hpage_size), (void *)aligned);
+
+       ret = madvise(aligned + page_size, page_size, MADV_COLLAPSE);
+       if (ret && errno == EINVAL)
+               ksft_print_msg("      got -EINVAL: unsigned-wrap bug not 
fixed\n");
+       ksft_test_result(ret == 0,
+                        "sub-PMD unaligned start returns 0 (ret=%d 
errno=%d)\n",
+                        ret, ret ? errno : 0);
+}
+
+int main(void)
+{
+       char *base, *aligned;
+       unsigned long map_size;
+       int probe_ret;
+
+       ksft_print_header();
+       ksft_set_plan(2);
+
+       page_size  = (unsigned long)getpagesize();
+       hpage_size = (unsigned long)read_pmd_pagesize();
+       if (!hpage_size)
+               ksft_exit_skip("transparent hugepages not available\n");
+
+       /*
+        * Probe: map one hpage-sized region, touch all pages, and attempt a
+        * real collapse to confirm MADV_COLLAPSE is supported.  EAGAIN is a
+        * transient resource failure and still counts as "available".
+        */
+       map_size = 2 * hpage_size;
+       base = mmap(NULL, map_size, PROT_READ | PROT_WRITE,
+                   MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+       if (base == MAP_FAILED)
+               ksft_exit_fail_msg("probe mmap failed: %s\n", strerror(errno));
+
+       aligned = (char *)(((unsigned long)base + hpage_size - 1)
+                          & ~(hpage_size - 1));
+
+       for (unsigned long i = 0; i < hpage_size; i += page_size)
+               aligned[i] = 0;
+
+       probe_ret = madvise(aligned, hpage_size, MADV_COLLAPSE);
+       munmap(base, map_size);
+       if (probe_ret && errno != EAGAIN)
+               ksft_exit_skip("MADV_COLLAPSE not available: %s\n",
+                              strerror(errno));
+
+       /*
+        * Both sub-PMD tests share a single 2 * hpage mapping so that
+        * every test range falls within the same VMA.
+        */
+       map_size = 2 * hpage_size;
+       base = mmap(NULL, map_size, PROT_READ | PROT_WRITE,
+                   MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+       if (base == MAP_FAILED)
+               ksft_exit_fail_msg("mmap failed: %s\n", strerror(errno));
+
+       for (unsigned long i = 0; i < map_size; i += page_size)
+               base[i] = 0;
+
+       aligned = (char *)(((unsigned long)base + hpage_size - 1)
+                          & ~(hpage_size - 1));
+
+       test_subpmd_aligned(aligned);
+       test_subpmd_unaligned(aligned);
+
+       munmap(base, map_size);
+
+       if (ksft_get_fail_cnt())
+               ksft_exit_fail_msg("%d out of %d tests failed\n",
+                                  ksft_get_fail_cnt(), ksft_test_num());
+       ksft_exit_pass();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh 
b/tools/testing/selftests/mm/run_vmtests.sh
index d8468451b3a3..58402f8261e0 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
+- madv_collapse
+       test madvise(2) MADV_COLLAPSE sub-PMD range handling
 - memfd_secret
        test memfd_secret(2)
 - process_mrelease
@@ -422,6 +424,9 @@ CATEGORY="madv_guard" run_test ./guard-regions
 # MADV_POPULATE_READ and MADV_POPULATE_WRITE tests
 CATEGORY="madv_populate" run_test ./madv_populate
 
+# MADV_COLLAPSE sub-PMD range tests
+CATEGORY="madv_collapse" run_test ./madv_collapse_range
+
 # PROCESS_MADV test
 CATEGORY="process_madv" run_test ./process_madv
 
-- 
2.43.0


Reply via email to