diff --git a/fs/proc/generic.c b/fs/proc/generic.c
index fc4d10c92d7c..35dd745a0357 100644
--- a/fs/proc/generic.c
+++ b/fs/proc/generic.c
@@ -485,6 +485,18 @@ static int pde_set_flags(struct proc_dir_entry *pde)
if (proc_ops->proc_flags & PROC_ENTRY_FULL_READS)
pde->flags |= PROC_ENTRY_FULL_READS;
}
+ if (proc_ops->proc_ioctl_buf) {
+ if (WARN_ON_ONCE(proc_ops->proc_ioctl))
+ return -EINVAL;
+ /* the table says what procfs copies, see pde_ioctl_arg() */
+ if (WARN_ON_ONCE(!proc_ops->proc_ioctl_args ||
+ !proc_ops->proc_nr_ioctl_args))
+ return -EINVAL;
+ pde->flags |= PROC_ENTRY_proc_ioctl_buf;
+ } else if (WARN_ON_ONCE(proc_ops->proc_ioctl_args ||
+ proc_ops->proc_nr_ioctl_args)) {
+ return -EINVAL;
+ }
return 0;
}
diff --git a/fs/proc/inode.c b/fs/proc/inode.c
index 3f29a1663d26..7e4ca5751abb 100644
--- a/fs/proc/inode.c
+++ b/fs/proc/inode.c
@@ -6,6 +6,7 @@
*/
#include <linux/cache.h>
+#include <linux/compat.h>
#include <linux/time.h>
#include <linux/proc_fs.h>
#include <linux/kernel.h>
@@ -571,11 +572,66 @@ static long pde_ioctl(struct proc_dir_entry *pde, struct file *file, unsigned in
return -ENOTTY;
}
+/* the table lives in proc_ops, which goes away with the entry */
+static bool pde_ioctl_arg(struct proc_dir_entry *pde, unsigned int cmd,
+ struct proc_ioctl_arg *arg)
+{
+ const struct proc_ops *proc_ops = pde->proc_ops;
+ bool found = false;
+
+ if (!pde_pin(pde))
+ return false;
+ for (unsigned int i = 0; i < proc_ops->proc_nr_ioctl_args; i++) {
+ if (proc_ops->proc_ioctl_args[i].cmd == cmd) {
+ *arg = proc_ops->proc_ioctl_args[i];
+ found = true;
+ break;
+ }
+ }
+ pde_unpin(pde);
+ return found;
+}
+
+/*
+ * procfs copies the argument in before it pins the entry and copies it back
+ * out after unpinning it. An ioctl that faults on its argument then doesn't
+ * hold up remove_proc_entry().
+ */
+static long pde_ioctl_buf(struct proc_dir_entry *pde, struct file *file,
+ unsigned int cmd, void __user *uarg, unsigned long arg)
+{
+ void *buf __free(kvfree) = NULL;
+ struct proc_ioctl_arg a;
+ size_t size;
+ long rv;
+
+ if (!pde_ioctl_arg(pde, cmd, &a))
+ return -ENOTTY;
+ size = max(a.in, a.out);
+ if (size) {
+ buf = kvzalloc(size, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ if (copy_from_user(buf, uarg, a.in))
+ return -EFAULT;
+ }
+
+ if (!pde_pin(pde))
+ return -ENOTTY;
+ rv = pde->proc_ops->proc_ioctl_buf(file, cmd, buf ?: (void *)arg);
+ pde_unpin(pde);
+ if (rv >= 0 && copy_to_user(uarg, buf, a.out))
+ return -EFAULT;
+ return rv;
+}
+
static long proc_reg_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct proc_dir_entry *pde = PDE(file_inode(file));
long rv;
+ if (pde_has_proc_ioctl_buf(pde))
+ return pde_ioctl_buf(pde, file, cmd, (void __user *)arg, arg);
if (!pde_pin(pde))
return -ENOTTY;
rv = pde_ioctl(pde, file, cmd, arg);@@ -597,6 +653,8 @@ static long proc_reg_compat_ioctl(struct file *file, unsigned int cmd, unsigned
struct proc_dir_entry *pde = PDE(file_inode(file));
long rv;
+ if (pde_has_proc_ioctl_buf(pde))
+ return pde_ioctl_buf(pde, file, cmd, compat_ptr(arg), arg);
if (!pde_pin(pde))
return -ENOTTY;
rv = pde_compat_ioctl(pde, file, cmd, arg);
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index 11bd3fa07705..93d20995b6fe 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -129,6 +129,11 @@ static inline bool pde_has_full_reads(const struct proc_dir_entry *pde)
return pde->flags & PROC_ENTRY_FULL_READS;
}
+static inline bool pde_has_proc_ioctl_buf(const struct proc_dir_entry *pde)
+{
+ return pde->flags & PROC_ENTRY_proc_ioctl_buf;
+}
+
/* 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 0f0997898ba3..2ba4e4a39e13 100644
--- a/include/linux/proc_fs.h
+++ b/include/linux/proc_fs.h
@@ -8,6 +8,7 @@
#include <linux/compiler.h>
#include <linux/types.h>
#include <linux/fs.h>
+#include <linux/ioctl.h>
struct proc_dir_entry;
struct seq_file;
@@ -45,8 +46,29 @@ enum {
* coming up short.
*/
PROC_ENTRY_FULL_READS = 1U << 11,
+ PROC_ENTRY_proc_ioctl_buf = 1U << 12,
};
+/*
+ * How many bytes of the argument of @cmd procfs copies in before calling
+ * ->proc_ioctl_buf() and copies back out after it. A command that copies
+ * neither gets its argument as is.
+ */
+struct proc_ioctl_arg {
+ unsigned int cmd;
+ unsigned int in;
+ unsigned int out;
+};
+
+#define PROC_IOCTL_ARG(_cmd, _in, _out) \
+ { .cmd = (_cmd), .in = (_in), .out = (_out) }
+
+/* A command that encodes the size and direction of its argument. */
+#define PROC_IOCTL_IOC(_cmd) \
+ PROC_IOCTL_ARG(_cmd, \
+ (_IOC_DIR(_cmd) & _IOC_WRITE) ? _IOC_SIZE(_cmd) : 0, \
+ (_IOC_DIR(_cmd) & _IOC_READ) ? _IOC_SIZE(_cmd) : 0)
+
struct proc_ops {
unsigned int proc_flags;
int (*proc_open)(struct inode *, struct file *);@@ -100,6 +122,19 @@ struct proc_ops {
*/
ssize_t (*proc_read_buf)(struct file *, char *, size_t, loff_t *);
unsigned int proc_read_size;
+ /*
+ * procfs looks the command up in ->proc_ioctl_args, copies in what it
+ * passes before pinning the entry, calls ->proc_ioctl_buf() with that
+ * copy and copies out what it returns after unpinning the entry.
+ * Commands that aren't in the table fail with -ENOTTY. 32-bit tasks
+ * go through the same table. The copy is zeroed and as large as the
+ * larger of the two sizes, it only goes back out if the ioctl
+ * succeeded, and a command that copies neither gets its argument as
+ * is.
+ */
+ long (*proc_ioctl_buf)(struct file *, unsigned int, void *);
+ const struct proc_ioctl_arg *proc_ioctl_args;
+ unsigned int proc_nr_ioctl_args;
} __randomize_layout;
/* definitions for hide_pid field */
--
2.53.0