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