Fork a process that maps shmem THPs with PMDs, in a VMA whose page
tables fork copies: a private mapping with an anon_vma, and a shared
one with a guard region. The child must find the PMDs before it
touches the mapping, and its exit must leave the parent's mapping and
the system-wide ShmemPmdMapped counter as they were.

With folio_dup_file_rmap_pmd() adding PTE mappings, the child's exit
takes the parent's share off ShmemPmdMapped while the parent still maps
the folios. Closing the file then reports "Bad page cache", or hits a
VM_BUG_ON with CONFIG_DEBUG_VM. A kernel that leaves file THPs to a
refault in the child skips the test.

The test mounts a tmpfs with huge=always over /tmp in a private mount
namespace, so it leaves the THP settings and the host's mounts and
files alone.

Signed-off-by: Konstantin Khorenko <[email protected]>
---
 MAINTAINERS                                |   1 +
 tools/testing/selftests/mm/Makefile        |   1 +
 tools/testing/selftests/mm/fork_file_thp.c | 227 +++++++++++++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh  |   1 +
 4 files changed, 230 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 7b5cab72d851b..1b5124e7e2bf9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17481,6 +17481,7 @@ F:      include/trace/events/huge_memory.h
 F:     mm/huge_memory.c
 F:     mm/khugepaged.c
 F:     mm/mm_slot.h
+F:     tools/testing/selftests/mm/fork_file_thp.c
 F:     tools/testing/selftests/mm/khugepaged.c
 F:     tools/testing/selftests/mm/split_huge_page_test.c
 F:     tools/testing/selftests/mm/transhuge-stress.c
diff --git a/tools/testing/selftests/mm/Makefile 
b/tools/testing/selftests/mm/Makefile
index 2d5366196e309..33ec3b7f4c048 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 += fork_file_thp
 
 ifneq ($(ARCH),arm64)
 TEST_GEN_FILES += soft-dirty
diff --git a/tools/testing/selftests/mm/fork_file_thp.c 
b/tools/testing/selftests/mm/fork_file_thp.c
new file mode 100644
index 0000000000000..76c8d1a0c9792
--- /dev/null
+++ b/tools/testing/selftests/mm/fork_file_thp.c
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * fork() of a process that maps shmem THPs with PMDs, in a VMA whose page
+ * tables fork copies: a private mapping with an anon_vma, and a shared
+ * mapping with a guard region.
+ *
+ * The child must get the PMDs without faulting, and its exit must leave the
+ * parent's mapping and the system-wide ShmemPmdMapped counter as they were.
+ * Kernels that leave file THPs to a refault in the child skip the test.
+ */
+#define _GNU_SOURCE
+#include <asm-generic/mman.h> /* Force the import of the tools version. */
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sched.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/wait.h>
+
+#include "kselftest.h"
+#include "vm_util.h"
+
+#define NR_PMDS                4
+#define MAX_PMDSIZE    (32UL << 20)
+/* ShmemPmdMapped is system-wide: retry if something else changed it. */
+#define ATTEMPTS       3
+
+static size_t pagesize, pmdsize;
+
+static long meminfo_kb(const char *key)
+{
+       char line[256];
+       long kb = -1;
+       FILE *f = fopen("/proc/meminfo", "r");
+
+       if (!f)
+               return -1;
+       while (fgets(line, sizeof(line), f))
+               if (!strncmp(line, key, strlen(key))) {
+                       kb = strtol(line + strlen(key), NULL, 10);
+                       break;
+               }
+       fclose(f);
+       return kb;
+}
+
+/* Skips the last page, which may be a guard page, and @skip. */
+static bool data_ok(const char *mem, size_t size, size_t skip)
+{
+       size_t off;
+
+       for (off = 0; off < size - pagesize; off += pagesize)
+               if (off != skip && mem[off] != (char)(off / pmdsize + 1))
+                       return false;
+       return true;
+}
+
+/* Child exit codes, apart from what kselftest helpers exit with. */
+enum result { PASS, NO_PMDS = 10, FAIL_DATA, FAIL_COUNTER, FAIL_PARENT,
+             FAIL_KILLED, COUNTER_OFF, CHANGED };
+
+static enum result fork_once(char *mem, size_t size, bool shared,
+                            long *before, long *after)
+{
+       /* The parent alone maps this much with PMDs. */
+       long own = (NR_PMDS - 1) * (pmdsize >> 10);
+       int status;
+       pid_t pid;
+
+       *before = *after = meminfo_kb("ShmemPmdMapped:");
+       if (*before < own)
+               return COUNTER_OFF;
+       fflush(stdout);
+       pid = fork();
+       if (pid < 0)
+               ksft_exit_fail_perror("fork");
+       if (!pid) {
+               /* Look before touching anything: the PMDs must be there. */
+               if (!check_huge_shmem(mem, size - pmdsize, NR_PMDS - 1, 
pmdsize))
+                       _exit(NO_PMDS);
+               /* A shared retry sees the previous child's write. */
+               if (!data_ok(mem, size, pmdsize))
+                       _exit(FAIL_DATA);
+               mem[pmdsize] = 'c';
+               _exit(PASS);
+       }
+       if (waitpid(pid, &status, 0) != pid)
+               ksft_exit_fail_perror("waitpid");
+       *after = meminfo_kb("ShmemPmdMapped:");
+
+       if (!WIFEXITED(status))
+               return FAIL_KILLED;
+       if (WEXITSTATUS(status) != PASS)
+               return WEXITSTATUS(status);
+       /* The child's exit took the parent's share off the counter. */
+       if (*after < own || *before - *after >= own)
+               return FAIL_COUNTER;
+       if (*after != *before)
+               return CHANGED;
+       if (mem[pmdsize] != (shared ? 'c' : 2) || !data_ok(mem, size, pmdsize) 
||
+           !check_huge_shmem(mem, size - pmdsize, NR_PMDS - 1, pmdsize))
+               return FAIL_PARENT;
+       return PASS;
+}
+
+static void test_fork(bool shared)
+{
+       size_t size = NR_PMDS * pmdsize, last = size - pagesize, off;
+       const char *desc = shared ? "shared mapping with a guard region" :
+                                   "private mapping with an anon_vma";
+       char *buf, *area, *mem;
+       enum result res = FAIL_PARENT;
+       long before, after;
+       int fd, i;
+
+       fd = open("/tmp/fork_file_thp", O_RDWR | O_CREAT | O_TRUNC, 0600);
+       if (fd < 0)
+               ksft_exit_fail_perror("open");
+       unlink("/tmp/fork_file_thp");
+       buf = malloc(pmdsize);
+       if (!buf)
+               ksft_exit_fail_msg("malloc failed\n");
+       for (off = 0; off < size; off += pmdsize) {
+               memset(buf, off / pmdsize + 1, pmdsize);
+               if (pwrite(fd, buf, pmdsize, off) != (ssize_t)pmdsize)
+                       ksft_exit_fail_perror("pwrite");
+       }
+       free(buf);
+
+       area = mmap(NULL, size + pmdsize, PROT_NONE,
+                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+       if (area == MAP_FAILED)
+               ksft_exit_fail_perror("mmap");
+       mem = (char *)(((unsigned long)area + pmdsize - 1) & ~(pmdsize - 1));
+       mem = mmap(mem, size, PROT_READ | PROT_WRITE,
+                  (shared ? MAP_SHARED : MAP_PRIVATE) | MAP_FIXED, fd, 0);
+       if (mem == MAP_FAILED)
+               ksft_exit_fail_perror("mmap");
+       for (off = 0; off < size; off += pmdsize)
+               FORCE_READ(mem[off]);
+
+       /* Make fork copy the page tables; the last PMD gets zapped. */
+       if (shared) {
+               if (madvise(mem + last, pagesize, MADV_GUARD_INSTALL)) {
+                       ksft_test_result_skip("%s: MADV_GUARD_INSTALL: %s\n",
+                                             desc, strerror(errno));
+                       goto out;
+               }
+       } else {
+               mem[last] = 'p';
+       }
+
+       if (!check_huge_shmem(mem, size - pmdsize, NR_PMDS - 1, pmdsize)) {
+               ksft_test_result_skip("%s: shmem is not mapped with PMDs\n",
+                                     desc);
+               goto out;
+       }
+
+       for (i = 0; i < ATTEMPTS; i++) {
+               res = fork_once(mem, size, shared, &before, &after);
+               if (res != CHANGED)
+                       break;
+       }
+
+       switch (res) {
+       case PASS:
+               ksft_test_result_pass("%s\n", desc);
+               break;
+       case NO_PMDS:
+               ksft_test_result_skip("%s: fork does not copy file THPs\n", 
desc);
+               break;
+       case COUNTER_OFF:
+               ksft_test_result_skip("%s: ShmemPmdMapped %ld kB is below what 
this process maps\n",
+                                     desc, before);
+               break;
+       case CHANGED:
+               ksft_test_result_skip("%s: ShmemPmdMapped keeps changing\n", 
desc);
+               break;
+       case FAIL_DATA:
+               ksft_test_result_fail("%s: child sees wrong data\n", desc);
+               break;
+       case FAIL_COUNTER:
+               ksft_test_result_fail("%s: ShmemPmdMapped %ld kB before fork, 
%ld kB after\n",
+                                     desc, before, after);
+               break;
+       case FAIL_PARENT:
+               ksft_test_result_fail("%s: parent's mapping changed\n", desc);
+               break;
+       case FAIL_KILLED:
+               ksft_test_result_fail("%s: child killed by a signal\n", desc);
+               break;
+       default:
+               ksft_test_result_fail("%s: child error %d\n", desc, res);
+               break;
+       }
+out:
+       munmap(area, size + pmdsize);
+       close(fd);
+}
+
+int main(void)
+{
+       ksft_print_header();
+       pagesize = getpagesize();
+       pmdsize = read_pmd_pagesize();
+       if (!pmdsize)
+               ksft_exit_skip("THP is not supported\n");
+       if (pmdsize > MAX_PMDSIZE)
+               ksft_exit_skip("PMD size %zu MiB is too large\n", pmdsize >> 
20);
+       if (geteuid())
+               ksft_exit_skip("needs root to mount tmpfs\n");
+       /* The tmpfs over /tmp lives and dies with a private mount namespace. */
+       if (unshare(CLONE_NEWNS) || mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, 
NULL))
+               ksft_exit_skip("private mount namespace: %s\n", 
strerror(errno));
+       if (mount("tmpfs", "/tmp", "tmpfs", 0, "huge=always"))
+               ksft_exit_skip("tmpfs with huge=always: %s\n", strerror(errno));
+
+       ksft_set_plan(2);
+       test_fork(false);
+       test_fork(true);
+       ksft_finished();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh 
b/tools/testing/selftests/mm/run_vmtests.sh
index d09f9f6a384ee..454a8b75a3bf3 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -445,6 +445,7 @@ fi
 CATEGORY="thp" run_test ./transhuge-stress -d 20
 
 CATEGORY="thp" run_test ./folio_split_race_test
+CATEGORY="thp" run_test ./fork_file_thp
 
 CATEGORY="migration" run_test ./migration
 

-- 
2.43.0


Reply via email to