Thread (59 messages) flat view 59 messages, 4 authors, 2d ago

Re: [PATCH v4 17/20] mm/vma: only permit MAP_PRIVATE /dev/zero to be mapped anonymous

From: "Lorenzo Stoakes (ARM)" <ljs@kernel.org>
Date: 2026-08-12 18:05:59
Also in: amd-gfx, dri-devel, intel-xe, kvm, linux-fsdevel, linux-kselftest, linux-mm, linux-perf-users, linux-s390, lkml

TL;DR - either we need to find a way to uniquely identify it (keep my approach
or move it to core mm) or we should drop this patch.

Given we're late in the cycle + you stringly objective to my approach I think
maybe best to drop it and respin?

Can do a follow up to add unique identification later, somehow.

The key change of 'make anon-assigned file-backed MAP_PRIVATE mappings not
special snowflakes' will still be achieved in the other patches (but have to
rework them a little obviously).

On Wed, Aug 12, 2026 at 07:15:05PM +0200, David Hildenbrand (Arm) wrote:
quoted
quoted
My brain is a bit slow after digging through this series.

We identify shmem, for example, through shmem_vm_ops/shmem_anon_vm_ops.

So naturally I am wondering: couldn't we do something similar to identify that?
Like, checking for zero_fops?
We don't assign vm_ops for a MAP_PRIVATE-/dev/zero mapping. So that won't work.

We could expose zero->f_ops but then it's literally in drivers/char/ and that's
just weird to expose in mm.h or whatever.
Thinking out loud: could we use a dummy (empty) vm_ops?

We'd place it in mm.h (zero_vm_ops) and just use it in drivers/char/.
The entire purpose of this patch is to _uniquely identify_ MAP_PRIVATE-mapped
/dev/zero and only permit this to make a mapping anonymous.

As per the commit message: 'mm/vma: only permit MAP_PRIVATE /dev/zero to be
mapped anonymous'

MAP_PRIVATE-/dev/zero to anon is a historical abberation. This is why it is a
unique case where semantics don't make sense.

Doing things by semantics makes sense if the semantics are a _class_ of
thing. This is not a class of thing it is a one of one.

Yes the method used is ugly, but the alternatives (I can think of) are:

1. (your proposal) allow any in-tree non-module mmap_prepare() driver to map
   anonymous pages, creating an entirely new semantic for memory mapping.

   (this is currently something that is possible, unfortunately).

   To be safe we would have to implement then a series of checks to prevent true
   insanities, because doing that would permit broken madness like a PFN map
   being treated as anon.

   Once this stuff is out there as an API, even in-tree, even non-module, people
   will abuse it because it'll go through some random tree and we'll have to
   support it.

   I mean we may as well just not make a change in this case other than maybe
   preventing .mmap from being able to do this.

2. My solution - ugly but uniquely identifies the single permitted situation
   where this behaviour is desired.

3. Export something from drivers/char/mem.c to mm - horrific for multiple
   reasons, violates separation of concerns etc. etc.

4. Move the driver code just for /dev/zero to mm - I thought a pretty horrible
   idea, but maybe we could have some mm/ bit and some driver/char bit like
   hugetlbfs or uffd? But I'm not sure there is a not-ugly way of doing this.
   MAP_SHARED /dev/zero does shmem stuff so maybe could live there.

Drivers not being able to get anon pages is a baked-in assumption in the kernel
(excepting MAP_PRIVATE-/dev/zero).

Anyway I don't want this to hold up the series so maybe I'll just drop the
patch and we can retain the current broken mess.
See below.
quoted
I'm giving a really minimal possible thing to export, which is the DEVZERO_MINOR
number which avoids all kinds of weirdness like that. No driver stuff exported,
just a number :) MEM_MAJOR is already available.

So I think it's the least bad choice in this one, very very specific scenario.
I'd hope we find something cleaner than the DEVZERO_MINOR thingy.
I think your objection here again is mistaking this for a class of thing rather
than a one-of-one.
Something slightly cleaned up chloppedi-schlop on top of mm-unstable.

vma tests seems to still work, but I haven't boot-tested this.
Yeah I don't love it :)

I mean this change eliminates the purpose of this patch, and we already handle
the /dev/zero MAP_PRIVATE like this:

static int mmap_zero_prepare(struct vm_area_desc *desc)
{
#ifndef CONFIG_MMU
	return -ENOSYS;
#endif
	if (vma_desc_test(desc, VMA_SHARED_BIT))
		return shmem_zero_setup_desc(desc);

	/*
	 * This is a highly unique situation where we mark a MAP_PRIVATE mapping
	 * of /dev/zero anonymous, despite it not being.
	 */
	vma_desc_set_anonymous(desc);
	return 0;
}

I'm not sure what the difference is between having to do
vma_desc_set_anonymous() and this new vm_ops (which would then get cleared which
is really weird).
quoted hunk ↗ jump to hunk

From 3cdc1d205a8e10dedd99b21a5f4f3f570ba24469 Mon Sep 17 00:00:00 2001
From: "David Hildenbrand (Arm)" <david@kernel.org>
Date: Wed, 12 Aug 2026 19:13:32 +0200
Subject: [PATCH] tmp

Signed-off-by: David Hildenbrand (Arm) <david@kernel.org>
---
 drivers/char/mem.c              |  5 +++--
 include/linux/mm.h              |  4 +---
 mm/init-mm.c                    |  1 +
 mm/vma.c                        | 10 +--------
 mm/vma_internal.h               |  1 -
 tools/testing/vma/include/dup.h | 38 +--------------------------------
 tools/testing/vma/shared.c      |  1 +
 tools/testing/vma/tests/mmap.c  | 10 +++------
 8 files changed, 11 insertions(+), 59 deletions(-)
diff --git a/drivers/char/mem.c b/drivers/char/mem.c
index dcfd896b733d8..147568c65c1be 100644
--- a/drivers/char/mem.c
+++ b/drivers/char/mem.c
@@ -506,7 +506,8 @@ static int mmap_zero_prepare(struct vm_area_desc *desc)
 	if (vma_desc_test(desc, VMA_SHARED_BIT))
 		return shmem_zero_setup_desc(desc);

-	/* MAP_PRIVATE semantics are taken care for us by core mm. */
+	/* Indicate MAP_PRIVATE mappings, so core mm can do the right thing. */
+	desc->vm_ops = &zero_vm_ops;
 	return 0;
 }
@@ -694,7 +695,7 @@ static const struct memdev {
 #ifdef CONFIG_DEVPORT
 	[4] = { "port", &port_fops, 0, 0 },
 #endif
-	[DEVZERO_MINOR] = { "zero", &zero_fops, FMODE_NOWAIT, 0666 },
+	[5] = { "zero", &zero_fops, FMODE_NOWAIT, 0666 },
 	[7] = { "full", &full_fops, 0, 0666 },
 	[8] = { "random", &random_fops, FMODE_NOWAIT, 0666 },
 	[9] = { "urandom", &urandom_fops, FMODE_NOWAIT, 0666 },
diff --git a/include/linux/mm.h b/include/linux/mm.h
index f940d20551d53..29f13cc6b52a2 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -740,9 +740,6 @@ static inline bool fault_flag_allow_retry_first(enum fault_flag flags)
 	{ FAULT_FLAG_INTERRUPTIBLE,	"INTERRUPTIBLE" }, \
 	{ FAULT_FLAG_VMA_LOCK,		"VMA_LOCK" }

-/* /dev/zero minor device number. Special due to MAP_PRIVATE semantics. */
-#define DEVZERO_MINOR	5
All of this to avoid this single #define...!
quoted hunk ↗ jump to hunk
-
 /*
  * vm_fault is filled by the pagefault handler and passed to the vma's
  * ->fault function. The vma's ->fault is responsible for returning a bitmask
@@ -990,6 +987,7 @@ static inline void mm_flags_clear_all(struct mm_struct *mm)
 }

 extern const struct vm_operations_struct vma_dummy_vm_ops;
+extern const struct vm_operations_struct zero_vm_ops;
See below but this is really not a great place to put it. Now we invite people
to invent new mad CoW schemes with anon pages... 'set a vm_ops that we clear' is
not great semantically either.

vma_dummy_vma_ops is set and then kept or overwritten with explicit meaning 'is
non-anon but does no special vm_ops stuff'.
quoted hunk ↗ jump to hunk
 static inline void vma_init(struct vm_area_struct *vma, struct mm_struct *mm)
 {
diff --git a/mm/init-mm.c b/mm/init-mm.c
index 3e792aad76261..2030f8e47a98a 100644
--- a/mm/init-mm.c
+++ b/mm/init-mm.c
@@ -18,6 +18,7 @@
 #endif

 const struct vm_operations_struct vma_dummy_vm_ops;
+const struct vm_operations_struct zero_vm_ops;
Already this creates a brand new class of vm_ops.

Other *_vm_ops have meaning in belonging to a specific mapping (hugetlb, etc.)
but they are _defined in the relevant files_ though maybe have extern vars I guess.

But this one is set once _then cleared_ by the core mm mmap code.

It's really weird and obscure and 'just so'.

So I don't think it's the right method.
quoted hunk ↗ jump to hunk
 /*
  * For dynamically allocated mm_structs, there is a dynamically sized cpumask
diff --git a/mm/vma.c b/mm/vma.c
index e7c8b6cb8347e..4f9b78791daf9 100644
--- a/mm/vma.c
+++ b/mm/vma.c
@@ -2623,15 +2623,7 @@ static int __mmap_new_file_vma(struct mmap_state *map,

 static bool map_is_dev_zero(const struct mmap_state *map)
 {
-	const struct file *file = map->file;
-	struct inode *inode;
-
-	if (!file)
-		return false;
-	inode = file_inode(file);
-	if (!S_ISCHR(inode->i_mode))
-		return false;
-	return imajor(inode) == MEM_MAJOR && iminor(inode) == DEVZERO_MINOR;
+	return map->vm_ops == &zero_vm_ops;
Err no, the function name is completely wrong now. This just tells you somebody
set a vm_ops you're about to remove (badly named too!)
quoted hunk ↗ jump to hunk
 }

 static void map_set_anon(struct mmap_state *map)
diff --git a/mm/vma_internal.h b/mm/vma_internal.h
index 385c0ab137774..4d300e7bbaf4c 100644
--- a/mm/vma_internal.h
+++ b/mm/vma_internal.h
@@ -23,7 +23,6 @@
 #include <linux/ksm.h>
 #include <linux/khugepaged.h>
 #include <linux/list.h>
-#include <linux/major.h>
Again all this to avoid simple include...
quoted hunk ↗ jump to hunk
 #include <linux/maple_tree.h>
 #include <linux/mempolicy.h>
 #include <linux/mm.h>
diff --git a/tools/testing/vma/include/dup.h b/tools/testing/vma/include/dup.h
index 41203b1c2323d..8457fabf53452 100644
--- a/tools/testing/vma/include/dup.h
+++ b/tools/testing/vma/include/dup.h
@@ -7,6 +7,7 @@ struct vm_area_struct;
 static inline void vma_start_write(struct vm_area_struct *vma);

 extern const struct vm_operations_struct vma_dummy_vm_ops;
+extern const struct vm_operations_struct zero_vm_ops;
 extern unsigned long stack_guard_gap;
 extern const struct vm_operations_struct vma_dummy_vm_ops;
 extern unsigned long rlimit(unsigned int limit);
@@ -15,21 +16,6 @@ struct task_struct *get_current(void);
 #define MMF_HAS_MDWE	28
 #define current get_current()

-#define MINORBITS	20
-#define MINORMASK	((1U << MINORBITS) - 1)
-
-#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
-#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
-#define MKDEV(ma, mi)	(((ma) << MINORBITS) | (mi))
-
-#define S_IFMT  00170000
-#define S_IFCHR  0020000
-
-#define S_ISCHR(m)	(((m) & S_IFMT) == S_IFCHR)
-
-#define MEM_MAJOR		1
-#define DEVZERO_MINOR	5
-
 /*
  * Define the task command name length as enum, then it can be visible to
  * BPF programs.
@@ -38,8 +24,6 @@ enum {
 	TASK_COMM_LEN = 16,
 };

-typedef unsigned short		umode_t;
-
 /* PARTIALLY implemented types. */
 struct mm_struct {
 	struct maple_tree mm_mt;
@@ -62,10 +46,6 @@ struct address_space {
 	unsigned long		flags;
 	atomic_t		i_mmap_writable;
 };
-struct inode {
-	umode_t			i_mode;
-	dev_t			i_rdev;
-};
 struct file_operations {
 	int (*mmap)(struct file *, struct vm_area_struct *);
 	int (*mmap_prepare)(struct vm_area_desc *);
@@ -73,7 +53,6 @@ struct file_operations {
 struct file {
 	struct address_space	*f_mapping;
 	const struct file_operations	*f_op;
-	struct inode			*f_inode;
 };
 struct anon_vma_chain {
 	struct anon_vma *anon_vma;
@@ -1665,18 +1644,3 @@ static inline pgoff_t linear_anon_page_index(const struct vm_area_struct *vma,

 	return pgoff;
 }
-
-static inline struct inode *file_inode(const struct file *f)
-{
-	return f->f_inode;
-}
-
-static inline unsigned iminor(const struct inode *inode)
-{
-	return MINOR(inode->i_rdev);
-}
-
-static inline unsigned imajor(const struct inode *inode)
-{
-	return MAJOR(inode->i_rdev);
-}
diff --git a/tools/testing/vma/shared.c b/tools/testing/vma/shared.c
index 4a39c9d504896..46d8d2e96bd71 100644
--- a/tools/testing/vma/shared.c
+++ b/tools/testing/vma/shared.c
@@ -9,6 +9,7 @@ unsigned long dac_mmap_min_addr = CONFIG_DEFAULT_MMAP_MIN_ADDR;
 unsigned long stack_guard_gap = 256UL<<PAGE_SHIFT;

 const struct vm_operations_struct vma_dummy_vm_ops;
+const struct vm_operations_struct zero_vm_ops;
 struct anon_vma dummy_anon_vma;
 struct task_struct __current;
diff --git a/tools/testing/vma/tests/mmap.c b/tools/testing/vma/tests/mmap.c
index ebe01362e530c..a63069d34a5a8 100644
--- a/tools/testing/vma/tests/mmap.c
+++ b/tools/testing/vma/tests/mmap.c
@@ -45,8 +45,9 @@ static bool test_mmap_region_basic(void)
 	return true;
 }

-static int dummy_mmap_prepare(struct vm_area_desc *desc)
+static int zero_mmap_prepare(struct vm_area_desc *desc)
 {
+	desc->vm_ops = &zero_vm_ops;
 	return 0;
 }
@@ -55,14 +56,9 @@ static bool test_pure_anon_dev_zero(void)
 	const vma_flags_t vma_flags = mk_vma_flags(VMA_READ_BIT, VMA_WRITE_BIT,
 			VMA_MAYREAD_BIT, VMA_MAYWRITE_BIT);
 	const struct file_operations f_op = {
-		.mmap_prepare = dummy_mmap_prepare,
-	};
-	struct inode inode = {
-		.i_mode = S_IFCHR,
-		.i_rdev = MKDEV(MEM_MAJOR, DEVZERO_MINOR),
+		.mmap_prepare = zero_mmap_prepare,
 	};
 	struct file file = {
-		.f_inode = &inode,
 		.f_op = &f_op,
 	};
 	struct mm_struct mm = {};
--
2.43.0



--
Cheers,

David
--
Cheers, Lorenzo
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help