* [withdrawn] selftests-mm-add-hugetlb_region_cache_race-regression-test.patch removed from -mm tree
@ 2026-07-15 4:04 Andrew Morton
0 siblings, 0 replies; only message in thread
From: Andrew Morton @ 2026-07-15 4:04 UTC (permalink / raw)
To: mm-commits, shuah, richard.weiyang, osalvador, muchun.song, david,
baoquan.he, caixiangfeng, akpm
The quilt patch titled
Subject: selftests/mm: add hugetlb_region_cache_race regression test
has been removed from the -mm tree. Its filename was
selftests-mm-add-hugetlb_region_cache_race-regression-test.patch
This patch was dropped because it was withdrawn
------------------------------------------------------
From: "Xiangfeng Cai" <caixiangfeng@bytedance.com>
Subject: selftests/mm: add hugetlb_region_cache_race regression test
Date: Tue, 14 Jul 2026 01:14:56 +0800
Add a regression test for the list corruption in
allocate_file_region_entries() fixed by the previous patch ("mm/hugetlb:
fix list corruption in allocate_file_region_entries()").
Triggering the bug requires a concurrent reservation operation to drain
resv->region_cache while allocate_file_region_entries() has dropped
resv->lock for its GFP_KERNEL allocation, forcing its retry loop to run
again. As the mmap() and fallocate(PUNCH_HOLE) paths serialise on
inode_lock, the cache has to be drained by faults from a separate address
space. The test forks several processes sharing one hugetlb inode via
memfd_create(MFD_HUGETLB); each mmap()s and faults ranges and punches
holes to keep the shared resv_map fragmented.
Two modes are provided:
- default: a safe single-process functional check that exercises the buggy
line without forcing a second loop iteration; safe on any kernel.
- --trigger: the concurrent reproducer, which panics a vulnerable
CONFIG_DEBUG_LIST=y kernel and is therefore opt-in. It faults pages in,
so it needs as many free huge pages as the file is large.
Assisted-by: Claude:claude-opus-4-8
Link: https://lore.kernel.org/20260713171456.300518-3-caixiangfeng@bytedance.com
Signed-off-by: Xiangfeng Cai <caixiangfeng@bytedance.com>
Cc: Baoquan He <baoquan.he@linux.dev>
Cc: David Hildenbrand <david@kernel.org>
Cc: Muchun Song <muchun.song@linux.dev>
Cc: Oscar Salvador <osalvador@suse.de>
Cc: Shuah Khan <shuah@kernel.org>
Cc: Wei Yang <richard.weiyang@linux.alibaba.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
---
tools/testing/selftests/mm/.gitignore | 1
tools/testing/selftests/mm/Makefile | 1
tools/testing/selftests/mm/hugetlb_region_cache_race.c | 315 ++++++++++
tools/testing/selftests/mm/run_vmtests.sh | 1
4 files changed, 318 insertions(+)
--- a/tools/testing/selftests/mm/.gitignore~selftests-mm-add-hugetlb_region_cache_race-regression-test
+++ a/tools/testing/selftests/mm/.gitignore
@@ -59,6 +59,7 @@ hugetlb_madv_vs_map
mseal_test
droppable
hugetlb_dio
+hugetlb_region_cache_race
pkey_sighandler_tests_32
pkey_sighandler_tests_64
guard-regions
diff --git a/tools/testing/selftests/mm/hugetlb_region_cache_race.c a/tools/testing/selftests/mm/hugetlb_region_cache_race.c
new file mode 100644
--- /dev/null
+++ a/tools/testing/selftests/mm/hugetlb_region_cache_race.c
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Regression test for list corruption in
+ * mm/hugetlb.c:allocate_file_region_entries().
+ *
+ * Fork processes sharing a hugetlb inode via `memfd_create(MFD_HUGETLB)`
+ * and have them fault ranges and punch holes to fragment the shared
+ * `resv_map` while draining `resv->region_cache` from a separate address
+ * space.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+
+#include <linux/falloc.h>
+#include <linux/memfd.h>
+
+#include "../kselftest.h"
+#include "hugepage_settings.h"
+
+/* Tunables (overridable via argv). */
+static unsigned long file_pages = 64; /* huge pages backing the shared file */
+static unsigned int nr_procs; /* 0 => auto (2 * nr online cpus) */
+static unsigned int duration_sec = 5; /* --trigger run time */
+
+static unsigned long huge_page_size; /* bytes */
+static int memfd = -1;
+static volatile int *stop; /* shared across forked children */
+
+/*
+ * Per-process SIGBUS escape: a fault can fail (pool exhausted) and raise
+ * SIGBUS; jump back and abandon the rest of the current range instead of
+ * dying. Each child is single-threaded, so a file-scope buffer is fine.
+ */
+static sigjmp_buf sigbus_env;
+
+static void sigbus_handler(int sig)
+{
+ siglongjmp(sigbus_env, 1);
+}
+
+static void install_sigbus_handler(void)
+{
+ struct sigaction sa = {
+ .sa_handler = sigbus_handler,
+ };
+
+ sigemptyset(&sa.sa_mask);
+ sigaction(SIGBUS, &sa, NULL);
+}
+
+static long sys_memfd_create(const char *name, unsigned int flags)
+{
+ return syscall(__NR_memfd_create, name, flags);
+}
+
+/*
+ * Map [start, start+len) of the shared file and fault every page in.
+ *
+ * The mmap() drives mmap-time hugetlb_reserve_pages -> region_chg (the
+ * wide-window victim when the range is fragmented) + region_add. The
+ * subsequent stores fault pages in *without* inode_lock, driving the
+ * lock-free region_add/region_chg that drains resv->region_cache -- the
+ * concurrency the buggy loop needs from *other* processes.
+ */
+static void map_and_fault_range(unsigned long start, unsigned long len)
+{
+ unsigned long i;
+ char *addr;
+
+ if (!len)
+ return;
+ addr = mmap(NULL, len * huge_page_size, PROT_READ | PROT_WRITE,
+ MAP_SHARED, memfd, start * huge_page_size);
+ if (addr == MAP_FAILED)
+ return; /* ENOMEM under pool pressure is fine */
+
+ for (i = 0; i < len; i++) {
+ if (sigsetjmp(sigbus_env, 1) == 0)
+ *(volatile char *)(addr + i * huge_page_size) = (char)i;
+ else
+ break; /* SIGBUS: pool exhausted, stop faulting */
+ }
+ munmap(addr, len * huge_page_size);
+}
+
+/*
+ * Remove [start, start+len) huge pages from the shared file, driving
+ * hugetlbfs_punch_hole -> remove_inode_hugepages -> region_del for the pages
+ * that were faulted in above. This frees pages back to the pool (bounding
+ * residency) and re-fragments the resv_map so later region_chg calls keep
+ * needing multiple entries.
+ */
+static void punch_range(unsigned long start, unsigned long len)
+{
+ if (!len)
+ return;
+ fallocate(memfd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
+ start * huge_page_size, len * huge_page_size);
+}
+
+/*
+ * One forked worker. Each worker is simultaneously a victim (its wide
+ * mmap-time region_chg) and, for the other workers, a lock-free cache
+ * drainer (its faults) -- exactly the shared-inode map+fault churn seen in
+ * the original crash (QEMU prealloc + SPDK/Bytelight on one guest-RAM file).
+ */
+static void child_worker(unsigned int id)
+{
+ unsigned int seed = id * 2654435761u ^ (unsigned int)time(NULL) ^
+ ((unsigned int)getpid() << 1);
+
+ install_sigbus_handler();
+
+ while (!*stop) {
+ unsigned long start = rand_r(&seed) % file_pages;
+ unsigned long len = 1 + rand_r(&seed) % (file_pages - start);
+
+ /*
+ * Bias toward map+fault (victims and drainers); punch a
+ * quarter of the time to keep the map fragmented and to cap
+ * how many pages stay resident.
+ */
+ if (rand_r(&seed) % 4)
+ map_and_fault_range(start, len);
+ else
+ punch_range(start, len);
+ }
+}
+
+/*
+ * Safe, single-process functional check. It fragments the shared resv_map
+ * (reservations for a shared mapping persist on the inode after munmap) so
+ * that a subsequent whole-file mmap makes region_chg compute
+ * regions_needed > 1, forcing allocate_file_region_entries to allocate and
+ * splice several entries. This exercises the buggy line, but without
+ * concurrency it always makes exactly one loop iteration, so it cannot trip
+ * the race and is safe on any kernel. Returns 0 on success.
+ */
+static int functional_check(void)
+{
+ unsigned long i;
+ void *addr;
+
+ /* Punch everything so we start from an empty resv_map. */
+ punch_range(0, file_pages);
+
+ /* Reserve every other huge page -> ~file_pages/2 disjoint regions. */
+ for (i = 0; i < file_pages; i += 2)
+ map_and_fault_range(i, 1);
+
+ /*
+ * One mapping spanning the whole file must fill all the holes in a
+ * single region_chg -> a multi-entry region_cache refill.
+ */
+ addr = mmap(NULL, file_pages * huge_page_size, PROT_READ | PROT_WRITE,
+ MAP_SHARED, memfd, 0);
+ if (addr == MAP_FAILED) {
+ ksft_print_msg("whole-file mmap failed: %s\n", strerror(errno));
+ return -1;
+ }
+ munmap(addr, file_pages * huge_page_size);
+ punch_range(0, file_pages);
+ return 0;
+}
+
+static int run_trigger(void)
+{
+ pid_t *pids;
+ unsigned int i, started = 0;
+
+ stop = mmap(NULL, sizeof(*stop), PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+ if (stop == MAP_FAILED)
+ ksft_exit_fail_msg("mmap(stop flag): %s\n", strerror(errno));
+ *stop = 0;
+
+ pids = calloc(nr_procs, sizeof(*pids));
+ if (!pids)
+ ksft_exit_fail_msg("calloc: %s\n", strerror(errno));
+
+ ksft_print_msg("stressing %u processes for %us on %lu huge pages (%lu MiB)\n",
+ nr_procs, duration_sec, file_pages,
+ (file_pages * huge_page_size) >> 20);
+ ksft_print_msg("WARNING: a vulnerable kernel will panic here\n");
+
+ for (i = 0; i < nr_procs; i++) {
+ pid_t pid = fork();
+
+ if (pid < 0) {
+ ksft_print_msg("fork: %s\n", strerror(errno));
+ break;
+ }
+ if (pid == 0) {
+ free(pids);
+ child_worker(i);
+ _exit(0);
+ }
+ pids[started++] = pid;
+ }
+
+ if (!started) {
+ free(pids);
+ munmap((void *)stop, sizeof(*stop));
+ ksft_exit_fail_msg("no worker processes started\n");
+ }
+
+ sleep(duration_sec);
+ *stop = 1;
+
+ for (i = 0; i < started; i++)
+ waitpid(pids[i], NULL, 0);
+ free(pids);
+ munmap((void *)stop, sizeof(*stop));
+ return 0;
+}
+
+static void usage(const char *prog)
+{
+ fprintf(stderr,
+ "usage: %s [--trigger] [-s huge_pages] [-t procs] [-d seconds]\n"
+ " (default) run a safe single-process functional check\n"
+ " --trigger fork+fault concurrent reproducer (CAN PANIC A BUGGY HOST)\n"
+ " -s huge pages backing the shared file (default %lu)\n"
+ " -t worker processes for --trigger (default 2 * nr_cpus)\n"
+ " -d --trigger run time in seconds (default %u)\n",
+ prog, file_pages, duration_sec);
+}
+
+int main(int argc, char *argv[])
+{
+ bool trigger = false;
+ int i, ret;
+
+ for (i = 1; i < argc; i++) {
+ if (!strcmp(argv[i], "--trigger")) {
+ trigger = true;
+ } else if (!strcmp(argv[i], "-s") && i + 1 < argc) {
+ file_pages = strtoul(argv[++i], NULL, 0);
+ } else if (!strcmp(argv[i], "-t") && i + 1 < argc) {
+ nr_procs = strtoul(argv[++i], NULL, 0);
+ } else if (!strcmp(argv[i], "-d") && i + 1 < argc) {
+ duration_sec = strtoul(argv[++i], NULL, 0);
+ } else {
+ usage(argv[0]);
+ return KSFT_FAIL;
+ }
+ }
+
+ ksft_print_header();
+ ksft_set_plan(1);
+
+ if (file_pages < 4)
+ file_pages = 4;
+ if (!nr_procs) {
+ long cpus = sysconf(_SC_NPROCESSORS_ONLN);
+
+ nr_procs = cpus > 0 ? (unsigned int)cpus * 2 : 4;
+ }
+ if (nr_procs < 2)
+ nr_procs = 2;
+
+ huge_page_size = default_huge_page_size();
+ if (!huge_page_size)
+ ksft_exit_skip("no hugetlbfs / huge page size available\n");
+
+ /*
+ * Both modes fault pages in (functional_check touches every page it
+ * maps, and --trigger's workers fault the ranges they map), so the
+ * whole file may become resident; require that many *free* huge pages,
+ * not just reservations.
+ */
+ if (hugetlb_free_default_pages() < file_pages)
+ ksft_exit_skip("need %lu free huge pages, have %lu (set nr_hugepages)\n",
+ file_pages, hugetlb_free_default_pages());
+
+ memfd = sys_memfd_create("hugetlb_region_cache_race",
+ MFD_HUGETLB |
+ (__builtin_ctzl(huge_page_size) << MFD_HUGE_SHIFT));
+ if (memfd < 0)
+ ksft_exit_skip("memfd_create(MFD_HUGETLB): %s\n",
+ strerror(errno));
+ if (ftruncate(memfd, file_pages * huge_page_size)) {
+ close(memfd);
+ ksft_exit_fail_msg("ftruncate: %s\n", strerror(errno));
+ }
+
+ if (trigger)
+ ret = run_trigger();
+ else
+ ret = functional_check();
+
+ close(memfd);
+
+ if (ret)
+ ksft_test_result_fail("hugetlb region_cache race check\n");
+ else if (trigger)
+ ksft_test_result_pass("survived concurrent region_cache churn\n");
+ else
+ ksft_test_result_pass("multi-entry region_cache refill works\n");
+
+ ksft_finished();
+}
--- a/tools/testing/selftests/mm/Makefile~selftests-mm-add-hugetlb_region_cache_race-regression-test
+++ a/tools/testing/selftests/mm/Makefile
@@ -64,6 +64,7 @@ TEST_GEN_FILES += hugetlb-madvise
TEST_GEN_FILES += hugetlb-mmap
TEST_GEN_FILES += hugetlb-mremap
TEST_GEN_FILES += hugetlb-read-hwpoison
+TEST_GEN_FILES += hugetlb_region_cache_race
TEST_GEN_FILES += hugetlb-shm
TEST_GEN_FILES += hugetlb-soft-offline
TEST_GEN_FILES += khugepaged
--- a/tools/testing/selftests/mm/run_vmtests.sh~selftests-mm-add-hugetlb_region_cache_race-regression-test
+++ a/tools/testing/selftests/mm/run_vmtests.sh
@@ -266,6 +266,7 @@ CATEGORY="hugetlb" run_test ./hugetlb-ma
CATEGORY="hugetlb" run_test ./hugetlb_dio
CATEGORY="hugetlb" run_test ./hugetlb_fault_after_madv
CATEGORY="hugetlb" run_test ./hugetlb_madv_vs_map
+CATEGORY="hugetlb" run_test ./hugetlb_region_cache_race
if test_selected "hugetlb"; then
echo "NOTE: These hugetlb tests provide minimal coverage. Use" | tap_prefix
_
Patches currently in -mm which might be from caixiangfeng@bytedance.com are
mm-hugetlb-fix-list-corruption-in-allocate_file_region_entries.patch
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-07-15 4:04 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-15 4:04 [withdrawn] selftests-mm-add-hugetlb_region_cache_race-regression-test.patch removed from -mm tree Andrew Morton
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.