[PATCH RESEND v2 19/23] proc: pin the entry around the vm_operations of its mappings

Christian Brauner brauner at kernel.org
Wed Nov 4 11:33:50 PST 2026


proc_reg_mmap() pins the entry around ->proc_mmap() and that's it. What
the entry hands out lives on without the pin: the vm_operations it put
on the mapping run on every fault, on every write to a read-only page
and on every ptrace access with the entry unpinned, and the mapping
itself survives remove_proc_entry(). For /proc/vmcore that means the
fault handler runs after vmcore_cleanup() freed what it looks at, and a
mapping of the old memory keeps pointing at it after fadump gave that
memory back to the page allocator. /proc/bus/pci has the same problem
when a device goes away.

So do what kernfs does for the mappings of sysfs files. procfs puts
vm_operations of its own on the mapping. They pin the entry around its
->open(), ->fault(), ->page_mkwrite() and ->access() and fail with
SIGBUS, or do nothing, once the entry is gone. The entry's
vm_operations are kept in its open node, so every mapping of an entry
has to use the same ones, like kernfs asks that of every mapping of an
open file. ->close() can't be wrapped, it runs when the mapping goes
away and that can be after the entry is gone, so a mapping that has
one is refused.

When the entry is removed procfs unmaps the mappings of every open file
of an entry that has been mapped before it calls ->proc_release(), so a
fault into one of them finds the entry gone. That unmaps the entry's
own address space, the way kernfs_drain_open_files() does it.
/proc/bus/pci maps through the iomem address space that /dev/mem and
the sysfs resource files share, see iomem_get_mapping(), and
unmapping that would take every /dev/mem and BAR mapping on the system
with it. So those mappings are left alone, like the sysfs resource
files leave theirs, and keep pointing at the BAR of a device that is
gone. To know the files an entry that can be mapped gets an open node
like an entry with a ->proc_release() has one. The mapping is linked
into the file's address space only after ->mmap() returned, so a
removal that runs in between doesn't find it. kernfs has the same
window.

Entries that are never removed aren't wrapped, procfs never pins them.

Signed-off-by: Christian Brauner (Amutable) <brauner at kernel.org>
---
 fs/proc/generic.c       |   6 +--
 fs/proc/inode.c         | 126 ++++++++++++++++++++++++++++++++++++++++++++----
 fs/proc/internal.h      |   9 ++--
 include/linux/proc_fs.h |  15 ++++++
 4 files changed, 140 insertions(+), 16 deletions(-)

diff --git a/fs/proc/generic.c b/fs/proc/generic.c
index 1bad3b068d53..81a6cb560c75 100644
--- a/fs/proc/generic.c
+++ b/fs/proc/generic.c
@@ -505,14 +505,14 @@ static int pde_set_flags(struct proc_dir_entry *pde)
 
 /*
  * procfs has to know the open files of an entry whose ->proc_release() it
- * calls at removal. A permanent entry is never removed and skips all that,
- * see proc_reg_open().
+ * calls at removal and whose mappings it undoes then. A permanent entry is
+ * never removed and skips all that, see proc_reg_open().
  */
 static bool pde_needs_open_node(const struct proc_dir_entry *pde)
 {
 	if (!S_ISREG(pde->mode) || pde_is_permanent(pde))
 		return false;
-	return pde->proc_ops->proc_release;
+	return pde->proc_ops->proc_release || pde->proc_ops->proc_mmap;
 }
 
 /*
diff --git a/fs/proc/inode.c b/fs/proc/inode.c
index 717cbaad65df..5e423bcad278 100644
--- a/fs/proc/inode.c
+++ b/fs/proc/inode.c
@@ -312,7 +312,8 @@ static void pde_seq_release(struct proc_dir_entry *pde, struct inode *inode,
  *
  * PDE is locked on entry, unlocked on exit.
  */
-static void close_pdeo(struct proc_dir_entry *pde, struct pde_opener *pdeo)
+static void close_pdeo(struct proc_dir_entry *pde, struct pde_opener *pdeo,
+		       bool rundown)
 	__releases(&pde->pde_unload_lock)
 {
 	/*
@@ -336,7 +337,17 @@ static void close_pdeo(struct proc_dir_entry *pde, struct pde_opener *pdeo)
 		spin_unlock(&pde->pde_unload_lock);
 
 		file = pdeo->file;
-		pde->proc_ops->proc_release(file_inode(file), file);
+		/*
+		 * A mapping of a removed entry faults, see proc_vm_ops. Like
+		 * kernfs this unmaps the entry's own address space. A file
+		 * that maps through another one, /proc/bus/pci through the
+		 * iomem address space of /dev/mem, keeps its mappings like a
+		 * sysfs resource file does.
+		 */
+		if (rundown && pde->open->vm_ops)
+			unmap_mapping_range(file_inode(file)->i_mapping, 0, 0, 1);
+		if (pde->proc_ops->proc_release)
+			pde->proc_ops->proc_release(file_inode(file), file);
 
 		spin_lock(&pde->pde_unload_lock);
 		/* Strictly after ->proc_release, see above. */
@@ -362,7 +373,7 @@ void proc_entry_rundown(struct proc_dir_entry *de)
 	while (!list_empty(&de->open->files)) {
 		struct pde_opener *pdeo;
 		pdeo = list_first_entry(&de->open->files, struct pde_opener, lh);
-		close_pdeo(de, pdeo);
+		close_pdeo(de, pdeo, true);
 		spin_lock(&de->pde_unload_lock);
 	}
 	spin_unlock(&de->pde_unload_lock);
@@ -642,12 +653,106 @@ static long proc_reg_compat_ioctl(struct file *file, unsigned int cmd, unsigned
 #define proc_reg_compat_ioctl NULL
 #endif
 
-static int pde_mmap(struct proc_dir_entry *pde, struct file *file, struct vm_area_struct *vma)
+/*
+ * The vm_operations of an entry's mappings run with the entry pinned, like
+ * the entry's file methods do, and a mapping of a removed entry faults.
+ * proc_entry_rundown() unmaps the mappings of every open file and a fault
+ * after that finds the entry gone. Like kernfs_vm_ops, and like there only
+ * these four methods of the entry's vm_operations are called.
+ */
+static void proc_vma_open(struct vm_area_struct *vma)
+{
+	struct proc_dir_entry *pde = PDE(file_inode(vma->vm_file));
+	const struct vm_operations_struct *vm_ops = pde->open->vm_ops;
+
+	if (!pde_pin(pde))
+		return;
+	if (vm_ops->open)
+		vm_ops->open(vma);
+	pde_unpin(pde);
+}
+
+static vm_fault_t proc_vma_fault(struct vm_fault *vmf)
+{
+	struct proc_dir_entry *pde = PDE(file_inode(vmf->vma->vm_file));
+	const struct vm_operations_struct *vm_ops = pde->open->vm_ops;
+	vm_fault_t ret = VM_FAULT_SIGBUS;
+
+	if (!pde_pin(pde))
+		return VM_FAULT_SIGBUS;
+	if (vm_ops->fault)
+		ret = vm_ops->fault(vmf);
+	pde_unpin(pde);
+	return ret;
+}
+
+static vm_fault_t proc_vma_page_mkwrite(struct vm_fault *vmf)
+{
+	struct proc_dir_entry *pde = PDE(file_inode(vmf->vma->vm_file));
+	const struct vm_operations_struct *vm_ops = pde->open->vm_ops;
+	vm_fault_t ret = 0;
+
+	if (!pde_pin(pde))
+		return VM_FAULT_SIGBUS;
+	if (vm_ops->page_mkwrite)
+		ret = vm_ops->page_mkwrite(vmf);
+	else
+		file_update_time(vmf->vma->vm_file);
+	pde_unpin(pde);
+	return ret;
+}
+
+static int proc_vma_access(struct vm_area_struct *vma, unsigned long addr,
+			   void *buf, int len, int write)
+{
+	struct proc_dir_entry *pde = PDE(file_inode(vma->vm_file));
+	const struct vm_operations_struct *vm_ops = pde->open->vm_ops;
+	int ret = -EINVAL;
+
+	if (!pde_pin(pde))
+		return -EINVAL;
+	if (vm_ops->access)
+		ret = vm_ops->access(vma, addr, buf, len, write);
+	pde_unpin(pde);
+	return ret;
+}
+
+static const struct vm_operations_struct proc_vm_ops = {
+	.open		= proc_vma_open,
+	.fault		= proc_vma_fault,
+	.page_mkwrite	= proc_vma_page_mkwrite,
+	.access		= proc_vma_access,
+};
+
+static int pde_mmap(struct proc_dir_entry *pde, struct file *file,
+		    struct vm_area_struct *vma)
 {
 	const auto mmap = pde->proc_ops->proc_mmap;
-	if (mmap)
-		return mmap(file, vma);
-	return -EIO;
+	struct pde_open_node *on = pde->open;
+	int rv;
+
+	if (!mmap)
+		return -EIO;
+	rv = mmap(file, vma);
+	if (rv || pde_is_permanent(pde))
+		return rv;
+	/* the entry mapped something else, that isn't ours to wrap */
+	if (vma->vm_file != file)
+		return 0;
+	/*
+	 * ->close() can't be wrapped, it runs when the mapping goes away and
+	 * that can be after the entry is gone, see close_pdeo().
+	 */
+	if (!vma->vm_ops || vma->vm_ops->close)
+		return -EINVAL;
+
+	/* every mapping of an entry uses the same vm_operations, see above */
+	guard(spinlock)(&pde->pde_unload_lock);
+	if (on->vm_ops && on->vm_ops != vma->vm_ops)
+		return -EINVAL;
+	on->vm_ops = vma->vm_ops;
+	vma->vm_ops = &proc_vm_ops;
+	return 0;
 }
 
 static int proc_reg_mmap(struct file *file, struct vm_area_struct *vma)
@@ -743,8 +848,9 @@ static int proc_reg_open(struct inode *inode, struct file *file)
 	 * 2) rmmod isn't blocked by opening file in /proc and sitting on
 	 *    the descriptor (including "rmmod foo </proc/foo" scenario).
 	 *
-	 * Save every "struct file" with custom ->release hook. Those entries
-	 * have an open node, see proc_register().
+	 * Save every "struct file" with custom ->release hook, and every
+	 * one that can be mapped, proc_entry_rundown() unmaps those. Those
+	 * entries have an open node, see proc_register().
 	 */
 	if (!use_pde(pde))
 		return -ENOENT;
@@ -794,7 +900,7 @@ static int proc_reg_release(struct inode *inode, struct file *file)
 	spin_lock(&pde->pde_unload_lock);
 	list_for_each_entry(pdeo, &pde->open->files, lh) {
 		if (pdeo->file == file) {
-			close_pdeo(pde, pdeo);
+			close_pdeo(pde, pdeo, false);
 			goto out;
 		}
 	}
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index ae0f9779cf1b..0b1c0585d6a6 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -373,12 +373,15 @@ struct pde_opener {
 } __randomize_layout;
 
 /*
- * The open files of an entry whose ->proc_release() procfs has to call when
- * the entry is removed while they are still open. Allocated with the entry,
- * see proc_register().
+ * The open files of an entry whose ->proc_release() procfs has to call and
+ * whose mappings it has to undo when the entry is removed while they are
+ * still open, that is of an entry with a ->proc_release() or a
+ * ->proc_mmap(). Allocated with the entry, see proc_register().
  */
 struct pde_open_node {
 	struct list_head files;		/* who did ->open, but not ->release */
+	/* of its mappings, the same for all of them, see pde_mmap() */
+	const struct vm_operations_struct *vm_ops;
 };
 extern const struct inode_operations proc_link_inode_operations;
 extern const struct inode_operations proc_pid_link_inode_operations;
diff --git a/include/linux/proc_fs.h b/include/linux/proc_fs.h
index 2ba4e4a39e13..5bf039d20990 100644
--- a/include/linux/proc_fs.h
+++ b/include/linux/proc_fs.h
@@ -86,6 +86,21 @@ struct proc_ops {
 #ifdef CONFIG_COMPAT
 	long	(*proc_compat_ioctl)(struct file *, unsigned int, unsigned long);
 #endif
+	/*
+	 * procfs pins the entry around ->proc_mmap() and wraps the
+	 * vm_operations of the mapping so that ->open(), ->fault(),
+	 * ->page_mkwrite() and ->access() run with the entry pinned as well.
+	 * Nothing else of them is called, like for a kernfs file. Every
+	 * mapping of an entry has to use the same vm_operations. A ->close()
+	 * can't be wrapped, it runs when the mapping goes away and that can
+	 * be after the entry is gone, so a mapping that has one is refused.
+	 * Removing the entry unmaps the mappings in its own address space,
+	 * like kernfs does, and a fault into one of them is SIGBUS from then
+	 * on. A file that maps through another address space, /proc/bus/pci
+	 * through the one of /dev/mem, keeps its mappings like a sysfs
+	 * resource file does. The mappings of a permanent entry are left
+	 * alone.
+	 */
 	int	(*proc_mmap)(struct file *, struct vm_area_struct *);
 	unsigned long (*proc_get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
 	/*

-- 
2.53.0




More information about the linux-um mailing list