Re: [PATCH v12 01/12] lib: introduce copy_struct_{to,from}_user helpers
From: Christian Brauner <hidden>
Date: 2019-09-05 11:06:17
Also in:
linux-api, linux-arch, linux-arm-kernel, linux-fsdevel, linux-kselftest, linux-mips, linux-s390, linux-sh, linuxppc-dev, lkml, sparclinux
On Thu, Sep 05, 2019 at 06:19:22AM +1000, Aleksa Sarai wrote:
quoted hunk ↗ jump to hunk
A common pattern for syscall extensions is increasing the size of a struct passed from userspace, such that the zero-value of the new fields result in the old kernel behaviour (allowing for a mix of userspace and kernel vintages to operate on one another in most cases). This is done in both directions -- hence two helpers -- though it's more common to have to copy user space structs into kernel space. Previously there was no common lib/ function that implemented the necessary extension-checking semantics (and different syscalls implemented them slightly differently or incompletely[1]). A future patch replaces all of the common uses of this pattern to use the new copy_struct_{to,from}_user() helpers. [1]: For instance {sched_setattr,perf_event_open,clone3}(2) all do do similar checks to copy_struct_from_user() while rt_sigprocmask(2) always rejects differently-sized struct arguments. Suggested-by: Rasmus Villemoes <linux@rasmusvillemoes.dk> Signed-off-by: Aleksa Sarai <redacted> --- include/linux/uaccess.h | 5 ++ lib/Makefile | 2 +- lib/struct_user.c | 182 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 lib/struct_user.cdiff --git a/include/linux/uaccess.h b/include/linux/uaccess.h index 34a038563d97..0ad9544a1aee 100644 --- a/include/linux/uaccess.h +++ b/include/linux/uaccess.h@@ -230,6 +230,11 @@ static inline unsigned long __copy_from_user_inatomic_nocache(void *to, #endif /* ARCH_HAS_NOCACHE_UACCESS */ +extern int copy_struct_to_user(void __user *dst, size_t usize, + const void *src, size_t ksize); +extern int copy_struct_from_user(void *dst, size_t ksize, + const void __user *src, size_t usize); + /* * probe_kernel_read(): safely attempt to read from a location * @dst: pointer to the buffer that shall take the datadiff --git a/lib/Makefile b/lib/Makefile index 29c02a924973..d86c71feaf0a 100644 --- a/lib/Makefile +++ b/lib/Makefile@@ -28,7 +28,7 @@ endif CFLAGS_string.o := $(call cc-option, -fno-stack-protector) endif -lib-y := ctype.o string.o vsprintf.o cmdline.o \ +lib-y := ctype.o string.o struct_user.o vsprintf.o cmdline.o \ rbtree.o radix-tree.o timerqueue.o xarray.o \ idr.o extable.o \ sha1.o chacha.o irq_regs.o argv_split.o \diff --git a/lib/struct_user.c b/lib/struct_user.c new file mode 100644 index 000000000000..7301ab1bbe98 --- /dev/null +++ b/lib/struct_user.c@@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2019 SUSE LLC + * Copyright (C) 2019 Aleksa Sarai <cyphar@cyphar.com> + */ + +#include <linux/types.h> +#include <linux/export.h> +#include <linux/uaccess.h> +#include <linux/kernel.h> +#include <linux/string.h> + +#define BUFFER_SIZE 64 + +/* + * "memset(p, 0, size)" but for user space buffers. Caller must have already + * checked access_ok(p, size). + */ +static int __memzero_user(void __user *p, size_t s) +{ + const char zeros[BUFFER_SIZE] = {}; + while (s > 0) { + size_t n = min(s, sizeof(zeros)); + + if (__copy_to_user(p, zeros, n)) + return -EFAULT; + + p += n; + s -= n; + } + return 0; +} + +/** + * copy_struct_to_user: copy a struct to user space + * @dst: Destination address, in user space. + * @usize: Size of @dst struct. + * @src: Source address, in kernel space. + * @ksize: Size of @src struct. + * + * Copies a struct from kernel space to user space, in a way that guarantees + * backwards-compatibility for struct syscall arguments (as long as future + * struct extensions are made such that all new fields are *appended* to the + * old struct, and zeroed-out new fields have the same meaning as the old + * struct). + * + * @ksize is just sizeof(*dst), and @usize should've been passed by user space. + * The recommended usage is something like the following: + * + * SYSCALL_DEFINE2(foobar, struct foo __user *, uarg, size_t, usize) + * { + * int err; + * struct foo karg = {}; + * + * // do something with karg + * + * err = copy_struct_to_user(uarg, usize, &karg, sizeof(karg)); + * if (err) + * return err; + * + * // ... + * } + * + * There are three cases to consider: + * * If @usize == @ksize, then it's copied verbatim. + * * If @usize < @ksize, then kernel space is "returning" a newer struct to an + * older user space. In order to avoid user space getting incomplete + * information (new fields might be important), all trailing bytes in @src + * (@ksize - @usize) must be zerored, otherwise -EFBIG is returned. + * * If @usize > @ksize, then the kernel is "returning" an older struct to a + * newer user space. The trailing bytes in @dst (@usize - @ksize) will be + * zero-filled. + * + * Returns (in all cases, some data may have been copied): + * * -EFBIG: (@usize < @ksize) and there are non-zero trailing bytes in @src. + * * -EFAULT: access to user space failed. + */ +int copy_struct_to_user(void __user *dst, size_t usize, + const void *src, size_t ksize) +{ + size_t size = min(ksize, usize); + size_t rest = abs(ksize - usize); + + if (unlikely(usize > PAGE_SIZE)) + return -EFAULT;
Looks like this should be -EFBIG.
+ if (unlikely(!access_ok(dst, usize)))
+ return -EFAULT;
+
+ /* Deal with trailing bytes. */
+ if (usize < ksize) {
+ if (memchr_inv(src + size, 0, rest))
+ return -EFBIG;
+ } else if (usize > ksize) {
+ if (__memzero_user(dst + size, rest))
+ return -EFAULT;Is zeroing that memory really our job? Seems to me we should just check it is zeroed.
+ }
+ /* Copy the interoperable parts of the struct. */
+ if (__copy_to_user(dst, src, size))
+ return -EFAULT;
+ return 0;
+}
+EXPORT_SYMBOL(copy_struct_to_user);
+
+/**
+ * copy_struct_from_user: copy a struct from user space
+ * @dst: Destination address, in kernel space. This buffer must be @ksize
+ * bytes long.
+ * @ksize: Size of @dst struct.
+ * @src: Source address, in user space.
+ * @usize: (Alleged) size of @src struct.
+ *
+ * Copies a struct from user space to kernel space, in a way that guarantees
+ * backwards-compatibility for struct syscall arguments (as long as future
+ * struct extensions are made such that all new fields are *appended* to the
+ * old struct, and zeroed-out new fields have the same meaning as the old
+ * struct).
+ *
+ * @ksize is just sizeof(*dst), and @usize should've been passed by user space.
+ * The recommended usage is something like the following:
+ *
+ * SYSCALL_DEFINE2(foobar, const struct foo __user *, uarg, size_t, usize)
+ * {
+ * int err;
+ * struct foo karg = {};
+ *
+ * err = copy_struct_from_user(&karg, sizeof(karg), uarg, size);
+ * if (err)
+ * return err;
+ *
+ * // ...
+ * }
+ *
+ * There are three cases to consider:
+ * * If @usize == @ksize, then it's copied verbatim.
+ * * If @usize < @ksize, then the user space has passed an old struct to a
+ * newer kernel. The rest of the trailing bytes in @dst (@ksize - @usize)
+ * are to be zero-filled.
+ * * If @usize > @ksize, then the user space has passed a new struct to an
+ * older kernel. The trailing bytes unknown to the kernel (@usize - @ksize)
+ * are checked to ensure they are zeroed, otherwise -E2BIG is returned.
+ *
+ * Returns (in all cases, some data may have been copied):
+ * * -E2BIG: (@usize > @ksize) and there are non-zero trailing bytes in @src.
+ * * -E2BIG: @usize is "too big" (at time of writing, >PAGE_SIZE).
+ * * -EFAULT: access to user space failed.
+ */
+int copy_struct_from_user(void *dst, size_t ksize,
+ const void __user *src, size_t usize)
+{
+ size_t size = min(ksize, usize);
+ size_t rest = abs(ksize - usize);
+
+ if (unlikely(usize > PAGE_SIZE))
+ return -EFAULT;That should be -E2BIG.
+ if (unlikely(!access_ok(src, usize))) + return -EFAULT; + + /* Deal with trailing bytes. */ + if (usize < ksize) + memset(dst + size, 0, rest);
I think kernel style mandates that if one branch in an if-else ladder
requires {} all other must use {} as well. So this should be:
if () {
// one line
} else {
// one line
// another line
}
That's a change in behavior for clone3() and sched at least, no? Unless
- which I guess you might have done - you have moved the "error out when
the struct is too small" part before the call to copy_struct_from_user()
for them.
+ else if (usize > ksize) {
+ const void __user *addr = src + size;
+ char buffer[BUFFER_SIZE] = {};
+
+ while (rest > 0) {
+ size_t bufsize = min(rest, sizeof(buffer));
+
+ if (__copy_from_user(buffer, addr, bufsize))
+ return -EFAULT;
+ if (memchr_inv(buffer, 0, bufsize))
+ return -E2BIG;
+
+ addr += bufsize;
+ rest -= bufsize;
+ }
+ }
+ /* Copy the interoperable parts of the struct. */
+ if (__copy_from_user(dst, src, size))
+ return -EFAULT;
+ return 0;
+}
+EXPORT_SYMBOL(copy_struct_from_user);
--
2.23.0