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) <[email protected]>
---
 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 83d273778a60..fc4d10c92d7c 100644
--- a/fs/proc/generic.c
+++ b/fs/proc/generic.c
@@ -476,6 +476,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 c0bb40e7ff96..3f29a1663d26 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>
 
@@ -400,11 +402,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);
@@ -814,7 +872,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 ed22c6dfb0bf..11bd3fa07705 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;
@@ -118,6 +119,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_ops */
 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


Reply via email to