* [PATCH v5 0/4] mm/huge_memory: copy PMDs at fork like the PTE path does
@ 2026-11-04 20:40 Konstantin Khorenko
2026-11-04 20:40 ` [PATCH v5 1/4] mm/rmap: duplicate a PMD mapping as one in folio_dup_file_rmap_pmd() Konstantin Khorenko
` (3 more replies)
0 siblings, 4 replies; 9+ messages in thread
From: Konstantin Khorenko @ 2026-11-04 20:40 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand (Arm),
Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, Lance Yang, Usama Arif, Kiryl Shutsemau,
Peter Xu
Cc: Alex Williamson, kvm, Loïc Molinari, Lorenzo Stoakes,
Shuah Khan, linux-kselftest, dri-devel, Madhavan Srinivasan,
Ritesh Harjani (IBM),
linuxppc-dev, linux-mm, linux-kernel, Konstantin Khorenko,
Jason Gunthorpe, stable, Sashiko
copy_huge_pmd() decides what to copy at fork by its own rules, apart
from copy_present_ptes(): a lockless check for special PMDs, and a skip
of every file THP. Following David's review of v3, this series makes it
decide the way the PTE path does, under the page table lock, with
vm_normal_page_pmd(). That fixes the two bugs v1-v3 were about. The
patches:
1. folio_dup_file_rmap_pmd() duplicated a PMD mapping as HPAGE_PMD_NR
PTE mappings. It had no caller until patch 2.
2. File THPs are copied at fork instead of being refaulted by the
child, as their PTEs would be.
3. The special-PMD read moves under the lock. That closes the race with
a driver zap (vfio-pci, panthor) and the missing page table deposit
on ppc64 hash that Sashiko found in v1.
4. A selftest for 1 and 2.
Stable
======
The race (since v6.12) and the ppc64 hash NULL deref (since v7.1) are
fixed in patch 3, which only works on top of the copy rework in patch
2, and patch 2 is the first caller of the helper patch 1 fixes. So
patches 1-3 carry Cc: stable, as David suggested on v4. They will not
apply to 6.12 as they are, I will do the backports.
A related refcount bug in copy_huge_pud() for device DAX is fixed in
a separate series, as it does not depend on this one:
https://patch.msgid.link/20261021-vstor-150143-copy_huge_pud-folio-refcount-v1-0-8bd88551f9be@virtuozzo.com
Testing
=======
v5 on v7.4-rc1, x86_64 with 4 NUMA nodes, the series with
CONFIG_DEBUG_VM, KASAN and lockdep:
- fork_file_thp (patch 4) skips on v7.4-rc1, where fork leaves file
THPs to a refault, and fails in both cases without patch 1, where
ShmemPmdMapped drops when the child exits and the kernel reports
"Bad page cache";
- with the series it passes, also under migrate_pages() of the parent
and under reclaim in a small memcg, where some runs skip; with other
processes churning PMD-mapped shmem it passes or skips, never fails;
- cow, guard-regions, uffd-unit-tests, uffd-wp-mremap,
split_huge_page_test, mremap_test, soft-dirty, pagemap_ioctl,
migration, madv_populate, transhuge-stress and khugepaged pass as on
v7.4-rc1.
The runs below are from v4, on v7.3-rc7 in a KVM guest with 4 vCPUs,
x86_64. Patches 1-3 changed since only by the device private CoW check.
Race with a driver zap. The guest's QXL device is bound to vfio-pci in
no-IOMMU mode, BAR0 of 64 MB mapped with PMDs. The reproducer below
forks while a second thread clears PCI_COMMAND_MEMORY through the vfio
config region after a random delay of up to 1 ms; the child waits for
the write to finish and reads one byte of each 2 MB of the BAR. A child
that reads anything without SIGBUS kept a stale mapping. I have not hit
it without help, so a test-only debugfs knob adds an mdelay(2) between
copy_pmd_range()'s lockless pmd_is_huge() and the PMD locks:
+-----------------------+-------+-------+----------------+
| kernel | delay | forks | stale children |
+-----------------------+-------+-------+----------------+
| 7.3-rc7 | none | 20900 | 0 |
| 7.3-rc7 | 2 ms | 150 | 128 |
| 7.3-rc7 + this series | none | 3000 | 0 |
| 7.3-rc7 + this series | 2 ms | 400 | 0 |
+-----------------------+-------+-------+----------------+
ppc64 hash is not tested on hardware. On x86 a test-only hack makes
arch_needs_pgtable_deposit() true and mremap move the table, as on
ppc64 hash. Without the series the first child of the reproducer oopses
at exit in pgtable_trans_huge_withdraw(); with it the reproducer and
the mm selftests pass.
Cost
====
fork() now copies file THPs instead of leaving them to a refault, which
costs fork() a reference, an rmap update and a counter per huge PMD and
saves the child a fault per huge PMD it touches. fork() of a process
with 4 GB of shmem THPs mapped MAP_PRIVATE and an anon_vma on the VMA,
medians of 2000 forks:
+-----------------------+------------+-------------------------+
| kernel | fork() | fork(), child reads all |
+-----------------------+------------+-------------------------+
| 7.3-rc7 | 86-129 us | 2380-2620 us |
| 7.3-rc7 + this series | 230-355 us | 820-930 us |
+-----------------------+------------+-------------------------+
Notes
=====
- drm_gem_shmem and mshv_vtl map special PMDs with
vmf_insert_pfn_pmd() as well.
- copy_huge_pud() still differs from the PMD tail in that it does not
mark a shared PUD clean in the child or handle uffd bits.
Reproducer. It writes the BAR as a control and reads it with the
memory disabled, which is harmless for QXL VRAM in a guest; do not
point it at a BAR that holds device registers. Build with
gcc -O2 -pthread -o fork_revoke fork_revoke.c
run as root with the device bound to vfio-pci; arguments: BDF,
iterations, BAR index, max revoke delay in microseconds, e.g.
./fork_revoke 0000:00:02.0 100 0 3000
// SPDX-License-Identifier: GPL-2.0
/*
* Fork while vfio-pci revokes a BAR mapped with huge PMDs.
*
* The parent maps a BAR of a vfio-pci device and faults it in. A second
* thread disables the device memory through the command register, which
* makes vfio-pci zap all BAR mappings, while the main thread forks. Once
* the memory is disabled, every access to the BAR must raise SIGBUS. A
* child that can still read a BAR chunk kept a mapping the zap removed.
*
* usage: fork_revoke <domain:bus:dev.fn> [iterations [bar [max_delay_us]]]
*
* The revoke starts after a random delay of up to max_delay_us, so that it
* lands at different points of fork.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <sched.h>
#include <setjmp.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <linux/pci_regs.h>
#include <linux/vfio.h>
#define SZ_2M (2UL << 20)
static int dev;
static off_t cfg_off;
static int go_pipe[2], done_pipe[2];
static volatile uint32_t *shared; /* [0]: iteration whose revoke finished */
static unsigned long iter;
static unsigned int max_delay_us;
static sigjmp_buf jb;
static void die(const char *what)
{
fprintf(stderr, "%s: %s\n", what, strerror(errno));
exit(2);
}
static int device_fd(const char *bdf)
{
char path[PATH_MAX], link[PATH_MAX], *group_id;
int container, group, noiommu;
ssize_t len;
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/iommu_group", bdf);
len = readlink(path, link, sizeof(link) - 1);
if (len < 0)
die("iommu_group");
link[len] = 0;
group_id = strrchr(link, '/') + 1;
snprintf(path, sizeof(path), "/dev/vfio/%s", group_id);
noiommu = access(path, F_OK);
if (noiommu)
snprintf(path, sizeof(path), "/dev/vfio/noiommu-%s", group_id);
container = open("/dev/vfio/vfio", O_RDWR);
group = open(path, O_RDWR);
if (container < 0 || group < 0)
die(path);
if (ioctl(group, VFIO_GROUP_SET_CONTAINER, &container) ||
ioctl(container, VFIO_SET_IOMMU,
noiommu ? VFIO_NOIOMMU_IOMMU : VFIO_TYPE1v2_IOMMU))
die("vfio container");
return ioctl(group, VFIO_GROUP_GET_DEVICE_FD, bdf);
}
static void set_memory(int on)
{
uint16_t cmd;
if (pread(dev, &cmd, 2, cfg_off + PCI_COMMAND) != 2)
die("read PCI_COMMAND");
cmd = on ? cmd | PCI_COMMAND_MEMORY : cmd & ~PCI_COMMAND_MEMORY;
if (pwrite(dev, &cmd, 2, cfg_off + PCI_COMMAND) != 2)
die("write PCI_COMMAND");
}
static void *revoker(void *arg)
{
unsigned long i;
while (read(go_pipe[0], &i, sizeof(i)) == sizeof(i)) {
if (max_delay_us)
usleep(random() % max_delay_us);
set_memory(0);
shared[0] = i;
if (write(done_pipe[1], &i, sizeof(i)) != sizeof(i))
die("done pipe");
}
return NULL;
}
static void sigbus(int sig)
{
siglongjmp(jb, 1);
}
/* Number of 2MB chunks of the BAR that can be read without SIGBUS. */
static unsigned long readable(volatile uint8_t *bar, size_t size)
{
volatile unsigned long n = 0;
volatile size_t off;
for (off = 0; off < size; off += SZ_2M) {
if (sigsetjmp(jb, 1))
continue;
(void)bar[off];
n++;
}
return n;
}
int main(int argc, char **argv)
{
unsigned long iterations = argc > 2 ? strtoul(argv[2], NULL, 0) : 1000;
unsigned int index = argc > 3 ? atoi(argv[3]) : 0;
struct vfio_region_info cfg = { .argsz = sizeof(cfg),
.index = VFIO_PCI_CONFIG_REGION_INDEX };
struct vfio_region_info bar = { .argsz = sizeof(bar), .index = index };
unsigned long stale_children = 0, stale_chunks = 0, n;
volatile uint8_t *map;
pthread_t thread;
size_t size, off;
uint8_t *resv;
int status;
pid_t pid;
if (argc < 2) {
fprintf(stderr, "usage: %s <bdf> [iterations [bar [max_delay_us]]]\n",
argv[0]);
return 2;
}
max_delay_us = argc > 4 ? atoi(argv[4]) : 0;
signal(SIGBUS, sigbus);
dev = device_fd(argv[1]);
if (dev < 0)
die("device fd");
if (ioctl(dev, VFIO_DEVICE_GET_REGION_INFO, &cfg) ||
ioctl(dev, VFIO_DEVICE_GET_REGION_INFO, &bar))
die("region info");
if (!(bar.flags & VFIO_REGION_INFO_FLAG_MMAP) || bar.size < SZ_2M) {
fprintf(stderr, "BAR %u: no mmap or smaller than 2MB\n", index);
return 2;
}
cfg_off = cfg.offset;
size = bar.size & ~(SZ_2M - 1);
/* A 2MB aligned address lets vfio-pci map the BAR with huge PMDs. */
resv = mmap(NULL, size + SZ_2M, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
if (resv == MAP_FAILED)
die("reserve");
resv = (uint8_t *)(((uintptr_t)resv + SZ_2M - 1) & ~(SZ_2M - 1));
map = mmap(resv, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED,
dev, bar.offset);
if (map == MAP_FAILED)
die("mmap BAR");
shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (shared == MAP_FAILED)
die("shared page");
/* Control: without a revoke the child reads what the parent wrote. */
set_memory(1);
for (off = 0; off < size; off += SZ_2M)
map[off] = (uint8_t)(off / SZ_2M) ^ 0x5a;
pid = fork();
if (!pid) {
for (off = 0; off < size; off += SZ_2M)
if (map[off] != ((uint8_t)(off / SZ_2M) ^ 0x5a))
_exit(1);
_exit(0);
}
if (pid < 0 || waitpid(pid, &status, 0) != pid ||
!WIFEXITED(status) || WEXITSTATUS(status)) {
fprintf(stderr, "control: child does not see the parent's BAR\n");
return 2;
}
printf("BAR %u: %zu MB, %lu iterations, revoke delay < %u us\n",
index, size >> 20, iterations, max_delay_us);
/* Premise: with the memory disabled the BAR cannot be read. */
set_memory(0);
n = readable(map, size);
set_memory(1);
if (n) {
fprintf(stderr, "memory disabled, parent still reads %lu chunks\n", n);
return 2;
}
if (pipe(go_pipe) || pipe(done_pipe))
die("pipe");
if (pthread_create(&thread, NULL, revoker, NULL))
die("thread");
for (iter = 1; iter <= iterations; iter++) {
set_memory(1);
for (off = 0; off < size; off += SZ_2M)
(void)map[off];
if (write(go_pipe[1], &iter, sizeof(iter)) != sizeof(iter))
die("go pipe");
pid = fork();
if (pid < 0)
die("fork");
if (!pid) {
while (shared[0] != iter)
sched_yield();
n = readable(map, size);
_exit(n > 255 ? 255 : n);
}
if (read(done_pipe[0], &n, sizeof(n)) != sizeof(n))
die("done pipe");
if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
die("child");
if (WEXITSTATUS(status)) {
stale_children++;
stale_chunks += WEXITSTATUS(status);
}
}
set_memory(1);
printf("children with a stale BAR mapping: %lu of %lu (%lu 2MB chunks)\n",
stale_children, iterations, stale_chunks);
return stale_children ? 1 : 0;
}
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
Changes in v5:
- Rebase onto v7.4-rc1.
- Patches 1-3: Cc: stable (David, Lorenzo).
- Patch 1: drop the list of consequences, the helper had no caller
(Zi Yan).
- Patch 2: the device private write path gets the CoW check of
copy_nonpresent_pte() (David). Drop fuse DAX as the example of
VM_MIXEDMAP, it no longer sets the flag. Say on the Cc: stable line
that the next patch needs it.
- Patch 4: up to three attempts, each on a fresh file. Fail only if
every attempt sees ShmemPmdMapped drop by exactly the parent's share,
pass on the first clean one, skip if none is clean: other processes
change the counter and reclaim or migration take PMDs from the parent
(Kiryl). Keep every mapping until exit: after the bug, an unmap
drops the counter once more, which hid the bug from the second case.
Use check_huge_file(), check_huge_shmem() is gone.
- Cover: v5 test results, box tables.
- Link to v4: https://patch.msgid.link/20261021-b4-prep-vstor-149847-copy_huge_pmd-recheck-special-pmd-v4-0-66f00b7a564c@virtuozzo.com
Changes in v4:
- Rework after David's review: copy_huge_pmd() decides under the PTL
with vm_normal_page_pmd(), like copy_present_ptes(), and copies file
THPs (patches 2, 3). The pmd_none() recheck and the separate deposit
patch are gone.
- New patch 1: fix folio_dup_file_rmap_pmd(), whose first caller is
patch 2.
- New selftest, patch 4.
- Move Assisted-by from the patches to the cover letter, after commit
1e035297ffdad ("docs: remove Assisted-by references").
- Link to v3: https://patch.msgid.link/20261014-b4-prep-vstor-149847-copy_huge_pmd-recheck-special-pmd-v3-0-85eaec8417f6@virtuozzo.com
Changes in v3:
- Patch 1: recheck the PMD with pmd_none(), as only a zap can change
it and a zap leaves it none (Lance Yang).
- Link to v2: https://patch.msgid.link/20261014-b4-prep-vstor-149847-copy_huge_pmd-recheck-special-pmd-v2-0-e1548488fa68@virtuozzo.com
Changes in v2:
- Add patch 2, the page table deposit for ppc64 hash, reported by
Sashiko.
- Patch 1: name panthor instead of drm_gem_shmem as the driver whose
stale child mapping reaches freed pages: drm_gem_shmem keeps the
pages of a purged object until the last mapping goes away.
- Link to v1: https://patch.msgid.link/20261014-b4-prep-vstor-149847-copy_huge_pmd-recheck-special-pmd-v1-1-d225c60bc316@virtuozzo.com
---
Konstantin Khorenko (4):
mm/rmap: duplicate a PMD mapping as one in folio_dup_file_rmap_pmd()
mm/huge_memory: copy file THPs at fork like copy_present_ptes() does
mm/huge_memory: read a special PMD under the lock in copy_huge_pmd()
selftests/mm: test fork of PMD-mapped shmem THPs
MAINTAINERS | 1 +
include/linux/rmap.h | 2 +-
mm/huge_memory.c | 85 +++++-----
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/fork_file_thp.c | 256 +++++++++++++++++++++++++++++
tools/testing/selftests/mm/run_vmtests.sh | 1 +
6 files changed, 301 insertions(+), 45 deletions(-)
---
base-commit: 05441d697f7f9a927264f4058cbbc6a03dfcac9d
change-id: 20261014-b4-prep-vstor-149847-copy_huge_pmd-recheck-special-pmd-bdf7efb612c2
Best regards,
--
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH v5 1/4] mm/rmap: duplicate a PMD mapping as one in folio_dup_file_rmap_pmd()
2026-11-04 20:40 [PATCH v5 0/4] mm/huge_memory: copy PMDs at fork like the PTE path does Konstantin Khorenko
@ 2026-11-04 20:40 ` Konstantin Khorenko
2026-11-04 22:57 ` Jose A. Perez de Azpillaga
2026-11-04 20:40 ` [PATCH v5 2/4] mm/huge_memory: copy file THPs at fork like copy_present_ptes() does Konstantin Khorenko
` (2 subsequent siblings)
3 siblings, 1 reply; 9+ messages in thread
From: Konstantin Khorenko @ 2026-11-04 20:40 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand (Arm),
Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, Lance Yang, Usama Arif, Kiryl Shutsemau,
Peter Xu
Cc: Alex Williamson, kvm, Loïc Molinari, Lorenzo Stoakes,
Shuah Khan, linux-kselftest, dri-devel, Madhavan Srinivasan,
Ritesh Harjani (IBM),
linuxppc-dev, linux-mm, linux-kernel, Konstantin Khorenko,
Jason Gunthorpe, stable
folio_dup_file_rmap_pmd() passes PGTABLE_LEVEL_PTE to
__folio_dup_file_rmap() for HPAGE_PMD_NR pages, so it adds HPAGE_PMD_NR
PTE mappings to the folio instead of one PMD mapping.
The helper has no caller yet, the next patch adds the first one. Pass
PGTABLE_LEVEL_PMD, as folio_try_dup_anon_rmap_pmd() does.
Fixes: d8ef5e311d7b ("mm/rmap: convert page_dup_file_rmap() to folio_dup_file_rmap_[pte|ptes|pmd]()")
Cc: stable@vger.kernel.org
Reviewed-by: Zi Yan <ziy@nvidia.com>
Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
Acked-by: David Hildenbrand (Arm) <david@kernel.org>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
include/linux/rmap.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/include/linux/rmap.h b/include/linux/rmap.h
index 74cca0e3c7264..87c88a9811933 100644
--- a/include/linux/rmap.h
+++ b/include/linux/rmap.h
@@ -558,7 +558,7 @@ static inline void folio_dup_file_rmap_pmd(struct folio *folio,
struct page *page, struct vm_area_struct *dst_vma)
{
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
- __folio_dup_file_rmap(folio, page, HPAGE_PMD_NR, dst_vma, PGTABLE_LEVEL_PTE);
+ __folio_dup_file_rmap(folio, page, HPAGE_PMD_NR, dst_vma, PGTABLE_LEVEL_PMD);
#else
WARN_ON_ONCE(true);
#endif
--
2.43.0
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH v5 1/4] mm/rmap: duplicate a PMD mapping as one in folio_dup_file_rmap_pmd()
2026-11-04 20:40 ` [PATCH v5 1/4] mm/rmap: duplicate a PMD mapping as one in folio_dup_file_rmap_pmd() Konstantin Khorenko
@ 2026-11-04 22:57 ` Jose A. Perez de Azpillaga
0 siblings, 0 replies; 9+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-11-04 22:57 UTC (permalink / raw)
To: Konstantin Khorenko, Andrew Morton, David Hildenbrand (Arm),
Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, Lance Yang, Usama Arif, Kiryl Shutsemau,
Peter Xu
Cc: Alex Williamson, kvm, Loïc Molinari, Lorenzo Stoakes,
Shuah Khan, linux-kselftest, dri-devel, Madhavan Srinivasan,
Ritesh Harjani (IBM),
linuxppc-dev, linux-mm, linux-kernel, Jason Gunthorpe, stable
On 11/4/26 9:40 PM, Konstantin Khorenko wrote:
> folio_dup_file_rmap_pmd() passes PGTABLE_LEVEL_PTE to
> __folio_dup_file_rmap() for HPAGE_PMD_NR pages, so it adds HPAGE_PMD_NR
> PTE mappings to the folio instead of one PMD mapping.
>
> The helper has no caller yet, the next patch adds the first one. Pass
> PGTABLE_LEVEL_PMD, as folio_try_dup_anon_rmap_pmd() does.
>
> Fixes: d8ef5e311d7b ("mm/rmap: convert page_dup_file_rmap() to folio_dup_file_rmap_[pte|ptes|pmd]()")
> Cc: stable@vger.kernel.org
> Reviewed-by: Zi Yan <ziy@nvidia.com>
> Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> Acked-by: David Hildenbrand (Arm) <david@kernel.org>
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
LGTM:
Reviewed-by: Jose A. Perez de Azpillaga <azpijr@gmail.com>
--
cheers,
jose a. p-a
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v5 2/4] mm/huge_memory: copy file THPs at fork like copy_present_ptes() does
2026-11-04 20:40 [PATCH v5 0/4] mm/huge_memory: copy PMDs at fork like the PTE path does Konstantin Khorenko
2026-11-04 20:40 ` [PATCH v5 1/4] mm/rmap: duplicate a PMD mapping as one in folio_dup_file_rmap_pmd() Konstantin Khorenko
@ 2026-11-04 20:40 ` Konstantin Khorenko
2026-11-04 23:05 ` Jose A. Perez de Azpillaga
2026-11-04 20:40 ` [PATCH v5 3/4] mm/huge_memory: read a special PMD under the lock in copy_huge_pmd() Konstantin Khorenko
2026-11-04 20:40 ` [PATCH v5 4/4] selftests/mm: test fork of PMD-mapped shmem THPs Konstantin Khorenko
3 siblings, 1 reply; 9+ messages in thread
From: Konstantin Khorenko @ 2026-11-04 20:40 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand (Arm),
Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, Lance Yang, Usama Arif, Kiryl Shutsemau,
Peter Xu
Cc: Alex Williamson, kvm, Loïc Molinari, Lorenzo Stoakes,
Shuah Khan, linux-kselftest, dri-devel, Madhavan Srinivasan,
Ritesh Harjani (IBM),
linuxppc-dev, linux-mm, linux-kernel, Konstantin Khorenko,
Jason Gunthorpe, stable
This fixes no bug: it makes fork copy a PMD the way copy_present_ptes()
copies a PTE. Until now copy_huge_pmd() skipped every folio PMD of a
non-anonymous VMA and left the child to fault them in again, while the
PTEs of the same VMA are copied.
Treat file folios as the PTE path does: take a reference, duplicate the
rmap and account them. fork then pays a reference and an rmap update
per PMD, and saves the child a fault per PMD it touches. The next patch
sends PFN mappings through the same tail.
That covers the VMAs vma_needs_copy() sends here:
- private file mappings with an anon_vma;
- VM_COPY_ON_FORK ones: uffd-wp and uffd-rwp, VM_MIXEDMAP, and
VM_MAYBE_GUARD, which shared shmem, file and DAX mappings get from a
guard region.
The DAX huge zero PMD is copied too, with a reference taken by
mm_get_huge_zero_folio() as for anonymous VMAs. A migration entry of a
file folio is copied and accounted like copy_nonpresent_pte() does, and
made read-only in both processes only in a CoW mapping. A device
private entry gets the same CoW check, as in copy_nonpresent_pte().
The copy then gets the same tail as __copy_present_ptes(): write
protect both sides in a CoW mapping, mark the child's copy clean in a
shared one, and drop the uffd-wp bit where the child is not armed. An
anonymous THP is now write protected only when its PMD is writable, as
a PTE is.
A page table is deposited under the copy where the fault paths deposit
one: in anonymous VMAs, and wherever the architecture needs one.
Cc: stable@vger.kernel.org # needed by the next patch
Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
mm/huge_memory.c | 63 +++++++++++++++++++++++++++++++++++---------------------
1 file changed, 39 insertions(+), 24 deletions(-)
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index bd44d58e86845..e00632edb3856 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -1897,12 +1897,13 @@ static void copy_huge_non_present_pmd(
pmd_t pmd, pgtable_t pgtable)
{
softleaf_t entry = softleaf_from_pmd(pmd);
- struct folio *src_folio;
+ struct folio *src_folio = softleaf_to_folio(entry);
VM_WARN_ON_ONCE(!pmd_is_valid_softleaf(pmd));
- if (softleaf_is_migration_write(entry) ||
- softleaf_is_migration_read_exclusive(entry)) {
+ if ((softleaf_is_migration_write(entry) ||
+ softleaf_is_migration_read_exclusive(entry)) &&
+ vma_is_cow_mapping(src_vma)) {
entry = make_readable_migration_entry(swp_offset(entry));
pmd = softleaf_to_pmd(entry);
if (pmd_swp_soft_dirty(*src_pmd))
@@ -1915,7 +1916,8 @@ static void copy_huge_non_present_pmd(
* For device private entries, since there are no
* read exclusive entries, writable = !readable
*/
- if (softleaf_is_device_private_write(entry)) {
+ if (softleaf_is_device_private_write(entry) &&
+ vma_is_cow_mapping(src_vma)) {
entry = make_readable_device_private_entry(swp_offset(entry));
pmd = softleaf_to_pmd(entry);
@@ -1926,7 +1928,6 @@ static void copy_huge_non_present_pmd(
set_pmd_at(src_mm, addr, src_pmd, pmd);
}
- src_folio = softleaf_to_folio(entry);
VM_WARN_ON(!folio_test_large(src_folio));
folio_get(src_folio);
@@ -1938,9 +1939,11 @@ static void copy_huge_non_present_pmd(
dst_vma, src_vma);
}
- add_mm_counter(dst_mm, MM_ANONPAGES, HPAGE_PMD_NR);
- mm_inc_nr_ptes(dst_mm);
- pgtable_trans_huge_deposit(dst_mm, dst_pmd, pgtable);
+ add_mm_counter(dst_mm, mm_counter(src_folio), HPAGE_PMD_NR);
+ if (pgtable) {
+ mm_inc_nr_ptes(dst_mm);
+ pgtable_trans_huge_deposit(dst_mm, dst_pmd, pgtable);
+ }
if (!userfaultfd_protected(dst_vma))
pmd = pmd_swp_clear_uffd(pmd);
set_pmd_at(dst_mm, addr, dst_pmd, pmd);
@@ -1956,6 +1959,7 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pmd_t pmd;
pgtable_t pgtable = NULL;
int ret = -ENOMEM;
+ bool writable;
pmd = pmdp_get_lockless(src_pmd);
if (unlikely(pmd_present(pmd) && pmd_special(pmd) &&
@@ -1976,13 +1980,11 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
goto set_pmd;
}
- /* Skip if can be re-fill on fault */
- if (!vma_is_anonymous(dst_vma))
- return 0;
-
- pgtable = pte_alloc_one(dst_mm);
- if (unlikely(!pgtable))
- goto out;
+ if (vma_is_anonymous(dst_vma) || arch_needs_pgtable_deposit()) {
+ pgtable = pte_alloc_one(dst_mm);
+ if (unlikely(!pgtable))
+ goto out;
+ }
dst_ptl = pmd_lock(dst_mm, dst_pmd);
src_ptl = pmd_lockptr(src_mm, src_pmd);
@@ -2000,7 +2002,8 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
}
if (unlikely(!pmd_trans_huge(pmd))) {
- pte_free(dst_mm, pgtable);
+ if (pgtable)
+ pte_free(dst_mm, pgtable);
goto out_unlock;
}
/*
@@ -2015,7 +2018,7 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
* copy. It just takes a reference.
*/
mm_get_huge_zero_folio(dst_mm);
- goto out_zero_page;
+ goto copy_pmd;
}
src_page = pmd_page(pmd);
@@ -2023,6 +2026,12 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
src_folio = page_folio(src_page);
folio_get(src_folio);
+ if (!folio_test_anon(src_folio)) {
+ folio_dup_file_rmap_pmd(src_folio, src_page, dst_vma);
+ add_mm_counter(dst_mm, mm_counter_file(src_folio),
+ HPAGE_PMD_NR);
+ goto copy_pmd;
+ }
if (unlikely(folio_try_dup_anon_rmap_pmd(src_folio, src_page, dst_vma, src_vma))) {
/* Page maybe pinned: split and retry the fault on PTEs. */
folio_put(src_folio);
@@ -2033,19 +2042,25 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
return -EAGAIN;
}
add_mm_counter(dst_mm, MM_ANONPAGES, HPAGE_PMD_NR);
-out_zero_page:
- mm_inc_nr_ptes(dst_mm);
- pgtable_trans_huge_deposit(dst_mm, dst_pmd, pgtable);
+copy_pmd:
+ if (pgtable) {
+ mm_inc_nr_ptes(dst_mm);
+ pgtable_trans_huge_deposit(dst_mm, dst_pmd, pgtable);
+ }
- /* See __copy_present_ptes(): restore accessible protection. */
+ /* See __copy_present_ptes(). */
+ writable = pmd_write(pmd);
if (!userfaultfd_protected(dst_vma)) {
if (userfaultfd_rwp(src_vma) && pmd_uffd(pmd))
pmd = pmd_modify(pmd, dst_vma->vm_page_prot);
pmd = pmd_clear_uffd(pmd);
}
-
- pmdp_set_wrprotect(src_mm, addr, src_pmd);
- pmd = pmd_wrprotect(pmd);
+ if (vma_is_cow_mapping(src_vma) && writable) {
+ pmdp_set_wrprotect(src_mm, addr, src_pmd);
+ pmd = pmd_wrprotect(pmd);
+ }
+ if (src_vma->vm_flags & VM_SHARED)
+ pmd = pmd_mkclean(pmd);
set_pmd:
pmd = pmd_mkold(pmd);
set_pmd_at(dst_mm, addr, dst_pmd, pmd);
--
2.43.0
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH v5 2/4] mm/huge_memory: copy file THPs at fork like copy_present_ptes() does
2026-11-04 20:40 ` [PATCH v5 2/4] mm/huge_memory: copy file THPs at fork like copy_present_ptes() does Konstantin Khorenko
@ 2026-11-04 23:05 ` Jose A. Perez de Azpillaga
0 siblings, 0 replies; 9+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-11-04 23:05 UTC (permalink / raw)
To: Konstantin Khorenko, Andrew Morton, David Hildenbrand (Arm),
Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, Lance Yang, Usama Arif, Kiryl Shutsemau,
Peter Xu
Cc: Alex Williamson, kvm, Loïc Molinari, Lorenzo Stoakes,
Shuah Khan, linux-kselftest, dri-devel, Madhavan Srinivasan,
Ritesh Harjani (IBM),
linuxppc-dev, linux-mm, linux-kernel, Jason Gunthorpe, stable
On 11/4/26 9:40 PM, Konstantin Khorenko wrote:
> This fixes no bug: it makes fork copy a PMD the way copy_present_ptes()
> copies a PTE. Until now copy_huge_pmd() skipped every folio PMD of a
> non-anonymous VMA and left the child to fault them in again, while the
> PTEs of the same VMA are copied.
>
> Treat file folios as the PTE path does: take a reference, duplicate the
> rmap and account them. fork then pays a reference and an rmap update
> per PMD, and saves the child a fault per PMD it touches. The next patch
> sends PFN mappings through the same tail.
>
> That covers the VMAs vma_needs_copy() sends here:
>
> - private file mappings with an anon_vma;
> - VM_COPY_ON_FORK ones: uffd-wp and uffd-rwp, VM_MIXEDMAP, and
> VM_MAYBE_GUARD, which shared shmem, file and DAX mappings get from a
> guard region.
>
> The DAX huge zero PMD is copied too, with a reference taken by
> mm_get_huge_zero_folio() as for anonymous VMAs. A migration entry of a
> file folio is copied and accounted like copy_nonpresent_pte() does, and
> made read-only in both processes only in a CoW mapping. A device
> private entry gets the same CoW check, as in copy_nonpresent_pte().
>
> The copy then gets the same tail as __copy_present_ptes(): write
> protect both sides in a CoW mapping, mark the child's copy clean in a
> shared one, and drop the uffd-wp bit where the child is not armed. An
> anonymous THP is now write protected only when its PMD is writable, as
> a PTE is.
>
> A page table is deposited under the copy where the fault paths deposit
> one: in anonymous VMAs, and wherever the architecture needs one.
>
> Cc: stable@vger.kernel.org # needed by the next patch
> Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
> Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
LGTM:
Reviewed-by: Jose A. Perez de Azpillaga <azpijr@gmail.com>
--
cheers,
jose a. p-a
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v5 3/4] mm/huge_memory: read a special PMD under the lock in copy_huge_pmd()
2026-11-04 20:40 [PATCH v5 0/4] mm/huge_memory: copy PMDs at fork like the PTE path does Konstantin Khorenko
2026-11-04 20:40 ` [PATCH v5 1/4] mm/rmap: duplicate a PMD mapping as one in folio_dup_file_rmap_pmd() Konstantin Khorenko
2026-11-04 20:40 ` [PATCH v5 2/4] mm/huge_memory: copy file THPs at fork like copy_present_ptes() does Konstantin Khorenko
@ 2026-11-04 20:40 ` Konstantin Khorenko
2026-11-04 23:09 ` Jose A. Perez de Azpillaga
2026-11-04 20:40 ` [PATCH v5 4/4] selftests/mm: test fork of PMD-mapped shmem THPs Konstantin Khorenko
3 siblings, 1 reply; 9+ messages in thread
From: Konstantin Khorenko @ 2026-11-04 20:40 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand (Arm),
Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, Lance Yang, Usama Arif, Kiryl Shutsemau,
Peter Xu
Cc: Alex Williamson, kvm, Loïc Molinari, Lorenzo Stoakes,
Shuah Khan, linux-kselftest, dri-devel, Madhavan Srinivasan,
Ritesh Harjani (IBM),
linuxppc-dev, linux-mm, linux-kernel, Konstantin Khorenko,
Jason Gunthorpe, stable, Sashiko
copy_huge_pmd() reads a special PMD without the page table lock and
copies that value to the child. The mmap write lock held by fork does
not stop unmap_mapping_range(): it zaps under i_mmap_rwsem and the PMD
lock.
The zap visits the parent VMA before the child's, as dup_mmap() links
the child right after the parent in i_mmap. If it clears the parent PMD
after fork has read it, and passes the child before fork copies it, the
child keeps a mapping the driver has revoked, for example:
- vfio-pci: the child reaches the BAR after the device memory was
disabled or the device was reset, instead of getting SIGBUS;
- panthor: the child maps the pages of an evicted buffer object after
the driver has released them.
The same branch deposits no page table under the copy. ppc64 in hash
mode keeps the hash slot array of every huge PMD in that table, so the
child dereferences a NULL table on the first hash fault, in
__hash_page_thp(), and on the zap, in hash__pmdp_huge_get_and_clear().
Take the locks first and let vm_normal_page_pmd() tell a PFN mapping
from a folio under them, as copy_present_ptes() does with
vm_normal_page(). The zap clears the parent PMD under the same lock, so
either fork finds it none and skips it, or the copy is in place before
the zap reaches the child.
The copy of a PFN mapping then goes through the same tail as a folio:
- it gets a page table where the architecture needs one;
- in a shared mapping it is marked clean in the child, as a PTE is;
- in a CoW mapping, which only VM_MIXEDMAP allows, both sides are write
protected instead of a warning being printed.
Fixes: bc02afbd4d73 ("mm/fork: accept huge pfnmap entries")
Fixes: d1503aa9ab80 ("powerpc/64s: Add support for huge pfnmaps")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://sashiko.dev/#/patchset/20261014-b4-prep-vstor-149847-copy_huge_pmd-recheck-special-pmd-v1-1-d225c60bc316@virtuozzo.com?part=1
Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
Acked-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
Acked-by: David Hildenbrand (Arm) <david@kernel.org>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
mm/huge_memory.c | 24 +++---------------------
1 file changed, 3 insertions(+), 21 deletions(-)
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index e00632edb3856..9cb5f99aba87f 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -1961,25 +1961,6 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
int ret = -ENOMEM;
bool writable;
- pmd = pmdp_get_lockless(src_pmd);
- if (unlikely(pmd_present(pmd) && pmd_special(pmd) &&
- !is_huge_zero_pmd(pmd))) {
- dst_ptl = pmd_lock(dst_mm, dst_pmd);
- src_ptl = pmd_lockptr(src_mm, src_pmd);
- spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
- /*
- * No need to recheck the pmd, it can't change with write
- * mmap lock held here.
- *
- * Meanwhile, making sure it's not a CoW VMA with writable
- * mapping, otherwise it means either the anon page wrongly
- * applied special bit, or we made the PRIVATE mapping be
- * able to wrongly write to the backend MMIO.
- */
- VM_WARN_ON_ONCE(vma_is_cow_mapping(src_vma) && pmd_write(pmd));
- goto set_pmd;
- }
-
if (vma_is_anonymous(dst_vma) || arch_needs_pgtable_deposit()) {
pgtable = pte_alloc_one(dst_mm);
if (unlikely(!pgtable))
@@ -2021,7 +2002,9 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
goto copy_pmd;
}
- src_page = pmd_page(pmd);
+ src_page = vm_normal_page_pmd(src_vma, addr, pmd);
+ if (!src_page)
+ goto copy_pmd;
VM_BUG_ON_PAGE(!PageHead(src_page), src_page);
src_folio = page_folio(src_page);
@@ -2061,7 +2044,6 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
}
if (src_vma->vm_flags & VM_SHARED)
pmd = pmd_mkclean(pmd);
-set_pmd:
pmd = pmd_mkold(pmd);
set_pmd_at(dst_mm, addr, dst_pmd, pmd);
--
2.43.0
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH v5 3/4] mm/huge_memory: read a special PMD under the lock in copy_huge_pmd()
2026-11-04 20:40 ` [PATCH v5 3/4] mm/huge_memory: read a special PMD under the lock in copy_huge_pmd() Konstantin Khorenko
@ 2026-11-04 23:09 ` Jose A. Perez de Azpillaga
0 siblings, 0 replies; 9+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-11-04 23:09 UTC (permalink / raw)
To: Konstantin Khorenko, Andrew Morton, David Hildenbrand (Arm),
Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, Lance Yang, Usama Arif, Kiryl Shutsemau,
Peter Xu
Cc: Alex Williamson, kvm, Loïc Molinari, Lorenzo Stoakes,
Shuah Khan, linux-kselftest, dri-devel, Madhavan Srinivasan,
Ritesh Harjani (IBM),
linuxppc-dev, linux-mm, linux-kernel, Jason Gunthorpe, stable,
Sashiko
On 11/4/26 9:40 PM, Konstantin Khorenko wrote:
> copy_huge_pmd() reads a special PMD without the page table lock and
> copies that value to the child. The mmap write lock held by fork does
> not stop unmap_mapping_range(): it zaps under i_mmap_rwsem and the PMD
> lock.
>
> The zap visits the parent VMA before the child's, as dup_mmap() links
> the child right after the parent in i_mmap. If it clears the parent PMD
> after fork has read it, and passes the child before fork copies it, the
> child keeps a mapping the driver has revoked, for example:
>
> - vfio-pci: the child reaches the BAR after the device memory was
> disabled or the device was reset, instead of getting SIGBUS;
> - panthor: the child maps the pages of an evicted buffer object after
> the driver has released them.
>
> The same branch deposits no page table under the copy. ppc64 in hash
> mode keeps the hash slot array of every huge PMD in that table, so the
> child dereferences a NULL table on the first hash fault, in
> __hash_page_thp(), and on the zap, in hash__pmdp_huge_get_and_clear().
>
> Take the locks first and let vm_normal_page_pmd() tell a PFN mapping
> from a folio under them, as copy_present_ptes() does with
> vm_normal_page(). The zap clears the parent PMD under the same lock, so
> either fork finds it none and skips it, or the copy is in place before
> the zap reaches the child.
>
> The copy of a PFN mapping then goes through the same tail as a folio:
>
> - it gets a page table where the architecture needs one;
> - in a shared mapping it is marked clean in the child, as a PTE is;
> - in a CoW mapping, which only VM_MIXEDMAP allows, both sides are write
> protected instead of a warning being printed.
>
> Fixes: bc02afbd4d73 ("mm/fork: accept huge pfnmap entries")
> Fixes: d1503aa9ab80 ("powerpc/64s: Add support for huge pfnmaps")
> Cc: stable@vger.kernel.org
> Reported-by: Sashiko <sashiko-bot@kernel.org>
> Closes: https://sashiko.dev/#/patchset/20261014-b4-prep-vstor-149847-copy_huge_pmd-recheck-special-pmd-v1-1-d225c60bc316@virtuozzo.com?part=1
> Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
> Acked-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> Acked-by: David Hildenbrand (Arm) <david@kernel.org>
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
LGTM:
Reviewed-by: Jose A. Perez de Azpillaga <azpijr@gmail.com>
--
cheers,
jose a. p-a
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v5 4/4] selftests/mm: test fork of PMD-mapped shmem THPs
2026-11-04 20:40 [PATCH v5 0/4] mm/huge_memory: copy PMDs at fork like the PTE path does Konstantin Khorenko
` (2 preceding siblings ...)
2026-11-04 20:40 ` [PATCH v5 3/4] mm/huge_memory: read a special PMD under the lock in copy_huge_pmd() Konstantin Khorenko
@ 2026-11-04 20:40 ` Konstantin Khorenko
2026-11-04 23:15 ` Jose A. Perez de Azpillaga
3 siblings, 1 reply; 9+ messages in thread
From: Konstantin Khorenko @ 2026-11-04 20:40 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand (Arm),
Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, Lance Yang, Usama Arif, Kiryl Shutsemau,
Peter Xu
Cc: Alex Williamson, kvm, Loïc Molinari, Lorenzo Stoakes,
Shuah Khan, linux-kselftest, dri-devel, Madhavan Srinivasan,
Ritesh Harjani (IBM),
linuxppc-dev, linux-mm, linux-kernel, Konstantin Khorenko,
Jason Gunthorpe
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. Unmapping them drops it once more, so every mapping stays
until exit, where the kernel 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.
ShmemPmdMapped is system-wide, and reclaim or NUMA balancing can take a
PMD from the parent while the test runs. So the test makes up to three
attempts, each on a fresh file, and fails only if all of them see the
counter drop by exactly the parent's share: the bug does that on every
fork, other processes only by chance. It passes on the first clean
attempt and skips when none is clean.
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 <khorenko@virtuozzo.com>
---
MAINTAINERS | 1 +
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/fork_file_thp.c | 256 +++++++++++++++++++++++++++++
tools/testing/selftests/mm/run_vmtests.sh | 1 +
4 files changed, 259 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 93abbb315065d..1e6c35a30a369 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17859,6 +17859,7 @@ F: mm/collapse.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 0ea72e9979347..2489027b2a36b 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -108,6 +108,7 @@ TEST_GEN_FILES += folio_split_race_test
TEST_GEN_FILES += folio_order_check
TEST_GEN_FILES += khugepaged_sync_check
TEST_GEN_FILES += khugepaged_race
+TEST_GEN_FILES += fork_file_thp
TEST_GEN_FILES += soft-dirty
ifeq ($(ARCH),x86_64)
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..aaecf7ba6f406
--- /dev/null
+++ b/tools/testing/selftests/mm/fork_file_thp.c
@@ -0,0 +1,256 @@
+// 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)
+/* Other processes, reclaim and NUMA balancing can disturb an attempt. */
+#define ATTEMPTS 3
+
+static size_t pagesize, pmdsize, size;
+
+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 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;
+}
+
+/* The last PMD is zapped by the setup, the others must be PMD-mapped. */
+static bool pmds_ok(char *mem)
+{
+ return check_huge_file(mem, size - pmdsize, NR_PMDS - 1, pmdsize);
+}
+
+/* Maps a fresh shmem file of NR_PMDS PMDs, PMD-aligned, until exit. */
+static char *map_file(bool shared)
+{
+ char *area, *buf, *mem;
+ size_t off;
+ int fd;
+
+ 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");
+ close(fd);
+ for (off = 0; off < size; off += pmdsize)
+ FORCE_READ(mem[off]);
+ return mem;
+}
+
+/* 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, LOST_PMDS, NO_THP };
+
+static enum result fork_once(char *mem, bool shared, long *before, long *after)
+{
+ /* The parent alone maps this much with PMDs. */
+ long own = (NR_PMDS - 1) * (pmdsize >> 10);
+ bool parent_pmds;
+ 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 (!pmds_ok(mem))
+ _exit(NO_PMDS);
+ if (!data_ok(mem, 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;
+ /* Reclaim or NUMA balancing took a PMD from the parent. */
+ parent_pmds = pmds_ok(mem);
+ if (WEXITSTATUS(status) == NO_PMDS && !parent_pmds)
+ return LOST_PMDS;
+ if (WEXITSTATUS(status) != PASS)
+ return WEXITSTATUS(status);
+ if (mem[pmdsize] != (shared ? 'c' : 2) || !data_ok(mem, pmdsize))
+ return FAIL_PARENT;
+ if (!parent_pmds)
+ return LOST_PMDS;
+ /* The child's exit took the parent's share off the counter. */
+ if (*before - *after == own)
+ return FAIL_COUNTER;
+ if (*after != *before)
+ return CHANGED;
+ return PASS;
+}
+
+static void test_fork(bool shared)
+{
+ const char *desc = shared ? "shared mapping with a guard region" :
+ "private mapping with an anon_vma";
+ long before = 0, after = 0;
+ enum result res = NO_THP;
+ int i, drops = 0;
+ char *mem;
+
+ for (i = 0; i < ATTEMPTS; i++) {
+ mem = map_file(shared);
+ /* Make fork copy the page tables; the last PMD gets zapped. */
+ if (shared) {
+ if (madvise(mem + size - pagesize, pagesize,
+ MADV_GUARD_INSTALL)) {
+ ksft_test_result_skip("%s: MADV_GUARD_INSTALL: %s\n",
+ desc, strerror(errno));
+ return;
+ }
+ } else {
+ mem[size - pagesize] = 'p';
+ }
+
+ res = pmds_ok(mem) ? fork_once(mem, shared, &before, &after) :
+ res == NO_THP ? NO_THP : LOST_PMDS;
+ /* Later children get only their own mapping. */
+ madvise(mem, size, MADV_DONTFORK);
+ if (res == FAIL_COUNTER)
+ drops++;
+ else if (res != CHANGED && res != LOST_PMDS && res != NO_THP)
+ break;
+ }
+ /* Other processes can mimic the bug's drop, but not on every attempt. */
+ if (res == FAIL_COUNTER && drops < ATTEMPTS)
+ res = CHANGED;
+
+ switch (res) {
+ case PASS:
+ ksft_test_result_pass("%s\n", desc);
+ break;
+ case NO_THP:
+ ksft_test_result_skip("%s: shmem is not mapped with PMDs\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 LOST_PMDS:
+ ksft_test_result_skip("%s: the parent keeps losing its PMDs\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;
+ }
+}
+
+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);
+ size = NR_PMDS * pmdsize;
+ 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 a1b45a3dedaee..790a6b6746465 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -433,6 +433,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
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH v5 4/4] selftests/mm: test fork of PMD-mapped shmem THPs
2026-11-04 20:40 ` [PATCH v5 4/4] selftests/mm: test fork of PMD-mapped shmem THPs Konstantin Khorenko
@ 2026-11-04 23:15 ` Jose A. Perez de Azpillaga
0 siblings, 0 replies; 9+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-11-04 23:15 UTC (permalink / raw)
To: Konstantin Khorenko, Andrew Morton, David Hildenbrand (Arm),
Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, Lance Yang, Usama Arif, Kiryl Shutsemau,
Peter Xu
Cc: Alex Williamson, kvm, Loïc Molinari, Lorenzo Stoakes,
Shuah Khan, linux-kselftest, dri-devel, Madhavan Srinivasan,
Ritesh Harjani (IBM),
linuxppc-dev, linux-mm, linux-kernel, Jason Gunthorpe
On 11/4/26 9:40 PM, Konstantin Khorenko wrote:
> + pmdsize = read_pmd_pagesize();
read_pmd_pagesize() is pmd_psize() in mm-new since 5d123bb07b18
("kselftest: mm: rename read_pmd_pagesize() to pmd_psize()"), so this
won't build there, will it?
--
cheers,
jose a. p-a
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-11-04 23:15 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-04 20:40 [PATCH v5 0/4] mm/huge_memory: copy PMDs at fork like the PTE path does Konstantin Khorenko
2026-11-04 20:40 ` [PATCH v5 1/4] mm/rmap: duplicate a PMD mapping as one in folio_dup_file_rmap_pmd() Konstantin Khorenko
2026-11-04 22:57 ` Jose A. Perez de Azpillaga
2026-11-04 20:40 ` [PATCH v5 2/4] mm/huge_memory: copy file THPs at fork like copy_present_ptes() does Konstantin Khorenko
2026-11-04 23:05 ` Jose A. Perez de Azpillaga
2026-11-04 20:40 ` [PATCH v5 3/4] mm/huge_memory: read a special PMD under the lock in copy_huge_pmd() Konstantin Khorenko
2026-11-04 23:09 ` Jose A. Perez de Azpillaga
2026-11-04 20:40 ` [PATCH v5 4/4] selftests/mm: test fork of PMD-mapped shmem THPs Konstantin Khorenko
2026-11-04 23:15 ` Jose A. Perez de Azpillaga
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®