Linux Kernel Selftest development
 help / color / mirror / Atom feed
* [PATCH 00/16] selftests/mm: improve khugepaged coverage
@ 2026-08-02 19:52 Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 01/16] selftests/mm: move is_backed_by_folio() into vm_util Kiryl Shutsemau
                   ` (17 more replies)
  0 siblings, 18 replies; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

mTHP collapse went in for 7.2 with no functional selftest coverage. Every
khugepaged collapse case is PMD-shaped, down to the detection: it reads
smaps AnonHugePages, which cannot see anything below the PMD order.

I am also reworking khugepaged's collapse mechanism, and that wants a suite
worth trusting before the mechanism changes underneath it. Nothing here
depends on the rework -- everything passes on an unmodified kernel and
documents what khugepaged already does.

 - vm_util grows folio-order helpers: is_backed_by_folio() moves out of
   split_huge_page_test.c, and a range-level form asks whether every
   order-aligned window of a range is backed by one folio of exactly that
   order, mapped head to tail.

 - folio_order_check validates those helpers against the kernel for every
   anon THP order it supports, before any collapse test trusts them.

 - khugepaged_full_pass() drives exactly one scan pass through the sysfs
   wake path, so a barrier completes on the daemon's own cadence instead of
   needing a short scan_sleep_millisecs.

 - khugepaged -o <order> runs order-parameterized anon collapse cases: a
   full table, only the populated window, default max_ptes_none and
   max_ptes_none=0, and collapse upward from smaller large folios. Any
   order up to the PMD order, where the same cases baseline the PMD path.

 - khugepaged_race races two faulters, MADV_DONTNEED, transient FOLL_PIN,
   fork and mremap against one of three collapse drivers: khugepaged a pass
   at a time, khugepaged free-running, or a MADV_COLLAPSE loop. Every
   racing page must read as its pattern or as zero. -z collapses
   hole-bearing windows; -p adds pageout and compaction, the only threads
   that elevate a source folio's refcount from the reclaim side. DEBUG_VM,
   page_table_check, KASAN and lockdep are the other half of the oracle.

 - A shared-source write race pins the CoW isolation contract: a co-sharer
   writing throughout a collapse must not see the collapsing side's pages.

 - Four tests fail on the environment rather than on the kernel: the
   collapse wait is a fixed three seconds whatever a huge page costs to
   build, collapse_compound_extreme wants a 512M page from the fault path,
   the shmem cases want a PMD-order page cache folio the page cache caps
   below, and the swap cases fail instead of skipping without swap. Scale
   the wait (a 2M PMD is unchanged) and skip the other three. This is why
   the suite now runs on arm64 with 64K pages.

Tested on mm-new (1dbd7c34bb92):

  x86-64 4K    105 pass, 3 skip, 0 fail
  arm64 64K     87 pass, 7 skip, 0 fail

The skips are structural: sub-PMD cases decline at the PMD order and the
mixed-source case at the smallest order, and on 64K pages
collapse_compound_extreme and the shmem cases skip for the reasons above.

Kiryl Shutsemau (Meta) (16):
  selftests/mm: move is_backed_by_folio() into vm_util
  selftests/mm: add folio-order check for VA ranges
  selftests/mm: add folio-order detection self-check
  selftests/mm: add order-parameterized khugepaged collapse cases
  selftests/mm: add khugepaged completion barrier helper
  selftests/mm: add khugepaged race harness
  selftests/mm: cover a shared-source collapse write race
  selftests/mm: skip collapse_compound_extreme where the PMD is too
    large
  selftests/mm: skip khugepaged swap tests when no swap is configured
  selftests/mm: verify synchronous khugepaged driving is attributable
  selftests/mm: race-harness variant for permissive hole occupancy
  selftests/mm: add memory-pressure threads to the khugepaged race
    harness
  selftests/mm: parameterize the mixed-source collapse case by source
    order
  selftests/mm: zap whole PTE tables in the khugepaged race harness
  selftests/mm: scale khugepaged's collapse wait with the PMD size
  selftests/mm: skip khugepaged shmem cases without a PMD page cache
    folio

 tools/testing/selftests/mm/.gitignore         |   3 +
 tools/testing/selftests/mm/Makefile           |   3 +
 .../testing/selftests/mm/folio_order_check.c  | 138 +++++
 .../testing/selftests/mm/hugepage_settings.c  |  66 ++-
 .../testing/selftests/mm/hugepage_settings.h  |   2 +
 tools/testing/selftests/mm/khugepaged.c       | 430 +++++++++++++++-
 tools/testing/selftests/mm/khugepaged_race.c  | 483 ++++++++++++++++++
 .../selftests/mm/khugepaged_sync_check.c      | 198 +++++++
 tools/testing/selftests/mm/run_vmtests.sh     |  17 +
 .../selftests/mm/split_huge_page_test.c       |  62 ---
 tools/testing/selftests/mm/vm_util.c          | 157 ++++++
 tools/testing/selftests/mm/vm_util.h          |   7 +
 12 files changed, 1490 insertions(+), 76 deletions(-)
 create mode 100644 tools/testing/selftests/mm/folio_order_check.c
 create mode 100644 tools/testing/selftests/mm/khugepaged_race.c
 create mode 100644 tools/testing/selftests/mm/khugepaged_sync_check.c

-- 
2.54.0


^ permalink raw reply	[flat|nested] 28+ messages in thread

* [PATCH 01/16] selftests/mm: move is_backed_by_folio() into vm_util
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-03 11:04   ` Mike Rapoport
  2026-08-02 19:52 ` [PATCH 02/16] selftests/mm: add folio-order check for VA ranges Kiryl Shutsemau
                   ` (16 subsequent siblings)
  17 siblings, 1 reply; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

The khugepaged selftest is about to gain mTHP collapse coverage, which
needs to verify that a VA range is backed by a folio of a given order.
split_huge_page_test.c already has the building block for that:
is_backed_by_folio() classifies the folio backing a page via
/proc/kpageflags compound head/tail flags.

Move it into vm_util so other tests can use it. No functional change.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 .../selftests/mm/split_huge_page_test.c       | 62 -------------------
 tools/testing/selftests/mm/vm_util.c          | 62 +++++++++++++++++++
 tools/testing/selftests/mm/vm_util.h          |  2 +
 3 files changed, 64 insertions(+), 62 deletions(-)

diff --git a/tools/testing/selftests/mm/split_huge_page_test.c b/tools/testing/selftests/mm/split_huge_page_test.c
index 32b991472f74..9b3885bd001b 100644
--- a/tools/testing/selftests/mm/split_huge_page_test.c
+++ b/tools/testing/selftests/mm/split_huge_page_test.c
@@ -42,68 +42,6 @@ const char *kpageflags_proc = "/proc/kpageflags";
 int pagemap_fd;
 int kpageflags_fd;
 
-static bool is_backed_by_folio(char *vaddr, int order, int pagemap_fd,
-		int kpageflags_fd)
-{
-	const uint64_t folio_head_flags = KPF_THP | KPF_COMPOUND_HEAD;
-	const uint64_t folio_tail_flags = KPF_THP | KPF_COMPOUND_TAIL;
-	const unsigned long nr_pages = 1UL << order;
-	unsigned long pfn_head;
-	uint64_t pfn_flags;
-	unsigned long pfn;
-	unsigned long i;
-
-	pfn = pagemap_get_pfn(pagemap_fd, vaddr);
-
-	/* non present page */
-	if (pfn == -1UL)
-		return false;
-
-	if (pageflags_get(pfn, kpageflags_fd, &pfn_flags))
-		goto fail;
-
-	/* check for order-0 pages */
-	if (!order) {
-		if (pfn_flags & (folio_head_flags | folio_tail_flags))
-			return false;
-		return true;
-	}
-
-	/* non THP folio */
-	if (!(pfn_flags & KPF_THP))
-		return false;
-
-	pfn_head = pfn & ~(nr_pages - 1);
-
-	if (pageflags_get(pfn_head, kpageflags_fd, &pfn_flags))
-		goto fail;
-
-	/* head PFN has no compound_head flag set */
-	if ((pfn_flags & folio_head_flags) != folio_head_flags)
-		return false;
-
-	/* check all tail PFN flags */
-	for (i = 1; i < nr_pages; i++) {
-		if (pageflags_get(pfn_head + i, kpageflags_fd, &pfn_flags))
-			goto fail;
-		if ((pfn_flags & folio_tail_flags) != folio_tail_flags)
-			return false;
-	}
-
-	/*
-	 * check the PFN after this folio, but if its flags cannot be obtained,
-	 * assume this folio has the expected order
-	 */
-	if (pageflags_get(pfn_head + nr_pages, kpageflags_fd, &pfn_flags))
-		return true;
-
-	/* If we find another tail page, then the folio is larger. */
-	return (pfn_flags & folio_tail_flags) != folio_tail_flags;
-fail:
-	ksft_exit_fail_msg("Failed to get folio info\n");
-	return false;
-}
-
 static int vaddr_pageflags_get(char *vaddr, int pagemap_fd, int kpageflags_fd,
 		uint64_t *flags)
 {
diff --git a/tools/testing/selftests/mm/vm_util.c b/tools/testing/selftests/mm/vm_util.c
index ef1ea11981a7..343a15e25a9f 100644
--- a/tools/testing/selftests/mm/vm_util.c
+++ b/tools/testing/selftests/mm/vm_util.c
@@ -303,6 +303,68 @@ int pageflags_get(unsigned long pfn, int kpageflags_fd, uint64_t *flags)
 	return 0;
 }
 
+bool is_backed_by_folio(char *vaddr, int order, int pagemap_fd,
+			int kpageflags_fd)
+{
+	const uint64_t folio_head_flags = KPF_THP | KPF_COMPOUND_HEAD;
+	const uint64_t folio_tail_flags = KPF_THP | KPF_COMPOUND_TAIL;
+	const unsigned long nr_pages = 1UL << order;
+	unsigned long pfn_head;
+	uint64_t pfn_flags;
+	unsigned long pfn;
+	unsigned long i;
+
+	pfn = pagemap_get_pfn(pagemap_fd, vaddr);
+
+	/* non present page */
+	if (pfn == -1UL)
+		return false;
+
+	if (pageflags_get(pfn, kpageflags_fd, &pfn_flags))
+		goto fail;
+
+	/* check for order-0 pages */
+	if (!order) {
+		if (pfn_flags & (folio_head_flags | folio_tail_flags))
+			return false;
+		return true;
+	}
+
+	/* non THP folio */
+	if (!(pfn_flags & KPF_THP))
+		return false;
+
+	pfn_head = pfn & ~(nr_pages - 1);
+
+	if (pageflags_get(pfn_head, kpageflags_fd, &pfn_flags))
+		goto fail;
+
+	/* head PFN has no compound_head flag set */
+	if ((pfn_flags & folio_head_flags) != folio_head_flags)
+		return false;
+
+	/* check all tail PFN flags */
+	for (i = 1; i < nr_pages; i++) {
+		if (pageflags_get(pfn_head + i, kpageflags_fd, &pfn_flags))
+			goto fail;
+		if ((pfn_flags & folio_tail_flags) != folio_tail_flags)
+			return false;
+	}
+
+	/*
+	 * check the PFN after this folio, but if its flags cannot be obtained,
+	 * assume this folio has the expected order
+	 */
+	if (pageflags_get(pfn_head + nr_pages, kpageflags_fd, &pfn_flags))
+		return true;
+
+	/* If we find another tail page, then the folio is larger. */
+	return (pfn_flags & folio_tail_flags) != folio_tail_flags;
+fail:
+	ksft_exit_fail_msg("Failed to get folio info\n");
+	return false;
+}
+
 /* If `ioctls' non-NULL, the allowed ioctls will be returned into the var */
 int uffd_register_with_ioctls(int uffd, void *addr, uint64_t len,
 			      bool miss, bool wp, bool minor, uint64_t *ioctls)
diff --git a/tools/testing/selftests/mm/vm_util.h b/tools/testing/selftests/mm/vm_util.h
index 7799154b67ee..5eb90e0d1d13 100644
--- a/tools/testing/selftests/mm/vm_util.h
+++ b/tools/testing/selftests/mm/vm_util.h
@@ -95,6 +95,8 @@ bool check_huge_file(void *addr, int nr_hpages, uint64_t hpage_size);
 bool check_huge_shmem(void *addr, 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);
+bool is_backed_by_folio(char *vaddr, int order, int pagemap_fd,
+			int kpageflags_fd);
 
 int uffd_register(int uffd, void *addr, uint64_t len,
 		  bool miss, bool wp, bool minor);
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 02/16] selftests/mm: add folio-order check for VA ranges
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 01/16] selftests/mm: move is_backed_by_folio() into vm_util Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-03 11:04   ` Mike Rapoport
  2026-08-02 19:52 ` [PATCH 03/16] selftests/mm: add folio-order detection self-check Kiryl Shutsemau
                   ` (15 subsequent siblings)
  17 siblings, 1 reply; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

is_backed_by_folio() answers "what order folio backs this page", but
mTHP collapse tests need the range-level question: is every
order-aligned window of this VA range backed by one folio of exactly
that order, mapped head-to-tail?

Add is_range_backed_by_folio_orders(): per window, require a present
and naturally aligned head PFN, a contiguous PFN run across the
window, and is_backed_by_folio() agreeing on the order. A window
assembled from pieces of different folios, or mapping a folio outside
its natural position, fails the check.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/vm_util.c | 46 ++++++++++++++++++++++++++++
 tools/testing/selftests/mm/vm_util.h |  2 ++
 2 files changed, 48 insertions(+)

diff --git a/tools/testing/selftests/mm/vm_util.c b/tools/testing/selftests/mm/vm_util.c
index 343a15e25a9f..793342095420 100644
--- a/tools/testing/selftests/mm/vm_util.c
+++ b/tools/testing/selftests/mm/vm_util.c
@@ -365,6 +365,52 @@ bool is_backed_by_folio(char *vaddr, int order, int pagemap_fd,
 	return false;
 }
 
+/*
+ * Check whether the range [start, start + len) is backed by folios of
+ * exactly @order, mapped at their natural alignment.
+ *
+ * is_backed_by_folio() classifies the folio backing one page; here we
+ * additionally require that each order-aligned window of the range maps
+ * one such folio head-to-tail: the VA range must be naturally aligned,
+ * each window's PFN run must be contiguous, and the first PFN must be
+ * the (naturally aligned) folio head.
+ *
+ * This is the check "did this range collapse into order-@order folios":
+ * a window assembled from parts of several folios, or mapping a folio
+ * shifted from its natural position, fails.
+ */
+bool is_range_backed_by_folio_orders(char *start, size_t len, int order,
+				     int pagemap_fd, int kpageflags_fd)
+{
+	const unsigned long nr_pages = 1UL << order;
+	const size_t window = nr_pages * psize();
+	char *vaddr;
+
+	if ((uintptr_t)start % window || len % window)
+		return false;
+
+	for (vaddr = start; vaddr < start + len; vaddr += window) {
+		unsigned long pfn = pagemap_get_pfn(pagemap_fd, vaddr);
+		unsigned long i;
+
+		/* Not present, or not mapping the folio head. */
+		if (pfn == -1UL || pfn % nr_pages)
+			return false;
+
+		for (i = 1; i < nr_pages; i++) {
+			if (pagemap_get_pfn(pagemap_fd, vaddr + i * psize()) !=
+			    pfn + i)
+				return false;
+		}
+
+		if (!is_backed_by_folio(vaddr, order, pagemap_fd,
+					kpageflags_fd))
+			return false;
+	}
+
+	return true;
+}
+
 /* If `ioctls' non-NULL, the allowed ioctls will be returned into the var */
 int uffd_register_with_ioctls(int uffd, void *addr, uint64_t len,
 			      bool miss, bool wp, bool minor, uint64_t *ioctls)
diff --git a/tools/testing/selftests/mm/vm_util.h b/tools/testing/selftests/mm/vm_util.h
index 5eb90e0d1d13..76e9938a908e 100644
--- a/tools/testing/selftests/mm/vm_util.h
+++ b/tools/testing/selftests/mm/vm_util.h
@@ -97,6 +97,8 @@ int64_t allocate_transhuge(void *ptr, int pagemap_fd);
 int pageflags_get(unsigned long pfn, int kpageflags_fd, uint64_t *flags);
 bool is_backed_by_folio(char *vaddr, int order, int pagemap_fd,
 			int kpageflags_fd);
+bool is_range_backed_by_folio_orders(char *start, size_t len, int order,
+				     int pagemap_fd, int kpageflags_fd);
 
 int uffd_register(int uffd, void *addr, uint64_t len,
 		  bool miss, bool wp, bool minor);
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 03/16] selftests/mm: add folio-order detection self-check
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 01/16] selftests/mm: move is_backed_by_folio() into vm_util Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 02/16] selftests/mm: add folio-order check for VA ranges Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-03 11:04   ` Mike Rapoport
  2026-08-02 19:52 ` [PATCH 04/16] selftests/mm: add order-parameterized khugepaged collapse cases Kiryl Shutsemau
                   ` (14 subsequent siblings)
  17 siblings, 1 reply; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

The upcoming khugepaged mTHP tests detect collapse results with the
vm_util folio-order helpers instead of smaps AnonHugePages, which only
sees PMD mappings. Before any collapse test trusts those helpers, make
sure they agree with the kernel about what backs a mapping.

For every anon THP order the kernel supports, fault memory in with
only that order enabled and require the helpers to classify the
backing as exactly that order: not a neighbouring order, and 4K-backed
memory as order 0.

Runs in the thp category of run_vmtests.sh. Verified on x86-64 4K
(orders 0, 2-9) and arm64 64K (orders 0, 2-13).

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/.gitignore         |   1 +
 tools/testing/selftests/mm/Makefile           |   1 +
 .../testing/selftests/mm/folio_order_check.c  | 138 ++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh     |   2 +
 4 files changed, 142 insertions(+)
 create mode 100644 tools/testing/selftests/mm/folio_order_check.c

diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore
index 9ccd9e1447e6..54eefd8f97bc 100644
--- a/tools/testing/selftests/mm/.gitignore
+++ b/tools/testing/selftests/mm/.gitignore
@@ -66,3 +66,4 @@ merge
 prctl_thp_disable
 rmap
 folio_split_race_test
+folio_order_check
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index 25d10ced3b3b..33917c76b871 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -104,6 +104,7 @@ TEST_GEN_FILES += guard-regions
 TEST_GEN_FILES += merge
 TEST_GEN_FILES += rmap
 TEST_GEN_FILES += folio_split_race_test
+TEST_GEN_FILES += folio_order_check
 
 ifneq ($(ARCH),arm64)
 TEST_GEN_FILES += soft-dirty
diff --git a/tools/testing/selftests/mm/folio_order_check.c b/tools/testing/selftests/mm/folio_order_check.c
new file mode 100644
index 000000000000..f70d766ff3eb
--- /dev/null
+++ b/tools/testing/selftests/mm/folio_order_check.c
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Self-check for the vm_util folio-order detection helpers,
+ * is_backed_by_folio() and is_range_backed_by_folio_orders().
+ *
+ * For every anon THP order the kernel supports, fault memory in with only
+ * that order enabled and verify the helpers report exactly that order:
+ * not a neighbouring order, and plain 4K memory as order 0. The helpers
+ * are what the khugepaged mTHP tests use to detect collapse results, so
+ * they must agree with the kernel's own idea of the backing before any
+ * collapse test relies on them.
+ */
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+#include "vm_util.h"
+#include "hugepage_settings.h"
+
+static int pagemap_fd;
+static int kpageflags_fd;
+
+/* mmap an anon VMA of exactly @size bytes at a @size-aligned address. */
+static char *alloc_aligned(size_t size)
+{
+	size_t len = size * 2;
+	uintptr_t aligned;
+	char *p;
+
+	p = mmap(NULL, len, PROT_READ | PROT_WRITE,
+		 MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+	if (p == MAP_FAILED)
+		ksft_exit_fail_perror("mmap()");
+
+	aligned = ((uintptr_t)p + size - 1) & ~(size - 1);
+	if (aligned != (uintptr_t)p)
+		munmap(p, aligned - (uintptr_t)p);
+	if (aligned + size != (uintptr_t)p + len)
+		munmap((char *)aligned + size,
+		       (uintptr_t)p + len - aligned - size);
+
+	return (char *)aligned;
+}
+
+/*
+ * Enable only @order (order 0: nothing), fault one aligned window in and
+ * check the helpers see exactly @order.
+ */
+static void check_order(int order)
+{
+	struct thp_settings settings = *thp_current_settings();
+	size_t size = psize() << order;
+	bool ok = true;
+	char *p;
+	int i;
+
+	for (i = 0; i < NR_ORDERS; i++)
+		settings.hugepages[i].enabled = THP_NEVER;
+	if (order)
+		settings.hugepages[order].enabled = THP_ALWAYS;
+	thp_push_settings(&settings);
+
+	p = alloc_aligned(size);
+	*p = 1;
+
+	if (!is_range_backed_by_folio_orders(p, size, order,
+					     pagemap_fd, kpageflags_fd)) {
+		ksft_print_msg("order %d not detected after fault\n", order);
+		ok = false;
+	}
+
+	/* A lower order must be rejected: the folio is larger. */
+	if (order && is_range_backed_by_folio_orders(p, size, order - 1,
+						     pagemap_fd,
+						     kpageflags_fd)) {
+		ksft_print_msg("order %d also reported as order %d\n",
+			       order, order - 1);
+		ok = false;
+	}
+
+	/* Order 0 pages must not look like any large folio, and vice versa. */
+	if (order && is_range_backed_by_folio_orders(p, size, 0,
+						     pagemap_fd,
+						     kpageflags_fd)) {
+		ksft_print_msg("order %d also reported as order 0\n", order);
+		ok = false;
+	}
+
+	munmap(p, size);
+	thp_pop_settings();
+
+	ksft_test_result(ok, "order %d classified\n", order);
+}
+
+int main(void)
+{
+	struct thp_settings settings;
+	unsigned long orders;
+	int order;
+
+	ksft_print_header();
+
+	if (!thp_available())
+		ksft_exit_skip("Transparent Hugepages not available\n");
+
+	pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+	if (pagemap_fd < 0)
+		ksft_exit_fail_perror("open(\"/proc/self/pagemap\")");
+	kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
+	if (kpageflags_fd < 0)
+		ksft_exit_skip("open(\"/proc/kpageflags\") requires root\n");
+
+	orders = thp_supported_orders();
+	if (!orders)
+		ksft_exit_skip("No supported THP orders\n");
+
+	ksft_set_plan(__builtin_popcountl(orders) + 1);
+
+	thp_save_settings();
+	thp_read_settings(&settings);
+	/* Base of the settings stack; the bottom entry is never popped. */
+	thp_push_settings(&settings);
+
+	check_order(0);
+	for (order = 1; order < NR_ORDERS; order++) {
+		if (!(orders & (1UL << order)))
+			continue;
+		check_order(order);
+	}
+
+	thp_restore_settings();
+
+	ksft_finished();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 687d115e3bd8..8bf898b71350 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -402,6 +402,8 @@ CATEGORY="pfnmap" run_test ./pfnmap
 # COW tests
 CATEGORY="cow" run_test ./cow
 
+CATEGORY="thp" run_test ./folio_order_check
+
 CATEGORY="thp" run_test ./khugepaged
 
 CATEGORY="thp" run_test ./khugepaged -s 2
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 04/16] selftests/mm: add order-parameterized khugepaged collapse cases
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (2 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 03/16] selftests/mm: add folio-order detection self-check Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-03 11:04   ` Mike Rapoport
  2026-08-02 19:52 ` [PATCH 05/16] selftests/mm: add khugepaged completion barrier helper Kiryl Shutsemau
                   ` (13 subsequent siblings)
  17 siblings, 1 reply; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

All khugepaged collapse cases are written against PMD collapse: the
regions, the thresholds and the detection (smaps AnonHugePages, which
only accounts PMD mappings) all assume the PMD-order product. The mTHP
collapse support merged in 7.2 has no functional selftest coverage.

Add an -o <order> mode running order-parameterized anon collapse cases
against khugepaged. The region is faulted to order 0 first (the target
order is "inherit" under enabled=madvise, so pre-MADV_HUGEPAGE faults
cannot produce large folios), then one full khugepaged pass is awaited
via the full_scans barrier, and results are detected per aligned
window with the vm_util folio-order helpers:

- collapse_order_full: a fully populated PTE table collapses to the
  target order across the range;
- collapse_order_single_window: only the populated window collapses,
  empty neighbours stay untouched;
- collapse_order_partial_window: default max_ptes_none permits
  collapse of a window with a single present PTE;
- collapse_order_max_ptes_none: with max_ptes_none=0, a full window
  collapses while one missing a single page must not.

-o accepts any order up to the PMD order. At the PMD order the cases
baseline the PMD collapse path with the same test text used for the
mTHP orders; single_window and max_ptes_none, which place a second
window beyond the one-hugepage area, have no room below the PMD and
skip there via skip_at_pmd_order(), while full and partial_window run
unchanged.

The default and -s modes are untouched. Passes on x86-64 4K (-o 2, 3,
5, 8) and arm64 64K (-o 5, the 2M contPTE case); the default PMD suite
is unchanged at 26/26.

collapse_order_mixed_sources is an order-parameterized case too: a
region faulted as smaller large folios collapses to the target
order (collapse accepts source folios of any order below the target).
It passes on the current kernel, so it rides here with the other order
cases.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/khugepaged.c   | 289 +++++++++++++++++++++-
 tools/testing/selftests/mm/run_vmtests.sh |   2 +
 2 files changed, 289 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index 10e8dedcb087..971e97a7330a 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -26,9 +26,13 @@
 
 #define BASE_ADDR ((void *)(1UL << 30))
 static unsigned long hpage_pmd_size;
+static int hpage_pmd_order;
 static unsigned long page_size;
 static int hpage_pmd_nr;
 static int anon_order;
+static int anon_target_order;
+static int pagemap_fd = -1;
+static int kpageflags_fd = -1;
 
 #define PID_SMAPS "/proc/self/smaps"
 #define TEST_FILE "collapse_test_file"
@@ -1088,6 +1092,231 @@ static void madvise_retracted_page_tables(struct collapse_context *c,
 	ksft_test_result_report(exit_status, "%s\n", __func__);
 }
 
+/*
+ * Order-parameterized collapse cases (-o <order>): khugepaged collapses
+ * 4K-faulted memory into folios of the given sub-PMD order. Detection is
+ * folio-based (vm_util), not smaps: AnonHugePages only accounts PMD
+ * mappings.
+ *
+ * The region is faulted before MADV_HUGEPAGE and the target order is
+ * configured "inherit" under enabled=madvise, so faults are always
+ * order 0 and the collapse product can only come from khugepaged.
+ */
+static size_t anon_order_size(void)
+{
+	return page_size << anon_target_order;
+}
+
+static bool range_collapsed(void *p, size_t len)
+{
+	return is_range_backed_by_folio_orders(p, len, anon_target_order,
+					       pagemap_fd, kpageflags_fd);
+}
+
+/* No aligned window in [p, p + len) is backed at the target order. */
+static bool range_not_collapsed(void *p, size_t len)
+{
+	size_t window = anon_order_size();
+	char *addr = p;
+
+	for (; len >= window; addr += window, len -= window) {
+		if (range_collapsed(addr, window))
+			return false;
+	}
+	return true;
+}
+
+/*
+ * Completion barrier: one full khugepaged pass that started after this
+ * call. Waiting for full_scans to advance by two guarantees it; a +1
+ * step might complete a pass that scanned our mm before the setup.
+ */
+static bool khugepaged_wait_full_pass(void)
+{
+	int full_scans = thp_read_num("khugepaged/full_scans") + 2;
+	int timeout = 60; /* 30 seconds */
+
+	while (timeout--) {
+		if (thp_read_num("khugepaged/full_scans") >= full_scans)
+			return true;
+		printf(".");
+		usleep(TICK);
+	}
+	return false;
+}
+
+/*
+ * Cases whose geometry needs a window strictly below the PMD skip at
+ * -o <PMD order>; the rest run there unchanged, baselining the legacy
+ * PMD engine with the same test text used for the mTHP orders.
+ */
+static bool skip_at_pmd_order(const char *name)
+{
+	if (anon_target_order < hpage_pmd_order)
+		return false;
+	ksft_test_result_skip("%s: needs a window below the PMD\n", name);
+	return true;
+}
+
+static void collapse_order_full(struct collapse_context *c, struct mem_ops *ops)
+{
+	void *p;
+
+	p = ops->setup_area(1);
+	ops->fault(p, 0, hpage_pmd_size);
+	if (!range_not_collapsed(p, hpage_pmd_size))
+		ksft_exit_fail_msg("Unexpected large folio after fault\n");
+
+	madvise(p, hpage_pmd_size, MADV_HUGEPAGE);
+	ksft_print_msg("Collapse fully populated PTE table to target order...");
+	if (!khugepaged_wait_full_pass())
+		fail("Timeout");
+	else if (range_collapsed(p, hpage_pmd_size))
+		success("OK");
+	else
+		fail("Fail");
+
+	validate_memory(p, 0, hpage_pmd_size);
+	ops->cleanup_area(p, hpage_pmd_size);
+	ksft_test_result_report(exit_status, "%s\n", __func__);
+}
+
+static void collapse_order_single_window(struct collapse_context *c,
+					 struct mem_ops *ops)
+{
+	size_t window = anon_order_size();
+	void *p;
+
+	if (skip_at_pmd_order(__func__))
+		return;
+
+	p = ops->setup_area(1);
+	ops->fault(p, window, 2 * window);
+	if (!range_not_collapsed(p, hpage_pmd_size))
+		ksft_exit_fail_msg("Unexpected large folio after fault\n");
+
+	madvise(p, hpage_pmd_size, MADV_HUGEPAGE);
+	ksft_print_msg("Collapse one fully populated window...");
+	if (!khugepaged_wait_full_pass())
+		fail("Timeout");
+	else if (range_collapsed(p + window, window) &&
+		 range_not_collapsed(p, window) &&
+		 range_not_collapsed(p + 2 * window,
+				     hpage_pmd_size - 2 * window))
+		success("OK");
+	else
+		fail("Fail");
+
+	validate_memory(p, window, 2 * window);
+	ops->cleanup_area(p, hpage_pmd_size);
+	ksft_test_result_report(exit_status, "%s\n", __func__);
+}
+
+static void collapse_order_partial_window(struct collapse_context *c,
+					  struct mem_ops *ops)
+{
+	void *p;
+
+	p = ops->setup_area(1);
+	ops->fault(p, 0, page_size);
+	if (!range_not_collapsed(p, hpage_pmd_size))
+		ksft_exit_fail_msg("Unexpected large folio after fault\n");
+
+	madvise(p, hpage_pmd_size, MADV_HUGEPAGE);
+	ksft_print_msg("Collapse window with single PTE entry present...");
+	if (!khugepaged_wait_full_pass())
+		fail("Timeout");
+	else if (range_collapsed(p, anon_order_size()))
+		success("OK");
+	else
+		fail("Fail");
+
+	validate_memory(p, 0, page_size);
+	ops->cleanup_area(p, hpage_pmd_size);
+	ksft_test_result_report(exit_status, "%s\n", __func__);
+}
+
+static void collapse_order_max_ptes_none(struct collapse_context *c,
+					 struct mem_ops *ops)
+{
+	struct thp_settings settings = *thp_current_settings();
+	size_t window = anon_order_size();
+	void *p;
+
+	if (skip_at_pmd_order(__func__))
+		return;
+
+	settings.khugepaged.max_ptes_none = 0;
+	thp_push_settings(&settings);
+
+	p = ops->setup_area(1);
+	ops->fault(p, 0, 2 * window - page_size);
+	if (!range_not_collapsed(p, hpage_pmd_size))
+		ksft_exit_fail_msg("Unexpected large folio after fault\n");
+
+	madvise(p, hpage_pmd_size, MADV_HUGEPAGE);
+	ksft_print_msg("Collapse full window, not the one missing a page...");
+	if (!khugepaged_wait_full_pass())
+		fail("Timeout");
+	else if (range_collapsed(p, window) &&
+		 range_not_collapsed(p + window, window))
+		success("OK");
+	else
+		fail("Fail");
+
+	validate_memory(p, 0, 2 * window - page_size);
+	ops->cleanup_area(p, hpage_pmd_size);
+	thp_pop_settings();
+	ksft_test_result_report(exit_status, "%s\n", __func__);
+}
+
+/* Smallest order khugepaged will consider for mTHP collapse. */
+#define MIN_MTHP_ORDER 2
+
+/*
+ * A region backed by large folios of a smaller order is a valid source
+ * for collapse to the target order:
+ * __collapse_huge_page_isolate() accepts any source folio of an order
+ * strictly below the candidate's.
+ */
+static void collapse_order_mixed_sources(struct collapse_context *c,
+					 struct mem_ops *ops)
+{
+	struct thp_settings settings = *thp_current_settings();
+	void *p;
+
+	if (anon_target_order <= MIN_MTHP_ORDER) {
+		ksft_test_result_skip("%s: no source order below target\n",
+				      __func__);
+		return;
+	}
+
+	/* Fault the whole region as order-MIN_MTHP_ORDER folios. */
+	settings.hugepages[MIN_MTHP_ORDER].enabled = THP_ALWAYS;
+	thp_push_settings(&settings);
+	p = ops->setup_area(1);
+	ops->fault(p, 0, hpage_pmd_size);
+	thp_pop_settings();
+
+	if (!is_range_backed_by_folio_orders(p, hpage_pmd_size, MIN_MTHP_ORDER,
+					     pagemap_fd, kpageflags_fd))
+		ksft_exit_fail_msg("Region not backed by order-%d folios after fault\n",
+				   MIN_MTHP_ORDER);
+
+	madvise(p, hpage_pmd_size, MADV_HUGEPAGE);
+	ksft_print_msg("Collapse region backed by smaller large folios...");
+	if (!khugepaged_wait_full_pass())
+		fail("Timeout");
+	else if (range_collapsed(p, hpage_pmd_size))
+		success("OK");
+	else
+		fail("Fail");
+
+	validate_memory(p, 0, hpage_pmd_size);
+	ops->cleanup_area(p, hpage_pmd_size);
+	ksft_test_result_report(exit_status, "%s\n", __func__);
+}
+
 static void usage(void)
 {
 	fprintf(stderr, "\nUsage: ./khugepaged [OPTIONS] <test type> [dir]\n\n");
@@ -1103,6 +1332,9 @@ static void usage(void)
 	fprintf(stderr,	"\t\t-h: This help message.\n");
 	fprintf(stderr,	"\t\t-s: mTHP size, expressed as page order.\n");
 	fprintf(stderr,	"\t\t    Defaults to 0. Use this size for anon or shmem allocations.\n");
+	fprintf(stderr,	"\t\t-o: collapse target order for khugepaged:anon.\n");
+	fprintf(stderr,	"\t\t    Runs the order-parameterized collapse cases instead\n");
+	fprintf(stderr,	"\t\t    of the PMD cases. Cannot be combined with -s.\n");
 	exit(1);
 }
 
@@ -1112,17 +1344,23 @@ static void parse_test_type(int argc, char **argv)
 	char *buf;
 	const char *token;
 
-	while ((opt = getopt(argc, argv, "s:h")) != -1) {
+	while ((opt = getopt(argc, argv, "s:o:h")) != -1) {
 		switch (opt) {
 		case 's':
 			anon_order = atoi(optarg);
 			break;
+		case 'o':
+			anon_target_order = atoi(optarg);
+			break;
 		case 'h':
 		default:
 			usage();
 		}
 	}
 
+	if (anon_target_order && anon_order)
+		usage();
+
 	argv += optind;
 	argc -= optind;
 
@@ -1207,7 +1445,6 @@ static int nr_test_cases;
 
 int main(int argc, char **argv)
 {
-	int hpage_pmd_order;
 	struct thp_settings default_settings = {
 		.thp_enabled = THP_MADVISE,
 		.thp_defrag = THP_DEFRAG_ALWAYS,
@@ -1244,6 +1481,13 @@ int main(int argc, char **argv)
 	hpage_pmd_nr = hpage_pmd_size / page_size;
 	hpage_pmd_order = __builtin_ctz(hpage_pmd_nr);
 
+	if (anon_target_order &&
+	    !(thp_supported_orders() & (1UL << anon_target_order)))
+		ksft_exit_skip("Order %d is not a supported anon THP order\n",
+			       anon_target_order);
+	if (anon_target_order > hpage_pmd_order)
+		ksft_exit_fail_msg("-o takes at most the PMD order\n");
+
 	default_settings.khugepaged.max_ptes_none = hpage_pmd_nr - 1;
 	default_settings.khugepaged.max_ptes_swap = hpage_pmd_nr / 8;
 	default_settings.khugepaged.max_ptes_shared = hpage_pmd_nr / 2;
@@ -1253,9 +1497,49 @@ int main(int argc, char **argv)
 	default_settings.shmem_hugepages[hpage_pmd_order].enabled = SHMEM_INHERIT;
 	default_settings.shmem_hugepages[anon_order].enabled = SHMEM_ALWAYS;
 
+	if (anon_target_order) {
+		/*
+		 * Only the target order may produce large folios, and only
+		 * from khugepaged: "inherit" under enabled=madvise keeps
+		 * pre-MADV_HUGEPAGE faults at order 0.
+		 */
+		default_settings.hugepages[hpage_pmd_order].enabled = THP_NEVER;
+		default_settings.hugepages[anon_target_order].enabled = THP_INHERIT;
+		/*
+		 * Order-parameterized cases are driven strictly by the
+		 * khugepaged_full_pass() barrier, which wakes the daemon
+		 * itself. A long scan_sleep keeps khugepaged from
+		 * free-running between barrier steps, and a pages_to_scan
+		 * covering everything makes one wake complete one full
+		 * pass (the barrier's contract), so a run performs a
+		 * deterministic number of passes — what the stage A/B
+		 * trace comparisons rely on.
+		 */
+		default_settings.khugepaged.scan_sleep_millisecs = 60000;
+		default_settings.khugepaged.pages_to_scan = 1UL << 24;
+
+		pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+		if (pagemap_fd < 0)
+			ksft_exit_fail_perror("open(\"/proc/self/pagemap\")");
+		kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
+		if (kpageflags_fd < 0)
+			ksft_exit_skip("open(\"/proc/kpageflags\") requires root\n");
+	}
+
 	save_settings();
 	thp_push_settings(&default_settings);
 
+	if (anon_target_order) {
+		TEST(collapse_order_full, khugepaged_context, anon_ops);
+		TEST(collapse_order_single_window, khugepaged_context, anon_ops);
+		TEST(collapse_order_partial_window, khugepaged_context, anon_ops);
+		TEST(collapse_order_max_ptes_none, khugepaged_context, anon_ops);
+		TEST(collapse_order_mixed_sources, khugepaged_context, anon_ops);
+
+		ksft_set_plan(nr_test_cases);
+		goto run;
+	}
+
 	TEST(collapse_full, khugepaged_context, anon_ops);
 	TEST(collapse_full, khugepaged_context, read_only_file_ops);
 	TEST(collapse_full, khugepaged_context, read_write_file_read_ops);
@@ -1336,6 +1620,7 @@ int main(int argc, char **argv)
 	ksft_set_plan(nr_test_cases + 1);
 
 	alloc_at_fault();
+run:
 	for (int i = 0; i < nr_test_cases; i++) {
 		struct test_case *t = &test_cases[i];
 
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 8bf898b71350..6f42e35b5227 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -408,6 +408,8 @@ CATEGORY="thp" run_test ./khugepaged
 
 CATEGORY="thp" run_test ./khugepaged -s 2
 
+CATEGORY="thp" run_test ./khugepaged -o 2 khugepaged:anon
+
 CATEGORY="thp" run_test ./khugepaged all:shmem
 
 CATEGORY="thp" run_test ./khugepaged -s 4 all:shmem
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 05/16] selftests/mm: add khugepaged completion barrier helper
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (3 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 04/16] selftests/mm: add order-parameterized khugepaged collapse cases Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-03 11:04   ` Mike Rapoport
  2026-08-02 19:52 ` [PATCH 06/16] selftests/mm: add khugepaged race harness Kiryl Shutsemau
                   ` (12 subsequent siblings)
  17 siblings, 1 reply; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

Race and functional tests need to drive khugepaged synchronously: set
up a layout, let exactly one full scan pass over it, check the result.
The khugepaged selftest already waits on full_scans advancing by two —
a completion barrier for one pass that started after setup — but it
relies on a short configured scan_sleep_millisecs to make progress.

Lift the pattern into a library helper, khugepaged_full_pass(), and
drive it by the sysfs wake path: any store to scan_sleep_millisecs
wakes the daemon, so the barrier completes promptly regardless of the
configured scan cadence. Wake exactly once per missing pass:
over-waking would queue a straggler pass behind the barrier that
overlaps and perturbs whatever the caller sets up next. One wake
completes one full pass only when the whole mm list fits in a single
scan batch, so callers must pair the helper with a large pages_to_scan.

Settings pushes and pops must not start passes nobody asked for
either, so thp_write_settings() now writes each khugepaged knob only
when it changes. Switch the khugepaged selftest order-parameterized
cases to the helper.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 .../testing/selftests/mm/hugepage_settings.c  | 66 ++++++++++++++++---
 .../testing/selftests/mm/hugepage_settings.h  |  2 +
 tools/testing/selftests/mm/khugepaged.c       | 17 +----
 3 files changed, 61 insertions(+), 24 deletions(-)

diff --git a/tools/testing/selftests/mm/hugepage_settings.c b/tools/testing/selftests/mm/hugepage_settings.c
index d7917dce3aba..a26a0cffa9c5 100644
--- a/tools/testing/selftests/mm/hugepage_settings.c
+++ b/tools/testing/selftests/mm/hugepage_settings.c
@@ -183,6 +183,17 @@ void thp_read_settings(struct thp_settings *settings)
 	}
 }
 
+/*
+ * Write only on change: any store to a khugepaged sysfs knob wakes the
+ * daemon, and settings pushes/pops must not start scan passes nobody
+ * asked for — khugepaged_full_pass() is the only sanctioned wake.
+ */
+static void thp_update_num(const char *name, unsigned long num)
+{
+	if (thp_read_num(name) != num)
+		thp_write_num(name, num);
+}
+
 void thp_write_settings(struct thp_settings *settings)
 {
 	struct khugepaged_settings *khugepaged = &settings->khugepaged;
@@ -198,15 +209,15 @@ void thp_write_settings(struct thp_settings *settings)
 			shmem_enabled_strings[settings->shmem_enabled]);
 	thp_write_num("use_zero_page", settings->use_zero_page);
 
-	thp_write_num("khugepaged/defrag", khugepaged->defrag);
-	thp_write_num("khugepaged/alloc_sleep_millisecs",
-			khugepaged->alloc_sleep_millisecs);
-	thp_write_num("khugepaged/scan_sleep_millisecs",
-			khugepaged->scan_sleep_millisecs);
-	thp_write_num("khugepaged/max_ptes_none", khugepaged->max_ptes_none);
-	thp_write_num("khugepaged/max_ptes_swap", khugepaged->max_ptes_swap);
-	thp_write_num("khugepaged/max_ptes_shared", khugepaged->max_ptes_shared);
-	thp_write_num("khugepaged/pages_to_scan", khugepaged->pages_to_scan);
+	thp_update_num("khugepaged/defrag", khugepaged->defrag);
+	thp_update_num("khugepaged/alloc_sleep_millisecs",
+		       khugepaged->alloc_sleep_millisecs);
+	thp_update_num("khugepaged/scan_sleep_millisecs",
+		       khugepaged->scan_sleep_millisecs);
+	thp_update_num("khugepaged/max_ptes_none", khugepaged->max_ptes_none);
+	thp_update_num("khugepaged/max_ptes_swap", khugepaged->max_ptes_swap);
+	thp_update_num("khugepaged/max_ptes_shared", khugepaged->max_ptes_shared);
+	thp_update_num("khugepaged/pages_to_scan", khugepaged->pages_to_scan);
 
 	if (dev_queue_read_ahead_path[0])
 		write_num(dev_queue_read_ahead_path, settings->read_ahead_kb);
@@ -230,6 +241,43 @@ void thp_write_settings(struct thp_settings *settings)
 	}
 }
 
+/*
+ * Completion barrier for khugepaged: wait until a full scan pass that
+ * started after this call has finished. full_scans must advance by two;
+ * a +1 step may complete a pass that examined this mm before the
+ * caller's setup was in place.
+ *
+ * Any store to scan_sleep_millisecs wakes the daemon, so the barrier
+ * works regardless of the configured scan cadence. It wakes exactly
+ * once per missing pass — over-waking would queue a straggler pass
+ * behind the barrier, perturbing whatever the caller sets up next.
+ * One wake completes one full pass only if the whole mm list fits in
+ * one scan batch, so callers must pair this with a large
+ * pages_to_scan.
+ */
+bool khugepaged_full_pass(unsigned int timeout_s)
+{
+	unsigned long deadline_ms = timeout_s * 1000UL;
+	unsigned long sleep_ms =
+		thp_read_num("khugepaged/scan_sleep_millisecs");
+	unsigned long elapsed_ms = 0;
+	int pass;
+
+	for (pass = 0; pass < 2; pass++) {
+		unsigned long target =
+			thp_read_num("khugepaged/full_scans") + 1;
+
+		thp_write_num("khugepaged/scan_sleep_millisecs", sleep_ms);
+		while (thp_read_num("khugepaged/full_scans") < target) {
+			if (elapsed_ms >= deadline_ms)
+				return false;
+			usleep(10 * 1000);
+			elapsed_ms += 10;
+		}
+	}
+	return true;
+}
+
 struct thp_settings *thp_current_settings(void)
 {
 	if (!settings_index) {
diff --git a/tools/testing/selftests/mm/hugepage_settings.h b/tools/testing/selftests/mm/hugepage_settings.h
index 726c73c43c05..8de446affeec 100644
--- a/tools/testing/selftests/mm/hugepage_settings.h
+++ b/tools/testing/selftests/mm/hugepage_settings.h
@@ -83,6 +83,8 @@ static inline void thp_save_settings(void)
 	hugepage_save_settings(/* thp = */ true, /* hugetlb = */ false);
 }
 
+bool khugepaged_full_pass(unsigned int timeout_s);
+
 void thp_set_read_ahead_path(char *path);
 unsigned long thp_supported_orders(void);
 unsigned long thp_shmem_supported_orders(void);
diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index 971e97a7330a..65fafab06410 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -1126,23 +1126,10 @@ static bool range_not_collapsed(void *p, size_t len)
 	return true;
 }
 
-/*
- * Completion barrier: one full khugepaged pass that started after this
- * call. Waiting for full_scans to advance by two guarantees it; a +1
- * step might complete a pass that scanned our mm before the setup.
- */
 static bool khugepaged_wait_full_pass(void)
 {
-	int full_scans = thp_read_num("khugepaged/full_scans") + 2;
-	int timeout = 60; /* 30 seconds */
-
-	while (timeout--) {
-		if (thp_read_num("khugepaged/full_scans") >= full_scans)
-			return true;
-		printf(".");
-		usleep(TICK);
-	}
-	return false;
+	/* Wait up to 30 seconds for the pass to complete. */
+	return khugepaged_full_pass(30);
 }
 
 /*
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 06/16] selftests/mm: add khugepaged race harness
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (4 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 05/16] selftests/mm: add khugepaged completion barrier helper Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-03 11:04   ` Mike Rapoport
  2026-08-02 19:52 ` [PATCH 07/16] selftests/mm: cover a shared-source collapse write race Kiryl Shutsemau
                   ` (11 subsequent siblings)
  17 siblings, 1 reply; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

Collapse serializes against faults, GUP, fork, mremap and zapping
through a protocol of locks, TLB flushes and refcount checks; none of
the khugepaged selftests exercise it under contention.

Add khugepaged_race: two faulters, MADV_DONTNEED, transient FOLL_PIN
(gup_test), fork and mremap threads race one of three collapse
drivers over the same ranges:

  stepped   khugepaged driven one full pass at a time via
            khugepaged_full_pass() — deterministic extent per step;
  free      free-running khugepaged (scan_sleep_millisecs=0) — soak;
  madvise   MADV_COLLAPSE + MADV_DONTNEED loop — the PMD-order axis.

All anon THP orders are enabled (inherit) and max_ptes_none is 0, so
a window collapses only once fully populated; the racing
MADV_DONTNEED then steers selection across orders as pages come
and go. Every racing page must
read as its pattern or zero, never anything else — checked
continuously by the faulters and fork children, and in a final sweep.
The kernel-side assertions (DEBUG_VM, page_table_check, KASAN,
lockdep) are the other half of the oracle; runners should inspect
dmesg.

The racing threads share three PMD-sized areas by default, plus one
owned by the mremap thread; -a overrides the count for
memory-constrained or emulated hosts, where a 512M PMD (arm64/64K)
makes the default playground multi-gigabyte.

Short runs are wired into run_vmtests.sh; soak length is -d.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/.gitignore        |   1 +
 tools/testing/selftests/mm/Makefile          |   1 +
 tools/testing/selftests/mm/khugepaged_race.c | 354 +++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh    |   7 +
 4 files changed, 363 insertions(+)
 create mode 100644 tools/testing/selftests/mm/khugepaged_race.c

diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore
index 54eefd8f97bc..b3b26447cd23 100644
--- a/tools/testing/selftests/mm/.gitignore
+++ b/tools/testing/selftests/mm/.gitignore
@@ -67,3 +67,4 @@ prctl_thp_disable
 rmap
 folio_split_race_test
 folio_order_check
+khugepaged_race
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index 33917c76b871..046bae8d1eff 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -105,6 +105,7 @@ TEST_GEN_FILES += merge
 TEST_GEN_FILES += rmap
 TEST_GEN_FILES += folio_split_race_test
 TEST_GEN_FILES += folio_order_check
+TEST_GEN_FILES += khugepaged_race
 
 ifneq ($(ARCH),arm64)
 TEST_GEN_FILES += soft-dirty
diff --git a/tools/testing/selftests/mm/khugepaged_race.c b/tools/testing/selftests/mm/khugepaged_race.c
new file mode 100644
index 000000000000..b586a114e4cd
--- /dev/null
+++ b/tools/testing/selftests/mm/khugepaged_race.c
@@ -0,0 +1,354 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * khugepaged race harness.
+ *
+ * Runs collapse against concurrent faults, transient GUP pins
+ * (gup_test), fork, mremap and MADV_DONTNEED over the same ranges, in
+ * one of three driver modes:
+ *
+ *   stepped	khugepaged, driven synchronously one full pass at a time
+ *		through khugepaged_full_pass() — the primary driver: the
+ *		full scan+collapse path with a deterministic extent per
+ *		step;
+ *   free	free-running khugepaged (scan_sleep_millisecs=0) — the
+ *		soak;
+ *   madvise	MADV_COLLAPSE in a loop — the legacy-PMD regression
+ *		axis.
+ *
+ * All anon THP orders are enabled (inherit) and max_ptes_none is set
+ * mid-range, so the MADV_DONTNEED holes steer selection across orders.
+ *
+ * Correctness signals: every racing page must read as its pattern or
+ * zero (MADV_DONTNEED), never anything else — checked continuously by
+ * the faulters and the fork children and once at the end — plus
+ * whatever DEBUG_VM / page_table_check / KASAN / lockdep report in
+ * dmesg, which the caller is expected to inspect.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+#include "vm_util.h"
+#include "hugepage_settings.h"
+#include "../../../../mm/gup_test.h"
+
+#define BASE_ADDR	((void *)(1UL << 30))
+/*
+ * Shared playground for faults/pins/fork/dontneed: several PMD-sized
+ * areas the racing threads spread across, plus one area owned by the
+ * mremap thread. More areas means more independent regions collapsing
+ * at once; the default suits a normal machine. On a memory-constrained
+ * host -- or under emulation, where a 512M PMD (arm64/64K) makes the
+ * default playground multi-gigabyte -- pass -a to shrink it.
+ */
+#define DEFAULT_SHARED_AREAS	3
+static int nr_shared_areas;
+static int nr_areas;
+
+static unsigned long hpage_pmd_size;
+static unsigned long page_size;
+static char *region;		/* NR_AREAS * hpage_pmd_size */
+static char *mremap_area;	/* region + NR_SHARED_AREAS areas */
+static char *mremap_scratch;	/* well above the region */
+static int gup_fd = -1;
+static volatile int stop;
+static volatile int corrupted;
+
+static unsigned int pattern(unsigned long page_idx)
+{
+	unsigned int val = (unsigned int)page_idx * 2654435761U;
+
+	return val ? val : 1;	/* never collides with the zero-fill */
+}
+
+static void check_page(unsigned long page_idx)
+{
+	unsigned int val = *(unsigned int *)(region + page_idx * page_size);
+
+	if (val && val != pattern(page_idx)) {
+		corrupted = 1;
+		ksft_print_msg("Corruption at page %lu: %#x != %#x\n",
+			       page_idx, val, pattern(page_idx));
+	}
+}
+
+static unsigned long rand_page(unsigned int *seed)
+{
+	return (unsigned long)rand_r(seed) %
+	       (nr_shared_areas * hpage_pmd_size / page_size);
+}
+
+static void *faulter_fn(void *arg)
+{
+	unsigned int seed = (unsigned long)arg;
+
+	while (!stop) {
+		unsigned long page_idx = rand_page(&seed);
+
+		if (rand_r(&seed) & 1)
+			*(unsigned int *)(region + page_idx * page_size) =
+				pattern(page_idx);
+		else
+			check_page(page_idx);
+	}
+	return NULL;
+}
+
+static void *dontneed_fn(void *arg)
+{
+	unsigned int seed = (unsigned long)arg;
+
+	while (!stop) {
+		unsigned long page_idx = rand_page(&seed);
+		unsigned long nr = 1UL << (rand_r(&seed) % 6);	/* 1..32 pages */
+
+		madvise(region + page_idx * page_size, nr * page_size,
+			MADV_DONTNEED);
+		usleep(rand_r(&seed) % 500);
+	}
+	return NULL;
+}
+
+static void *pinner_fn(void *arg)
+{
+	unsigned int seed = (unsigned long)arg;
+
+	while (!stop) {
+		struct gup_test gup = {};
+		unsigned long page_idx = rand_page(&seed);
+
+		gup.addr = (unsigned long)(region + page_idx * page_size);
+		gup.size = 16 * page_size;
+		gup.nr_pages_per_call = 16;
+		gup.gup_flags = 1;	/* FOLL_WRITE */
+		/* Racing MADV_DONTNEED makes transient failures expected. */
+		ioctl(gup_fd, PIN_FAST_BENCHMARK, &gup);
+		usleep(rand_r(&seed) % 200);
+	}
+	return NULL;
+}
+
+static void *forker_fn(void *arg)
+{
+	unsigned int seed = (unsigned long)arg;
+
+	while (!stop) {
+		pid_t pid = fork();
+
+		if (pid == 0) {
+			for (int i = 0; i < 16; i++)
+				check_page(rand_page(&seed));
+			_exit(corrupted);
+		}
+		if (pid > 0) {
+			int wstatus;
+
+			waitpid(pid, &wstatus, 0);
+			if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus))
+				corrupted = 1;
+		}
+		usleep(rand_r(&seed) % 2000);
+	}
+	return NULL;
+}
+
+static void *mremapper_fn(void *arg)
+{
+	unsigned int seed = (unsigned long)arg;
+
+	while (!stop) {
+		void *p;
+
+		p = mremap(mremap_area, hpage_pmd_size, hpage_pmd_size,
+			   MREMAP_MAYMOVE | MREMAP_FIXED, mremap_scratch);
+		if (p == MAP_FAILED)
+			ksft_exit_fail_perror("mremap() away");
+		for (int i = 0; i < 8; i++)
+			mremap_scratch[(rand_r(&seed) %
+				(hpage_pmd_size / page_size)) * page_size] = 1;
+		p = mremap(mremap_scratch, hpage_pmd_size, hpage_pmd_size,
+			   MREMAP_MAYMOVE | MREMAP_FIXED, mremap_area);
+		if (p == MAP_FAILED)
+			ksft_exit_fail_perror("mremap() back");
+		usleep(rand_r(&seed) % 2000);
+	}
+	return NULL;
+}
+
+static unsigned long now_ms(void)
+{
+	struct timeval tv;
+
+	gettimeofday(&tv, NULL);
+	return tv.tv_sec * 1000UL + tv.tv_usec / 1000;
+}
+
+static void usage(void)
+{
+	fprintf(stderr,
+		"Usage: khugepaged_race [-d seconds] [-m stepped|free|madvise] [-a areas]\n"
+		"\t-a: number of shared PMD-sized playground areas (default 3)\n");
+	exit(1);
+}
+
+int main(int argc, char **argv)
+{
+	static const char * const thread_names[] = {
+		"faulter", "faulter2", "dontneed", "pinner", "forker",
+		"mremapper",
+	};
+	void *(*const thread_fns[])(void *) = {
+		faulter_fn, faulter_fn, dontneed_fn, pinner_fn, forker_fn,
+		mremapper_fn,
+	};
+	const int nr_threads = ARRAY_SIZE(thread_names);
+	pthread_t threads[ARRAY_SIZE(thread_names)];
+	const char *mode = "stepped";
+	struct thp_settings settings;
+	unsigned long end_ms;
+	int duration_s = 10;
+	unsigned long thread_mask = ~0UL;
+	int nr_areas_arg = 0;
+	unsigned long i;
+	int steps = 0;
+	int opt;
+
+	while ((opt = getopt(argc, argv, "a:d:m:t:h")) != -1) {
+		switch (opt) {
+		case 'a':
+			nr_areas_arg = atoi(optarg);
+			break;
+		case 'd':
+			duration_s = atoi(optarg);
+			break;
+		case 'm':
+			mode = optarg;
+			break;
+		case 't':
+			/* debug: bitmask of racing threads to start */
+			thread_mask = strtoul(optarg, NULL, 0);
+			break;
+		default:
+			usage();
+		}
+	}
+	if (strcmp(mode, "stepped") && strcmp(mode, "free") &&
+	    strcmp(mode, "madvise"))
+		usage();
+
+	ksft_print_header();
+	if (!thp_available())
+		ksft_exit_skip("Transparent Hugepages not available\n");
+
+	page_size = getpagesize();
+	hpage_pmd_size = read_pmd_pagesize();
+	if (!hpage_pmd_size)
+		ksft_exit_fail_msg("Reading PMD pagesize failed\n");
+
+	gup_fd = open("/sys/kernel/debug/gup_test", O_RDWR);
+	if (gup_fd < 0)
+		ksft_exit_skip("/sys/kernel/debug/gup_test requires CONFIG_GUP_TEST and root\n");
+
+	nr_shared_areas = nr_areas_arg > 0 ? nr_areas_arg : DEFAULT_SHARED_AREAS;
+	nr_areas = nr_shared_areas + 1;
+
+	ksft_set_plan(1);
+
+	thp_save_settings();
+	thp_read_settings(&settings);
+	settings.thp_enabled = THP_MADVISE;
+	settings.thp_defrag = THP_DEFRAG_ALWAYS;
+	settings.shmem_enabled = SHMEM_NEVER;
+	settings.khugepaged.defrag = 1;
+	settings.khugepaged.scan_sleep_millisecs =
+		strcmp(mode, "free") ? 1000 : 0;
+	settings.khugepaged.alloc_sleep_millisecs = 10;
+	/*
+	 * Strict occupancy: mTHP collapse only supports 0 or
+	 * HPAGE_PMD_NR - 1 and coerces anything else to 0 anyway, and 0
+	 * also keeps khugepaged from burning the whole step in doomed
+	 * PMD-sized allocations on 512M-PMD configs: under racing
+	 * MADV_DONTNEED a fully populated PMD area is rare.
+	 */
+	settings.khugepaged.max_ptes_none = 0;
+	settings.khugepaged.pages_to_scan =
+		nr_areas * (hpage_pmd_size / page_size) * 8;
+	for (i = 0; i < NR_ORDERS; i++) {
+		if (thp_supported_orders() & (1UL << i))
+			settings.hugepages[i].enabled = THP_INHERIT;
+	}
+	/* Base of the settings stack; the bottom entry is never popped. */
+	thp_push_settings(&settings);
+
+	region = mmap(BASE_ADDR, nr_areas * hpage_pmd_size,
+		      PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE,
+		      -1, 0);
+	if (region != BASE_ADDR)
+		ksft_exit_fail_msg("Failed to allocate VMA at %p\n", BASE_ADDR);
+	mremap_area = region + nr_shared_areas * hpage_pmd_size;
+	mremap_scratch = (char *)BASE_ADDR + 2 * nr_areas * hpage_pmd_size;
+
+	/* Populate so the first pass has something to collapse. */
+	for (i = 0; i < nr_shared_areas * hpage_pmd_size / page_size; i++)
+		*(unsigned int *)(region + i * page_size) = pattern(i);
+	memset(mremap_area, 1, hpage_pmd_size);
+	madvise(region, nr_areas * hpage_pmd_size, MADV_HUGEPAGE);
+
+	for (i = 0; i < nr_threads; i++) {
+		if (!(thread_mask & (1UL << i))) {
+			threads[i] = 0;
+			continue;
+		}
+		if (pthread_create(&threads[i], NULL, thread_fns[i],
+				   (void *)(i + 1)))
+			ksft_exit_fail_perror("pthread_create()");
+	}
+
+	end_ms = now_ms() + duration_s * 1000UL;
+	if (!strcmp(mode, "stepped")) {
+		while (now_ms() < end_ms && !corrupted) {
+			if (!khugepaged_full_pass(600))
+				ksft_exit_fail_msg("khugepaged pass timed out\n");
+			steps++;
+		}
+	} else if (!strcmp(mode, "free")) {
+		while (now_ms() < end_ms && !corrupted)
+			usleep(100 * 1000);
+	} else {	/* madvise */
+		while (now_ms() < end_ms && !corrupted) {
+			for (i = 0; i < nr_shared_areas; i++) {
+				madvise(region + i * hpage_pmd_size,
+					hpage_pmd_size, MADV_COLLAPSE);
+			}
+			madvise(region, nr_shared_areas * hpage_pmd_size,
+				MADV_DONTNEED);
+			steps++;
+		}
+	}
+
+	stop = 1;
+	for (i = 0; i < nr_threads; i++) {
+		if (threads[i])
+			pthread_join(threads[i], NULL);
+	}
+
+	/* Final integrity sweep. */
+	for (i = 0; i < nr_shared_areas * hpage_pmd_size / page_size; i++)
+		check_page(i);
+
+	thp_restore_settings();
+
+	ksft_test_result(!corrupted, "%s: %ds, %d steps, no corruption\n",
+			 mode, duration_s, steps);
+	ksft_finished();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 6f42e35b5227..a8b6b839cb97 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -404,6 +404,13 @@ CATEGORY="cow" run_test ./cow
 
 CATEGORY="thp" run_test ./folio_order_check
 
+
+CATEGORY="thp" run_test ./khugepaged_race -d 5 -m stepped
+
+CATEGORY="thp" run_test ./khugepaged_race -d 5 -m free
+
+CATEGORY="thp" run_test ./khugepaged_race -d 5 -m madvise
+
 CATEGORY="thp" run_test ./khugepaged
 
 CATEGORY="thp" run_test ./khugepaged -s 2
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 07/16] selftests/mm: cover a shared-source collapse write race
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (5 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 06/16] selftests/mm: add khugepaged race harness Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 08/16] selftests/mm: skip collapse_compound_extreme where the PMD is too large Kiryl Shutsemau
                   ` (10 subsequent siblings)
  17 siblings, 0 replies; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

collapse_fork already checks that a fork-shared range collapses in the
process that asks while a co-sharer keeps its own page, but with the
co-sharer quiescent. Add a case that keeps a co-sharer writing to the
shared range throughout the collapse: CoW must keep the two sides'
content independent -- the collapsing child sees the pre-fork content,
the writing parent sees only its own writes. This exercises the
requirement that CoW keeps a shared source isolated from the collapsing
side throughout the collapse, even under an actively writing co-sharer.

Passes on the unmodified kernel, pinning down the CoW-isolation contract
that khugepaged collapse already provides.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/khugepaged.c | 58 +++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index 65fafab06410..81001e15765c 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -1047,6 +1047,61 @@ static void collapse_max_ptes_shared(struct collapse_context *c, struct mem_ops
 	ksft_test_result_report(exit_status, "%s\n", __func__);
 }
 
+/*
+ * Content stays isolated while a co-sharer writes concurrently. A shared
+ * source is copied live (not frozen), relying on it being CoW - immutable
+ * for the duration of the copy; a co-sharer's write goes to a CoW copy. The
+ * collapsing child must see the pre-fork content, the writing parent only
+ * its own writes.
+ */
+static void collapse_fork_cow_race(struct collapse_context *c, struct mem_ops *ops)
+{
+	const unsigned long shared = 64 * page_size;
+	const int stride = page_size / sizeof(int);
+	int wstatus, i, n = shared / page_size;
+	int *ip;
+	void *p;
+
+	p = ops->setup_area(1);
+	ip = p;
+	ops->fault(p, 0, shared);		/* shared prefix, pre-fork pattern */
+
+	ksft_print_msg("Fork, collapse in the child while the parent rewrites...");
+	if (!fork()) {
+		ops->fault(p, shared, hpage_pmd_size);	/* private remainder */
+		c->collapse("Collapse a range shared with a writing co-sharer",
+			    p, 1, ops, true);
+		for (i = 0; i < n; i++)
+			if (ip[i * stride] != i + 0xdead0000) {
+				fail("Fail: child content");
+				ops->cleanup_area(p, hpage_pmd_size);
+				_exit(exit_status);
+			}
+		success("OK");
+		ops->cleanup_area(p, hpage_pmd_size);
+		_exit(exit_status);
+	}
+
+	/* Hammer the parent's own writes over the shared prefix. */
+	for (int it = 0; it < 200000; it++)
+		for (i = 0; i < n; i++)
+			ip[i * stride] = i + 0xbeef0000;
+
+	wait(&wstatus);
+	exit_status = WEXITSTATUS(wstatus);
+
+	ksft_print_msg("Check the parent sees only its own writes...");
+	for (i = 0; i < n; i++)
+		if (ip[i * stride] != i + 0xbeef0000)
+			break;
+	if (i == n)
+		success("OK");
+	else
+		fail("Fail: parent content");
+	ops->cleanup_area(p, hpage_pmd_size);
+	ksft_test_result_report(exit_status, "%s\n", __func__);
+}
+
 static void madvise_collapse_existing_thps(struct collapse_context *c,
 					   struct mem_ops *ops)
 {
@@ -1595,6 +1650,9 @@ int main(int argc, char **argv)
 	TEST(collapse_max_ptes_shared, khugepaged_context, anon_ops);
 	TEST(collapse_max_ptes_shared, madvise_context, anon_ops);
 
+	TEST(collapse_fork_cow_race, khugepaged_context, anon_ops);
+	TEST(collapse_fork_cow_race, madvise_context, anon_ops);
+
 	TEST(madvise_collapse_existing_thps, madvise_context, anon_ops);
 	TEST(madvise_collapse_existing_thps, madvise_context, read_only_file_ops);
 	TEST(madvise_collapse_existing_thps, madvise_context, read_write_file_read_ops);
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 08/16] selftests/mm: skip collapse_compound_extreme where the PMD is too large
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (6 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 07/16] selftests/mm: cover a shared-source collapse write race Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 09/16] selftests/mm: skip khugepaged swap tests when no swap is configured Kiryl Shutsemau
                   ` (9 subsequent siblings)
  17 siblings, 0 replies; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

Its fault-time THP construction (x hpage_pmd_nr) cannot hand out a
512M order-13 page on arm64/64K, bailing the whole binary. Skip when
hpage_pmd_size > 32M; MADV_COLLAPSE-driven cases still exercise PMD-order
collapse there. No effect on 4K (2M) or 16K (32M) PMDs.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/khugepaged.c | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index 81001e15765c..b43e060b4118 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -857,6 +857,21 @@ static void collapse_compound_extreme(struct collapse_context *c, struct mem_ops
 	void *p;
 	int i;
 
+	/*
+	 * Builds a PMD's worth of distinct PTE-mapped compound pages by cycling
+	 * hpage_pmd_nr fault-time THPs through mremap. Fault-time THP allocation
+	 * is best-effort, and this needs hpage_pmd_nr PMD-order pages in a row:
+	 * fine at a 2M (4K base) or 32M (16K base) PMD, but a 512M PMD (arm64/64K)
+	 * is an order-13 allocation the allocator cannot reliably hand out even
+	 * once, let alone 8192 times. Cap at a 32M PMD; MADV_COLLAPSE-driven cases
+	 * still cover PMD-order collapse on the larger configs.
+	 */
+	if (hpage_pmd_size > (32UL << 20)) {
+		ksft_test_result_skip("%s: PMD too large for fault-time THP construction\n",
+				      __func__);
+		return;
+	}
+
 	p = ops->setup_area(1);
 	ksft_print_msg("Construct PTE page table full of different PTE-mapped compound pages\n");
 	for (i = 0; i < hpage_pmd_nr; i++) {
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 09/16] selftests/mm: skip khugepaged swap tests when no swap is configured
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (7 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 08/16] selftests/mm: skip collapse_compound_extreme where the PMD is too large Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-03 11:04   ` Mike Rapoport
  2026-08-02 19:52 ` [PATCH 10/16] selftests/mm: verify synchronous khugepaged driving is attributable Kiryl Shutsemau
                   ` (8 subsequent siblings)
  17 siblings, 1 reply; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

collapse_swapin_single_pte and collapse_max_ptes_swap swap pages out with
MADV_PAGEOUT and then require them to be swapped. With no swap area
configured MADV_PAGEOUT is a no-op, so check_swap() finds nothing and the
tests report a failure that only reflects the environment, not khugepaged.

Skip both when /proc/swaps shows no swap area, so a missing swap device
yields a SKIP rather than a spurious failure. A real swap-out failure with
swap present still fails as before.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/khugepaged.c | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index b43e060b4118..b074b005b62f 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -181,6 +181,21 @@ static void get_finfo(const char *dir)
 	ksft_exit_fail_msg("%s: Could not read: %s\n", __func__, path);
 }
 
+static bool has_swap(void)
+{
+	FILE *fp = fopen("/proc/swaps", "r");
+	char line[MAX_LINE_LENGTH];
+	bool ret = false;
+
+	if (!fp)
+		return false;
+	/* First line is the header; any following line is a swap area. */
+	if (fgets(line, sizeof(line), fp) && fgets(line, sizeof(line), fp))
+		ret = true;
+	fclose(fp);
+	return ret;
+}
+
 static bool check_swap(void *addr, unsigned long size)
 {
 	bool swap = false;
@@ -739,6 +754,10 @@ static void collapse_swapin_single_pte(struct collapse_context *c, struct mem_op
 	void *p;
 
 	p = ops->setup_area(1);
+	if (!has_swap()) {
+		skip("Skip (no swap configured)");
+		goto out;
+	}
 	ops->fault(p, 0, hpage_pmd_size);
 
 	ksft_print_msg("Swapout one page...");
@@ -765,6 +784,10 @@ static void collapse_max_ptes_swap(struct collapse_context *c, struct mem_ops *o
 	void *p;
 
 	p = ops->setup_area(1);
+	if (!has_swap()) {
+		skip("Skip (no swap configured)");
+		goto out;
+	}
 	ops->fault(p, 0, hpage_pmd_size);
 
 	ksft_print_msg("Swapout %d of %d pages...", max_ptes_swap + 1, hpage_pmd_nr);
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 10/16] selftests/mm: verify synchronous khugepaged driving is attributable
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (8 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 09/16] selftests/mm: skip khugepaged swap tests when no swap is configured Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 11/16] selftests/mm: race-harness variant for permissive hole occupancy Kiryl Shutsemau
                   ` (7 subsequent siblings)
  17 siblings, 0 replies; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

The khugepaged tests attribute per-attempt outcomes through the
huge_memory tracepoints. The anon-path events carry no virtual address,
but an attempt is identifiable anyway: mm_collapse_huge_page_isolate()
reports a source folio PFN and order, which the test matches against the
PFNs it recorded from pagemap before the pass. Count an attempt from
whichever attribution signal fires, so the check does not depend on which
of the anon tracepoints a given kernel emits.

Add minimal tracefs helpers to vm_util (enable/clear one event subsystem,
open the trace buffer) and a khugepaged_sync_check test: per step, prepare
one aligned window, record its source PFNs, run one khugepaged_full_pass()
barrier and require the window collapsed with exactly one attributed
attempt. Five steps; scan_sleep_millisecs is set high so the test only
completes inside its timeout if the sysfs store really wakes the daemon.

Passes 5/5 on x86-64 4K and arm64 64K.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/.gitignore         |   1 +
 tools/testing/selftests/mm/Makefile           |   1 +
 .../selftests/mm/khugepaged_sync_check.c      | 198 ++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh     |   2 +
 tools/testing/selftests/mm/vm_util.c          |  49 +++++
 tools/testing/selftests/mm/vm_util.h          |   3 +
 6 files changed, 254 insertions(+)
 create mode 100644 tools/testing/selftests/mm/khugepaged_sync_check.c

diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore
index b3b26447cd23..616043a0fcd2 100644
--- a/tools/testing/selftests/mm/.gitignore
+++ b/tools/testing/selftests/mm/.gitignore
@@ -67,4 +67,5 @@ prctl_thp_disable
 rmap
 folio_split_race_test
 folio_order_check
+khugepaged_sync_check
 khugepaged_race
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index 046bae8d1eff..026d4b61414d 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -105,6 +105,7 @@ TEST_GEN_FILES += merge
 TEST_GEN_FILES += rmap
 TEST_GEN_FILES += folio_split_race_test
 TEST_GEN_FILES += folio_order_check
+TEST_GEN_FILES += khugepaged_sync_check
 TEST_GEN_FILES += khugepaged_race
 
 ifneq ($(ARCH),arm64)
diff --git a/tools/testing/selftests/mm/khugepaged_sync_check.c b/tools/testing/selftests/mm/khugepaged_sync_check.c
new file mode 100644
index 000000000000..13215cb370ce
--- /dev/null
+++ b/tools/testing/selftests/mm/khugepaged_sync_check.c
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Synchronous khugepaged driving check.
+ *
+ * Race tests drive khugepaged through the existing sysfs controls: a
+ * store to scan_sleep_millisecs wakes the daemon, and full_scans
+ * advancing by two is a completion barrier for one full pass that
+ * started after setup (khugepaged_full_pass()). Verify the pair gives
+ * deterministic, attributable results: one barrier step over one
+ * prepared window produces exactly one collapse attempt on that
+ * window's source pages — mm_collapse_huge_page_isolate events
+ * filtered by source PFN and order — and the window is collapsed
+ * afterwards, repeatably.
+ *
+ * scan_sleep_millisecs is set to 60s to prove the wake path: without
+ * the wake, one barrier step would sleep multiples of that and blow
+ * the timeout. It also keeps the daemon from free-running between
+ * steps, per the khugepaged_full_pass() discipline.
+ */
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+#include "vm_util.h"
+#include "hugepage_settings.h"
+
+#define BASE_ADDR ((void *)(1UL << 30))
+#define TARGET_ORDER 2	/* smallest order khugepaged considers */
+#define NR_ITERATIONS 5
+
+static int pagemap_fd;
+static int kpageflags_fd;
+static unsigned long hpage_pmd_size;
+
+/*
+ * Count collapse attempts attributable to our window: legacy-engine
+ * isolate events whose scan_pfn is one of the window's source PFNs,
+ * plus batch-engine per-candidate install events at the window's
+ * address. Either engine reports exactly once per attempt.
+ */
+static int count_attributed(unsigned long *pfns, int nr_pfns,
+			    unsigned long addr, unsigned int order)
+{
+	char line[1024];
+	int count = 0;
+	FILE *fp;
+
+	fp = tracing_open_trace();
+	if (!fp)
+		ksft_exit_fail_msg("Cannot open trace buffer\n");
+
+	while (fgets(line, sizeof(line), fp)) {
+		char *s;
+		unsigned long val;
+		unsigned int ord;
+		char *o;
+		int i;
+
+		s = strstr(line, "mm_collapse_huge_page_isolate:");
+		if (s) {
+			if (sscanf(s, "mm_collapse_huge_page_isolate: scan_pfn=0x%lx",
+				   &val) != 1)
+				continue;
+			o = strstr(s, "order=");
+			if (!o || sscanf(o, "order=%u", &ord) != 1 ||
+			    ord != order)
+				continue;
+			for (i = 0; i < nr_pfns; i++) {
+				if (val == pfns[i]) {
+					count++;
+					break;
+				}
+			}
+			continue;
+		}
+
+		s = strstr(line, "mm_collapse_candidate:");
+		if (s) {
+			if (!strstr(s, "pass=install") ||
+			    !strstr(s, "result=succeeded"))
+				continue;
+			o = strstr(s, "addr=");
+			if (!o || sscanf(o, "addr=0x%lx", &val) != 1 ||
+			    val != addr)
+				continue;
+			o = strstr(s, "order=");
+			if (!o || sscanf(o, "order=%u", &ord) != 1 ||
+			    ord != order)
+				continue;
+			count++;
+		}
+	}
+	fclose(fp);
+	return count;
+}
+
+static void one_step(int iteration)
+{
+	const size_t window = getpagesize() << TARGET_ORDER;
+	const int nr_pages = 1 << TARGET_ORDER;
+	unsigned long pfns[1 << TARGET_ORDER];
+	bool collapsed;
+	int attributed;
+	char *p;
+	int i;
+
+	p = mmap(BASE_ADDR, hpage_pmd_size, PROT_READ | PROT_WRITE,
+		 MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+	if (p != BASE_ADDR)
+		ksft_exit_fail_msg("Failed to allocate VMA at %p\n",
+				   BASE_ADDR);
+
+	/* Prepare one window; record its source PFNs. */
+	for (i = 0; i < nr_pages; i++) {
+		p[i * getpagesize()] = i + 1;
+		pfns[i] = pagemap_get_pfn(pagemap_fd, p + i * getpagesize());
+		if (pfns[i] == -1UL)
+			ksft_exit_fail_msg("Source page not present\n");
+	}
+
+	if (tracing_events_start("huge_memory"))
+		ksft_exit_fail_msg("Cannot enable huge_memory events\n");
+
+	madvise(p, hpage_pmd_size, MADV_HUGEPAGE);
+	/* Wait up to 120 seconds for the pass to complete. */
+	if (!khugepaged_full_pass(120))
+		ksft_exit_fail_msg("khugepaged did not complete a full pass\n");
+
+	tracing_events_stop("huge_memory");
+
+	collapsed = is_range_backed_by_folio_orders(p, window, TARGET_ORDER,
+						    pagemap_fd, kpageflags_fd);
+	attributed = count_attributed(pfns, nr_pages, (unsigned long)p,
+				      TARGET_ORDER);
+
+	ksft_test_result(collapsed && attributed == 1,
+			 "step %d: window collapsed, %d attributed result(s)\n",
+			 iteration, attributed);
+
+	munmap(p, hpage_pmd_size);
+}
+
+int main(void)
+{
+	struct thp_settings settings;
+	int i;
+
+	ksft_print_header();
+
+	if (!thp_available())
+		ksft_exit_skip("Transparent Hugepages not available\n");
+	if (!(thp_supported_orders() & (1UL << TARGET_ORDER)))
+		ksft_exit_skip("Order %d is not a supported anon THP order\n",
+			       TARGET_ORDER);
+
+	hpage_pmd_size = read_pmd_pagesize();
+	if (!hpage_pmd_size)
+		ksft_exit_fail_msg("Reading PMD pagesize failed\n");
+	pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+	if (pagemap_fd < 0)
+		ksft_exit_fail_perror("open(\"/proc/self/pagemap\")");
+	kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
+	if (kpageflags_fd < 0)
+		ksft_exit_skip("open(\"/proc/kpageflags\") requires root\n");
+	if (tracing_events_start("huge_memory"))
+		ksft_exit_skip("tracefs unavailable\n");
+	tracing_events_stop("huge_memory");
+
+	ksft_set_plan(NR_ITERATIONS);
+
+	thp_save_settings();
+	thp_read_settings(&settings);
+	settings.thp_enabled = THP_MADVISE;
+	settings.thp_defrag = THP_DEFRAG_ALWAYS;
+	settings.khugepaged.defrag = 1;
+	settings.khugepaged.scan_sleep_millisecs = 60000;
+	settings.khugepaged.alloc_sleep_millisecs = 60000;
+	settings.khugepaged.max_ptes_none = (hpage_pmd_size / getpagesize()) - 1;
+	/* One wake must complete one full pass; see khugepaged_full_pass(). */
+	settings.khugepaged.pages_to_scan = 1UL << 24;
+	for (i = 0; i < NR_ORDERS; i++)
+		settings.hugepages[i].enabled = THP_NEVER;
+	settings.hugepages[TARGET_ORDER].enabled = THP_INHERIT;
+	/* Base of the settings stack; the bottom entry is never popped. */
+	thp_push_settings(&settings);
+
+	for (i = 0; i < NR_ITERATIONS; i++)
+		one_step(i);
+
+	thp_restore_settings();
+
+	ksft_finished();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index a8b6b839cb97..f61ec76d8e00 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -405,6 +405,8 @@ CATEGORY="cow" run_test ./cow
 CATEGORY="thp" run_test ./folio_order_check
 
 
+CATEGORY="thp" run_test ./khugepaged_sync_check
+
 CATEGORY="thp" run_test ./khugepaged_race -d 5 -m stepped
 
 CATEGORY="thp" run_test ./khugepaged_race -d 5 -m free
diff --git a/tools/testing/selftests/mm/vm_util.c b/tools/testing/selftests/mm/vm_util.c
index 793342095420..886fb3de9c01 100644
--- a/tools/testing/selftests/mm/vm_util.c
+++ b/tools/testing/selftests/mm/vm_util.c
@@ -411,6 +411,55 @@ bool is_range_backed_by_folio_orders(char *start, size_t len, int order,
 	return true;
 }
 
+#define TRACEFS_ROOT "/sys/kernel/tracing"
+
+static int tracing_events_write(const char *subsys, const char *val)
+{
+	char path[256];
+	int fd;
+
+	snprintf(path, sizeof(path), TRACEFS_ROOT "/events/%s/enable",
+		 subsys);
+	fd = open(path, O_WRONLY);
+	if (fd < 0)
+		return -1;
+	if (write(fd, val, 1) != 1) {
+		close(fd);
+		return -1;
+	}
+	close(fd);
+	return 0;
+}
+
+/*
+ * Enable one ftrace event subsystem (e.g. "huge_memory") and clear the
+ * trace buffer. Returns -1 if tracefs is unavailable; parse the results
+ * via tracing_open_trace() after tracing_events_stop().
+ */
+int tracing_events_start(const char *subsys)
+{
+	int fd;
+
+	if (tracing_events_write(subsys, "1"))
+		return -1;
+
+	fd = open(TRACEFS_ROOT "/trace", O_WRONLY | O_TRUNC);
+	if (fd < 0)
+		return -1;
+	close(fd);
+	return 0;
+}
+
+int tracing_events_stop(const char *subsys)
+{
+	return tracing_events_write(subsys, "0");
+}
+
+FILE *tracing_open_trace(void)
+{
+	return fopen(TRACEFS_ROOT "/trace", "r");
+}
+
 /* If `ioctls' non-NULL, the allowed ioctls will be returned into the var */
 int uffd_register_with_ioctls(int uffd, void *addr, uint64_t len,
 			      bool miss, bool wp, bool minor, uint64_t *ioctls)
diff --git a/tools/testing/selftests/mm/vm_util.h b/tools/testing/selftests/mm/vm_util.h
index 76e9938a908e..4ff5a1c5ab8e 100644
--- a/tools/testing/selftests/mm/vm_util.h
+++ b/tools/testing/selftests/mm/vm_util.h
@@ -115,6 +115,9 @@ int close_procmap(struct procmap_fd *procmap);
 int write_sysfs(const char *file_path, unsigned long val);
 int read_sysfs(const char *file_path, unsigned long *val);
 bool softdirty_supported(void);
+int tracing_events_start(const char *subsys);
+int tracing_events_stop(const char *subsys);
+FILE *tracing_open_trace(void);
 
 static inline int open_self_procmap(struct procmap_fd *procmap_out)
 {
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 11/16] selftests/mm: race-harness variant for permissive hole occupancy
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (9 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 10/16] selftests/mm: verify synchronous khugepaged driving is attributable Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 12/16] selftests/mm: add memory-pressure threads to the khugepaged race harness Kiryl Shutsemau
                   ` (6 subsequent siblings)
  17 siblings, 0 replies; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

The race harness runs with max_ptes_none=0: strict occupancy keeps
selection honest under racing MADV_DONTNEED and avoids doomed PMD-sized
allocations on 512M-PMD configs. That regime never exercises collapse of
partially populated windows -- every candidate it emits is fully occupied.

Collapsing a window that contains holes is a different path: the hole is
not copied from anywhere, it is zero-filled into the new folio, and the
slot has to be re-checked under the page table lock at install time
because a racing fault may have filled it in the meantime. None of that is
reached at max_ptes_none=0.

Add -z, which selects the other supported end of the occupancy scale
(HPAGE_PMD_NR - 1, scaled per order): selection then emits hole-heavy
windows and those paths take the brunt of the racing faults and zaps.

Also drop the stale claim that max_ptes_none sits "mid-range" from the
header comment; the harness has always pinned it to an end of the scale.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/khugepaged_race.c | 33 ++++++++++++++------
 tools/testing/selftests/mm/run_vmtests.sh    |  2 ++
 2 files changed, 25 insertions(+), 10 deletions(-)

diff --git a/tools/testing/selftests/mm/khugepaged_race.c b/tools/testing/selftests/mm/khugepaged_race.c
index b586a114e4cd..2e36e242caa7 100644
--- a/tools/testing/selftests/mm/khugepaged_race.c
+++ b/tools/testing/selftests/mm/khugepaged_race.c
@@ -15,8 +15,12 @@
  *   madvise	MADV_COLLAPSE in a loop — the legacy-PMD regression
  *		axis.
  *
- * All anon THP orders are enabled (inherit) and max_ptes_none is set
- * mid-range, so the MADV_DONTNEED holes steer selection across orders.
+ * All anon THP orders are enabled (inherit). max_ptes_none is 0 by
+ * default — racing MADV_DONTNEED then steers selection across orders —
+ * or the permissive limit with -z, which floods the batch engine with
+ * hole and zeropage slots so the population paths (park-time zeropage
+ * clear, zero-filled copy, install-time pte_none() verify and abort)
+ * race the faulters directly.
  *
  * Correctness signals: every racing page must read as its pattern or
  * zero (MADV_DONTNEED), never anything else — checked continuously by
@@ -196,7 +200,8 @@ static unsigned long now_ms(void)
 static void usage(void)
 {
 	fprintf(stderr,
-		"Usage: khugepaged_race [-d seconds] [-m stepped|free|madvise] [-a areas]\n"
+		"Usage: khugepaged_race [-d seconds] [-m stepped|free|madvise] [-z] [-a areas]\n"
+		"\t-z: permissive max_ptes_none (hole-heavy windows)\n"
 		"\t-a: number of shared PMD-sized playground areas (default 3)\n");
 	exit(1);
 }
@@ -219,11 +224,12 @@ int main(int argc, char **argv)
 	int duration_s = 10;
 	unsigned long thread_mask = ~0UL;
 	int nr_areas_arg = 0;
+	bool permissive_none = false;
 	unsigned long i;
 	int steps = 0;
 	int opt;
 
-	while ((opt = getopt(argc, argv, "a:d:m:t:h")) != -1) {
+	while ((opt = getopt(argc, argv, "a:d:m:t:zh")) != -1) {
 		switch (opt) {
 		case 'a':
 			nr_areas_arg = atoi(optarg);
@@ -238,6 +244,9 @@ int main(int argc, char **argv)
 			/* debug: bitmask of racing threads to start */
 			thread_mask = strtoul(optarg, NULL, 0);
 			break;
+		case 'z':
+			permissive_none = true;
+			break;
 		default:
 			usage();
 		}
@@ -274,13 +283,17 @@ int main(int argc, char **argv)
 		strcmp(mode, "free") ? 1000 : 0;
 	settings.khugepaged.alloc_sleep_millisecs = 10;
 	/*
-	 * Strict occupancy: mTHP collapse only supports 0 or
-	 * HPAGE_PMD_NR - 1 and coerces anything else to 0 anyway, and 0
-	 * also keeps khugepaged from burning the whole step in doomed
-	 * PMD-sized allocations on 512M-PMD configs: under racing
-	 * MADV_DONTNEED a fully populated PMD area is rare.
+	 * mTHP collapse only supports the two ends of the occupancy
+	 * scale: 0 or HPAGE_PMD_NR - 1 (anything else coerces to 0).
+	 * Strict is the default — it also keeps khugepaged from burning
+	 * the whole step in doomed PMD-sized allocations on 512M-PMD
+	 * configs, where a fully populated area is rare under racing
+	 * MADV_DONTNEED. -z selects the permissive end: selection then
+	 * emits hole-heavy windows and the engine's population paths
+	 * take the brunt of the racing faults and zaps.
 	 */
-	settings.khugepaged.max_ptes_none = 0;
+	settings.khugepaged.max_ptes_none = permissive_none ?
+		(hpage_pmd_size / page_size) - 1 : 0;
 	settings.khugepaged.pages_to_scan =
 		nr_areas * (hpage_pmd_size / page_size) * 8;
 	for (i = 0; i < NR_ORDERS; i++) {
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index f61ec76d8e00..83a04b1e2520 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -413,6 +413,8 @@ CATEGORY="thp" run_test ./khugepaged_race -d 5 -m free
 
 CATEGORY="thp" run_test ./khugepaged_race -d 5 -m madvise
 
+CATEGORY="thp" run_test ./khugepaged_race -d 5 -m stepped -z
+
 CATEGORY="thp" run_test ./khugepaged
 
 CATEGORY="thp" run_test ./khugepaged -s 2
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 12/16] selftests/mm: add memory-pressure threads to the khugepaged race harness
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (10 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 11/16] selftests/mm: race-harness variant for permissive hole occupancy Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 13/16] selftests/mm: parameterize the mixed-source collapse case by source order Kiryl Shutsemau
                   ` (5 subsequent siblings)
  17 siblings, 0 replies; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

The race harness exercises collapse against faults, pins, fork, mremap and
MADV_DONTNEED, but nothing in it ever elevates a source folio's refcount
from the reclaim or compaction side: LRU isolation, migration of a source
folio out from under the collapse, or swap traffic churning the LRU while
a collapse is in progress.

Add -p, which starts two more racing threads:

 - pageout: cycles MADV_PAGEOUT over a dedicated neighbor region
   (4 PMD areas, clamped to [16M, 64M]), faulting everything back in and
   verifying content each round -- swap traffic and LRU churn with an
   exact correctness check, since a page's pattern must survive the round
   trip through swap. Disabled with a note when the host has no swap:
   without it there is no anon reclaim to drive.

 - compactor: writes to /proc/sys/vm/compact_memory in a loop. Compaction
   isolates and migrates folios, so it competes with a collapse for the
   very pages it is trying to gather, with transient refcount elevations
   and migration entries of its own.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/khugepaged_race.c | 109 ++++++++++++++++++-
 tools/testing/selftests/mm/run_vmtests.sh    |   2 +
 2 files changed, 107 insertions(+), 4 deletions(-)

diff --git a/tools/testing/selftests/mm/khugepaged_race.c b/tools/testing/selftests/mm/khugepaged_race.c
index 2e36e242caa7..304c72b4ee3c 100644
--- a/tools/testing/selftests/mm/khugepaged_race.c
+++ b/tools/testing/selftests/mm/khugepaged_race.c
@@ -22,6 +22,12 @@
  * clear, zero-filled copy, install-time pte_none() verify and abort)
  * race the faulters directly.
  *
+ * -p adds memory pressure to any of the above: MADV_PAGEOUT cycling
+ * on a dedicated neighbor region (swap traffic and LRU churn; skipped
+ * with a note when the host has no swap) and a compact_memory trigger
+ * loop (compaction migrates source folios, racing collapse's freeze
+ * with refcount elevation and migration entries of its own).
+ *
  * Correctness signals: every racing page must read as its pattern or
  * zero (MADV_DONTNEED), never anything else — checked continuously by
  * the faulters and the fork children and once at the end — plus
@@ -64,6 +70,8 @@ static unsigned long page_size;
 static char *region;		/* NR_AREAS * hpage_pmd_size */
 static char *mremap_area;	/* region + NR_SHARED_AREAS areas */
 static char *mremap_scratch;	/* well above the region */
+static char *pageout_area;	/* -p: dedicated pressure region */
+static size_t pageout_size;
 static int gup_fd = -1;
 static volatile int stop;
 static volatile int corrupted;
@@ -189,6 +197,70 @@ static void *mremapper_fn(void *arg)
 	return NULL;
 }
 
+/*
+ * -p: swap traffic and LRU churn on a region of our own. The content
+ * check is exact — a page out and back through swap must preserve the
+ * pattern, and nothing else ever writes here.
+ */
+static void *pageout_fn(void *arg)
+{
+	unsigned int seed = (unsigned long)arg;
+	unsigned long nr = pageout_size / page_size;
+	unsigned long i;
+
+	for (i = 0; i < nr; i++)
+		*(unsigned int *)(pageout_area + i * page_size) = pattern(i);
+
+	while (!stop) {
+		madvise(pageout_area, pageout_size, MADV_PAGEOUT);
+		for (i = 0; i < nr && !stop; i++) {
+			unsigned int val = *(unsigned int *)(pageout_area +
+							     i * page_size);
+
+			if (val != pattern(i)) {
+				corrupted = 1;
+				ksft_print_msg("Pageout corruption at page %lu: %#x != %#x\n",
+					       i, val, pattern(i));
+			}
+		}
+		usleep(rand_r(&seed) % 2000);
+	}
+	return NULL;
+}
+
+/* -p: compaction migrates the collapse sources out from under us. */
+static void *compactor_fn(void *arg)
+{
+	unsigned int seed = (unsigned long)arg;
+	int fd = open("/proc/sys/vm/compact_memory", O_WRONLY);
+
+	if (fd < 0) {
+		ksft_print_msg("No compact_memory; compactor idle\n");
+		return NULL;
+	}
+	while (!stop) {
+		if (write(fd, "1", 1) < 0)
+			break;
+		usleep(10000 + rand_r(&seed) % 100000);
+	}
+	close(fd);
+	return NULL;
+}
+
+static bool swap_available(void)
+{
+	char line[256];
+	int lines = 0;
+	FILE *fp = fopen("/proc/swaps", "r");
+
+	if (!fp)
+		return false;
+	while (fgets(line, sizeof(line), fp))
+		lines++;
+	fclose(fp);
+	return lines > 1;
+}
+
 static unsigned long now_ms(void)
 {
 	struct timeval tv;
@@ -200,8 +272,9 @@ static unsigned long now_ms(void)
 static void usage(void)
 {
 	fprintf(stderr,
-		"Usage: khugepaged_race [-d seconds] [-m stepped|free|madvise] [-z] [-a areas]\n"
+		"Usage: khugepaged_race [-d seconds] [-m stepped|free|madvise] [-z] [-p] [-a areas]\n"
 		"\t-z: permissive max_ptes_none (hole-heavy windows)\n"
+		"\t-p: memory pressure (pageout + compaction) threads\n"
 		"\t-a: number of shared PMD-sized playground areas (default 3)\n");
 	exit(1);
 }
@@ -210,12 +283,13 @@ int main(int argc, char **argv)
 {
 	static const char * const thread_names[] = {
 		"faulter", "faulter2", "dontneed", "pinner", "forker",
-		"mremapper",
+		"mremapper", "pageout", "compactor",
 	};
 	void *(*const thread_fns[])(void *) = {
 		faulter_fn, faulter_fn, dontneed_fn, pinner_fn, forker_fn,
-		mremapper_fn,
+		mremapper_fn, pageout_fn, compactor_fn,
 	};
+	const unsigned long pageout_bit = 1UL << 6, compactor_bit = 1UL << 7;
 	const int nr_threads = ARRAY_SIZE(thread_names);
 	pthread_t threads[ARRAY_SIZE(thread_names)];
 	const char *mode = "stepped";
@@ -225,11 +299,12 @@ int main(int argc, char **argv)
 	unsigned long thread_mask = ~0UL;
 	int nr_areas_arg = 0;
 	bool permissive_none = false;
+	bool pressure = false;
 	unsigned long i;
 	int steps = 0;
 	int opt;
 
-	while ((opt = getopt(argc, argv, "a:d:m:t:zh")) != -1) {
+	while ((opt = getopt(argc, argv, "a:d:m:t:zph")) != -1) {
 		switch (opt) {
 		case 'a':
 			nr_areas_arg = atoi(optarg);
@@ -247,6 +322,9 @@ int main(int argc, char **argv)
 		case 'z':
 			permissive_none = true;
 			break;
+		case 'p':
+			pressure = true;
+			break;
 		default:
 			usage();
 		}
@@ -271,6 +349,14 @@ int main(int argc, char **argv)
 	nr_shared_areas = nr_areas_arg > 0 ? nr_areas_arg : DEFAULT_SHARED_AREAS;
 	nr_areas = nr_shared_areas + 1;
 
+	if (!pressure) {
+		thread_mask &= ~(pageout_bit | compactor_bit);
+	} else if (!swap_available()) {
+		/* No swap, no anon reclaim: compaction-only pressure. */
+		ksft_print_msg("-p without swap: pageout thread disabled\n");
+		thread_mask &= ~pageout_bit;
+	}
+
 	ksft_set_plan(1);
 
 	thp_save_settings();
@@ -311,6 +397,21 @@ int main(int argc, char **argv)
 	mremap_area = region + nr_shared_areas * hpage_pmd_size;
 	mremap_scratch = (char *)BASE_ADDR + 2 * nr_areas * hpage_pmd_size;
 
+	if (thread_mask & pageout_bit) {
+		/*
+		 * Big enough to cycle real reclaim, small enough not to
+		 * dominate a TCG guest: 4 PMD areas, clamped to [16M, 64M].
+		 */
+		pageout_size = 4 * hpage_pmd_size;
+		pageout_size = pageout_size < (16UL << 20) ? (16UL << 20) :
+			       pageout_size > (64UL << 20) ? (64UL << 20) :
+			       pageout_size;
+		pageout_area = mmap(NULL, pageout_size, PROT_READ | PROT_WRITE,
+				    MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+		if (pageout_area == MAP_FAILED)
+			ksft_exit_fail_perror("mmap() pageout area");
+	}
+
 	/* Populate so the first pass has something to collapse. */
 	for (i = 0; i < nr_shared_areas * hpage_pmd_size / page_size; i++)
 		*(unsigned int *)(region + i * page_size) = pattern(i);
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 83a04b1e2520..f826cf940c3d 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -415,6 +415,8 @@ CATEGORY="thp" run_test ./khugepaged_race -d 5 -m madvise
 
 CATEGORY="thp" run_test ./khugepaged_race -d 5 -m stepped -z
 
+CATEGORY="thp" run_test ./khugepaged_race -d 5 -m stepped -p
+
 CATEGORY="thp" run_test ./khugepaged
 
 CATEGORY="thp" run_test ./khugepaged -s 2
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 13/16] selftests/mm: parameterize the mixed-source collapse case by source order
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (11 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 12/16] selftests/mm: add memory-pressure threads to the khugepaged race harness Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 14/16] selftests/mm: zap whole PTE tables in the khugepaged race harness Kiryl Shutsemau
                   ` (4 subsequent siblings)
  17 siblings, 0 replies; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

collapse_order_mixed_sources faults its region as order-2 folios and
collapses them to the -o target, covering collapse of sources that are
already large folios of an order below the target. But it only ever tests
one source order, and order 2 sits below the contpte threshold on both
arm64 page-size configs -- so a source-side contpte unfold is never
exercised deterministically by this suite.

Let -s name the source order when combined with -o (it was rejected
before): the mixed-source case then faults at order @anon_order instead of
the fixed order 2, keeping order 2 as the default when -s is absent. -s
stays constrained to a supported mTHP order strictly below the target; the
other order-parameterized cases keep their order-0 sources (the source
order is enabled locally, not globally), so -s under -o is a knob on the
mixed-source case alone.

This makes e.g. -s 5 -o 7 on arm64/64K collapse contpte-mapped sources
into a larger mTHP, covering the source-unfold path directly.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/khugepaged.c | 42 +++++++++++++++++++------
 1 file changed, 32 insertions(+), 10 deletions(-)

diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index b074b005b62f..21a8fb24dc43 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -1363,28 +1363,32 @@ static void collapse_order_mixed_sources(struct collapse_context *c,
 					 struct mem_ops *ops)
 {
 	struct thp_settings settings = *thp_current_settings();
+	int source_order = anon_order ? anon_order : MIN_MTHP_ORDER;
 	void *p;
 
-	if (anon_target_order <= MIN_MTHP_ORDER) {
+	/* Sources must be a supported mTHP order strictly below the target. */
+	if (source_order >= anon_target_order ||
+	    !(thp_supported_orders() & (1UL << source_order))) {
 		ksft_test_result_skip("%s: no source order below target\n",
 				      __func__);
 		return;
 	}
 
-	/* Fault the whole region as order-MIN_MTHP_ORDER folios. */
-	settings.hugepages[MIN_MTHP_ORDER].enabled = THP_ALWAYS;
+	/* Fault the whole region as order-@source_order folios. */
+	settings.hugepages[source_order].enabled = THP_ALWAYS;
 	thp_push_settings(&settings);
 	p = ops->setup_area(1);
 	ops->fault(p, 0, hpage_pmd_size);
 	thp_pop_settings();
 
-	if (!is_range_backed_by_folio_orders(p, hpage_pmd_size, MIN_MTHP_ORDER,
+	if (!is_range_backed_by_folio_orders(p, hpage_pmd_size, source_order,
 					     pagemap_fd, kpageflags_fd))
 		ksft_exit_fail_msg("Region not backed by order-%d folios after fault\n",
-				   MIN_MTHP_ORDER);
+				   source_order);
 
 	madvise(p, hpage_pmd_size, MADV_HUGEPAGE);
-	ksft_print_msg("Collapse region backed by smaller large folios...");
+	ksft_print_msg("Collapse region backed by order-%d sources...",
+		       source_order);
 	if (!khugepaged_wait_full_pass())
 		fail("Timeout");
 	else if (range_collapsed(p, hpage_pmd_size))
@@ -1414,7 +1418,9 @@ static void usage(void)
 	fprintf(stderr,	"\t\t    Defaults to 0. Use this size for anon or shmem allocations.\n");
 	fprintf(stderr,	"\t\t-o: collapse target order for khugepaged:anon.\n");
 	fprintf(stderr,	"\t\t    Runs the order-parameterized collapse cases instead\n");
-	fprintf(stderr,	"\t\t    of the PMD cases. Cannot be combined with -s.\n");
+	fprintf(stderr,	"\t\t    of the PMD cases.\n");
+	fprintf(stderr,	"\t\t    With -s, -s names the mTHP source order for the\n");
+	fprintf(stderr,	"\t\t    mixed-source case (source order below the target).\n");
 	exit(1);
 }
 
@@ -1438,7 +1444,15 @@ static void parse_test_type(int argc, char **argv)
 		}
 	}
 
-	if (anon_target_order && anon_order)
+	/*
+	 * -s and -o compose: -s then names the mTHP source order for the
+	 * mixed-source case, which needs a source strictly below the
+	 * target (and at or above the smallest mTHP order). Alone, -s is
+	 * the source order for the default PMD suite; alone, -o is the
+	 * collapse target for the order-parameterized suite.
+	 */
+	if (anon_target_order && anon_order &&
+	    (anon_order < MIN_MTHP_ORDER || anon_order >= anon_target_order))
 		usage();
 
 	argv += optind;
@@ -1573,9 +1587,17 @@ int main(int argc, char **argv)
 	default_settings.khugepaged.max_ptes_shared = hpage_pmd_nr / 2;
 	default_settings.khugepaged.pages_to_scan = hpage_pmd_nr * 8;
 	default_settings.hugepages[hpage_pmd_order].enabled = THP_INHERIT;
-	default_settings.hugepages[anon_order].enabled = THP_ALWAYS;
 	default_settings.shmem_hugepages[hpage_pmd_order].enabled = SHMEM_INHERIT;
-	default_settings.shmem_hugepages[anon_order].enabled = SHMEM_ALWAYS;
+	/*
+	 * Under -o the order-parameterized cases want order-0 sources by
+	 * default; the mixed-source case enables its own (possibly -s
+	 * selected) source order locally. Enabling it globally here would
+	 * make every case fault that order.
+	 */
+	if (!anon_target_order) {
+		default_settings.hugepages[anon_order].enabled = THP_ALWAYS;
+		default_settings.shmem_hugepages[anon_order].enabled = SHMEM_ALWAYS;
+	}
 
 	if (anon_target_order) {
 		/*
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 14/16] selftests/mm: zap whole PTE tables in the khugepaged race harness
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (12 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 13/16] selftests/mm: parameterize the mixed-source collapse case by source order Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 15/16] selftests/mm: scale khugepaged's collapse wait with the PMD size Kiryl Shutsemau
                   ` (3 subsequent siblings)
  17 siblings, 0 replies; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

The race harness's MADV_DONTNEED thread zaps 1..32 pages at a time —
never a whole PMD-aligned area, so the empty-table reclaim
(CONFIG_PT_RECLAIM), which only engages when a zap spans the full
table, never ran against a collapse in any soak. Fuzzing had to find
the resulting class instead: the table vanishing between the
engine's park and install passes.

Make the thread zap a whole PMD-aligned area once every 64
iterations, keeping the fine-grained zaps as the common case.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/khugepaged_race.c | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/mm/khugepaged_race.c b/tools/testing/selftests/mm/khugepaged_race.c
index 304c72b4ee3c..2aa45c9e8a77 100644
--- a/tools/testing/selftests/mm/khugepaged_race.c
+++ b/tools/testing/selftests/mm/khugepaged_race.c
@@ -124,8 +124,23 @@ static void *dontneed_fn(void *arg)
 		unsigned long page_idx = rand_page(&seed);
 		unsigned long nr = 1UL << (rand_r(&seed) % 6);	/* 1..32 pages */
 
-		madvise(region + page_idx * page_size, nr * page_size,
-			MADV_DONTNEED);
+		/*
+		 * Once in a while zap a whole PMD-aligned area: only a
+		 * zap spanning the full table triggers the empty-table
+		 * reclaim (CONFIG_PT_RECLAIM), which can free a table
+		 * out from under a parked collapse — sub-table zaps
+		 * never reach that path.
+		 */
+		if (!(rand_r(&seed) % 64)) {
+			unsigned long area = page_idx /
+					(hpage_pmd_size / page_size);
+
+			madvise(region + area * hpage_pmd_size,
+				hpage_pmd_size, MADV_DONTNEED);
+		} else {
+			madvise(region + page_idx * page_size,
+				nr * page_size, MADV_DONTNEED);
+		}
 		usleep(rand_r(&seed) % 500);
 	}
 	return NULL;
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 15/16] selftests/mm: scale khugepaged's collapse wait with the PMD size
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (13 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 14/16] selftests/mm: zap whole PTE tables in the khugepaged race harness Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-02 19:52 ` [PATCH 16/16] selftests/mm: skip khugepaged shmem cases without a PMD page cache folio Kiryl Shutsemau
                   ` (2 subsequent siblings)
  17 siblings, 0 replies; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

wait_for_scan() gives khugepaged a fixed three seconds, whatever a huge
page costs to build. collapse_full() asks for four of them, which is 8M at
a 2M PMD and 2G at a 512M PMD -- arm64 with 64K base pages -- and three
seconds does not cover copying 2G. The escape hatch does not help either:
it wants two full khugepaged passes inside the same three seconds, and one
pass over a 512M PMD takes about that long by itself.

So collapse_full fails there on a collapse that works. Measured with a
probe that faults 4 x 512M, marks it MADV_HUGEPAGE and polls: all four PMDs
collapse, with collapse_alloc=4 at the PMD size and no allocation
failures. Raising only this budget makes the test pass, and it does not
turn into a "Fail" -- which is what khugepaged completing two passes
without collapsing would produce.

Scale the budget with the memory to be collapsed: keep three seconds as
the floor and add a second per 64M. That leaves a 2M PMD at exactly the
three seconds it has now, and gives 35s at a 512M PMD, where the collapse
measures under 3s. Keying it on nr_hpages * hpage_pmd_size rather than the
PMD size alone matters because the callers ask for one or four; scaling
linearly on PMD size alone would ask for 768s, which is not a budget so
much as a hang.

This also brings the helper in line with khugepaged_full_pass(), which
already allows 30s and is why the order-parameterized cases pass at a 512M
PMD while this one did not.

arm64/64K: khugepaged all:anon 21 pass/1 fail -> 22 pass/0 fail. x86-64
unchanged, 129 ok.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/khugepaged.c | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index 21a8fb24dc43..9213ce1658d0 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -573,8 +573,17 @@ static void madvise_collapse(const char *msg, char *p, int nr_hpages,
 static bool wait_for_scan(const char *msg, char *p, int nr_hpages,
 			  struct mem_ops *ops)
 {
+	/*
+	 * The budget has to cover khugepaged copying nr_hpages *
+	 * hpage_pmd_size, plus two of its passes over the mm. Three seconds
+	 * does that at a 2M PMD, but the same test moves 2G at a 512M PMD
+	 * (arm64 with 64K base pages) and 3s is then marginal: it fails on a
+	 * collapse that completes correctly, just not inside the budget.
+	 * Allow a further second per 64M to collapse.
+	 */
+	const unsigned long bytes = (unsigned long)nr_hpages * hpage_pmd_size;
+	int timeout = 6 + 2 * (bytes / (64UL << 20));
 	int full_scans;
-	int timeout = 6; /* 3 seconds */
 
 	/* Sanity check */
 	if (!ops->check_huge(p, 0))
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 16/16] selftests/mm: skip khugepaged shmem cases without a PMD page cache folio
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (14 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 15/16] selftests/mm: scale khugepaged's collapse wait with the PMD size Kiryl Shutsemau
@ 2026-08-02 19:52 ` Kiryl Shutsemau
  2026-08-02 22:50   ` Zi Yan
  2026-08-03  1:21 ` [PATCH 00/16] selftests/mm: improve khugepaged coverage Baolin Wang
  2026-08-03 11:04 ` Mike Rapoport
  17 siblings, 1 reply; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-02 19:52 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

The page cache caps folio order at MAX_PAGECACHE_ORDER, and
xas_split_alloc() puts that cap below the PMD order where a PMD is 512M --
arm64 with 64K base pages, as include/linux/pagemap.h says outright.
shmem_huge_global_enabled() then offers no PMD order at all, so
MADV_COLLAPSE of a shmem range answers -EINVAL and khugepaged passes over
it.

The shmem cases nonetheless ask for a PMD-sized shmem folio, so on such a
configuration four of them fail and the run bails out in the middle:

not ok 2 collapse_full not ok 4 collapse_single_pte_entry # Allocate huge
page...Bail out! madvise(MADV_COLLAPSE): Invalid argument (22)

That is the kernel declining something it deliberately does not support,
not a collapse defect.  Skip those cases where the PMD order is not a shmem
order, which thp_shmem_supported_orders() already reports -- it reads the
same per-size shmem_enabled controls the kernel only publishes for orders
the page cache can hold.  A tmpfs-backed file argument is skipped on the
same grounds, and a run left with nothing to collapse into skips outright.

Anonymous collapse is unaffected: its orders are not capped this way, and
the anonymous cases pass at a 512M PMD.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 tools/testing/selftests/mm/khugepaged.c | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index 9213ce1658d0..c5a3c3922581 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -1584,6 +1584,27 @@ int main(int argc, char **argv)
 	hpage_pmd_nr = hpage_pmd_size / page_size;
 	hpage_pmd_order = __builtin_ctz(hpage_pmd_nr);
 
+	/*
+	 * The page cache caps folio order at MAX_PAGECACHE_ORDER, which
+	 * xas_split_alloc() puts below the PMD order on arm64 with 64K pages.
+	 * A PMD-sized page cache folio is then impossible, so the kernel
+	 * refuses these collapses by design and there is nothing to test.
+	 */
+	if (!(thp_shmem_supported_orders() & (1UL << hpage_pmd_order))) {
+		if (shmem_ops) {
+			ksft_print_msg("no PMD-order page cache folio: skipping shmem\n");
+			shmem_ops = NULL;
+		}
+		if (finfo.type == VMA_SHMEM && read_only_file_ops) {
+			ksft_print_msg("no PMD-order page cache folio: skipping tmpfs file\n");
+			read_only_file_ops = NULL;
+			read_write_file_read_ops = NULL;
+			read_write_file_write_ops = NULL;
+		}
+		if (!anon_ops && !shmem_ops && !read_only_file_ops)
+			ksft_exit_skip("Nothing left to collapse into\n");
+	}
+
 	if (anon_target_order &&
 	    !(thp_supported_orders() & (1UL << anon_target_order)))
 		ksft_exit_skip("Order %d is not a supported anon THP order\n",
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* Re: [PATCH 16/16] selftests/mm: skip khugepaged shmem cases without a PMD page cache folio
  2026-08-02 19:52 ` [PATCH 16/16] selftests/mm: skip khugepaged shmem cases without a PMD page cache folio Kiryl Shutsemau
@ 2026-08-02 22:50   ` Zi Yan
  0 siblings, 0 replies; 28+ messages in thread
From: Zi Yan @ 2026-08-02 22:50 UTC (permalink / raw)
  To: Kiryl Shutsemau, Andrew Morton, David Hildenbrand,
	Lorenzo Stoakes, Nico Pache
  Cc: Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	linux-mm, linux-kselftest, linux-kernel, Kiryl Shutsemau (Meta)

On Sun Aug 2, 2026 at 3:52 PM EDT, Kiryl Shutsemau wrote:
> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
>
> The page cache caps folio order at MAX_PAGECACHE_ORDER, and
> xas_split_alloc() puts that cap below the PMD order where a PMD is 512M --
> arm64 with 64K base pages, as include/linux/pagemap.h says outright.
> shmem_huge_global_enabled() then offers no PMD order at all, so
> MADV_COLLAPSE of a shmem range answers -EINVAL and khugepaged passes over
> it.

IIRC, after READ_ONLY_THP_FOR_FS is removed, all pagecache folios are
split using non uniform split, xas_try_split(), so does shmem (except
shmem in swapcache not splittable). In theory, we can get rid of the
cap, since xas_try_split() does not split more than one level like
one can try to make xas_split_alloc() split more than two level (e.g.,
512MB to 64KB on arm64 with 64KB base page).

>
> The shmem cases nonetheless ask for a PMD-sized shmem folio, so on such a
> configuration four of them fail and the run bails out in the middle:
>
> not ok 2 collapse_full not ok 4 collapse_single_pte_entry # Allocate huge
> page...Bail out! madvise(MADV_COLLAPSE): Invalid argument (22)
>
> That is the kernel declining something it deliberately does not support,
> not a collapse defect.  Skip those cases where the PMD order is not a shmem
> order, which thp_shmem_supported_orders() already reports -- it reads the
> same per-size shmem_enabled controls the kernel only publishes for orders
> the page cache can hold.  A tmpfs-backed file argument is skipped on the
> same grounds, and a run left with nothing to collapse into skips outright.
>
> Anonymous collapse is unaffected: its orders are not capped this way, and
> the anonymous cases pass at a 512M PMD.
>
> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> ---
>  tools/testing/selftests/mm/khugepaged.c | 21 +++++++++++++++++++++
>  1 file changed, 21 insertions(+)
>
> diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
> index 9213ce1658d0..c5a3c3922581 100644
> --- a/tools/testing/selftests/mm/khugepaged.c
> +++ b/tools/testing/selftests/mm/khugepaged.c
> @@ -1584,6 +1584,27 @@ int main(int argc, char **argv)
>  	hpage_pmd_nr = hpage_pmd_size / page_size;
>  	hpage_pmd_order = __builtin_ctz(hpage_pmd_nr);
>  
> +	/*
> +	 * The page cache caps folio order at MAX_PAGECACHE_ORDER, which
> +	 * xas_split_alloc() puts below the PMD order on arm64 with 64K pages.
> +	 * A PMD-sized page cache folio is then impossible, so the kernel
> +	 * refuses these collapses by design and there is nothing to test.
> +	 */
> +	if (!(thp_shmem_supported_orders() & (1UL << hpage_pmd_order))) {
> +		if (shmem_ops) {
> +			ksft_print_msg("no PMD-order page cache folio: skipping shmem\n");
> +			shmem_ops = NULL;
> +		}
> +		if (finfo.type == VMA_SHMEM && read_only_file_ops) {
> +			ksft_print_msg("no PMD-order page cache folio: skipping tmpfs file\n");
> +			read_only_file_ops = NULL;
> +			read_write_file_read_ops = NULL;
> +			read_write_file_write_ops = NULL;
> +		}
> +		if (!anon_ops && !shmem_ops && !read_only_file_ops)
> +			ksft_exit_skip("Nothing left to collapse into\n");
> +	}
> +
>  	if (anon_target_order &&
>  	    !(thp_supported_orders() & (1UL << anon_target_order)))
>  		ksft_exit_skip("Order %d is not a supported anon THP order\n",




-- 
Best Regards,
Yan, Zi


^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 00/16] selftests/mm: improve khugepaged coverage
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (15 preceding siblings ...)
  2026-08-02 19:52 ` [PATCH 16/16] selftests/mm: skip khugepaged shmem cases without a PMD page cache folio Kiryl Shutsemau
@ 2026-08-03  1:21 ` Baolin Wang
  2026-08-05 15:15   ` Kiryl Shutsemau
  2026-08-03 11:04 ` Mike Rapoport
  17 siblings, 1 reply; 28+ messages in thread
From: Baolin Wang @ 2026-08-03  1:21 UTC (permalink / raw)
  To: Kiryl Shutsemau, Andrew Morton, David Hildenbrand,
	Lorenzo Stoakes, Nico Pache
  Cc: Barry Song, Dev Jain, Hugh Dickins, Lance Yang, Liam R. Howlett,
	Michal Hocko, Mike Rapoport, Ryan Roberts, Shuah Khan,
	Suren Baghdasaryan, Usama Arif, Vlastimil Babka, Zi Yan, linux-mm,
	linux-kselftest, linux-kernel, Kiryl Shutsemau (Meta)



On 8/3/26 3:52 AM, Kiryl Shutsemau wrote:
> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
> 
> mTHP collapse went in for 7.2 with no functional selftest coverage. Every
> khugepaged collapse case is PMD-shaped, down to the detection: it reads
> smaps AnonHugePages, which cannot see anything below the PMD order.

Yes, I’ve already sent out several versions of mTHP collapse test 
cases[1]. Not sure if you’ve seen my patchset, and it looks like some of 
our work overlaps :(

[1] 
https://lore.kernel.org/all/cover.1785564857.git.baolin.wang@linux.alibaba.com/

> I am also reworking khugepaged's collapse mechanism, and that wants a suite
> worth trusting before the mechanism changes underneath it. Nothing here
> depends on the rework -- everything passes on an unmodified kernel and
> documents what khugepaged already does.
> 
>   - vm_util grows folio-order helpers: is_backed_by_folio() moves out of
>     split_huge_page_test.c, and a range-level form asks whether every
>     order-aligned window of a range is backed by one folio of exactly that
>     order, mapped head to tail.
> 
>   - folio_order_check validates those helpers against the kernel for every
>     anon THP order it supports, before any collapse test trusts them.
> 
>   - khugepaged_full_pass() drives exactly one scan pass through the sysfs
>     wake path, so a barrier completes on the daemon's own cadence instead of
>     needing a short scan_sleep_millisecs.
> 
>   - khugepaged -o <order> runs order-parameterized anon collapse cases: a
>     full table, only the populated window, default max_ptes_none and
>     max_ptes_none=0, and collapse upward from smaller large folios. Any
>     order up to the PMD order, where the same cases baseline the PMD path.
> 
>   - khugepaged_race races two faulters, MADV_DONTNEED, transient FOLL_PIN,
>     fork and mremap against one of three collapse drivers: khugepaged a pass
>     at a time, khugepaged free-running, or a MADV_COLLAPSE loop. Every
>     racing page must read as its pattern or as zero. -z collapses
>     hole-bearing windows; -p adds pageout and compaction, the only threads
>     that elevate a source folio's refcount from the reclaim side. DEBUG_VM,
>     page_table_check, KASAN and lockdep are the other half of the oracle.
> 
>   - A shared-source write race pins the CoW isolation contract: a co-sharer
>     writing throughout a collapse must not see the collapsing side's pages.
> 
>   - Four tests fail on the environment rather than on the kernel: the
>     collapse wait is a fixed three seconds whatever a huge page costs to
>     build, collapse_compound_extreme wants a 512M page from the fault path,
>     the shmem cases want a PMD-order page cache folio the page cache caps
>     below, and the swap cases fail instead of skipping without swap. Scale
>     the wait (a 2M PMD is unchanged) and skip the other three. This is why
>     the suite now runs on arm64 with 64K pages.
> 
> Tested on mm-new (1dbd7c34bb92):
> 
>    x86-64 4K    105 pass, 3 skip, 0 fail
>    arm64 64K     87 pass, 7 skip, 0 fail
> 
> The skips are structural: sub-PMD cases decline at the PMD order and the
> mixed-source case at the smallest order, and on 64K pages
> collapse_compound_extreme and the shmem cases skip for the reasons above.
> 
> Kiryl Shutsemau (Meta) (16):
>    selftests/mm: move is_backed_by_folio() into vm_util
>    selftests/mm: add folio-order check for VA ranges
>    selftests/mm: add folio-order detection self-check
>    selftests/mm: add order-parameterized khugepaged collapse cases
>    selftests/mm: add khugepaged completion barrier helper
>    selftests/mm: add khugepaged race harness
>    selftests/mm: cover a shared-source collapse write race
>    selftests/mm: skip collapse_compound_extreme where the PMD is too
>      large
>    selftests/mm: skip khugepaged swap tests when no swap is configured
>    selftests/mm: verify synchronous khugepaged driving is attributable
>    selftests/mm: race-harness variant for permissive hole occupancy
>    selftests/mm: add memory-pressure threads to the khugepaged race
>      harness
>    selftests/mm: parameterize the mixed-source collapse case by source
>      order
>    selftests/mm: zap whole PTE tables in the khugepaged race harness
>    selftests/mm: scale khugepaged's collapse wait with the PMD size
>    selftests/mm: skip khugepaged shmem cases without a PMD page cache
>      folio
> 
>   tools/testing/selftests/mm/.gitignore         |   3 +
>   tools/testing/selftests/mm/Makefile           |   3 +
>   .../testing/selftests/mm/folio_order_check.c  | 138 +++++
>   .../testing/selftests/mm/hugepage_settings.c  |  66 ++-
>   .../testing/selftests/mm/hugepage_settings.h  |   2 +
>   tools/testing/selftests/mm/khugepaged.c       | 430 +++++++++++++++-
>   tools/testing/selftests/mm/khugepaged_race.c  | 483 ++++++++++++++++++
>   .../selftests/mm/khugepaged_sync_check.c      | 198 +++++++
>   tools/testing/selftests/mm/run_vmtests.sh     |  17 +
>   .../selftests/mm/split_huge_page_test.c       |  62 ---
>   tools/testing/selftests/mm/vm_util.c          | 157 ++++++
>   tools/testing/selftests/mm/vm_util.h          |   7 +
>   12 files changed, 1490 insertions(+), 76 deletions(-)
>   create mode 100644 tools/testing/selftests/mm/folio_order_check.c
>   create mode 100644 tools/testing/selftests/mm/khugepaged_race.c
>   create mode 100644 tools/testing/selftests/mm/khugepaged_sync_check.c
> 


^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 00/16] selftests/mm: improve khugepaged coverage
  2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
                   ` (16 preceding siblings ...)
  2026-08-03  1:21 ` [PATCH 00/16] selftests/mm: improve khugepaged coverage Baolin Wang
@ 2026-08-03 11:04 ` Mike Rapoport
  17 siblings, 0 replies; 28+ messages in thread
From: Mike Rapoport @ 2026-08-03 11:04 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache,
	Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
> 
> mTHP collapse went in for 7.2 with no functional selftest coverage. Every
> khugepaged collapse case is PMD-shaped, down to the detection: it reads
> smaps AnonHugePages, which cannot see anything below the PMD order.
> 
> I am also reworking khugepaged's collapse mechanism, and that wants a suite
> worth trusting before the mechanism changes underneath it. Nothing here
> depends on the rework -- everything passes on an unmodified kernel and
> documents what khugepaged already does.

My overall feeling is that changelogs need more human touch to be
parseable.

>  - vm_util grows folio-order helpers: is_backed_by_folio() moves out of
>    split_huge_page_test.c, and a range-level form asks whether every
>    order-aligned window of a range is backed by one folio of exactly that
>    order, mapped head to tail.
> 
>  - folio_order_check validates those helpers against the kernel for every
>    anon THP order it supports, before any collapse test trusts them.
> 
>  - khugepaged_full_pass() drives exactly one scan pass through the sysfs
>    wake path, so a barrier completes on the daemon's own cadence instead of
>    needing a short scan_sleep_millisecs.
> 
>  - khugepaged -o <order> runs order-parameterized anon collapse cases: a
>    full table, only the populated window, default max_ptes_none and
>    max_ptes_none=0, and collapse upward from smaller large folios. Any
>    order up to the PMD order, where the same cases baseline the PMD path.
> 
>  - khugepaged_race races two faulters, MADV_DONTNEED, transient FOLL_PIN,
>    fork and mremap against one of three collapse drivers: khugepaged a pass
>    at a time, khugepaged free-running, or a MADV_COLLAPSE loop. Every
>    racing page must read as its pattern or as zero. -z collapses
>    hole-bearing windows; -p adds pageout and compaction, the only threads
>    that elevate a source folio's refcount from the reclaim side. DEBUG_VM,
>    page_table_check, KASAN and lockdep are the other half of the oracle.
> 
>  - A shared-source write race pins the CoW isolation contract: a co-sharer
>    writing throughout a collapse must not see the collapsing side's pages.
> 
>  - Four tests fail on the environment rather than on the kernel: the
>    collapse wait is a fixed three seconds whatever a huge page costs to
>    build, collapse_compound_extreme wants a 512M page from the fault path,
>    the shmem cases want a PMD-order page cache folio the page cache caps
>    below, and the swap cases fail instead of skipping without swap. Scale
>    the wait (a 2M PMD is unchanged) and skip the other three. This is why
>    the suite now runs on arm64 with 64K pages.
> 
> Tested on mm-new (1dbd7c34bb92):
> 
>   x86-64 4K    105 pass, 3 skip, 0 fail
>   arm64 64K     87 pass, 7 skip, 0 fail
> 
> The skips are structural: sub-PMD cases decline at the PMD order and the
> mixed-source case at the smallest order, and on 64K pages
> collapse_compound_extreme and the shmem cases skip for the reasons above.
> 
> Kiryl Shutsemau (Meta) (16):
>   selftests/mm: move is_backed_by_folio() into vm_util
>   selftests/mm: add folio-order check for VA ranges
>   selftests/mm: add folio-order detection self-check
>   selftests/mm: add order-parameterized khugepaged collapse cases
>   selftests/mm: add khugepaged completion barrier helper
>   selftests/mm: add khugepaged race harness
>   selftests/mm: cover a shared-source collapse write race
>   selftests/mm: skip collapse_compound_extreme where the PMD is too
>     large
>   selftests/mm: skip khugepaged swap tests when no swap is configured
>   selftests/mm: verify synchronous khugepaged driving is attributable
>   selftests/mm: race-harness variant for permissive hole occupancy
>   selftests/mm: add memory-pressure threads to the khugepaged race
>     harness
>   selftests/mm: parameterize the mixed-source collapse case by source
>     order
>   selftests/mm: zap whole PTE tables in the khugepaged race harness
>   selftests/mm: scale khugepaged's collapse wait with the PMD size
>   selftests/mm: skip khugepaged shmem cases without a PMD page cache
>     folio

I'd put the commits that update the existing tests at the top of the
series and than add the new tests and infrastrucure.

-- 
Sincerely yours,
Mike.


^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 01/16] selftests/mm: move is_backed_by_folio() into vm_util
  2026-08-02 19:52 ` [PATCH 01/16] selftests/mm: move is_backed_by_folio() into vm_util Kiryl Shutsemau
@ 2026-08-03 11:04   ` Mike Rapoport
  0 siblings, 0 replies; 28+ messages in thread
From: Mike Rapoport @ 2026-08-03 11:04 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache,
	Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

On Sun, 02 Aug 2026 20:52:33 +0100, Kiryl Shutsemau <kirill@shutemov.name> wrote:
> The khugepaged selftest is about to gain mTHP collapse coverage, which
> needs to verify that a VA range is backed by a folio of a given order.
> split_huge_page_test.c already has the building block for that:
> is_backed_by_folio() classifies the folio backing a page via
> /proc/kpageflags compound head/tail flags.
> 
> Move it into vm_util so other tests can use it. No functional change.
> 
> [...]

Acked-by: Mike Rapoport (Microsoft) <rppt@kernel.org>

-- 
Sincerely yours,
Mike.


^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 02/16] selftests/mm: add folio-order check for VA ranges
  2026-08-02 19:52 ` [PATCH 02/16] selftests/mm: add folio-order check for VA ranges Kiryl Shutsemau
@ 2026-08-03 11:04   ` Mike Rapoport
  0 siblings, 0 replies; 28+ messages in thread
From: Mike Rapoport @ 2026-08-03 11:04 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache,
	Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

> is_backed_by_folio() answers "what order folio backs this page", but
> mTHP collapse tests need the range-level question: is every
> order-aligned window of this VA range backed by one folio of exactly

Nit:                          ^ address (even without Virtual) reads
better to me here and in the subject.

> that order, mapped head-to-tail?
> 
> Add is_range_backed_by_folio_orders(): per window, require a present
> and naturally aligned head PFN, a contiguous PFN run across the
> window, and is_backed_by_folio() agreeing on the order. A window
> assembled from pieces of different folios, or mapping a folio outside
> its natural position, fails the check.

I'd need claude to understand this one ;-P

> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
>
> diff --git a/tools/testing/selftests/mm/vm_util.c b/tools/testing/selftests/mm/vm_util.c
> index 343a15e25a9f..793342095420 100644
> --- a/tools/testing/selftests/mm/vm_util.c
> +++ b/tools/testing/selftests/mm/vm_util.c
> @@ -365,6 +365,52 @@ bool is_backed_by_folio(char *vaddr, int order, int pagemap_fd,
>  	return false;
>  }
>  
> +/*
> + * Check whether the range [start, start + len) is backed by folios of
> + * exactly @order, mapped at their natural alignment.
> + *
> + * is_backed_by_folio() classifies the folio backing one page; here we
> + * additionally require that each order-aligned window of the range maps

I don't think the description should be incremental on top of
is_backed_by_folio() 

> + * one such folio head-to-tail: the VA range must be naturally aligned,

Please spell out VA or just use "address". selftests are userspace,
"virtual" is kinda implied.

-- 
Sincerely yours,
Mike.


^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 03/16] selftests/mm: add folio-order detection self-check
  2026-08-02 19:52 ` [PATCH 03/16] selftests/mm: add folio-order detection self-check Kiryl Shutsemau
@ 2026-08-03 11:04   ` Mike Rapoport
  0 siblings, 0 replies; 28+ messages in thread
From: Mike Rapoport @ 2026-08-03 11:04 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache,
	Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

> The upcoming khugepaged mTHP tests detect collapse results with the
> vm_util folio-order helpers instead of smaps AnonHugePages, which only
> sees PMD mappings. Before any collapse test trusts those helpers, make
> sure they agree with the kernel about what backs a mapping.
> 
> For every anon THP order the kernel supports, fault memory in with
> only that order enabled and require the helpers to classify the
> backing as exactly that order: not a neighbouring order, and 4K-backed
> memory as order 0.
> 
> Runs in the thp category of run_vmtests.sh. Verified on x86-64 4K
> (orders 0, 2-9) and arm64 64K (orders 0, 2-13).
> 
> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
>
> diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore
> index 9ccd9e1447e6..54eefd8f97bc 100644
> --- a/tools/testing/selftests/mm/.gitignore
> +++ b/tools/testing/selftests/mm/.gitignore
> @@ -66,3 +66,4 @@ merge
>  prctl_thp_disable
>  rmap
>  folio_split_race_test
> +folio_order_check
> diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
> index 25d10ced3b3b..33917c76b871 100644
> --- a/tools/testing/selftests/mm/Makefile
> +++ b/tools/testing/selftests/mm/Makefile
> @@ -104,6 +104,7 @@ TEST_GEN_FILES += guard-regions
>  TEST_GEN_FILES += merge
>  TEST_GEN_FILES += rmap
>  TEST_GEN_FILES += folio_split_race_test
> +TEST_GEN_FILES += folio_order_check
>  
>  ifneq ($(ARCH),arm64)
>  TEST_GEN_FILES += soft-dirty
> diff --git a/tools/testing/selftests/mm/folio_order_check.c b/tools/testing/selftests/mm/folio_order_check.c
> new file mode 100644
> index 000000000000..f70d766ff3eb
> --- /dev/null
> +++ b/tools/testing/selftests/mm/folio_order_check.c
> @@ -0,0 +1,138 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Self-check for the vm_util folio-order detection helpers,
> + * is_backed_by_folio() and is_range_backed_by_folio_orders().
> + *
> + * For every anon THP order the kernel supports, fault memory in with only
> + * that order enabled and verify the helpers report exactly that order:
> + * not a neighbouring order, and plain 4K memory as order 0. The helpers
> + * are what the khugepaged mTHP tests use to detect collapse results, so
> + * they must agree with the kernel's own idea of the backing before any
> + * collapse test relies on them.
> + */
> +#define _GNU_SOURCE
> +#include <fcntl.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <sys/mman.h>
> +#include <unistd.h>
> +
> +#include "kselftest.h"
> +#include "vm_util.h"
> +#include "hugepage_settings.h"
> +
> +static int pagemap_fd;
> +static int kpageflags_fd;
> +
> +/* mmap an anon VMA of exactly @size bytes at a @size-aligned address. */
> +static char *alloc_aligned(size_t size)
> +{
> +	size_t len = size * 2;
> +	uintptr_t aligned;
> +	char *p;
> +
> +	p = mmap(NULL, len, PROT_READ | PROT_WRITE,
> +		 MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
> +	if (p == MAP_FAILED)
> +		ksft_exit_fail_perror("mmap()");
> +
> +	aligned = ((uintptr_t)p + size - 1) & ~(size - 1);

We should really add ALIGN() to vm_util.h :)

> +	if (aligned != (uintptr_t)p)
> +		munmap(p, aligned - (uintptr_t)p);
> +	if (aligned + size != (uintptr_t)p + len)
> +		munmap((char *)aligned + size,
> +		       (uintptr_t)p + len - aligned - size);
> +
> +	return (char *)aligned;
> +}
> +
> +/*
> + * Enable only @order (order 0: nothing), fault one aligned window in and
> + * check the helpers see exactly @order.
> + */
> +static void check_order(int order)
> +{
> +	struct thp_settings settings = *thp_current_settings();
> +	size_t size = psize() << order;
> +	bool ok = true;
> +	char *p;
> +	int i;
> +
> +	for (i = 0; i < NR_ORDERS; i++)
> +		settings.hugepages[i].enabled = THP_NEVER;
> +	if (order)
> +		settings.hugepages[order].enabled = THP_ALWAYS;
> +	thp_push_settings(&settings);
> +
> +	p = alloc_aligned(size);
> +	*p = 1;
> +
> +	if (!is_range_backed_by_folio_orders(p, size, order,
> +					     pagemap_fd, kpageflags_fd)) {
> +		ksft_print_msg("order %d not detected after fault\n", order);
> +		ok = false;
> +	}
> +
> +	/* A lower order must be rejected: the folio is larger. */
> +	if (order && is_range_backed_by_folio_orders(p, size, order - 1,
> +						     pagemap_fd,
> +						     kpageflags_fd)) {
> +		ksft_print_msg("order %d also reported as order %d\n",
> +			       order, order - 1);
> +		ok = false;
> +	}
> +
> +	/* Order 0 pages must not look like any large folio, and vice versa. */
> +	if (order && is_range_backed_by_folio_orders(p, size, 0,
> +						     pagemap_fd,
> +						     kpageflags_fd)) {
> +		ksft_print_msg("order %d also reported as order 0\n", order);
> +		ok = false;
> +	}
> +
> +	munmap(p, size);
> +	thp_pop_settings();
> +
> +	ksft_test_result(ok, "order %d classified\n", order);
> +}
> +
> +int main(void)
> +{
> +	struct thp_settings settings;
> +	unsigned long orders;
> +	int order;
> +
> +	ksft_print_header();
> +
> +	if (!thp_available())
> +		ksft_exit_skip("Transparent Hugepages not available\n");
> +
> +	pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
> +	if (pagemap_fd < 0)
> +		ksft_exit_fail_perror("open(\"/proc/self/pagemap\")");

Do we need quotes in perror() message?
open(/proc/self/pagemap) will do I think.


> +	kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
> +	if (kpageflags_fd < 0)
> +		ksft_exit_skip("open(\"/proc/kpageflags\") requires root\n");

ditto

> +
> +	orders = thp_supported_orders();
> +	if (!orders)
> +		ksft_exit_skip("No supported THP orders\n");
> +
> +	ksft_set_plan(__builtin_popcountl(orders) + 1);
> +
> +	thp_save_settings();
> +	thp_read_settings(&settings);
> +	/* Base of the settings stack; the bottom entry is never popped. */
> +	thp_push_settings(&settings);
> +
> +	check_order(0);
> +	for (order = 1; order < NR_ORDERS; order++) {
> +		if (!(orders & (1UL << order)))
> +			continue;
> +		check_order(order);
> +	}
> +
> +	thp_restore_settings();

It's implicitly called at_exit().

-- 
Sincerely yours,
Mike.


^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 04/16] selftests/mm: add order-parameterized khugepaged collapse cases
  2026-08-02 19:52 ` [PATCH 04/16] selftests/mm: add order-parameterized khugepaged collapse cases Kiryl Shutsemau
@ 2026-08-03 11:04   ` Mike Rapoport
  0 siblings, 0 replies; 28+ messages in thread
From: Mike Rapoport @ 2026-08-03 11:04 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache,
	Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

> All khugepaged collapse cases are written against PMD collapse: the
> regions, the thresholds and the detection (smaps AnonHugePages, which
> only accounts PMD mappings) all assume the PMD-order product. The mTHP
> collapse support merged in 7.2 has no functional selftest coverage.
> 
> Add an -o <order> mode running order-parameterized anon collapse cases
> against khugepaged. The region is faulted to order 0 first (the target
> order is "inherit" under enabled=madvise, so pre-MADV_HUGEPAGE faults
> cannot produce large folios), then one full khugepaged pass is awaited
> via the full_scans barrier, and results are detected per aligned
> window with the vm_util folio-order helpers:

Having options is usefull for targeted testing, but I think that the
default should be to run all supported confugrations, like e.g. cow test
does. That's the point of a selftest: check that everything works.

Can we refactor the existing code so that simple 'khugepaged' invocation
will test everything it can before adding new options and tests?

-- 
Sincerely yours,
Mike.


^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 05/16] selftests/mm: add khugepaged completion barrier helper
  2026-08-02 19:52 ` [PATCH 05/16] selftests/mm: add khugepaged completion barrier helper Kiryl Shutsemau
@ 2026-08-03 11:04   ` Mike Rapoport
  0 siblings, 0 replies; 28+ messages in thread
From: Mike Rapoport @ 2026-08-03 11:04 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache,
	Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

> Race and functional tests need to drive khugepaged synchronously: set
> up a layout, let exactly one full scan pass over it, check the result.
> The khugepaged selftest already waits on full_scans advancing by two —
> a completion barrier for one pass that started after setup — but it
> relies on a short configured scan_sleep_millisecs to make progress.
> 
> Lift the pattern into a library helper, khugepaged_full_pass(), and
> drive it by the sysfs wake path: any store to scan_sleep_millisecs
> wakes the daemon, so the barrier completes promptly regardless of the
> configured scan cadence. Wake exactly once per missing pass:
> over-waking would queue a straggler pass behind the barrier that
> overlaps and perturbs whatever the caller sets up next. One wake
> completes one full pass only when the whole mm list fits in a single
> scan batch, so callers must pair the helper with a large pages_to_scan.
> 
> Settings pushes and pops must not start passes nobody asked for
> either, so thp_write_settings() now writes each khugepaged knob only
> when it changes. Switch the khugepaged selftest order-parameterized
> cases to the helper.
> 
> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
>
> diff --git a/tools/testing/selftests/mm/hugepage_settings.c b/tools/testing/selftests/mm/hugepage_settings.c
> index d7917dce3aba..a26a0cffa9c5 100644
> --- a/tools/testing/selftests/mm/hugepage_settings.c
> +++ b/tools/testing/selftests/mm/hugepage_settings.c
> @@ -183,6 +183,17 @@ void thp_read_settings(struct thp_settings *settings)
>  	}
>  }
>  
> +/*
> + * Write only on change: any store to a khugepaged sysfs knob wakes the
> + * daemon, and settings pushes/pops must not start scan passes nobody
> + * asked for — khugepaged_full_pass() is the only sanctioned wake.
> + */
> +static void thp_update_num(const char *name, unsigned long num)
> +{
> +	if (thp_read_num(name) != num)
> +		thp_write_num(name, num);
> +}

Don't you want to expose this to the users?

> +
>  void thp_write_settings(struct thp_settings *settings)
>  {
>  	struct khugepaged_settings *khugepaged = &settings->khugepaged;
> @@ -198,15 +209,15 @@ void thp_write_settings(struct thp_settings *settings)
>  			shmem_enabled_strings[settings->shmem_enabled]);
>  	thp_write_num("use_zero_page", settings->use_zero_page);
>  
> -	thp_write_num("khugepaged/defrag", khugepaged->defrag);
> -	thp_write_num("khugepaged/alloc_sleep_millisecs",
> -			khugepaged->alloc_sleep_millisecs);
> -	thp_write_num("khugepaged/scan_sleep_millisecs",
> -			khugepaged->scan_sleep_millisecs);
> -	thp_write_num("khugepaged/max_ptes_none", khugepaged->max_ptes_none);
> -	thp_write_num("khugepaged/max_ptes_swap", khugepaged->max_ptes_swap);
> -	thp_write_num("khugepaged/max_ptes_shared", khugepaged->max_ptes_shared);
> -	thp_write_num("khugepaged/pages_to_scan", khugepaged->pages_to_scan);
> +	thp_update_num("khugepaged/defrag", khugepaged->defrag);
> +	thp_update_num("khugepaged/alloc_sleep_millisecs",
> +		       khugepaged->alloc_sleep_millisecs);
> +	thp_update_num("khugepaged/scan_sleep_millisecs",
> +		       khugepaged->scan_sleep_millisecs);
> +	thp_update_num("khugepaged/max_ptes_none", khugepaged->max_ptes_none);
> +	thp_update_num("khugepaged/max_ptes_swap", khugepaged->max_ptes_swap);
> +	thp_update_num("khugepaged/max_ptes_shared", khugepaged->max_ptes_shared);
> +	thp_update_num("khugepaged/pages_to_scan", khugepaged->pages_to_scan);
>  
>  	if (dev_queue_read_ahead_path[0])
>  		write_num(dev_queue_read_ahead_path, settings->read_ahead_kb);
> @@ -230,6 +241,43 @@ void thp_write_settings(struct thp_settings *settings)
>  	}
>  }
>  
> +/*
> + * Completion barrier for khugepaged: wait until a full scan pass that
> + * started after this call has finished. full_scans must advance by two;
> + * a +1 step may complete a pass that examined this mm before the
> + * caller's setup was in place.
> + *
> + * Any store to scan_sleep_millisecs wakes the daemon, so the barrier
> + * works regardless of the configured scan cadence. It wakes exactly
> + * once per missing pass — over-waking would queue a straggler pass
> + * behind the barrier, perturbing whatever the caller sets up next.
> + * One wake completes one full pass only if the whole mm list fits in
> + * one scan batch, so callers must pair this with a large
> + * pages_to_scan.
> + */
> +bool khugepaged_full_pass(unsigned int timeout_s)
> +{
> +	unsigned long deadline_ms = timeout_s * 1000UL;
> +	unsigned long sleep_ms =
> +		thp_read_num("khugepaged/scan_sleep_millisecs");
> +	unsigned long elapsed_ms = 0;
> +	int pass;
> +
> +	for (pass = 0; pass < 2; pass++) {
> +		unsigned long target =
> +			thp_read_num("khugepaged/full_scans") + 1;
> +
> +		thp_write_num("khugepaged/scan_sleep_millisecs", sleep_ms);
> +		while (thp_read_num("khugepaged/full_scans") < target) {
> +			if (elapsed_ms >= deadline_ms)
> +				return false;
> +			usleep(10 * 1000);
> +			elapsed_ms += 10;
> +		}
> +	}
> +	return true;
> +}
> +
>  struct thp_settings *thp_current_settings(void)
>  {
>  	if (!settings_index) {
> diff --git a/tools/testing/selftests/mm/hugepage_settings.h b/tools/testing/selftests/mm/hugepage_settings.h
> index 726c73c43c05..8de446affeec 100644
> --- a/tools/testing/selftests/mm/hugepage_settings.h
> +++ b/tools/testing/selftests/mm/hugepage_settings.h
> @@ -83,6 +83,8 @@ static inline void thp_save_settings(void)
>  	hugepage_save_settings(/* thp = */ true, /* hugetlb = */ false);
>  }
>  
> +bool khugepaged_full_pass(unsigned int timeout_s);
> +
>  void thp_set_read_ahead_path(char *path);
>  unsigned long thp_supported_orders(void);
>  unsigned long thp_shmem_supported_orders(void);
> diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
> index 971e97a7330a..65fafab06410 100644
> --- a/tools/testing/selftests/mm/khugepaged.c
> +++ b/tools/testing/selftests/mm/khugepaged.c
> @@ -1126,23 +1126,10 @@ static bool range_not_collapsed(void *p, size_t len)
>  	return true;
>  }
>  
> -/*
> - * Completion barrier: one full khugepaged pass that started after this
> - * call. Waiting for full_scans to advance by two guarantees it; a +1
> - * step might complete a pass that scanned our mm before the setup.
> - */
>  static bool khugepaged_wait_full_pass(void)
>  {
> -	int full_scans = thp_read_num("khugepaged/full_scans") + 2;
> -	int timeout = 60; /* 30 seconds */
> -
> -	while (timeout--) {
> -		if (thp_read_num("khugepaged/full_scans") >= full_scans)
> -			return true;
> -		printf(".");
> -		usleep(TICK);
> -	}
> -	return false;
> +	/* Wait up to 30 seconds for the pass to complete. */
> +	return khugepaged_full_pass(30);

This implies that _full_pass() helper should be in a patch before its
users :)

-- 
Sincerely yours,
Mike.


^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 06/16] selftests/mm: add khugepaged race harness
  2026-08-02 19:52 ` [PATCH 06/16] selftests/mm: add khugepaged race harness Kiryl Shutsemau
@ 2026-08-03 11:04   ` Mike Rapoport
  0 siblings, 0 replies; 28+ messages in thread
From: Mike Rapoport @ 2026-08-03 11:04 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache,
	Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

> Collapse serializes against faults, GUP, fork, mremap and zapping
> through a protocol of locks, TLB flushes and refcount checks; none of
> the khugepaged selftests exercise it under contention.
> 
> Add khugepaged_race: two faulters, MADV_DONTNEED, transient FOLL_PIN
> (gup_test), fork and mremap threads race one of three collapse
> drivers over the same ranges:
> 
>   stepped   khugepaged driven one full pass at a time via
>             khugepaged_full_pass() — deterministic extent per step;
>   free      free-running khugepaged (scan_sleep_millisecs=0) — soak;
>   madvise   MADV_COLLAPSE + MADV_DONTNEED loop — the PMD-order axis.
> 
> diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
> index 6f42e35b5227..a8b6b839cb97 100755
> --- a/tools/testing/selftests/mm/run_vmtests.sh
> +++ b/tools/testing/selftests/mm/run_vmtests.sh
> @@ -404,6 +404,13 @@ CATEGORY="cow" run_test ./cow
>  
>  CATEGORY="thp" run_test ./folio_order_check
>  
> +
> +CATEGORY="thp" run_test ./khugepaged_race -d 5 -m stepped
> +
> +CATEGORY="thp" run_test ./khugepaged_race -d 5 -m free
> +
> +CATEGORY="thp" run_test ./khugepaged_race -d 5 -m madvise

Please set the default duration so it will be 5 seconds for each mode
and make ./khugepaged_race run all the modes by default.

-- 
Sincerely yours,
Mike.


^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 09/16] selftests/mm: skip khugepaged swap tests when no swap is configured
  2026-08-02 19:52 ` [PATCH 09/16] selftests/mm: skip khugepaged swap tests when no swap is configured Kiryl Shutsemau
@ 2026-08-03 11:04   ` Mike Rapoport
  0 siblings, 0 replies; 28+ messages in thread
From: Mike Rapoport @ 2026-08-03 11:04 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache,
	Baolin Wang, Barry Song, Dev Jain, Hugh Dickins, Lance Yang,
	Liam R. Howlett, Michal Hocko, Mike Rapoport, Ryan Roberts,
	Shuah Khan, Suren Baghdasaryan, Usama Arif, Vlastimil Babka,
	Zi Yan, linux-mm, linux-kselftest, linux-kernel,
	Kiryl Shutsemau (Meta)

> collapse_swapin_single_pte and collapse_max_ptes_swap swap pages out with
> MADV_PAGEOUT and then require them to be swapped. With no swap area
> configured MADV_PAGEOUT is a no-op, so check_swap() finds nothing and the
> tests report a failure that only reflects the environment, not khugepaged.
> 
> Skip both when /proc/swaps shows no swap area, so a missing swap device
> yields a SKIP rather than a spurious failure. A real swap-out failure with
> swap present still fails as before.
> 
> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
>
> diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
> index b43e060b4118..b074b005b62f 100644
> --- a/tools/testing/selftests/mm/khugepaged.c
> +++ b/tools/testing/selftests/mm/khugepaged.c
> @@ -181,6 +181,21 @@ static void get_finfo(const char *dir)
>  	ksft_exit_fail_msg("%s: Could not read: %s\n", __func__, path);
>  }
>  
> +static bool has_swap(void)

Would be usefull in vm_util.

-- 
Sincerely yours,
Mike.


^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 00/16] selftests/mm: improve khugepaged coverage
  2026-08-03  1:21 ` [PATCH 00/16] selftests/mm: improve khugepaged coverage Baolin Wang
@ 2026-08-05 15:15   ` Kiryl Shutsemau
  0 siblings, 0 replies; 28+ messages in thread
From: Kiryl Shutsemau @ 2026-08-05 15:15 UTC (permalink / raw)
  To: Baolin Wang
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Nico Pache,
	Barry Song, Dev Jain, Hugh Dickins, Lance Yang, Liam R. Howlett,
	Michal Hocko, Mike Rapoport, Ryan Roberts, Shuah Khan,
	Suren Baghdasaryan, Usama Arif, Vlastimil Babka, Zi Yan, linux-mm,
	linux-kselftest, linux-kernel

On Mon, Aug 03, 2026 at 09:21:09AM +0800, Baolin Wang wrote:
> 
> 
> On 8/3/26 3:52 AM, Kiryl Shutsemau wrote:
> > From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
> > 
> > mTHP collapse went in for 7.2 with no functional selftest coverage. Every
> > khugepaged collapse case is PMD-shaped, down to the detection: it reads
> > smaps AnonHugePages, which cannot see anything below the PMD order.
> 
> Yes, I’ve already sent out several versions of mTHP collapse test cases[1].
> Not sure if you’ve seen my patchset, and it looks like some of our work
> overlaps :(

My bad. I've missed it. Acked now.

I will rebase my patches on top of yours and removing duplication.

-- 
  Kiryl Shutsemau / Kirill A. Shutemov

^ permalink raw reply	[flat|nested] 28+ messages in thread

end of thread, other threads:[~2026-08-05 15:15 UTC | newest]

Thread overview: 28+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-02 19:52 [PATCH 00/16] selftests/mm: improve khugepaged coverage Kiryl Shutsemau
2026-08-02 19:52 ` [PATCH 01/16] selftests/mm: move is_backed_by_folio() into vm_util Kiryl Shutsemau
2026-08-03 11:04   ` Mike Rapoport
2026-08-02 19:52 ` [PATCH 02/16] selftests/mm: add folio-order check for VA ranges Kiryl Shutsemau
2026-08-03 11:04   ` Mike Rapoport
2026-08-02 19:52 ` [PATCH 03/16] selftests/mm: add folio-order detection self-check Kiryl Shutsemau
2026-08-03 11:04   ` Mike Rapoport
2026-08-02 19:52 ` [PATCH 04/16] selftests/mm: add order-parameterized khugepaged collapse cases Kiryl Shutsemau
2026-08-03 11:04   ` Mike Rapoport
2026-08-02 19:52 ` [PATCH 05/16] selftests/mm: add khugepaged completion barrier helper Kiryl Shutsemau
2026-08-03 11:04   ` Mike Rapoport
2026-08-02 19:52 ` [PATCH 06/16] selftests/mm: add khugepaged race harness Kiryl Shutsemau
2026-08-03 11:04   ` Mike Rapoport
2026-08-02 19:52 ` [PATCH 07/16] selftests/mm: cover a shared-source collapse write race Kiryl Shutsemau
2026-08-02 19:52 ` [PATCH 08/16] selftests/mm: skip collapse_compound_extreme where the PMD is too large Kiryl Shutsemau
2026-08-02 19:52 ` [PATCH 09/16] selftests/mm: skip khugepaged swap tests when no swap is configured Kiryl Shutsemau
2026-08-03 11:04   ` Mike Rapoport
2026-08-02 19:52 ` [PATCH 10/16] selftests/mm: verify synchronous khugepaged driving is attributable Kiryl Shutsemau
2026-08-02 19:52 ` [PATCH 11/16] selftests/mm: race-harness variant for permissive hole occupancy Kiryl Shutsemau
2026-08-02 19:52 ` [PATCH 12/16] selftests/mm: add memory-pressure threads to the khugepaged race harness Kiryl Shutsemau
2026-08-02 19:52 ` [PATCH 13/16] selftests/mm: parameterize the mixed-source collapse case by source order Kiryl Shutsemau
2026-08-02 19:52 ` [PATCH 14/16] selftests/mm: zap whole PTE tables in the khugepaged race harness Kiryl Shutsemau
2026-08-02 19:52 ` [PATCH 15/16] selftests/mm: scale khugepaged's collapse wait with the PMD size Kiryl Shutsemau
2026-08-02 19:52 ` [PATCH 16/16] selftests/mm: skip khugepaged shmem cases without a PMD page cache folio Kiryl Shutsemau
2026-08-02 22:50   ` Zi Yan
2026-08-03  1:21 ` [PATCH 00/16] selftests/mm: improve khugepaged coverage Baolin Wang
2026-08-05 15:15   ` Kiryl Shutsemau
2026-08-03 11:04 ` Mike Rapoport

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox