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
Signed-off-by: Xiangfeng Cai <[email protected]>
---
 tools/testing/selftests/mm/.gitignore         |   1 +
 tools/testing/selftests/mm/Makefile           |   1 +
 .../selftests/mm/hugetlb_region_cache_race.c  | 315 ++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh     |   1 +
 4 files changed, 318 insertions(+)
 create mode 100644 tools/testing/selftests/mm/hugetlb_region_cache_race.c

diff --git a/tools/testing/selftests/mm/.gitignore 
b/tools/testing/selftests/mm/.gitignore
index 9ccd9e1447e6..7db8657960d9 100644
--- a/tools/testing/selftests/mm/.gitignore
+++ b/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/Makefile 
b/tools/testing/selftests/mm/Makefile
index e6df968f0971..421a0b7fdddf 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -67,6 +67,7 @@ TEST_GEN_FILES += hugetlb-read-hwpoison
 TEST_GEN_FILES += hugetlb-shm
 TEST_GEN_FILES += hugetlb-soft-offline
 TEST_GEN_FILES += hugetlb-vmemmap
+TEST_GEN_FILES += hugetlb_region_cache_race
 TEST_GEN_FILES += khugepaged
 TEST_GEN_FILES += madv_populate
 TEST_GEN_FILES += map_fixed_noreplace
diff --git a/tools/testing/selftests/mm/hugetlb_region_cache_race.c 
b/tools/testing/selftests/mm/hugetlb_region_cache_race.c
new file mode 100644
index 000000000000..6e3f753640be
--- /dev/null
+++ b/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();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh 
b/tools/testing/selftests/mm/run_vmtests.sh
index 8c296dedf047..0cb857056915 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -267,6 +267,7 @@ CATEGORY="hugetlb" run_test ./hugetlb-madvise
 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
-- 
2.55.0.122.gf85a7e6620

Reply via email to