[PATCH RESEND v2 16/23] proc: let procfs copy out reads that don't go through seq_file

From: Christian Brauner

Date: Wed Nov 04 2026 - 16:25:49 EST


proc_reg_read() and proc_reg_read_iter() pin the entry and call
->proc_read() or ->proc_read_iter() which copy to userspace
themselves. Like for seq_file reads and for writes, a reader can make
a page fault in that copy take as long as it wants and
remove_proc_entry() waits for it.

So do what kernfs does for these reads as well. An entry that implements
->proc_read_buf() fills a kernel buffer of at most ->proc_read_size
bytes, a page if the entry doesn't care, and procfs copies it to
userspace after unpinning the entry. The entry moves the position like
it did for ->proc_read() but that only sticks if the copy to userspace
worked, like in kernfs_file_read_iter(). If the copy fails halfway the
read fails with -EFAULT and the position stays, where
simple_read_from_buffer() returned what it had copied until then.
procfs remembers ->proc_read_size in the entry like ->proc_write_size.

Larger reads come up short, like with kernfs. An entry whose readers
want all of a large file in one read asks for PROC_ENTRY_FULL_READS and
procfs fills the read in pieces of ->proc_read_size, unpinning the entry
in between. A piece whose copy fails ends the read with what the pieces
before it returned. kexec-tools treats any short read of /proc/vmcore
as fatal for example (cf. [1]).

These entries are read through ->read_iter() so readv(), splice(), aio,
the RWF flags, kernel_read() and io_uring work for them like for the
->proc_read_iter() ones. io_uring's nonblocking attempt gets -EAGAIN
though, like loop_rw_iter() gave it for ->proc_read() entries. Neither
the allocation nor the entry can honor IOCB_NOWAIT, and io_uring would
take a short read of a record file for an unfinished one and glue the
next record to it. So it retries from a worker like before.

An entry that implements ->proc_read_buf() and also declares a seq_file
or one of the other read methods fails to register.

No users yet.

[1]: https://git.kernel.org/pub/scm/utils/kernel/kexec/kexec-tools.git/tree/util_lib/elf_info.c

Signed-off-by: Christian Brauner (Amutable) <brauner@xxxxxxxxxx>
---
fs/proc/generic.c | 9 ++++++++
fs/proc/inode.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++++-
fs/proc/internal.h | 11 +++++++++
include/linux/proc_fs.h | 16 +++++++++++++
4 files changed, 95 insertions(+), 1 deletion(-)

diff --git a/fs/proc/generic.c b/fs/proc/generic.c
index 5e2ed5a04ec9..2d503250de75 100644
--- a/fs/proc/generic.c
+++ b/fs/proc/generic.c
@@ -474,6 +474,15 @@ static int pde_set_flags(struct proc_dir_entry *pde)
if (proc_ops->proc_flags & PROC_ENTRY_SHORT_WRITES)
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))
+ return -EINVAL;
+ pde->flags |= PROC_ENTRY_proc_read_buf;
+ pde->read_size = proc_ops->proc_read_size ?: PAGE_SIZE;
+ if (proc_ops->proc_flags & PROC_ENTRY_FULL_READS)
+ pde->flags |= PROC_ENTRY_FULL_READS;
+ }
return 0;
}

diff --git a/fs/proc/inode.c b/fs/proc/inode.c
index df1caa0f238d..caa47933c233 100644
--- a/fs/proc/inode.c
+++ b/fs/proc/inode.c
@@ -23,6 +23,8 @@
#include <linux/sysctl.h>
#include <linux/seq_file.h>
#include <linux/slab.h>
+#include <linux/sched/signal.h>
+#include <linux/uio.h>
#include <linux/mount.h>
#include <linux/bug.h>
#include <linux/wait_bit.h>
@@ -377,11 +379,67 @@ static loff_t proc_reg_llseek(struct file *file, loff_t offset, int whence)
return rv;
}

+/*
+ * The entry fills a kernel buffer and procfs copies that to userspace after
+ * unpinning the entry. A reader that faults on its buffer then doesn't hold
+ * up remove_proc_entry().
+ */
+static ssize_t pde_read_buf(struct proc_dir_entry *pde, struct kiocb *iocb,
+ struct iov_iter *iter)
+{
+ struct file *file = iocb->ki_filp;
+ char *buf __free(kvfree) = NULL;
+ ssize_t rv, done = 0;
+ size_t len, count;
+ loff_t pos;
+
+ if (!iov_iter_count(iter))
+ return 0;
+ /* neither the allocation nor the entry can honor IOCB_NOWAIT */
+ if ((iocb->ki_flags & IOCB_NOWAIT) && !(file->f_flags & O_NONBLOCK))
+ return -EAGAIN;
+ len = min_t(size_t, iov_iter_count(iter), pde->read_size);
+ buf = kvmalloc(len, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ /* PROC_ENTRY_FULL_READS fills the read in pieces */
+ do {
+ count = min_t(size_t, iov_iter_count(iter), len);
+ pos = iocb->ki_pos;
+ if (!pde_pin(pde)) {
+ rv = -EIO;
+ break;
+ }
+ rv = pde->proc_ops->proc_read_buf(file, buf, count, &pos);
+ pde_unpin(pde);
+ if (rv <= 0)
+ break;
+ if (WARN_ON_ONCE(rv > count)) {
+ rv = -EIO;
+ break;
+ }
+ if (copy_to_iter(buf, rv, iter) != rv) {
+ rv = -EFAULT;
+ break;
+ }
+ iocb->ki_pos = pos;
+ done += rv;
+ if (!pde_has_full_reads(pde) || rv < count)
+ break;
+ cond_resched();
+ } while (iov_iter_count(iter) && !fatal_signal_pending(current));
+
+ return done ?: rv;
+}
+
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;

+ 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);
@@ -793,7 +851,7 @@ struct inode *proc_get_inode(struct super_block *sb, struct proc_dir_entry *de)
inode->i_op = de->proc_iops;
if (pde_is_seq_file(de))
inode->i_fop = &proc_seq_file_ops;
- else if (pde_has_proc_read_iter(de))
+ 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;
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index 1d36fe4b1af5..b5c78f4f392c 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -52,6 +52,7 @@ struct proc_dir_entry {
void *data;
unsigned int state_size;
unsigned int write_size;
+ unsigned int read_size;
unsigned int low_ino;
nlink_t nlink;
kuid_t uid;
@@ -120,6 +121,16 @@ static inline bool pde_has_short_writes(const struct proc_dir_entry *pde)
return pde->flags & PROC_ENTRY_SHORT_WRITES;
}

+static inline bool pde_has_proc_read_buf(const struct proc_dir_entry *pde)
+{
+ return pde->flags & PROC_ENTRY_proc_read_buf;
+}
+
+static inline bool pde_has_full_reads(const struct proc_dir_entry *pde)
+{
+ return pde->flags & PROC_ENTRY_FULL_READS;
+}
+
/* procfs owns the seq_file, see pde_seq_start() */
static inline bool pde_is_seq_file(const struct proc_dir_entry *pde)
{
diff --git a/include/linux/proc_fs.h b/include/linux/proc_fs.h
index c81c93325044..0f0997898ba3 100644
--- a/include/linux/proc_fs.h
+++ b/include/linux/proc_fs.h
@@ -38,6 +38,13 @@ enum {
* failing with -EINVAL.
*/
PROC_ENTRY_SHORT_WRITES = 1U << 9,
+ PROC_ENTRY_proc_read_buf = 1U << 10,
+
+ /*
+ * Reads larger than ->proc_read_size are filled in pieces instead of
+ * coming up short.
+ */
+ PROC_ENTRY_FULL_READS = 1U << 11,
};

struct proc_ops {
@@ -84,6 +91,15 @@ struct proc_ops {
*/
ssize_t (*proc_write_buf)(struct file *, char *, size_t, loff_t *);
unsigned int proc_write_size;
+ /*
+ * procfs calls ->proc_read_buf() to fill a kernel buffer of at most
+ * ->proc_read_size bytes, PAGE_SIZE if that's 0, and copies it to
+ * userspace afterwards. Larger reads come up short unless the entry
+ * asks for PROC_ENTRY_FULL_READS. The position the entry moves to only
+ * sticks if the copy works.
+ */
+ ssize_t (*proc_read_buf)(struct file *, char *, size_t, loff_t *);
+ unsigned int proc_read_size;
} __randomize_layout;

/* definitions for hide_pid field */

--
2.53.0