[PATCH RESEND v2 4/6] proc: drop ->proc_read() and ->proc_read_iter()

From: Christian Brauner

Date: Wed Nov 04 2026 - 21:34:02 EST


Nothing implements them anymore. Every entry that can be removed fills
procfs' buffer through ->proc_read_buf() or is a seq_file entry, and
the permanent ones that still read straight into userspace have
file_operations of their own now.

With them go the flag, the registration checks that kept them apart
from ->proc_read_buf() and the seq_file entries, proc_reg_read(), and
two of the three sets of file_operations for regular files:
proc_reg_llseek() and proc_reg_read_iter() hand a seq_file entry to
seq_lseek() and seq_read_iter() themselves now, like
kernfs_fop_read_iter() does, so one set serves every entry that has
proc_ops. That set has ->splice_read(), so a splice() from an entry
that can't be read at all, a write-only one, fails with -EIO now
instead of -EINVAL.

Nothing in struct proc_ops takes a __user pointer anymore.

Signed-off-by: Christian Brauner (Amutable) <brauner@xxxxxxxxxx>
---
fs/proc/generic.c | 14 ++++------
fs/proc/inode.c | 69 +++++--------------------------------------------
fs/proc/internal.h | 5 ----
include/linux/proc_fs.h | 4 +--
4 files changed, 12 insertions(+), 80 deletions(-)

diff --git a/fs/proc/generic.c b/fs/proc/generic.c
index 4e0142fec582..2515df3a0110 100644
--- a/fs/proc/generic.c
+++ b/fs/proc/generic.c
@@ -454,8 +454,7 @@ static int pde_set_flags(struct proc_dir_entry *pde)
}
if (pde_is_seq_file(pde)) {
/* procfs reads and seeks through the seq_file itself */
- if (WARN_ON_ONCE(proc_ops->proc_read || proc_ops->proc_read_iter ||
- proc_ops->proc_lseek))
+ if (WARN_ON_ONCE(proc_ops->proc_lseek))
return -EINVAL;
/* and frees it, see pde_seq_release() */
if (WARN_ON_ONCE(proc_ops->proc_release == seq_release ||
@@ -463,11 +462,8 @@ static int pde_set_flags(struct proc_dir_entry *pde)
proc_ops->proc_release == single_release))
return -EINVAL;
pde->flags |= PROC_ENTRY_proc_lseek;
- } else {
- if (proc_ops->proc_read_iter)
- pde->flags |= PROC_ENTRY_proc_read_iter;
- if (proc_ops->proc_lseek)
- pde->flags |= PROC_ENTRY_proc_lseek;
+ } else if (proc_ops->proc_lseek) {
+ pde->flags |= PROC_ENTRY_proc_lseek;
}
if (proc_ops->proc_poll)
pde->flags |= PROC_ENTRY_proc_poll;
@@ -482,8 +478,8 @@ static int pde_set_flags(struct proc_dir_entry *pde)
pde->flags |= PROC_ENTRY_SHORT_WRITES;
}
if (proc_ops->proc_read_buf) {
- if (WARN_ON_ONCE(pde_is_seq_file(pde) || proc_ops->proc_read ||
- proc_ops->proc_read_iter))
+ /* procfs reads through the seq_file itself */
+ if (WARN_ON_ONCE(pde_is_seq_file(pde)))
return -EINVAL;
pde->flags |= PROC_ENTRY_proc_read_buf;
pde->read_size = proc_ops->proc_read_size ?: PAGE_SIZE;
diff --git a/fs/proc/inode.c b/fs/proc/inode.c
index 49ae30b6e515..9719c1b89de3 100644
--- a/fs/proc/inode.c
+++ b/fs/proc/inode.c
@@ -413,6 +413,9 @@ static loff_t proc_reg_llseek(struct file *file, loff_t offset, int whence)
struct proc_dir_entry *pde = PDE(file_inode(file));
loff_t rv;

+ /* procfs seeks through the seq_file itself, see pde_seq_start() */
+ if (pde_is_seq_file(pde))
+ return seq_lseek(file, offset, whence);
if (!pde_pin(pde))
return -EINVAL;
rv = pde->proc_ops->proc_lseek(file, offset, whence);
@@ -477,37 +480,15 @@ static ssize_t pde_read_buf(struct proc_dir_entry *pde, struct kiocb *iocb,
static ssize_t proc_reg_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
struct proc_dir_entry *pde = PDE(file_inode(iocb->ki_filp));
- ssize_t ret;

+ /* procfs reads through the seq_file itself, see pde_seq_start() */
+ if (pde_is_seq_file(pde))
+ return seq_read_iter(iocb, iter);
if (pde_has_proc_read_buf(pde))
return pde_read_buf(pde, iocb, iter);
- if (!pde_pin(pde))
- return -EIO;
- ret = pde->proc_ops->proc_read_iter(iocb, iter);
- pde_unpin(pde);
- return ret;
-}
-
-static ssize_t pde_read(struct proc_dir_entry *pde, struct file *file, char __user *buf, size_t count, loff_t *ppos)
-{
- const auto read = pde->proc_ops->proc_read;
- if (read)
- return read(file, buf, count, ppos);
return -EIO;
}

-static ssize_t proc_reg_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
-{
- struct proc_dir_entry *pde = PDE(file_inode(file));
- ssize_t rv;
-
- if (!pde_pin(pde))
- return -EIO;
- rv = pde_read(pde, file, buf, count, ppos);
- pde_unpin(pde);
- return rv;
-}
-
/*
* procfs copies in what is written before it pins the entry. A writer that
* faults on its buffer then doesn't hold up remove_proc_entry().
@@ -923,41 +904,7 @@ static int proc_reg_release(struct inode *inode, struct file *file)
return 0;
}

-/* For seq_file entries, see pde_seq_ops. */
-static const struct file_operations proc_seq_file_ops = {
- .llseek = seq_lseek,
- .read_iter = seq_read_iter,
- .splice_read = copy_splice_read,
- .write = proc_reg_write,
- .poll = proc_reg_poll,
- .unlocked_ioctl = proc_reg_unlocked_ioctl,
- .compat_ioctl = proc_reg_compat_ioctl,
- .mmap = proc_reg_mmap,
- .get_unmapped_area = proc_reg_get_unmapped_area,
-#ifndef CONFIG_MMU
- .mmap_capabilities = proc_reg_mmap_capabilities,
-#endif
- .open = proc_reg_open,
- .release = proc_reg_release,
-};
-
static const struct file_operations proc_reg_file_ops = {
- .llseek = proc_reg_llseek,
- .read = proc_reg_read,
- .write = proc_reg_write,
- .poll = proc_reg_poll,
- .unlocked_ioctl = proc_reg_unlocked_ioctl,
- .compat_ioctl = proc_reg_compat_ioctl,
- .mmap = proc_reg_mmap,
- .get_unmapped_area = proc_reg_get_unmapped_area,
-#ifndef CONFIG_MMU
- .mmap_capabilities = proc_reg_mmap_capabilities,
-#endif
- .open = proc_reg_open,
- .release = proc_reg_release,
-};
-
-static const struct file_operations proc_iter_file_ops = {
.llseek = proc_reg_llseek,
.read_iter = proc_reg_read_iter,
.write = proc_reg_write,
@@ -1026,10 +973,6 @@ struct inode *proc_get_inode(struct super_block *sb, struct proc_dir_entry *de)
inode->i_op = de->proc_iops;
if (pde_has_proc_fops(de))
inode->i_fop = de->proc_fops;
- else if (pde_is_seq_file(de))
- inode->i_fop = &proc_seq_file_ops;
- else if (pde_has_proc_read_iter(de) || pde_has_proc_read_buf(de))
- inode->i_fop = &proc_iter_file_ops;
else
inode->i_fop = &proc_reg_file_ops;
} else if (S_ISDIR(inode->i_mode)) {
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index 84c5c13651aa..0a22237e6778 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -103,11 +103,6 @@ static inline bool pde_has_proc_fops(const struct proc_dir_entry *pde)
return pde->flags & PROC_ENTRY_proc_fops;
}

-static inline bool pde_has_proc_read_iter(const struct proc_dir_entry *pde)
-{
- return pde->flags & PROC_ENTRY_proc_read_iter;
-}
-
static inline bool pde_has_proc_lseek(const struct proc_dir_entry *pde)
{
return pde->flags & PROC_ENTRY_proc_lseek;
diff --git a/include/linux/proc_fs.h b/include/linux/proc_fs.h
index 05e0d6e4f918..9c09766b8f72 100644
--- a/include/linux/proc_fs.h
+++ b/include/linux/proc_fs.h
@@ -17,6 +17,7 @@ struct seq_operations;
enum {
/*
* All /proc entries using this ->proc_ops instance are never removed.
+ * procfs calls into them without pinning them.
*
* If in doubt, ignore this flag.
*/
@@ -26,7 +27,6 @@ enum {
PROC_ENTRY_PERMANENT = 1U << 0,
#endif

- PROC_ENTRY_proc_read_iter = 1U << 1,
PROC_ENTRY_proc_fops = 1U << 2,
PROC_ENTRY_proc_lseek = 1U << 3,
PROC_ENTRY_proc_seq_ops = 1U << 4,
@@ -76,8 +76,6 @@ struct proc_ioctl_arg {
struct proc_ops {
unsigned int proc_flags;
int (*proc_open)(struct inode *, struct file *);
- ssize_t (*proc_read)(struct file *, char __user *, size_t, loff_t *);
- ssize_t (*proc_read_iter)(struct kiocb *, struct iov_iter *);
/*
* mandatory unless nonseekable_open() or equivalent is used, procfs
* seeks seq_file entries itself

--
2.53.0