* [PATCH 0/4] mm, uio: huge PFN mappings for /dev/mem and UIO
@ 2026-11-11 11:30 Eliav Farber
2026-11-11 11:30 ` [PATCH 1/4] mm: add MMAP_PFN_TRACK action and pfn_range_track() for lazy PFN maps Eliav Farber
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Eliav Farber @ 2026-11-11 11:30 UTC (permalink / raw)
To: Andrew Morton, Greg Kroah-Hartman
Cc: David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Jann Horn, Pedro Falcato, linux-mm, linux-kernel, Eliav Farber
/dev/mem and UIO both hand large, well-aligned physical regions to
userspace, and both populate them eagerly at mmap() time with
remap_pfn_range(), that is with 4K PTEs however large and well aligned
the region is. vfio-pci moved to fault-driven population in commit
f9e54c3a2f5b ("vfio/pci: implement huge_fault support") and gets PMD and
PUD mappings where alignment allows. This series does the same for
/dev/mem and UIO, and factors out the logic vfio-pci open-codes so that a
third driver does not have to repeat it.
Two things have to be solved to do this outside vfio-pci:
1. remap_pfn_range() reserves the range's memory type in the PAT memtype
tree, via pfnmap_track() -> reserve_pfn_range(). The vmf_insert_pfn*()
helpers only look the memory type up, they never reserve. A driver
that populates lazily therefore loses the conflict detection that
rejects a second mapping of the same physical range with a
conflicting cache attribute. Patch 1 adds pfn_range_track(), which
sets the VMA up as remap_pfn_range() would and reserves the range for
the VMA's lifetime, but leaves population to the fault handler. For
->mmap_prepare drivers, where no VMA exists yet, it adds an
MMAP_PFN_TRACK action.
2. The order dispatch itself: the window and alignment checks, and the
pfn_modify_allowed() check that vmf_insert_pfn_pmd() and
vmf_insert_pfn_pud() skip but the order-0 path performs. Patch 2 adds
vmf_pfn_order_ok() and vmf_insert_pfn_order(), split in two so a
driver can apply its own policy between the check and the insert.
Patches 3 and 4 convert /dev/mem and UIO to use them.
Patch 2 also passes the fault's write flag to vmf_insert_pfn_pmd() and
vmf_insert_pfn_pud(), as the DAX and DRM ->huge_fault handlers do, rather
than the literal false that vfio-pci passes. With write=false,
insert_pmd() leaves a present read-only entry in place and returns
VM_FAULT_NOPAGE, and wp_huge_pmd() splits the entry only on
VM_FAULT_FALLBACK, so a store to a mapping that mm write-protected for
tracking (soft-dirty, uffd-wp, fs writeback) faults forever. vfio-pci has
the same problem today and has no ->pfn_mkwrite either; that needs a
separate patch, which I have not written.
Testing
=======
Tested on x86_64 at 7.4-rc2, and on arm64 at 7.3.4, which is what the
test hardware runs. The four patches are identical in both trees.
On arm64 Cortex-A75: a local selftest over UIO map0, UIO map1 and
/dev/mem reports 64 passed, 0 failed and 3 skipped of 67, the skips
being the soft-dirty case, which needs CONFIG_MEM_SOFT_DIRTY.
tools/testing/selftests/mm/pfnmap.c passes 6/6 on /dev/mem and 6/6 on
/dev/uio0. dmesg is free of warnings and call traces. The fault counts
confirm the huge path: 4 faults for four PMD windows where a 4K mapping
needs 2048, 2 faults for a two-window PMD-offset mapping, 1024 for a
4K-offset mapping of 1024 pages, 2048 for a deliberately misaligned VA,
and 0 on a second pass.
x86_64 under QEMU, four kernel configs, with a 4G ivshmem BAR as the test
region and a small lab module exposing it through UIO:
THP always, DEBUG_VM=y 30/30
the same plus lockdep 30/30
THP=n 30/30
THP always, MEM_SOFT_DIRTY=y 31/31
The checks cover the PMD and PUD paths and their fallbacks, PAT
reservation and the rejection of a conflicting cache attribute over a
tracked range, iomem revoke and re-population after release, COW
mappings, fork, remote access through /proc/pid/mem, and the UIO entry
point. The soft-dirty config adds the write-protected store case; the
others report it as a single skip.
On x86, tools/testing/selftests/mm/pfnmap.c passes 6/6 pointed at the
UIO device. Reaching the /dev/mem entry point needs STRICT_DEVMEM=n, as
the test's System RAM target is otherwise refused, so a fifth config was
built with it disabled; it passes 6/6 there too. The suite above is not
a useful instrument in that config, four of its checks being
STRICT_DEVMEM behaviour that inverts when it is off.
The write flag in patch 2 was confirmed by building the same tree twice
and changing nothing else: with the fault's write flag passed, a store to
a soft-dirty write-protected PUD and PMD mapping completes; with the
literal false restored, both stores time out in a fault loop under a 10s
alarm.
Build-only checks, all with KCFLAGS=-Werror W=1:
m68k ColdFire, CONFIG_MMU=n vmlinux links
riscv64, THP and UIO enabled mm/ and drivers/uio/ build
tools/testing/vma 29/29
riscv64 is the only other architecture selecting
HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD, and its defconfig leaves THP and UIO
off, so both were turned on for that build.
Two things none of this reaches: 1G PUD mappings anywhere but x86, as
the test hardware has no suitable 1G-aligned region, and the huge zero
folio fallback in patch 2, which needs STRICT_DEVMEM=n and has no check
targeting it.
Performance
===========
Measured on arm64 Cortex-A75 at 7.3.4, 64-byte stride, medians of 5
runs. Three kernels: the base commit, which populates eagerly at mmap()
as today; this series; and this series with CONFIG_TRANSPARENT_HUGEPAGE
off, which disables ARCH_SUPPORTS_HUGE_PFNMAP and so makes
vmf_insert_pfn_order() fall back to order 0.
A 256MB /dev/mem region:
eager 4K series series, 4K
mmap() 421,536 ns 4,456 ns 3,912 ns
first touch 2.60 ms 2.66 ms 41.42 ms
faults 0 128 65536
setup total 3.03 ms 2.66 ms 41.42 ms
dTLB read misses 658,700 8,247 660,730
sweep per access 39.01 ns 39.49 ns 39.28 ns
An 8MB UIO region:
eager 4K series series, 4K
mmap() 16,048 ns 2,888 ns 2,536 ns
first touch 81 us 82 us 1.34 ms
faults 0 4 2048
setup total 97 us 85 us 1.34 ms
dTLB read misses 20,503 70 20,499
sweep per access 39.12 ns 38.77 ns 39.80 ns
Against today's eager mapping, the series cuts mmap() by 95x on the
256MB region and 5.6x on the 8MB one, as populating the range no longer
happens inside the syscall. Total setup, for a consumer that goes on to
touch every page, improves by about 12% on both targets: the work moves
out of mmap() into the fault handler rather than disappearing. A
consumer that touches only part of the range pays only for that part,
which this benchmark cannot show because it touches everything.
The dTLB footprint falls about 80x on the 256MB region, and from 20,503
to 70 on the UIO one. That does not become throughput here: the
per-access sweep cost is the same in all three kernels, because at 39 ns
per 64-byte access the sweep is bound by the device memory path and
page-table walks hide underneath it.
The third column is the series with huge entries disabled. Nobody would
run it; it is there to separate what huge entries contribute from what
fault-driven population contributes, and it is the worst of the three,
paying 65536 faults at about 600 ns each.
Touching the same 256MB region in random page order leaves the dTLB
counts at 552,176 for the series against 662,577 eager. I do not have an
explanation for that: 128 PMD entries should cover the region, so random
order was expected to benefit as well.
Eliav Farber (4):
mm: add MMAP_PFN_TRACK action and pfn_range_track() for lazy PFN maps
mm: add vmf_insert_pfn_order() and vmf_pfn_order_ok() helpers
mm/char-mem: implement huge_fault support for /dev/mem
uio: implement huge_fault support for physical mappings
drivers/uio/uio.c | 102 +++++++++++++++++++++--
include/linux/mm.h | 47 +++++++++++
include/linux/mm_types.h | 1 +
mm/char-mem.c | 103 ++++++++++++++++++++++-
mm/internal.h | 3 +
mm/memory.c | 142 ++++++++++++++++++++++++++++++++
mm/util.c | 7 ++
tools/testing/vma/include/dup.h | 1 +
8 files changed, 396 insertions(+), 10 deletions(-)
base-commit: 067c5c24cef23d913b67eaaf71ae67e7169a6620
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 1/4] mm: add MMAP_PFN_TRACK action and pfn_range_track() for lazy PFN maps
2026-11-11 11:30 [PATCH 0/4] mm, uio: huge PFN mappings for /dev/mem and UIO Eliav Farber
@ 2026-11-11 11:30 ` Eliav Farber
2026-11-11 11:30 ` [PATCH 2/4] mm: add vmf_insert_pfn_order() and vmf_pfn_order_ok() helpers Eliav Farber
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Eliav Farber @ 2026-11-11 11:30 UTC (permalink / raw)
To: Andrew Morton, Greg Kroah-Hartman
Cc: David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Jann Horn, Pedro Falcato, linux-mm, linux-kernel, Eliav Farber
Drivers that expose a physical PFN range to userspace have traditionally
used remap_pfn_range(), which both populates the page tables eagerly and,
on x86, reserves the range's memory type in the PAT memtype tree via
pfnmap_track() -> reserve_pfn_range() -> memtype_reserve(). That
reservation is what lets the kernel detect and reject a second mapping of
the same physical range with a conflicting cache attribute (e.g. WB vs
UC/WC), which can otherwise cause data corruption or inconsistent cache
state.
Drivers that instead populate a PFN mapping lazily from a fault handler
(via vmf_insert_pfn()/vmf_insert_pfn_pmd()/vmf_insert_pfn_pud(), e.g. to
obtain huge PFN mappings on fault) get no such reservation: those helpers
only call pfnmap_setup_cachemode_pfn(), a lookup-only wrapper that never
reserves. Such mappings are therefore never registered in the PAT memtype
tree and conflicting mappings go undetected.
Add pfn_range_track(): it sets up the VMA exactly as remap_pfn_range()
would (VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP flags), reserves
and tracks the whole VMA's PFN range for its lifetime by attaching a
pfnmap_track_ctx to the VMA (released automatically on VMA teardown),
but does not populate any page tables - population is left to the
caller's fault handler. COW mappings are rejected, as the fault-driven
vmf_insert_pfn*() helpers cannot populate them.
On architectures without PFN tracking (!__HAVE_PFNMAP_TRACKING) the
reservation is a no-op, matching remap_pfn_range()'s notrack path; the
VMA flag setup still happens.
For drivers that use ->mmap_prepare, where no VMA exists yet, add an
MMAP_PFN_TRACK mmap action and a mmap_action_pfn_track() helper. It sets
up the VMA like MMAP_REMAP_PFN, and on completion tracks the range like
pfn_range_track() instead of populating it. Mirror the new action type
in the tools/testing/vma copy of the enum.
Signed-off-by: Eliav Farber <farbere@amazon.com>
---
include/linux/mm.h | 19 +++++++
include/linux/mm_types.h | 1 +
mm/internal.h | 3 ++
mm/memory.c | 93 +++++++++++++++++++++++++++++++++
mm/util.c | 7 +++
tools/testing/vma/include/dup.h | 1 +
6 files changed, 124 insertions(+)
diff --git a/include/linux/mm.h b/include/linux/mm.h
index 6af3b7008f1a..c11bdee1596a 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -4752,6 +4752,23 @@ static inline void mmap_action_ioremap_full(struct vm_area_desc *desc,
mmap_action_ioremap(desc, desc->start, start_pfn, vma_desc_size(desc));
}
+/**
+ * mmap_action_pfn_track - helper for mmap_prepare hook to specify that the PFN
+ * range backing the entirety of a VMA should be tracked, but not mapped.
+ * @desc: The VMA descriptor for the VMA.
+ * @start_pfn: The first PFN in the range.
+ *
+ * The VMA is set up as for mmap_action_remap_full(), but no page tables are
+ * populated: the mapping is populated on demand from the driver's fault
+ * handler. Must not be used for a COW mapping. See pfn_range_track().
+ */
+static inline void mmap_action_pfn_track(struct vm_area_desc *desc,
+ unsigned long start_pfn)
+{
+ mmap_action_remap_full(desc, start_pfn);
+ desc->action.type = MMAP_PFN_TRACK;
+}
+
/**
* mmap_action_simple_ioremap - helper for mmap_prepare hook to specify that the
* physical range in [start_phys_addr, start_phys_addr + size) should be I/O
@@ -4973,6 +4990,8 @@ struct vm_area_struct *find_extend_vma_locked(struct mm_struct *,
unsigned long addr);
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t pgprot);
+int pfn_range_track(struct vm_area_struct *vma, unsigned long addr,
+ unsigned long pfn, unsigned long size);
int vm_insert_page(struct vm_area_struct *, unsigned long addr, struct page *);
int vm_insert_pages(struct vm_area_struct *vma, unsigned long addr,
diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h
index 1ed35d258d2e..b4dfc1b12c2e 100644
--- a/include/linux/mm_types.h
+++ b/include/linux/mm_types.h
@@ -821,6 +821,7 @@ enum mmap_action_type {
MMAP_SIMPLE_IO_REMAP, /* I/O remap with guardrails. */
MMAP_KERNEL_PAGES, /* Map kernel page range from array. */
MMAP_DISCONTIG_KERNEL_PAGES, /* Map kernel discontig page range. */
+ MMAP_PFN_TRACK, /* Track PFN range, map on fault. */
};
enum discontig_kernel_page_action {
diff --git a/mm/internal.h b/mm/internal.h
index e3f92883bf23..58a81f6041a9 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1546,6 +1546,9 @@ int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm);
int remap_pfn_range_prepare(struct vm_area_desc *desc);
int remap_pfn_range_complete(struct vm_area_struct *vma,
struct mmap_action *action);
+int pfn_range_track_prepare(struct vm_area_desc *desc);
+int pfn_range_track_complete(struct vm_area_struct *vma,
+ struct mmap_action *action);
int simple_ioremap_prepare(struct vm_area_desc *desc);
int map_kernel_pages_prepare(struct vm_area_desc *desc);
int map_kernel_pages_complete(struct vm_area_struct *vma,
diff --git a/mm/memory.c b/mm/memory.c
index ec3d9d99ccee..ab29ec719ae1 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -3420,6 +3420,99 @@ int remap_pfn_range_complete(struct vm_area_struct *vma,
return do_remap_pfn_range(vma, start, pfn, size, prot);
}
+#ifdef __HAVE_PFNMAP_TRACKING
+static int do_pfn_range_track(struct vm_area_struct *vma, unsigned long pfn,
+ unsigned long size)
+{
+ pgprot_t prot = vma->vm_page_prot;
+ struct pfnmap_track_ctx *ctx;
+
+ if (vma->pfnmap_track_ctx)
+ return -EINVAL;
+
+ /*
+ * prot only receives the reserved cachemode here; the fault-time
+ * vmf_insert_pfn*() helpers look it up again via
+ * pfnmap_setup_cachemode_pfn().
+ */
+ ctx = pfnmap_track_ctx_alloc(pfn, PAGE_ALIGN(size), &prot);
+ if (IS_ERR(ctx))
+ return PTR_ERR(ctx);
+
+ vma->pfnmap_track_ctx = ctx;
+ return 0;
+}
+#else
+static int do_pfn_range_track(struct vm_area_struct *vma, unsigned long pfn,
+ unsigned long size)
+{
+ /* No PFN tracking on this arch; cachemode is left as-is. */
+ return 0;
+}
+#endif
+
+/**
+ * pfn_range_track - reserve and track a PFN range for a VMA without populating
+ * @vma: user vma to track the range for
+ * @addr: target page aligned user address to start at
+ * @pfn: page frame number of kernel physical memory address
+ * @size: size of mapping area
+ *
+ * Reserves the PFN range's memory type (PAT on x86) and attaches a tracking
+ * context to @vma for its lifetime, so that conflicting cache-attribute
+ * mappings of the same physical range are detected, but does not create any
+ * page table entries. Intended for drivers that populate the mapping lazily
+ * from a fault handler (e.g. via vmf_insert_pfn()), where the fault helpers
+ * only look up the cachemode and do not perform the reservation themselves. The
+ * VMA flags remap_pfn_range() would have set are set here too.
+ *
+ * Must cover the whole VMA. Must not be a COW mapping: the fault-driven
+ * vmf_insert_pfn*() helpers cannot populate one. Must be called with the mmap
+ * lock held for writing: it sets VMA flags and attaches the tracking context.
+ *
+ * Return: %0 on success, negative error code otherwise.
+ */
+int pfn_range_track(struct vm_area_struct *vma, unsigned long addr,
+ unsigned long pfn, unsigned long size)
+{
+ int err;
+
+ if (vma_is_cow_mapping(vma))
+ return -EINVAL;
+
+ if (addr != vma->vm_start || addr + PAGE_ALIGN(size) != vma->vm_end)
+ return -EINVAL;
+
+ err = remap_pfn_range_prepare_vma(vma, addr, pfn, size);
+ if (err)
+ return err;
+
+ return do_pfn_range_track(vma, pfn, size);
+}
+EXPORT_SYMBOL_GPL(pfn_range_track);
+
+int pfn_range_track_prepare(struct vm_area_desc *desc)
+{
+ const struct mmap_action *action = &desc->action;
+
+ /* See pfn_range_track(). */
+ if (vma_desc_is_cow_mapping(desc))
+ return -EINVAL;
+
+ if (action->remap.start != desc->start ||
+ action->remap.size != vma_desc_size(desc))
+ return -EINVAL;
+
+ return remap_pfn_range_prepare(desc);
+}
+
+int pfn_range_track_complete(struct vm_area_struct *vma,
+ struct mmap_action *action)
+{
+ return do_pfn_range_track(vma, action->remap.start_pfn,
+ action->remap.size);
+}
+
static int __simple_ioremap_prep(unsigned long vm_len, pgoff_t vm_pgoff,
phys_addr_t start_phys, unsigned long size,
unsigned long *pfnp)
diff --git a/mm/util.c b/mm/util.c
index 80f6c714844f..5332030ed370 100644
--- a/mm/util.c
+++ b/mm/util.c
@@ -1546,6 +1546,8 @@ int mmap_action_prepare(struct vm_area_desc *desc)
return 0;
case MMAP_REMAP_PFN:
return remap_pfn_range_prepare(desc);
+ case MMAP_PFN_TRACK:
+ return pfn_range_track_prepare(desc);
case MMAP_IO_REMAP_PFN:
return io_remap_pfn_range_prepare(desc);
case MMAP_SIMPLE_IO_REMAP:
@@ -1583,6 +1585,9 @@ int mmap_action_complete(struct vm_area_struct *vma,
case MMAP_REMAP_PFN:
err = remap_pfn_range_complete(vma, action);
break;
+ case MMAP_PFN_TRACK:
+ err = pfn_range_track_complete(vma, action);
+ break;
case MMAP_KERNEL_PAGES:
err = map_kernel_pages_complete(vma, action);
break;
@@ -1611,6 +1616,7 @@ int mmap_action_prepare(struct vm_area_desc *desc)
case MMAP_SIMPLE_IO_REMAP:
case MMAP_KERNEL_PAGES:
case MMAP_DISCONTIG_KERNEL_PAGES:
+ case MMAP_PFN_TRACK:
WARN_ON_ONCE(1); /* nommu cannot handle these. */
break;
}
@@ -1633,6 +1639,7 @@ int mmap_action_complete(struct vm_area_struct *vma,
case MMAP_SIMPLE_IO_REMAP:
case MMAP_KERNEL_PAGES:
case MMAP_DISCONTIG_KERNEL_PAGES:
+ case MMAP_PFN_TRACK:
WARN_ON_ONCE(1); /* nommu cannot handle this. */
err = -EINVAL;
diff --git a/tools/testing/vma/include/dup.h b/tools/testing/vma/include/dup.h
index dc4927d26296..7bb7392c7fb5 100644
--- a/tools/testing/vma/include/dup.h
+++ b/tools/testing/vma/include/dup.h
@@ -455,6 +455,7 @@ enum mmap_action_type {
MMAP_SIMPLE_IO_REMAP, /* I/O remap with guardrails. */
MMAP_KERNEL_PAGES, /* Map kernel page range from array. */
MMAP_DISCONTIG_KERNEL_PAGES, /* Map kernel discontig page range. */
+ MMAP_PFN_TRACK, /* Track PFN range, map on fault. */
};
/*
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 2/4] mm: add vmf_insert_pfn_order() and vmf_pfn_order_ok() helpers
2026-11-11 11:30 [PATCH 0/4] mm, uio: huge PFN mappings for /dev/mem and UIO Eliav Farber
2026-11-11 11:30 ` [PATCH 1/4] mm: add MMAP_PFN_TRACK action and pfn_range_track() for lazy PFN maps Eliav Farber
@ 2026-11-11 11:30 ` Eliav Farber
2026-11-11 11:30 ` [PATCH 3/4] mm/char-mem: implement huge_fault support for /dev/mem Eliav Farber
2026-11-11 11:30 ` [PATCH 4/4] uio: implement huge_fault support for physical mappings Eliav Farber
3 siblings, 0 replies; 5+ messages in thread
From: Eliav Farber @ 2026-11-11 11:30 UTC (permalink / raw)
To: Andrew Morton, Greg Kroah-Hartman
Cc: David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Jann Horn, Pedro Falcato, linux-mm, linux-kernel, Eliav Farber
->fault/->huge_fault handlers of VM_PFNMAP mappings that want huge PFN
mappings open-code the same steps: check that the order-aligned fault
window lies within the VMA and that the pfn is aligned to the order,
then call vmf_insert_pfn(), vmf_insert_pfn_pmd() or vmf_insert_pfn_pud()
depending on the order, guarded by
CONFIG_ARCH_SUPPORTS_{PMD,PUD}_PFNMAP, and fall back to a smaller order
otherwise. vfio-pci has its own is_aligned_for_order() and
vfio_pci_vmf_insert_pfn() for this.
Add vmf_pfn_order_ok() for the window and alignment check, and
vmf_insert_pfn_order() for the order dispatch. The latter also checks
pfn_modify_allowed() for huge orders: the order-0 vmf_insert_pfn() does
this, but vmf_insert_pfn_pmd() and vmf_insert_pfn_pud() do not. The two
are separate so that a driver can apply its own policy (access checks,
device locking) between them.
vmf_insert_pfn_order() passes the fault's write flag to
vmf_insert_pfn_pmd() and vmf_insert_pfn_pud(), as the DAX and DRM
->huge_fault handlers do. With write=false, insert_pmd() leaves a
read-only entry in place and returns VM_FAULT_NOPAGE, so a store to a
mapping that mm write-protected for tracking (soft-dirty, uffd-wp)
faults forever; wp_huge_pmd() splits the entry only when the handler
returns VM_FAULT_FALLBACK. Passing it marks the entry young, dirty and,
where the VMA allows, writable, as wp_page_reuse() does at PTE level.
The order-0 path needs nothing: wp_pfn_shared() resolves that fault.
The helper also declines the huge zero folio's pfn, which
vmf_insert_pfn_pmd() refuses with SIGBUS, so that the fault is retried
at PTE order where it maps.
They will be used by the /dev/mem and UIO huge_fault handlers added
later in this series.
Signed-off-by: Eliav Farber <farbere@amazon.com>
---
include/linux/mm.h | 28 ++++++++++++++++++++++++++
mm/memory.c | 49 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 77 insertions(+)
diff --git a/include/linux/mm.h b/include/linux/mm.h
index c11bdee1596a..57437b317709 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -5010,6 +5010,34 @@ vm_fault_t vmf_insert_mixed(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn);
vm_fault_t vmf_insert_mixed_mkwrite(struct vm_area_struct *vma,
unsigned long addr, unsigned long pfn);
+vm_fault_t vmf_insert_pfn_order(struct vm_fault *vmf, unsigned long pfn,
+ unsigned int order);
+
+/**
+ * vmf_pfn_order_ok - check that a pfn can be mapped at a given fault order
+ * @vmf: The fault being handled.
+ * @pfn: The pfn to map at the start of the order-aligned fault window.
+ * @order: The fault order (0 for a single page).
+ *
+ * For a huge (order > 0) fault, the order-aligned window around @vmf->address
+ * must lie within the VMA and @pfn must be aligned to @order. @pfn is only
+ * looked at once the window is known to lie within the VMA, so a caller may
+ * compute it before calling, even when the window starts below vm_start.
+ *
+ * Return: true if vmf_insert_pfn_order() may map @pfn at @order, false if the
+ * fault should fall back to a smaller order.
+ */
+static inline bool vmf_pfn_order_ok(const struct vm_fault *vmf,
+ unsigned long pfn, unsigned int order)
+{
+ const struct vm_area_struct *vma = vmf->vma;
+ unsigned long addr = vmf->address & ~((PAGE_SIZE << order) - 1);
+
+ return !order || (addr >= vma->vm_start &&
+ addr + (PAGE_SIZE << order) <= vma->vm_end &&
+ IS_ALIGNED(pfn, 1UL << order));
+}
+
int vm_iomap_memory(struct vm_area_struct *vma, phys_addr_t start, unsigned long len);
static inline vm_fault_t vmf_insert_page(struct vm_area_struct *vma,
diff --git a/mm/memory.c b/mm/memory.c
index ab29ec719ae1..0c583442e049 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -3002,6 +3002,55 @@ vm_fault_t vmf_insert_pfn(struct vm_area_struct *vma, unsigned long addr,
}
EXPORT_SYMBOL(vmf_insert_pfn);
+/**
+ * vmf_insert_pfn_order - insert a pfn mapping of a given fault order
+ * @vmf: The fault being handled.
+ * @pfn: The pfn to map at the start of the order-aligned fault window.
+ * @order: The fault order (0 for a single page).
+ *
+ * Helper for ->fault and ->huge_fault handlers of VM_PFNMAP mappings: maps @pfn
+ * with a PTE for order 0, or with a PMD/PUD for PMD_ORDER/PUD_ORDER where the
+ * architecture supports huge PFN mappings. The caller must have checked the
+ * window and the pfn alignment with vmf_pfn_order_ok().
+ *
+ * vma cannot be a COW mapping.
+ *
+ * Return: vm_fault_t value; %VM_FAULT_FALLBACK if @order cannot be mapped, so
+ * that the fault is retried at a smaller order.
+ */
+vm_fault_t vmf_insert_pfn_order(struct vm_fault *vmf, unsigned long pfn,
+ unsigned int order)
+{
+ struct vm_area_struct *vma = vmf->vma;
+ bool write = vmf->flags & FAULT_FLAG_WRITE;
+
+ if (!order)
+ return vmf_insert_pfn(vma, vmf->address, pfn);
+
+ /*
+ * vmf_insert_pfn_pmd() refuses the huge zero folio's pfn with SIGBUS,
+ * while a PTE mapping of it is fine, so fall back to a smaller order.
+ */
+ if (is_huge_zero_pfn(pfn))
+ return VM_FAULT_FALLBACK;
+
+ /*
+ * vmf_insert_pfn_pmd()/_pud() don't call pfn_modify_allowed(), unlike
+ * the order-0 path, so check it explicitly for the L1TF mitigation.
+ */
+ if (!pfn_modify_allowed(pfn, vma->vm_page_prot))
+ return VM_FAULT_FALLBACK;
+
+ if (IS_ENABLED(CONFIG_ARCH_SUPPORTS_PMD_PFNMAP) && order == PMD_ORDER)
+ return vmf_insert_pfn_pmd(vmf, pfn, write);
+
+ if (IS_ENABLED(CONFIG_ARCH_SUPPORTS_PUD_PFNMAP) && order == PUD_ORDER)
+ return vmf_insert_pfn_pud(vmf, pfn, write);
+
+ return VM_FAULT_FALLBACK;
+}
+EXPORT_SYMBOL_GPL(vmf_insert_pfn_order);
+
static bool vm_mixed_ok(struct vm_area_struct *vma, unsigned long pfn,
bool mkwrite)
{
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 3/4] mm/char-mem: implement huge_fault support for /dev/mem
2026-11-11 11:30 [PATCH 0/4] mm, uio: huge PFN mappings for /dev/mem and UIO Eliav Farber
2026-11-11 11:30 ` [PATCH 1/4] mm: add MMAP_PFN_TRACK action and pfn_range_track() for lazy PFN maps Eliav Farber
2026-11-11 11:30 ` [PATCH 2/4] mm: add vmf_insert_pfn_order() and vmf_pfn_order_ok() helpers Eliav Farber
@ 2026-11-11 11:30 ` Eliav Farber
2026-11-11 11:30 ` [PATCH 4/4] uio: implement huge_fault support for physical mappings Eliav Farber
3 siblings, 0 replies; 5+ messages in thread
From: Eliav Farber @ 2026-11-11 11:30 UTC (permalink / raw)
To: Andrew Morton, Greg Kroah-Hartman
Cc: David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Jann Horn, Pedro Falcato, linux-mm, linux-kernel, Eliav Farber
/dev/mem is commonly used to map large physical regions (reserved
memory, device memory) into userspace. mmap_mem_prepare() maps the whole
range at mmap() time with 4K PTEs, however large and well aligned it is,
which costs many page-table entries and TLB misses.
Map non-COW mappings on fault instead, as vfio-pci does since commit
f9e54c3a2f5b ("vfio/pci: implement huge_fault support"): request
MMAP_PFN_TRACK rather than an eager remap, so the VMA is set up and its
memory type tracked as before, and add ->fault/->huge_fault handlers
that map the faulting window with vmf_insert_pfn_order(), using a PMD or
PUD where alignment allows. COW mappings cannot be populated by
vmf_insert_pfn*() and stay eager; if revoke_iomem() zaps them, the fault
handler refuses them with SIGBUS, as before.
The handler re-checks range_is_allowed(): with CONFIG_IO_STRICT_DEVMEM,
revoke_iomem() zaps a range once a driver claims it, and a fault must
not map it back in. With CONFIG_STRICT_DEVMEM this costs one
devmem_is_allowed() call per page of the faulting window.
Unlike the eager mapping, a revoke is not permanent: once the driver
releases the range, a fault repopulates the mapping, as a fresh mmap()
of it would.
generic_access_phys() only follows present PTEs, so .access now faults
the page in first; otherwise remote access (ptrace, /proc/pid/mem) to an
untouched page would fail. A mapping that does not permit the fault,
PROT_NONE for example, keeps that gap: fixup_user_fault() refuses it,
where the eager mapping left a present PTE.
Validation at mmap() time is unchanged, except for the L1TF pfn check:
the eager mapping rejected a disallowed pfn with -EAGAIN from mmap(),
where the fault handler now raises SIGBUS on first touch.
vmf_insert_pfn() refuses the zero pfn, where remap_pfn_range() mapped
it, so a page of RAM that happens to be the zero page now SIGBUSes.
That needs /dev/mem access to RAM, i.e. a kernel without
CONFIG_STRICT_DEVMEM.
Without huge PFN mappings, or for small or unaligned mappings, the
range is populated one page per fault, paid only for pages that are
actually touched.
Signed-off-by: Eliav Farber <farbere@amazon.com>
---
mm/char-mem.c | 103 ++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 99 insertions(+), 4 deletions(-)
diff --git a/mm/char-mem.c b/mm/char-mem.c
index c5761ddedbd7..b9ed8473baeb 100644
--- a/mm/char-mem.c
+++ b/mm/char-mem.c
@@ -318,9 +318,92 @@ static inline int private_mapping_ok(struct vm_area_desc *desc)
}
#endif
+#ifdef CONFIG_MMU
+/*
+ * Populate /dev/mem mappings on fault, using a PMD or PUD mapping where the
+ * architecture, the fault address and the pfn allow it.
+ */
+static vm_fault_t mmap_mem_huge_fault(struct vm_fault *vmf, unsigned int order)
+{
+ unsigned long addr = vmf->address & ~((PAGE_SIZE << order) - 1);
+ struct vm_area_struct *vma = vmf->vma;
+ unsigned long pfn;
+
+ /*
+ * COW mappings are mapped eagerly at mmap() time and only lose their
+ * pages to revoke_iomem(); keep them unmapped, as without a fault
+ * handler. vmf_insert_pfn*() would also BUG_ON them.
+ */
+ if (vma_is_cow_mapping(vma))
+ return VM_FAULT_SIGBUS;
+
+ /*
+ * vm_pgoff is the base pfn of the mapping. vmf_pfn_order_ok() ignores
+ * pfn if the window starts below vm_start.
+ */
+ pfn = vma->vm_pgoff + ((addr - vma->vm_start) >> PAGE_SHIFT);
+ if (!vmf_pfn_order_ok(vmf, pfn, order))
+ return VM_FAULT_FALLBACK;
+
+ /*
+ * Access was checked at mmap() time, but with CONFIG_IO_STRICT_DEVMEM
+ * revoke_iomem() zaps the mapping once a driver claims part of the
+ * range. Re-check here so a revoked page is not faulted back in. Like
+ * the mmap()-time check, it is not serialised against revoke_iomem(),
+ * so a fault that passed it can still install a page revoked right
+ * after.
+ */
+ if (!range_is_allowed(pfn, PAGE_SIZE << order))
+ return order ? VM_FAULT_FALLBACK : VM_FAULT_SIGBUS;
+
+ return vmf_insert_pfn_order(vmf, pfn, order);
+}
+
+static vm_fault_t mmap_mem_fault(struct vm_fault *vmf)
+{
+ return mmap_mem_huge_fault(vmf, 0);
+}
+#endif /* CONFIG_MMU */
+
+#ifdef CONFIG_HAVE_IOREMAP_PROT
+/*
+ * generic_access_phys() only follows present PTEs, and a lazily populated
+ * mapping has none for pages the target has not touched yet. Fault the page in
+ * first, so that remote access (ptrace, /proc/pid/mem) keeps working.
+ *
+ * fixup_user_fault() refuses a fault the vma does not permit, so an untouched
+ * page of a mapping without VM_READ stays unreadable, where the eager mapping
+ * left a present PTE.
+ */
+static int mmap_mem_access(struct vm_area_struct *vma, unsigned long addr,
+ void *buf, int len, int write)
+{
+ unsigned int fault_flags = FAULT_FLAG_REMOTE;
+
+ if (write)
+ fault_flags |= FAULT_FLAG_WRITE;
+
+ /*
+ * COW mappings are populated at mmap() time, and a write fault would
+ * copy the page instead. With unlocked == NULL, fixup_user_fault()
+ * keeps the mmap lock held and the vma valid.
+ */
+ if (!vma_is_cow_mapping(vma))
+ fixup_user_fault(vma->vm_mm, addr, fault_flags, NULL);
+
+ return generic_access_phys(vma, addr, buf, len, write);
+}
+#endif
+
static const struct vm_operations_struct mmap_mem_ops = {
+#ifdef CONFIG_MMU
+ .fault = mmap_mem_fault,
+#endif
+#ifdef CONFIG_ARCH_SUPPORTS_HUGE_PFNMAP
+ .huge_fault = mmap_mem_huge_fault,
+#endif
#ifdef CONFIG_HAVE_IOREMAP_PROT
- .access = generic_access_phys
+ .access = mmap_mem_access,
#endif
};
@@ -356,11 +439,23 @@ static int mmap_mem_prepare(struct vm_area_desc *desc)
desc->page_prot);
desc->vm_ops = &mmap_mem_ops;
-
- /* Remap-pfn-range will mark the range with the I/O flag. */
- mmap_action_remap_full(desc, desc->pgoff);
desc->action.error_override = -EAGAIN;
+ if (vma_desc_is_cow_mapping(desc)) {
+ /*
+ * vmf_insert_pfn*() cannot populate COW mappings: map them now.
+ * Remap-pfn-range will mark the range with the I/O flag.
+ */
+ mmap_action_remap_full(desc, desc->pgoff);
+ return 0;
+ }
+
+ /*
+ * Populate on fault (see mmap_mem_huge_fault()). Tracking the PFN range
+ * keeps its memory type reserved, for PAT conflict detection.
+ */
+ mmap_action_pfn_track(desc, desc->pgoff);
+
return 0;
}
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 4/4] uio: implement huge_fault support for physical mappings
2026-11-11 11:30 [PATCH 0/4] mm, uio: huge PFN mappings for /dev/mem and UIO Eliav Farber
` (2 preceding siblings ...)
2026-11-11 11:30 ` [PATCH 3/4] mm/char-mem: implement huge_fault support for /dev/mem Eliav Farber
@ 2026-11-11 11:30 ` Eliav Farber
3 siblings, 0 replies; 5+ messages in thread
From: Eliav Farber @ 2026-11-11 11:30 UTC (permalink / raw)
To: Andrew Morton, Greg Kroah-Hartman
Cc: David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Jann Horn, Pedro Falcato, linux-mm, linux-kernel, Eliav Farber
UIO physical mappings (UIO_MEM_PHYS, UIO_MEM_IOVA) often expose large
device memory regions to userspace. uio_mmap_physical() maps them at
mmap() time with remap_pfn_range(), i.e. with 4K PTEs, even when the
region and the user address are aligned for huge mappings.
Map non-COW mappings on fault instead, as vfio-pci does since commit
f9e54c3a2f5b ("vfio/pci: implement huge_fault support"): call
pfn_range_track() rather than remap_pfn_range(), so the VMA is set up
and its memory type tracked as before, and add ->fault/->huge_fault
handlers that map the faulting window with vmf_insert_pfn_order(), using
a PMD or PUD where alignment allows. Like uio_vma_fault(), the handlers
take idev->info_lock and fail with SIGBUS once the device is gone. COW
mappings cannot be populated by vmf_insert_pfn*() and stay eager.
vm_pgoff holds the UIO map index, which a fault handler cannot use: a
VMA split (partial munmap(), mprotect(), ...) adds the split offset to
vm_pgoff of the upper part, so the index no longer identifies the map.
Store the base pfn in vm_pgoff instead, as remap_pfn_range() does for
COW mappings; splits then keep it correct. As a side effect,
/proc/<pid>/maps shows the physical address as the offset of such a
mapping, rather than N * PAGE_SIZE.
generic_access_phys() only follows present PTEs, so .access now faults
the page in first; otherwise remote access (ptrace, /proc/pid/mem) to an
untouched page would fail. A mapping that does not permit the fault,
PROT_NONE for example, keeps that gap: fixup_user_fault() refuses it,
where the eager mapping left a present PTE.
Validation at mmap() time is unchanged, except for the L1TF pfn check:
remap_pfn_range() rejected a disallowed pfn with -EACCES from mmap(),
where the fault handler now raises SIGBUS on first touch.
Once the device is unregistered, a page with no present entry gets
SIGBUS, where the eager mapping kept it mapped. Not only untouched
pages: splitting a huge entry with a partial munmap() or mprotect()
zaps it, since __split_huge_pmd_locked() clears a PFN-mapped PMD rather
than converting it to PTEs.
Without huge PFN mappings, or for small or unaligned mappings, the
region is populated one page per fault, paid only for pages that are
actually touched.
Signed-off-by: Eliav Farber <farbere@amazon.com>
---
drivers/uio/uio.c | 102 +++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 96 insertions(+), 6 deletions(-)
diff --git a/drivers/uio/uio.c b/drivers/uio/uio.c
index f8fa20522660..6b4c79340e5c 100644
--- a/drivers/uio/uio.c
+++ b/drivers/uio/uio.c
@@ -733,9 +733,80 @@ static int uio_mmap_logical(struct vm_area_struct *vma)
return 0;
}
+/*
+ * Populate UIO physical mappings on fault, using a PMD or PUD mapping where the
+ * architecture, the fault address and the pfn allow it.
+ */
+static vm_fault_t uio_physical_huge_fault(struct vm_fault *vmf,
+ unsigned int order)
+{
+ struct vm_area_struct *vma = vmf->vma;
+ unsigned long addr = vmf->address & ~((PAGE_SIZE << order) - 1);
+ struct uio_device *idev = vma->vm_private_data;
+ vm_fault_t ret = VM_FAULT_SIGBUS;
+ unsigned long pfn;
+
+ /*
+ * vm_pgoff is the base pfn of the mapping (see uio_mmap_physical()).
+ * vmf_pfn_order_ok() ignores pfn if the window starts below vm_start.
+ */
+ pfn = vma->vm_pgoff + ((addr - vma->vm_start) >> PAGE_SHIFT);
+ if (!vmf_pfn_order_ok(vmf, pfn, order))
+ return VM_FAULT_FALLBACK;
+
+ mutex_lock(&idev->info_lock);
+
+ if (idev->info)
+ ret = vmf_insert_pfn_order(vmf, pfn, order);
+
+ mutex_unlock(&idev->info_lock);
+
+ return ret;
+}
+
+static vm_fault_t uio_physical_fault(struct vm_fault *vmf)
+{
+ return uio_physical_huge_fault(vmf, 0);
+}
+
+#ifdef CONFIG_HAVE_IOREMAP_PROT
+/*
+ * generic_access_phys() only follows present PTEs, and a lazily populated
+ * mapping has none for pages the target has not touched yet. Fault the page in
+ * first (through the handlers above, so info_lock is honoured), so that remote
+ * access (ptrace, /proc/pid/mem) keeps working.
+ *
+ * fixup_user_fault() refuses a fault the vma does not permit, so an untouched
+ * page of a mapping without VM_READ stays unreadable, where the eager mapping
+ * left a present PTE.
+ */
+static int uio_physical_access(struct vm_area_struct *vma, unsigned long addr,
+ void *buf, int len, int write)
+{
+ unsigned int fault_flags = FAULT_FLAG_REMOTE;
+
+ if (write)
+ fault_flags |= FAULT_FLAG_WRITE;
+
+ /*
+ * COW mappings are populated at mmap() time, and a write fault would
+ * copy the page instead. With unlocked == NULL, fixup_user_fault()
+ * keeps the mmap lock held and the vma valid.
+ */
+ if (!vma_is_cow_mapping(vma))
+ fixup_user_fault(vma->vm_mm, addr, fault_flags, NULL);
+
+ return generic_access_phys(vma, addr, buf, len, write);
+}
+#endif
+
static const struct vm_operations_struct uio_physical_vm_ops = {
#ifdef CONFIG_HAVE_IOREMAP_PROT
- .access = generic_access_phys,
+ .access = uio_physical_access,
+#endif
+ .fault = uio_physical_fault,
+#ifdef CONFIG_ARCH_SUPPORTS_HUGE_PFNMAP
+ .huge_fault = uio_physical_huge_fault,
#endif
};
@@ -767,11 +838,30 @@ static int uio_mmap_physical(struct vm_area_struct *vma)
* So we just do the physical mmap without a page
* offset.
*/
- return remap_pfn_range(vma,
- vma->vm_start,
- mem->addr >> PAGE_SHIFT,
- vma->vm_end - vma->vm_start,
- vma->vm_page_prot);
+ if (vma_is_cow_mapping(vma)) {
+ /* COW mappings cannot be populated on fault: map them now. */
+ return remap_pfn_range(vma,
+ vma->vm_start,
+ mem->addr >> PAGE_SHIFT,
+ vma->vm_end - vma->vm_start,
+ vma->vm_page_prot);
+ }
+
+ /*
+ * Populate on fault (see uio_physical_huge_fault()). Tracking the PFN
+ * range keeps its memory type reserved, for PAT conflict detection.
+ *
+ * vm_pgoff holds the map index, which the fault handler cannot use:
+ * splitting the VMA adds the page offset of the split to vm_pgoff of
+ * the upper part, so uio_find_mem_index() then returns the wrong map,
+ * or none, and the start of that part would be mapped to mem->addr.
+ * Store the base pfn instead, as remap_pfn_range() does for COW
+ * mappings; VMA splits then keep vm_pgoff correct for each part.
+ */
+ vma->vm_pgoff = mem->addr >> PAGE_SHIFT;
+
+ return pfn_range_track(vma, vma->vm_start, mem->addr >> PAGE_SHIFT,
+ vma->vm_end - vma->vm_start);
}
static int uio_mmap_dma_coherent(struct vm_area_struct *vma)
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-11-11 11:31 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-11 11:30 [PATCH 0/4] mm, uio: huge PFN mappings for /dev/mem and UIO Eliav Farber
2026-11-11 11:30 ` [PATCH 1/4] mm: add MMAP_PFN_TRACK action and pfn_range_track() for lazy PFN maps Eliav Farber
2026-11-11 11:30 ` [PATCH 2/4] mm: add vmf_insert_pfn_order() and vmf_pfn_order_ok() helpers Eliav Farber
2026-11-11 11:30 ` [PATCH 3/4] mm/char-mem: implement huge_fault support for /dev/mem Eliav Farber
2026-11-11 11:30 ` [PATCH 4/4] uio: implement huge_fault support for physical mappings Eliav Farber
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®