mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v2 0/2] mm/huge_memory: take a reference on a device DAX PUD copied by fork
@ 2026-11-05 17:00 Konstantin Khorenko
  2026-11-05 17:00 ` [PATCH v2 1/2] mm/huge_memory: take a reference on a folio " Konstantin Khorenko
  2026-11-05 17:00 ` [PATCH v2 2/2] selftests/mm: test fork of a device DAX PUD Konstantin Khorenko
  0 siblings, 2 replies; 7+ messages in thread
From: Konstantin Khorenko @ 2026-11-05 17:00 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Zi Yan, Lorenzo Stoakes,
	Alistair Popple, Dan Williams
  Cc: linux-mm, nvdimm, linux-kselftest, linux-kernel,
	Konstantin Khorenko, stable

copy_huge_pud() copies a device DAX PUD into the child without the
reference, rmap and counter that vmf_insert_folio_pud() took, so the
child's unmap takes them from the parent. fork copies a device DAX VMA
once it has a guard region, so it takes no more than:

1. map a devdax instance with 1 GiB alignment and fault in a PUD;
2. MADV_GUARD_INSTALL a page elsewhere in the VMA;
3. fork, and let the child exit.

Patch 1 is the fix, patch 2 a selftest for it. I found it while
reworking copy_huge_pmd() for fork after David's review [1]; it is
independent of that series.

v2 is based on mm-hotfixes-unstable, as patch 1 is a fix for stable.

Testing
=======

 * mm-hotfixes-unstable x86_64 KVM guest with CONFIG_DEBUG_VM
 * pmem from memmap=3G!4G
 * devdax made with "ndctl create-namespace -m devdax -a 1G":

  +----------------------+--------------+------------------------------+
  | kernel               | fork_dax_pud | log                          |
  +----------------------+--------------+------------------------------+
  | mm-hotfixes-unstable | not ok       | BUG: Bad rss-counter state   |
  |                      |              | mm:... type:MM_FILEPAGES     |
  |                      |              | val:-262144                  |
  +----------------------+--------------+------------------------------+
  | + this series        | ok           | clean                        |
  +----------------------+--------------+------------------------------+

A variant that writes to the device and forks three times also hit,
on mm-hotfixes-unstable:

  WARNING: ./include/linux/rmap.h:159 at
  __folio_large_mapcount_sanity_checks+0x90/0x150

With the series it is clean, and the guard-regions selftest passes.

The selftest only runs on a device given in FORK_DAX_PUD_DEVICE or
argv[1], as on an unfixed kernel it leaves that device's folio with a
mapcount below zero. On pmem emulated in RAM, with
CONFIG_X86_PMEM_LEGACY and CONFIG_DEV_DAX_PMEM:

  boot with memmap=3G!4G
  ndctl create-namespace -f -e namespace0.0 -m devdax -a 1G
  FORK_DAX_PUD_DEVICE=/dev/dax0.0 ./fork_dax_pud

[1] https://lore.kernel.org/linux-mm/20261014-b4-prep-vstor-149847-copy_huge_pmd-recheck-special-pmd-v3-0-85eaec8417f6@virtuozzo.com/

Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
Changes in v2:
- Patch 1: copy_huge_pud() takes only the VMAs, and the TODO moves next
  to the rmap duplication, as in David's fix for the same bug:
  https://lore.kernel.org/all/26862633-c3d1-4b82-8ca2-346cb78570d7@kernel.org/
- Patch 2: read_sysfs() and a new rss_file() in vm_util instead of local
  helpers (Sashiko).
- Assisted-by moved from the patches to this cover letter.
- Rebased onto mm-hotfixes-unstable.
- Link to v1: https://patch.msgid.link/20261021-vstor-150143-copy_huge_pud-folio-refcount-v1-0-8bd88551f9be@virtuozzo.com

---
Konstantin Khorenko (2):
      mm/huge_memory: take a reference on a folio PUD copied by fork
      selftests/mm: test fork of a device DAX PUD

 MAINTAINERS                               |   1 +
 include/linux/huge_mm.h                   |   5 +-
 include/linux/rmap.h                      |  22 ++++++
 mm/huge_memory.c                          |  27 ++++---
 mm/memory.c                               |   4 +-
 tools/testing/selftests/mm/Makefile       |   1 +
 tools/testing/selftests/mm/fork_dax_pud.c | 114 ++++++++++++++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh |   1 +
 tools/testing/selftests/mm/vm_util.c      |  21 ++++++
 tools/testing/selftests/mm/vm_util.h      |   1 +
 10 files changed, 183 insertions(+), 14 deletions(-)
---
base-commit: af05d765a4476d2e48099148c80f9b13f462e309
change-id: 20261021-vstor-150143-copy_huge_pud-folio-refcount-ea269e668270

Best regards,
--  
Konstantin Khorenko,
Virtuozzo Linux Kernel Team


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

* [PATCH v2 1/2] mm/huge_memory: take a reference on a folio PUD copied by fork
  2026-11-05 17:00 [PATCH v2 0/2] mm/huge_memory: take a reference on a device DAX PUD copied by fork Konstantin Khorenko
@ 2026-11-05 17:00 ` Konstantin Khorenko
  2026-11-05 19:28   ` David Hildenbrand (Arm)
  2026-11-05 20:19   ` Zi Yan
  2026-11-05 17:00 ` [PATCH v2 2/2] selftests/mm: test fork of a device DAX PUD Konstantin Khorenko
  1 sibling, 2 replies; 7+ messages in thread
From: Konstantin Khorenko @ 2026-11-05 17:00 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Zi Yan, Lorenzo Stoakes,
	Alistair Popple, Dan Williams
  Cc: linux-mm, nvdimm, linux-kselftest, linux-kernel,
	Konstantin Khorenko, stable

Device DAX maps its memory with vmf_insert_folio_pud(), which takes a
folio reference, adds a PUD rmap and accounts the pages to the mm.
zap_huge_pud() drops all three, but copy_huge_pud() copies the PUD into
the child without taking any of them. Each child's unmap then drops
what the parent took:

- the folio is a reference short, so with two children its refcount
  drops to zero and free_zone_device_folio() runs while the parent
  still maps it;
- the child's file counter goes negative, "Bad rss-counter state" when
  its mm is freed;
- the parent's unmap drives the entire mapcount below zero.

fork copies the page tables of a device DAX mapping that has a guard
region, and commit f807123d578d ("mm: allow guard regions in file-backed
and read-only mappings") allowed MADV_GUARD_INSTALL there.

Take the reference, the rmap and the counter for a PUD that
vm_normal_page_pud() resolves to a page, as copy_present_ptes() does for
a PTE, with a new folio_dup_file_rmap_pud() helper. copy_huge_pud() now
needs both VMAs, so pass only them and take the mms from them.

Fixes: aed877c2b425 ("device/dax: properly refcount device dax pages when mapping")
Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
Cc: stable@vger.kernel.org
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 include/linux/huge_mm.h |  5 ++---
 include/linux/rmap.h    | 22 ++++++++++++++++++++++
 mm/huge_memory.c        | 27 +++++++++++++++++++--------
 mm/memory.c             |  4 +---
 4 files changed, 44 insertions(+), 14 deletions(-)

diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
index c745f7ad22987..fac05120ab774 100644
--- a/include/linux/huge_mm.h
+++ b/include/linux/huge_mm.h
@@ -12,9 +12,8 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
 		  pmd_t *dst_pmd, pmd_t *src_pmd, unsigned long addr,
 		  struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma);
 bool huge_pmd_set_accessed(struct vm_fault *vmf);
-int copy_huge_pud(struct mm_struct *dst_mm, struct mm_struct *src_mm,
-		  pud_t *dst_pud, pud_t *src_pud, unsigned long addr,
-		  struct vm_area_struct *vma);
+int copy_huge_pud(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
+		pud_t *dst_pud, pud_t *src_pud, unsigned long addr);
 
 #ifdef CONFIG_HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD
 void huge_pud_set_accessed(struct vm_fault *vmf, pud_t orig_pud);
diff --git a/include/linux/rmap.h b/include/linux/rmap.h
index 74cca0e3c7264..52fff8dc74213 100644
--- a/include/linux/rmap.h
+++ b/include/linux/rmap.h
@@ -564,6 +564,28 @@ static inline void folio_dup_file_rmap_pmd(struct folio *folio,
 #endif
 }
 
+/**
+ * folio_dup_file_rmap_pud - duplicate a PUD mapping of a page range of a folio
+ * @folio:	The folio to duplicate the mapping of
+ * @page:	The first page to duplicate the mapping of
+ * @dst_vma:	The destination vm area
+ *
+ * The page range of the folio is defined by [page, page + HPAGE_PUD_NR)
+ *
+ * The caller needs to hold the page table lock.
+ */
+static inline void folio_dup_file_rmap_pud(struct folio *folio,
+		struct page *page, struct vm_area_struct *dst_vma)
+{
+#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && \
+	defined(CONFIG_HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD)
+	__folio_dup_file_rmap(folio, page, HPAGE_PUD_NR, dst_vma,
+			      PGTABLE_LEVEL_PUD);
+#else
+	WARN_ON_ONCE(true);
+#endif
+}
+
 static __always_inline int __folio_try_dup_anon_rmap(struct folio *folio,
 		struct page *page, int nr_pages, struct vm_area_struct *dst_vma,
 		struct vm_area_struct *src_vma, enum pgtable_level level)
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 14599d0fff013..30c21e85d21bf 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -2072,11 +2072,13 @@ void touch_pud(struct vm_area_struct *vma, unsigned long addr,
 		update_mmu_cache_pud(vma, addr, pud);
 }
 
-int copy_huge_pud(struct mm_struct *dst_mm, struct mm_struct *src_mm,
-		  pud_t *dst_pud, pud_t *src_pud, unsigned long addr,
-		  struct vm_area_struct *vma)
+int copy_huge_pud(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
+		pud_t *dst_pud, pud_t *src_pud, unsigned long addr)
 {
+	struct mm_struct *dst_mm = dst_vma->vm_mm;
+	struct mm_struct *src_mm = src_vma->vm_mm;
 	spinlock_t *dst_ptl, *src_ptl;
+	struct page *page;
 	pud_t pud;
 	int ret;
 
@@ -2089,11 +2091,20 @@ int copy_huge_pud(struct mm_struct *dst_mm, struct mm_struct *src_mm,
 	if (unlikely(!pud_trans_huge(pud)))
 		goto out_unlock;
 
-	/*
-	 * TODO: once we support anonymous pages, use
-	 * folio_try_dup_anon_rmap_*() and split if duplicating fails.
-	 */
-	if (vma_is_cow_mapping(vma) && pud_write(pud)) {
+	page = vm_normal_page_pud(src_vma, addr, pud);
+	if (page) {
+		struct folio *folio = page_folio(page);
+
+		folio_get(folio);
+		/*
+		 * TODO: once we support anonymous pages, use
+		 * folio_try_dup_anon_rmap_*() and split if duplicating fails.
+		 */
+		folio_dup_file_rmap_pud(folio, page, dst_vma);
+		add_mm_counter(dst_mm, mm_counter_file(folio), HPAGE_PUD_NR);
+	}
+
+	if (vma_is_cow_mapping(src_vma) && pud_write(pud)) {
 		pudp_set_wrprotect(src_mm, addr, src_pud);
 		pud = pud_wrprotect(pud);
 	}
diff --git a/mm/memory.c b/mm/memory.c
index b65b56fe97148..66fbe8ed5bac0 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -1463,7 +1463,6 @@ copy_pud_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
 	       unsigned long end)
 {
 	struct mm_struct *dst_mm = dst_vma->vm_mm;
-	struct mm_struct *src_mm = src_vma->vm_mm;
 	pud_t *src_pud, *dst_pud;
 	unsigned long next;
 
@@ -1477,8 +1476,7 @@ copy_pud_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
 			int err;
 
 			VM_BUG_ON_VMA(next-addr != HPAGE_PUD_SIZE, src_vma);
-			err = copy_huge_pud(dst_mm, src_mm,
-					    dst_pud, src_pud, addr, src_vma);
+			err = copy_huge_pud(dst_vma, src_vma, dst_pud, src_pud, addr);
 			if (err == -ENOMEM)
 				return -ENOMEM;
 			if (!err)

-- 
2.43.0


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

* [PATCH v2 2/2] selftests/mm: test fork of a device DAX PUD
  2026-11-05 17:00 [PATCH v2 0/2] mm/huge_memory: take a reference on a device DAX PUD copied by fork Konstantin Khorenko
  2026-11-05 17:00 ` [PATCH v2 1/2] mm/huge_memory: take a reference on a folio " Konstantin Khorenko
@ 2026-11-05 17:00 ` Konstantin Khorenko
  2026-11-05 19:30   ` David Hildenbrand (Arm)
  1 sibling, 1 reply; 7+ messages in thread
From: Konstantin Khorenko @ 2026-11-05 17:00 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Zi Yan, Lorenzo Stoakes,
	Alistair Popple, Dan Williams
  Cc: linux-mm, nvdimm, linux-kselftest, linux-kernel, Konstantin Khorenko

Fork a process that maps a device DAX instance with a PUD, in a VMA
whose page tables fork copies because it has a guard region. The child
must account the copied PUD in its RssFile, which a new rss_file() in
vm_util reads as rss_anon() reads RssAnon.

Without a reference, an rmap and a counter for the copied PUD, the
child's RssFile misses the PUD, and its exit leaves the child's file
counter negative: "Bad rss-counter state".

It runs only on a device named in argv[1] or $FORK_DAX_PUD_DEVICE: on
a kernel without the fix it leaves the device folio's mapcount below
zero.

Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 MAINTAINERS                               |   1 +
 tools/testing/selftests/mm/Makefile       |   1 +
 tools/testing/selftests/mm/fork_dax_pud.c | 114 ++++++++++++++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh |   1 +
 tools/testing/selftests/mm/vm_util.c      |  21 ++++++
 tools/testing/selftests/mm/vm_util.h      |   1 +
 6 files changed, 139 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index d63d8b2152773..247e16f3276f4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17931,6 +17931,7 @@ F:	mm/huge_memory.c
 F:	mm/khugepaged.c
 F:	mm/mm_slot.h
 F:	tools/mm/thpmaps
+F:	tools/testing/selftests/mm/fork_dax_pud.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..130500f5f6e0a 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_dax_pud
 TEST_GEN_FILES += soft-dirty
 
 ifeq ($(ARCH),x86_64)
diff --git a/tools/testing/selftests/mm/fork_dax_pud.c b/tools/testing/selftests/mm/fork_dax_pud.c
new file mode 100644
index 0000000000000..0d19c30b1dc6f
--- /dev/null
+++ b/tools/testing/selftests/mm/fork_dax_pud.c
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * fork() of a process that maps a device DAX instance with a PUD, in a VMA
+ * whose page tables fork copies because it has a guard region. The child
+ * must account the copied PUD in its RssFile.
+ *
+ * Runs only on a device named in argv[1] or $FORK_DAX_PUD_DEVICE that
+ * nothing else maps, with 1 GiB alignment and at least 2 GiB. To make one
+ * on pmem emulated in RAM, with CONFIG_X86_PMEM_LEGACY and
+ * CONFIG_DEV_DAX_PMEM:
+ *
+ *	boot with memmap=3G!4G
+ *	ndctl create-namespace -f -e namespace0.0 -m devdax -a 1G
+ *	FORK_DAX_PUD_DEVICE=/dev/dax0.0 ./fork_dax_pud
+ */
+#define _GNU_SOURCE
+#include <asm-generic/mman.h> /* Force the import of the tools version. */
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+
+#include "kselftest.h"
+#include "vm_util.h"
+
+#define SZ_1G		(1UL << 30)
+#define DAX_SYSFS	"/sys/bus/dax/devices"
+
+static sigjmp_buf sigbus_jmp;
+
+static void sigbus_handler(int sig)
+{
+	siglongjmp(sigbus_jmp, 1);
+}
+
+static bool dax_attr(const char *dev, const char *attr, unsigned long *val)
+{
+	char path[PATH_MAX];
+
+	snprintf(path, sizeof(path), DAX_SYSFS "/%s/%s", dev, attr);
+	return !read_sysfs(path, val);
+}
+
+static bool dax_dev_ok(const char *path)
+{
+	const char *name = strrchr(path, '/');
+	unsigned long align, size;
+
+	name = name ? name + 1 : path;
+	return dax_attr(name, "align", &align) && align == SZ_1G &&
+	       dax_attr(name, "size", &size) && size >= 2 * SZ_1G;
+}
+
+int main(int argc, char **argv)
+{
+	const char *path = argc > 1 ? argv[1] : getenv("FORK_DAX_PUD_DEVICE");
+	char *area, *mem;
+	int fd, status;
+	pid_t pid;
+
+	ksft_print_header();
+	if (!path)
+		ksft_exit_skip("no devdax device given\n");
+	if (!dax_dev_ok(path))
+		ksft_exit_skip("%s: needs 1 GiB alignment and 2 GiB\n", path);
+
+	fd = open(path, O_RDONLY);
+	if (fd < 0)
+		ksft_exit_skip("%s: %s\n", path, strerror(errno));
+	area = mmap(NULL, 3 * SZ_1G, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	if (area == MAP_FAILED)
+		ksft_exit_fail_perror("mmap");
+	mem = (char *)(((unsigned long)area + SZ_1G - 1) & ~(SZ_1G - 1));
+	mem = mmap(mem, 2 * SZ_1G, PROT_READ, MAP_SHARED | MAP_FIXED, fd, 0);
+	if (mem == MAP_FAILED)
+		ksft_exit_skip("mmap %s: %s\n", path, strerror(errno));
+
+	/* A PUD in the first GiB, a guard page at the end of the second. */
+	signal(SIGBUS, sigbus_handler);
+	if (sigsetjmp(sigbus_jmp, 1))
+		ksft_exit_skip("%s cannot be mapped with a PUD\n", path);
+	FORCE_READ(mem[0]);
+	signal(SIGBUS, SIG_DFL);
+	if (madvise(mem + 2 * SZ_1G - getpagesize(), getpagesize(),
+		    MADV_GUARD_INSTALL))
+		ksft_exit_skip("MADV_GUARD_INSTALL: %s\n", strerror(errno));
+
+	ksft_set_plan(1);
+	fflush(stdout);
+	pid = fork();
+	if (pid < 0)
+		ksft_exit_fail_perror("fork");
+	if (!pid)
+		_exit(rss_file() < (SZ_1G >> 10) ? 10 : 0);
+	if (waitpid(pid, &status, 0) != pid)
+		ksft_exit_fail_perror("waitpid");
+
+	if (!WIFEXITED(status) || WEXITSTATUS(status))
+		ksft_test_result_fail("child does not account the copied PUD\n");
+	else
+		ksft_test_result_pass("devdax PUD copied by fork\n");
+
+	munmap(area, 3 * SZ_1G);
+	close(fd);
+	ksft_finished();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index a1b45a3dedaee..0d5f3b325e84b 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_dax_pud
 
 CATEGORY="migration" run_test ./migration
 
diff --git a/tools/testing/selftests/mm/vm_util.c b/tools/testing/selftests/mm/vm_util.c
index 515031b8283da..1407ef83d44eb 100644
--- a/tools/testing/selftests/mm/vm_util.c
+++ b/tools/testing/selftests/mm/vm_util.c
@@ -197,6 +197,27 @@ unsigned long rss_anon(void)
 	return rss_anon;
 }
 
+unsigned long rss_file(void)
+{
+	unsigned long rss_file = 0;
+	FILE *fp;
+	char buffer[MAX_LINE_LENGTH];
+
+	fp = fopen(STATUS_FILE_PATH, "r");
+	if (!fp)
+		ksft_exit_fail_msg("%s: Failed to open file %s\n", __func__, STATUS_FILE_PATH);
+
+	if (!check_for_pattern(fp, "RssFile:", buffer, sizeof(buffer)))
+		goto err_out;
+
+	if (sscanf(buffer, "RssFile:%10lu kB", &rss_file) != 1)
+		ksft_exit_fail_msg("Reading status error\n");
+
+err_out:
+	fclose(fp);
+	return rss_file;
+}
+
 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.h b/tools/testing/selftests/mm/vm_util.h
index b5d59729d4327..211f9f6699231 100644
--- a/tools/testing/selftests/mm/vm_util.h
+++ b/tools/testing/selftests/mm/vm_util.h
@@ -94,6 +94,7 @@ void clear_softdirty(void);
 bool check_for_pattern(FILE *fp, const char *pattern, char *buf, size_t len);
 uint64_t read_pmd_pagesize(void);
 unsigned long rss_anon(void);
+unsigned long rss_file(void);
 bool check_huge_anon(void *addr, size_t len, int nr_hpages, uint64_t hpage_size);
 bool check_huge_file(void *addr, size_t len, int nr_hpages, uint64_t hpage_size);
 int64_t allocate_transhuge(void *ptr, int pagemap_fd);

-- 
2.43.0


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

* Re: [PATCH v2 1/2] mm/huge_memory: take a reference on a folio PUD copied by fork
  2026-11-05 17:00 ` [PATCH v2 1/2] mm/huge_memory: take a reference on a folio " Konstantin Khorenko
@ 2026-11-05 19:28   ` David Hildenbrand (Arm)
  2026-11-05 20:19   ` Zi Yan
  1 sibling, 0 replies; 7+ messages in thread
From: David Hildenbrand (Arm) @ 2026-11-05 19:28 UTC (permalink / raw)
  To: Konstantin Khorenko, Andrew Morton, Zi Yan, Lorenzo Stoakes,
	Alistair Popple, Dan Williams
  Cc: linux-mm, nvdimm, linux-kselftest, linux-kernel, stable

On 11/5/26 18:00, Konstantin Khorenko wrote:
> Device DAX maps its memory with vmf_insert_folio_pud(), which takes a
> folio reference, adds a PUD rmap and accounts the pages to the mm.
> zap_huge_pud() drops all three, but copy_huge_pud() copies the PUD into
> the child without taking any of them. Each child's unmap then drops
> what the parent took:
> 
> - the folio is a reference short, so with two children its refcount
>   drops to zero and free_zone_device_folio() runs while the parent
>   still maps it;
> - the child's file counter goes negative, "Bad rss-counter state" when
>   its mm is freed;
> - the parent's unmap drives the entire mapcount below zero.
> 
> fork copies the page tables of a device DAX mapping that has a guard
> region, and commit f807123d578d ("mm: allow guard regions in file-backed
> and read-only mappings") allowed MADV_GUARD_INSTALL there.
> 
> Take the reference, the rmap and the counter for a PUD that
> vm_normal_page_pud() resolves to a page, as copy_present_ptes() does for
> a PTE, with a new folio_dup_file_rmap_pud() helper. copy_huge_pud() now
> needs both VMAs, so pass only them and take the mms from them.
> 
> Fixes: aed877c2b425 ("device/dax: properly refcount device dax pages when mapping")
> Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
> Cc: stable@vger.kernel.org
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
> ---

Thanks, I'll wait a couple of days and then queue this.

Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>

-- 
Cheers,

David

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

* Re: [PATCH v2 2/2] selftests/mm: test fork of a device DAX PUD
  2026-11-05 17:00 ` [PATCH v2 2/2] selftests/mm: test fork of a device DAX PUD Konstantin Khorenko
@ 2026-11-05 19:30   ` David Hildenbrand (Arm)
  0 siblings, 0 replies; 7+ messages in thread
From: David Hildenbrand (Arm) @ 2026-11-05 19:30 UTC (permalink / raw)
  To: Konstantin Khorenko, Andrew Morton, Zi Yan, Lorenzo Stoakes,
	Alistair Popple, Dan Williams
  Cc: linux-mm, nvdimm, linux-kselftest, linux-kernel

On 11/5/26 18:00, Konstantin Khorenko wrote:
> Fork a process that maps a device DAX instance with a PUD, in a VMA
> whose page tables fork copies because it has a guard region. The child
> must account the copied PUD in its RssFile, which a new rss_file() in
> vm_util reads as rss_anon() reads RssAnon.
> 
> Without a reference, an rmap and a counter for the copied PUD, the
> child's RssFile misses the PUD, and its exit leaves the child's file
> counter negative: "Bad rss-counter state".
> 
> It runs only on a device named in argv[1] or $FORK_DAX_PUD_DEVICE: on
> a kernel without the fix it leaves the device folio's mapcount below
> zero.
> 
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
> ---
>  MAINTAINERS                               |   1 +
>  tools/testing/selftests/mm/Makefile       |   1 +
>  tools/testing/selftests/mm/fork_dax_pud.c | 114 ++++++++++++++++++++++++++++++
>  tools/testing/selftests/mm/run_vmtests.sh |   1 +
>  tools/testing/selftests/mm/vm_util.c      |  21 ++++++
>  tools/testing/selftests/mm/vm_util.h      |   1 +
>  6 files changed, 139 insertions(+)
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index d63d8b2152773..247e16f3276f4 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -17931,6 +17931,7 @@ F:	mm/huge_memory.c
>  F:	mm/khugepaged.c
>  F:	mm/mm_slot.h
>  F:	tools/mm/thpmaps
> +F:	tools/testing/selftests/mm/fork_dax_pud.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..130500f5f6e0a 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_dax_pud
>  TEST_GEN_FILES += soft-dirty
>  
>  ifeq ($(ARCH),x86_64)
> diff --git a/tools/testing/selftests/mm/fork_dax_pud.c b/tools/testing/selftests/mm/fork_dax_pud.c
> new file mode 100644
> index 0000000000000..0d19c30b1dc6f
> --- /dev/null
> +++ b/tools/testing/selftests/mm/fork_dax_pud.c
> @@ -0,0 +1,114 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * fork() of a process that maps a device DAX instance with a PUD, in a VMA
> + * whose page tables fork copies because it has a guard region. The child
> + * must account the copied PUD in its RssFile.
> + *
> + * Runs only on a device named in argv[1] or $FORK_DAX_PUD_DEVICE that
> + * nothing else maps, with 1 GiB alignment and at least 2 GiB. To make one
> + * on pmem emulated in RAM, with CONFIG_X86_PMEM_LEGACY and
> + * CONFIG_DEV_DAX_PMEM:
> + *
> + *	boot with memmap=3G!4G
> + *	ndctl create-namespace -f -e namespace0.0 -m devdax -a 1G
> + *	FORK_DAX_PUD_DEVICE=/dev/dax0.0 ./fork_dax_pud
> + */
> +#define _GNU_SOURCE
> +#include <asm-generic/mman.h> /* Force the import of the tools version. */
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <limits.h>
> +#include <setjmp.h>
> +#include <signal.h>
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <unistd.h>
> +#include <sys/mman.h>
> +#include <sys/wait.h>
> +
> +#include "kselftest.h"
> +#include "vm_util.h"
> +
> +#define SZ_1G		(1UL << 30)
> +#define DAX_SYSFS	"/sys/bus/dax/devices"
> +
> +static sigjmp_buf sigbus_jmp;
> +
> +static void sigbus_handler(int sig)
> +{
> +	siglongjmp(sigbus_jmp, 1);
> +}
> +
> +static bool dax_attr(const char *dev, const char *attr, unsigned long *val)
> +{
> +	char path[PATH_MAX];
> +
> +	snprintf(path, sizeof(path), DAX_SYSFS "/%s/%s", dev, attr);
> +	return !read_sysfs(path, val);
> +}
> +
> +static bool dax_dev_ok(const char *path)
> +{
> +	const char *name = strrchr(path, '/');
> +	unsigned long align, size;
> +
> +	name = name ? name + 1 : path;
> +	return dax_attr(name, "align", &align) && align == SZ_1G &&
> +	       dax_attr(name, "size", &size) && size >= 2 * SZ_1G;
> +}
> +
> +int main(int argc, char **argv)
> +{
> +	const char *path = argc > 1 ? argv[1] : getenv("FORK_DAX_PUD_DEVICE");
> +	char *area, *mem;
> +	int fd, status;
> +	pid_t pid;
> +
> +	ksft_print_header();
> +	if (!path)
> +		ksft_exit_skip("no devdax device given\n");
> +	if (!dax_dev_ok(path))
> +		ksft_exit_skip("%s: needs 1 GiB alignment and 2 GiB\n", path);
> +
> +	fd = open(path, O_RDONLY);
> +	if (fd < 0)
> +		ksft_exit_skip("%s: %s\n", path, strerror(errno));
> +	area = mmap(NULL, 3 * SZ_1G, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +	if (area == MAP_FAILED)
> +		ksft_exit_fail_perror("mmap");
> +	mem = (char *)(((unsigned long)area + SZ_1G - 1) & ~(SZ_1G - 1));
> +	mem = mmap(mem, 2 * SZ_1G, PROT_READ, MAP_SHARED | MAP_FIXED, fd, 0);
> +	if (mem == MAP_FAILED)
> +		ksft_exit_skip("mmap %s: %s\n", path, strerror(errno));
> +
> +	/* A PUD in the first GiB, a guard page at the end of the second. */
> +	signal(SIGBUS, sigbus_handler);
> +	if (sigsetjmp(sigbus_jmp, 1))
> +		ksft_exit_skip("%s cannot be mapped with a PUD\n", path);
> +	FORCE_READ(mem[0]);
> +	signal(SIGBUS, SIG_DFL);
> +	if (madvise(mem + 2 * SZ_1G - getpagesize(), getpagesize(),
> +		    MADV_GUARD_INSTALL))
> +		ksft_exit_skip("MADV_GUARD_INSTALL: %s\n", strerror(errno));
> +
> +	ksft_set_plan(1);
> +	fflush(stdout);
> +	pid = fork();
> +	if (pid < 0)
> +		ksft_exit_fail_perror("fork");
> +	if (!pid)
> +		_exit(rss_file() < (SZ_1G >> 10) ? 10 : 0);
> +	if (waitpid(pid, &status, 0) != pid)
> +		ksft_exit_fail_perror("waitpid");
> +
> +	if (!WIFEXITED(status) || WEXITSTATUS(status))
> +		ksft_test_result_fail("child does not account the copied PUD\n");
> +	else
> +		ksft_test_result_pass("devdax PUD copied by fork\n");
> +
> +	munmap(area, 3 * SZ_1G);
> +	close(fd);
> +	ksft_finished();
> +}

I prefer to not merge this. It requires an actual dax device, so it's not very
applicable on CI systems, and we don't want selftests to take parameters in one
form or the other.

-- 
Cheers,

David

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

* Re: [PATCH v2 1/2] mm/huge_memory: take a reference on a folio PUD copied by fork
  2026-11-05 17:00 ` [PATCH v2 1/2] mm/huge_memory: take a reference on a folio " Konstantin Khorenko
  2026-11-05 19:28   ` David Hildenbrand (Arm)
@ 2026-11-05 20:19   ` Zi Yan
  2026-11-05 20:31     ` David Hildenbrand (Arm)
  1 sibling, 1 reply; 7+ messages in thread
From: Zi Yan @ 2026-11-05 20:19 UTC (permalink / raw)
  To: Konstantin Khorenko, Andrew Morton, David Hildenbrand,
	Lorenzo Stoakes, Alistair Popple, Dan Williams
  Cc: linux-mm, nvdimm, linux-kselftest, linux-kernel, stable

On Thu Nov 5, 2026 at 12:00 PM EST, Konstantin Khorenko wrote:
> Device DAX maps its memory with vmf_insert_folio_pud(), which takes a
> folio reference, adds a PUD rmap and accounts the pages to the mm.
> zap_huge_pud() drops all three, but copy_huge_pud() copies the PUD into
> the child without taking any of them. Each child's unmap then drops
> what the parent took:
>
> - the folio is a reference short, so with two children its refcount
>   drops to zero and free_zone_device_folio() runs while the parent
>   still maps it;
> - the child's file counter goes negative, "Bad rss-counter state" when
>   its mm is freed;
> - the parent's unmap drives the entire mapcount below zero.
>
> fork copies the page tables of a device DAX mapping that has a guard
> region, and commit f807123d578d ("mm: allow guard regions in file-backed
> and read-only mappings") allowed MADV_GUARD_INSTALL there.
>
> Take the reference, the rmap and the counter for a PUD that
> vm_normal_page_pud() resolves to a page, as copy_present_ptes() does for
> a PTE, with a new folio_dup_file_rmap_pud() helper. copy_huge_pud() now
> needs both VMAs, so pass only them and take the mms from them.
>
> Fixes: aed877c2b425 ("device/dax: properly refcount device dax pages when mapping")
> Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
> Cc: stable@vger.kernel.org
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
> ---
>  include/linux/huge_mm.h |  5 ++---
>  include/linux/rmap.h    | 22 ++++++++++++++++++++++
>  mm/huge_memory.c        | 27 +++++++++++++++++++--------
>  mm/memory.c             |  4 +---
>  4 files changed, 44 insertions(+), 14 deletions(-)
>

I assume this supersedes David's version[1], which uses
folio_add_file_rmap_pud().

[1] https://lore.kernel.org/all/20261031-fixes_core-v3-11-d6437171ecc3@kernel.org/

LGTM.

Reviewed-by: Zi Yan <ziy@nvidia.com>


-- 
Best Regards,
Yan, Zi


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

* Re: [PATCH v2 1/2] mm/huge_memory: take a reference on a folio PUD copied by fork
  2026-11-05 20:19   ` Zi Yan
@ 2026-11-05 20:31     ` David Hildenbrand (Arm)
  0 siblings, 0 replies; 7+ messages in thread
From: David Hildenbrand (Arm) @ 2026-11-05 20:31 UTC (permalink / raw)
  To: Zi Yan, Konstantin Khorenko, Andrew Morton, Lorenzo Stoakes,
	Alistair Popple, Dan Williams
  Cc: linux-mm, nvdimm, linux-kselftest, linux-kernel, stable

On 11/5/26 21:19, Zi Yan wrote:
> On Thu Nov 5, 2026 at 12:00 PM EST, Konstantin Khorenko wrote:
>> Device DAX maps its memory with vmf_insert_folio_pud(), which takes a
>> folio reference, adds a PUD rmap and accounts the pages to the mm.
>> zap_huge_pud() drops all three, but copy_huge_pud() copies the PUD into
>> the child without taking any of them. Each child's unmap then drops
>> what the parent took:
>>
>> - the folio is a reference short, so with two children its refcount
>>   drops to zero and free_zone_device_folio() runs while the parent
>>   still maps it;
>> - the child's file counter goes negative, "Bad rss-counter state" when
>>   its mm is freed;
>> - the parent's unmap drives the entire mapcount below zero.
>>
>> fork copies the page tables of a device DAX mapping that has a guard
>> region, and commit f807123d578d ("mm: allow guard regions in file-backed
>> and read-only mappings") allowed MADV_GUARD_INSTALL there.
>>
>> Take the reference, the rmap and the counter for a PUD that
>> vm_normal_page_pud() resolves to a page, as copy_present_ptes() does for
>> a PTE, with a new folio_dup_file_rmap_pud() helper. copy_huge_pud() now
>> needs both VMAs, so pass only them and take the mms from them.
>>
>> Fixes: aed877c2b425 ("device/dax: properly refcount device dax pages when mapping")
>> Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
>> Cc: stable@vger.kernel.org
>> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
>> ---
>>  include/linux/huge_mm.h |  5 ++---
>>  include/linux/rmap.h    | 22 ++++++++++++++++++++++
>>  mm/huge_memory.c        | 27 +++++++++++++++++++--------
>>  mm/memory.c             |  4 +---
>>  4 files changed, 44 insertions(+), 14 deletions(-)
>>
> 
> I assume this supersedes David's version[1], which uses
> folio_add_file_rmap_pud().
> 
> [1] https://lore.kernel.org/all/20261031-fixes_core-v3-11-d6437171ecc3@kernel.org/

Ugh, I thought I dropped it to wait for this one here. But turns out the flood
of patches confused me :( (hoping I didn't drop another one by mistake)

It's already part of the fixes PULL request:

https://lore.kernel.org/linux-mm/20261105084704.5703-1-david@kernel.org/

Konstantin, sorry! Can you send a follow up to introduce+use the dup variant?

-- 
Cheers,

David

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

end of thread, other threads:[~2026-11-05 20:31 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-05 17:00 [PATCH v2 0/2] mm/huge_memory: take a reference on a device DAX PUD copied by fork Konstantin Khorenko
2026-11-05 17:00 ` [PATCH v2 1/2] mm/huge_memory: take a reference on a folio " Konstantin Khorenko
2026-11-05 19:28   ` David Hildenbrand (Arm)
2026-11-05 20:19   ` Zi Yan
2026-11-05 20:31     ` David Hildenbrand (Arm)
2026-11-05 17:00 ` [PATCH v2 2/2] selftests/mm: test fork of a device DAX PUD Konstantin Khorenko
2026-11-05 19:30   ` David Hildenbrand (Arm)

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®