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
