* [LTP] [PATCH v1] madvise13: Add test for MADV_COLLAPSE
@ 2026-10-30 3:19 Xuewen Wang
2026-10-30 6:43 ` [LTP] " linuxtestproject.agent
0 siblings, 1 reply; 2+ messages in thread
From: Xuewen Wang @ 2026-10-30 3:19 UTC (permalink / raw)
To: chrubis, pvorel, andrea.cervesato, li.wang
Cc: wangxuewen_ky, Xuewen Wang, ltp
Add a test verifying that MADV_COLLAPSE synchronously collapses
PMD-sized windows of base pages into PMD-mapped anonymous THPs,
covering the error paths (EINVAL, ENOMEM), the no-op ranges and the
clamping of a page-aligned but not PMD-aligned range.
khugepaged is quiesced for the duration of the run, as it can
legally collapse the THP-eligible windows before the test does.
Tested on x86_64 and arm64, THP sysfs mode always/madvise/never: 28/28 TPASS
on 6.6.160 and 7.3.0-rc3, including 12x -i 100 with khugepaged
scan_sleep_millisecs=1 and mTHP enabled.
Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
include/lapi/mmap.h | 4 +
runtest/syscalls | 1 +
testcases/kernel/syscalls/madvise/.gitignore | 1 +
testcases/kernel/syscalls/madvise/madvise13.c | 376 ++++++++++++++++++
4 files changed, 382 insertions(+)
create mode 100644 testcases/kernel/syscalls/madvise/madvise13.c
diff --git a/include/lapi/mmap.h b/include/lapi/mmap.h
index f20eb3bd5..d0373d898 100644
--- a/include/lapi/mmap.h
+++ b/include/lapi/mmap.h
@@ -87,6 +87,10 @@
# define MADV_PAGEOUT 21
#endif
+#ifndef MADV_COLLAPSE
+# define MADV_COLLAPSE 25
+#endif
+
#ifndef MAP_DROPPABLE
# define MAP_DROPPABLE 0x08
#endif
diff --git a/runtest/syscalls b/runtest/syscalls
index 4d3f53ecd..7e255c3bd 100644
--- a/runtest/syscalls
+++ b/runtest/syscalls
@@ -1094,6 +1094,7 @@ madvise09 madvise09
madvise10 madvise10
madvise11 madvise11
madvise12 madvise12
+madvise13 madvise13
newuname01 newuname01
diff --git a/testcases/kernel/syscalls/madvise/.gitignore b/testcases/kernel/syscalls/madvise/.gitignore
index 758e601a9..3e7dbe653 100644
--- a/testcases/kernel/syscalls/madvise/.gitignore
+++ b/testcases/kernel/syscalls/madvise/.gitignore
@@ -9,3 +9,4 @@
/madvise10
/madvise11
/madvise12
+/madvise13
diff --git a/testcases/kernel/syscalls/madvise/madvise13.c b/testcases/kernel/syscalls/madvise/madvise13.c
new file mode 100644
index 000000000..34159f922
--- /dev/null
+++ b/testcases/kernel/syscalls/madvise/madvise13.c
@@ -0,0 +1,376 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 Xuewen Wang <wangxuewen@kylinos.cn>
+ */
+
+/*\
+ * Verify that MADV_COLLAPSE synchronously collapses a PMD-sized window of
+ * base pages into a PMD-mapped anonymous transparent hugepage.
+ *
+ * This is a test for feature implemented in
+ * 7d8faaf15545 ("mm/madvise: introduce MADV_COLLAPSE sync hugepage collapse")
+ *
+ * MADV_COLLAPSE ignores the system-wide THP sysfs mode and, unlike
+ * khugepaged, the max_ptes_none/max_ptes_swap limits.
+ *
+ * khugepaged is quiesced for the duration of the run, as it can legally
+ * collapse the THP-eligible windows before the test collapses them.
+ *
+ * [Algorithm]
+ *
+ * - carve PMD-aligned windows out of anonymous mappings
+ * - populate a window with base pages under MADV_NOHUGEPAGE (so that page
+ * faults cannot install THPs upfront), verify that no THP is present
+ * yet, then collapse it and verify the AnonHugePages counter in
+ * /proc/self/smaps and the page contents
+ * - collapse the same window again (idempotency)
+ * - collapse a window with a single populated page and verify the page
+ * content and the zero-fill of the unpopulated pages
+ * - verify the error paths (MADV_NOHUGEPAGE, untouched window, unmapped
+ * page) and the no-op ranges (shorter than one PMD window, zero length)
+ * - verify that a page-aligned but not PMD-aligned start is clamped to
+ * whole PMD windows instead of being rejected
+ * - discard the windows with MADV_DONTNEED at the start of each run, so
+ * the test is safe to run repeatedly (-i)
+ */
+
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include "tst_test.h"
+#include "tst_safe_stdio.h"
+#include "tst_sysfs_assert.h"
+#include "tst_path_defs.h"
+#include "tst_memutils.h"
+#include "lapi/mmap.h"
+
+#define HPAGE_PMD_SYSFS PATH_MM_THP "/hpage_pmd_size"
+
+#define WIN_FULL 0
+#define WIN_PARTIAL 1
+#define WIN_NOHUGEP 2
+#define WIN_EMPTY 3
+
+static char *windows[4];
+static char *bases[4];
+static char *clamp_base;
+static char *clamp_first;
+static char *clamp_second;
+static size_t pmd_size;
+static unsigned long pmd_kb;
+static size_t page_size;
+
+/*
+ * Returns the AnonHugePages counter [kB] of the VMA which contains addr,
+ * or -1 if no such VMA exists or the smaps entry has no AnonHugePages
+ * field. The bounds of the VMA are reported through vstart/vend.
+ */
+static long smaps_anon_huge(const void *addr, unsigned long *vstart,
+ unsigned long *vend)
+{
+ FILE *f = SAFE_FOPEN("/proc/self/smaps", "r");
+ char line[512];
+ unsigned long start, end, val = 0;
+ int in_vma = 0, seen = 0;
+
+ while (fgets(line, sizeof(line), f)) {
+ if (sscanf(line, "%lx-%lx", &start, &end) == 2) {
+ if (in_vma)
+ break;
+ in_vma = (unsigned long)addr >= start &&
+ (unsigned long)addr < end;
+ if (in_vma) {
+ *vstart = start;
+ *vend = end;
+ }
+ continue;
+ }
+
+ if (in_vma && sscanf(line, "AnonHugePages: %lu", &val) == 1) {
+ seen = 1;
+ break;
+ }
+ }
+
+ SAFE_FCLOSE(f);
+
+ if (!in_vma || !seen)
+ return -1;
+
+ return (long)val;
+}
+
+static void check_anon_huge(const char *desc, const void *addr,
+ unsigned long expected)
+{
+ unsigned long vstart, vend;
+ long ahp = smaps_anon_huge(addr, &vstart, &vend);
+
+ if (ahp < 0) {
+ tst_res(TFAIL, "%s: no VMA/AnonHugePages entry for %p in smaps",
+ desc, addr);
+ return;
+ }
+
+ /*
+ * The checked window must be an isolated VMA, otherwise the
+ * AnonHugePages counter covers more than the window itself.
+ */
+ if (vstart != (unsigned long)addr || vend - vstart != pmd_size)
+ tst_brk(TBROK, "%s: VMA %lx-%lx is not the isolated window %p",
+ desc, vstart, vend, addr);
+
+ if ((unsigned long)ahp == expected)
+ tst_res(TPASS, "%s: AnonHugePages %lu kB as expected", desc, expected);
+ else
+ tst_res(TFAIL, "%s: AnonHugePages %ld kB, expected %lu kB",
+ desc, ahp, expected);
+}
+
+static void populate_base(void *addr, size_t len, char c)
+{
+ TST_EXP_PASS(madvise(addr, pmd_size, MADV_NOHUGEPAGE));
+ memset(addr, c, len);
+ TST_EXP_PASS(madvise(addr, pmd_size, MADV_HUGEPAGE));
+}
+
+static char pattern_byte(size_t i)
+{
+ return (char)(i ^ (i >> 12));
+}
+
+static void fill_pattern(void *addr, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ ((char *)addr)[i] = pattern_byte(i);
+}
+
+static void verify_pattern(const char *desc, void *addr, size_t len)
+{
+ size_t i, bad = 0, first = 0;
+
+ for (i = 0; i < len; i++) {
+ if (((char *)addr)[i] != pattern_byte(i)) {
+ if (!bad)
+ first = i;
+ bad++;
+ }
+ }
+
+ if (!bad) {
+ tst_res(TPASS, "%s: page contents preserved (%zu bytes)", desc, len);
+ return;
+ }
+
+ tst_res(TFAIL, "%s: %zu of %zu bytes corrupted, first at offset %zu",
+ desc, bad, len, first);
+}
+
+static void verify_zero(const char *desc, void *addr, size_t from, size_t len)
+{
+ size_t i;
+
+ for (i = from; i < from + len; i++) {
+ if (((char *)addr)[i]) {
+ tst_res(TFAIL, "%s: byte %zu is %d, expected zero fill",
+ desc, i, ((char *)addr)[i]);
+ return;
+ }
+ }
+
+ tst_res(TPASS, "%s: %zu bytes zero-filled as expected", desc, len);
+}
+
+static void setup(void)
+{
+ int i;
+
+ page_size = SAFE_SYSCONF(_SC_PAGESIZE);
+
+ if (access(HPAGE_PMD_SYSFS, F_OK))
+ tst_brk(TCONF, "transparent hugepages not exposed (%s)",
+ HPAGE_PMD_SYSFS);
+
+ pmd_size = TST_SYSFS_READ_LI(HPAGE_PMD_SYSFS);
+ if (pmd_size < 2 * page_size || pmd_size % page_size)
+ tst_brk(TCONF, "unexpected PMD THP size %zu", pmd_size);
+
+ pmd_kb = pmd_size >> 10;
+ tst_res(TINFO, "PMD THP size: %zu kB", pmd_size >> 10);
+
+ /*
+ * The static .min_mem_avail covers the usual 2 MB PMD size, this
+ * runtime check additionally skips machines which could not hold
+ * the test windows on architectures with larger PMD sizes.
+ */
+ if (8 * pmd_size > (size_t)tst_available_mem() * 1024)
+ tst_brk(TCONF, "not enough free memory (PMD THP size %zu kB)",
+ pmd_size >> 10);
+
+ for (i = 0; i < 4; i++) {
+ bases[i] = SAFE_MMAP(NULL, 2 * pmd_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ windows[i] = (char *)(((unsigned long)bases[i] + pmd_size - 1) &
+ ~((unsigned long)pmd_size - 1));
+ }
+
+ /*
+ * Dedicated 4*pmd mapping for the clamp checks: ranges starting one
+ * page inside the first window provably stay within the mapping
+ * regardless of where the mmap() base landed, so the result cannot
+ * turn into an unmapped-gap ENOMEM.
+ */
+ clamp_base = SAFE_MMAP(NULL, 4 * pmd_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ clamp_first = (char *)(((unsigned long)clamp_base + pmd_size - 1) &
+ ~((unsigned long)pmd_size - 1));
+ clamp_second = clamp_first + pmd_size;
+}
+
+static void run(void)
+{
+ int i;
+
+ /*
+ * Zap any THP and pages left over from a previous iteration, as
+ * MADV_NOHUGEPAGE in populate_base() only flips VMA flags and does
+ * not split an already PMD-mapped THP.
+ */
+ for (i = 0; i < 4; i++) {
+ if (madvise(windows[i], pmd_size, MADV_DONTNEED))
+ tst_brk(TBROK | TERRNO,
+ "MADV_DONTNEED failed for window %d", i);
+ }
+
+ if (madvise(clamp_base, 4 * pmd_size, MADV_DONTNEED))
+ tst_brk(TBROK | TERRNO, "MADV_DONTNEED failed for clamp mapping");
+
+ /* fully populated window is collapsed into a single THP */
+ TST_EXP_PASS(madvise(windows[WIN_FULL], pmd_size, MADV_NOHUGEPAGE));
+ fill_pattern(windows[WIN_FULL], pmd_size);
+ /*
+ * Check the baseline while MADV_NOHUGEPAGE still blocks khugepaged
+ * from legally collapsing the fully populated window.
+ */
+ check_anon_huge("base-page populated window before collapse",
+ windows[WIN_FULL], 0);
+ TST_EXP_PASS(madvise(windows[WIN_FULL], pmd_size, MADV_HUGEPAGE));
+
+ TST_EXP_PASS(madvise(windows[WIN_FULL], pmd_size, MADV_COLLAPSE));
+ check_anon_huge("window after MADV_COLLAPSE", windows[WIN_FULL], pmd_kb);
+ verify_pattern("window after MADV_COLLAPSE", windows[WIN_FULL], pmd_size);
+
+ /* collapsing an already PMD-mapped window is a no-op */
+ TST_EXP_PASS(madvise(windows[WIN_FULL], pmd_size, MADV_COLLAPSE));
+ check_anon_huge("window after repeated MADV_COLLAPSE", windows[WIN_FULL], pmd_kb);
+
+ /* window with a single resident page is collapsed as well */
+ populate_base(windows[WIN_PARTIAL], page_size, 'b');
+
+ TST_EXP_PASS(madvise(windows[WIN_PARTIAL], pmd_size, MADV_COLLAPSE));
+ check_anon_huge("partially populated window after MADV_COLLAPSE",
+ windows[WIN_PARTIAL], pmd_kb);
+
+ if (windows[WIN_PARTIAL][0] == 'b')
+ tst_res(TPASS, "page content preserved after collapse");
+ else
+ tst_res(TFAIL, "page content corrupted after collapse");
+
+ verify_zero("unpopulated pages zero-filled by collapse",
+ windows[WIN_PARTIAL], page_size, pmd_size - page_size);
+
+ /* MADV_NOHUGEPAGE blocks the collapse */
+ TST_EXP_PASS(madvise(windows[WIN_NOHUGEP], pmd_size, MADV_NOHUGEPAGE));
+ memset(windows[WIN_NOHUGEP], 'c', pmd_size);
+
+ TST_EXP_FAIL(madvise(windows[WIN_NOHUGEP], pmd_size, MADV_COLLAPSE), EINVAL);
+ check_anon_huge("MADV_NOHUGEPAGE window after MADV_COLLAPSE",
+ windows[WIN_NOHUGEP], 0);
+
+ /* window without any resident page cannot be collapsed */
+ TST_EXP_FAIL(madvise(windows[WIN_EMPTY], pmd_size, MADV_COLLAPSE), EINVAL);
+
+ /*
+ * Unmapped range: drop one page from the untouched mapping slack,
+ * a standalone hole could be reused by the allocator.
+ */
+ SAFE_MUNMAP(windows[WIN_EMPTY] + pmd_size, page_size);
+ TST_EXP_FAIL(madvise(windows[WIN_EMPTY] + pmd_size, page_size,
+ MADV_COLLAPSE), ENOMEM);
+
+ /* range shorter than one PMD window is clamped into a no-op */
+ TST_EXP_PASS(madvise(windows[WIN_FULL], page_size, MADV_COLLAPSE));
+
+ /* zero-length range is a no-op */
+ TST_EXP_PASS(madvise(windows[WIN_FULL], 0, MADV_COLLAPSE));
+
+ /*
+ * A page-aligned start which is not PMD-aligned is clamped to whole
+ * PMD windows instead of being rejected. Touch one page in the first
+ * and third window first: the touch splits each window into a VMA of
+ * its own, as smaps counts AnonHugePages per VMA and an untouched
+ * window would share its VMA with the mapping slack. It also
+ * initializes the VMA's anon_vma, without which hugepage_vma_check()
+ * rejects the whole call with EINVAL, even for a range which clamps
+ * to an empty set.
+ */
+ madvise(clamp_first, pmd_size, MADV_NOHUGEPAGE);
+ memset(clamp_first, 'e', page_size);
+ madvise(clamp_first, pmd_size, MADV_HUGEPAGE);
+
+ populate_base(clamp_second, pmd_size, 'd');
+
+ madvise(clamp_second + pmd_size, pmd_size, MADV_NOHUGEPAGE);
+ memset(clamp_second + pmd_size, 'f', page_size);
+ madvise(clamp_second + pmd_size, pmd_size, MADV_HUGEPAGE);
+
+ /* the range clamps to an empty set: nothing is collapsed */
+ TST_EXP_PASS(madvise(clamp_first + page_size, pmd_size, MADV_COLLAPSE));
+ check_anon_huge("range clamped to an empty set", clamp_second, 0);
+
+ /*
+ * Only the fully covered second window is collapsed, the partial
+ * first window is skipped by the clamp.
+ */
+ TST_EXP_PASS(madvise(clamp_first + page_size, 2 * pmd_size, MADV_COLLAPSE));
+ check_anon_huge("window collapsed after clamping", clamp_second, pmd_kb);
+ check_anon_huge("partial window skipped by clamping", clamp_first, 0);
+}
+
+static void cleanup(void)
+{
+ int i;
+
+ for (i = 0; i < 4; i++) {
+ if (bases[i])
+ SAFE_MUNMAP(bases[i], 2 * pmd_size);
+ }
+
+ if (clamp_base)
+ SAFE_MUNMAP(clamp_base, 4 * pmd_size);
+}
+
+static struct tst_test test = {
+ .test_all = run,
+ .setup = setup,
+ .cleanup = cleanup,
+ .min_kver = "6.1",
+ .min_mem_avail = 16,
+ .needs_root = 1,
+ .save_restore = (const struct tst_path_val[]) {
+ {PATH_MM_THP "/khugepaged/scan_sleep_millisecs", "30000"},
+ {}
+ },
+ .needs_kconfigs = (const char *[]) {
+ "CONFIG_TRANSPARENT_HUGEPAGE=y",
+ NULL
+ },
+ .tags = (const struct tst_tag[]) {
+ {"linux-git", "7d8faaf155454f8798ec56404faca29a82689c77"},
+ {}
+ },
+};
--
2.43.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [LTP] madvise13: Add test for MADV_COLLAPSE
2026-10-30 3:19 [LTP] [PATCH v1] madvise13: Add test for MADV_COLLAPSE Xuewen Wang
@ 2026-10-30 6:43 ` linuxtestproject.agent
0 siblings, 0 replies; 2+ messages in thread
From: linuxtestproject.agent @ 2026-10-30 6:43 UTC (permalink / raw)
To: Xuewen Wang; +Cc: ltp
Reviewed commit: ac63c2921 ("madvise13: Add test for MADV_COLLAPSE")
Verdict - Reviewed
No issues found.
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-30 6:43 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-30 3:19 [LTP] [PATCH v1] madvise13: Add test for MADV_COLLAPSE Xuewen Wang
2026-10-30 6:43 ` [LTP] " linuxtestproject.agent
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.