diff mbox series

[6/6] lib: Add function strscpy_from_user()

Message ID 20190218232308.11241-7-tobin@kernel.org (mailing list archive)
State New, archived
Headers show
Series lib: Add safe string funtions | expand

Commit Message

Tobin C. Harding Feb. 18, 2019, 11:23 p.m. UTC
Currently we have strncpy_from_userspace().  If the user string is
longer than the destination kernel buffer we get an error code -EFAULT.
We are unable to recover from here because this is the same error
returned if the access to userspace fails totally.

There is no reason we cannot continue execution with the user string
truncated.

Add a function strscpy_from_user() that guarantees the string written is
null-terminated.  If user string is longer than destination buffer
truncates the string.  Returns the number of characters written
excluding the null-terminator.

Signed-off-by: Tobin C. Harding <tobin@kernel.org>
---
 lib/strncpy_from_user.c | 43 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)

Comments

Jann Horn Feb. 19, 2019, 2:09 a.m. UTC | #1
On Tue, Feb 19, 2019 at 12:25 AM Tobin C. Harding <tobin@kernel.org> wrote:
> Currently we have strncpy_from_userspace().  If the user string is
> longer than the destination kernel buffer we get an error code -EFAULT.

No, see the other thread. If the user string is too long,
strncpy_from_userspace() fills the output buffer with non-null bytes
and returns the supplied length.

> We are unable to recover from here because this is the same error
> returned if the access to userspace fails totally.
>
> There is no reason we cannot continue execution with the user string
> truncated.
>
> Add a function strscpy_from_user() that guarantees the string written is
> null-terminated.  If user string is longer than destination buffer
> truncates the string.  Returns the number of characters written
> excluding the null-terminator.
>
> Signed-off-by: Tobin C. Harding <tobin@kernel.org>
> ---
>  lib/strncpy_from_user.c | 43 +++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 43 insertions(+)
>
> diff --git a/lib/strncpy_from_user.c b/lib/strncpy_from_user.c
> index 11fe9a4a00fd..6bd603ccec7a 100644
> --- a/lib/strncpy_from_user.c
> +++ b/lib/strncpy_from_user.c
> @@ -120,3 +120,46 @@ long strncpy_from_user(char *dst, const char __user *src, long count)
>         return -EFAULT;
>  }
>  EXPORT_SYMBOL(strncpy_from_user);
> +
> +/**
> + * strscpy_from_user() - Copy a NUL terminated string from userspace.
> + * @dst:   Destination address, in kernel space.  This buffer must be at
> + *         least @count bytes long.
> + * @src:   Source address, in user space.
> + * @count: Maximum number of bytes to copy, including the trailing %NUL.
> + *
> + * Copies a NUL-terminated string from userspace to kernel space.  When
> + * the function returns @dst is guaranteed to be null terminated.
> + *
> + * Return: If access to userspace fails, returns -EFAULT.

That's wrong. Actually, you only return -EFAULT if the specified
source address points to an address outside the userspace address
range.

> Otherwise,
> + *         return the number of characters copied excluding the trailing
> + *         %NUL.
> + */
> +long strscpy_from_user(char *dst, const char __user *src, long count)
> +{
> +       unsigned long max_addr, src_addr;
> +
> +       if (unlikely(count <= 0))
> +               return 0;

The "supply a signed long and quietly bail out if it's smaller than
zero" pattern seems bad to me. If count is zero, you can't guarantee
that the buffer will be null-terminated, and if it's smaller than
zero, something has gone very wrong.

> +       max_addr = user_addr_max();
> +       src_addr = (unsigned long)src;
> +       if (likely(src_addr < max_addr)) {
> +               unsigned long max = max_addr - src_addr;
> +               long retval;
> +
> +               kasan_check_write(dst, count);
> +               check_object_size(dst, count, false);
> +               if (user_access_begin(src, max)) {
> +                       retval = do_strncpy_from_user(dst, src, count, max);
> +                       user_access_end();
> +                       if (retval == -EFAULT) {
> +                               dst[count-1] = '\0';
> +                               return count - 1;

Uh... this looks bad. If do_strncpy_from_user() gets a fault -
anywhere -, you just put a nullbyte at the end of the supplied buffer
and return? As far as I can tell, this means that the caller will
think that you've filled the entire buffer, but actually everything
except for the last byte might be uninitialized.

> +                       }
> +                       return retval;
> +               }
> +       }
> +       return -EFAULT;
> +}
> +EXPORT_SYMBOL(strscpy_from_user);
> --
> 2.20.1
>
Jann Horn Feb. 19, 2019, 2:12 a.m. UTC | #2
On Tue, Feb 19, 2019 at 12:25 AM Tobin C. Harding <tobin@kernel.org> wrote:
> Currently we have strncpy_from_userspace().  If the user string is
> longer than the destination kernel buffer we get an error code -EFAULT.
> We are unable to recover from here because this is the same error
> returned if the access to userspace fails totally.
>
> There is no reason we cannot continue execution with the user string
> truncated.
>
> Add a function strscpy_from_user() that guarantees the string written is
> null-terminated.  If user string is longer than destination buffer
> truncates the string.  Returns the number of characters written
> excluding the null-terminator.
>
> Signed-off-by: Tobin C. Harding <tobin@kernel.org>
> ---
>  lib/strncpy_from_user.c | 43 +++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 43 insertions(+)
>
> diff --git a/lib/strncpy_from_user.c b/lib/strncpy_from_user.c
> index 11fe9a4a00fd..6bd603ccec7a 100644
> --- a/lib/strncpy_from_user.c
> +++ b/lib/strncpy_from_user.c

This file is only built when CONFIG_GENERIC_STRNCPY_FROM_USER is set.
Some architectures have their own versions of strncpy_from_user() and
don't set that, so on those architectures, your code wouldn't be built
into the kernel.
Tobin Harding Feb. 19, 2019, 9:53 p.m. UTC | #3
On Tue, Feb 19, 2019 at 03:12:33AM +0100, Jann Horn wrote:
> On Tue, Feb 19, 2019 at 12:25 AM Tobin C. Harding <tobin@kernel.org> wrote:
> > Currently we have strncpy_from_userspace().  If the user string is
> > longer than the destination kernel buffer we get an error code -EFAULT.
> > We are unable to recover from here because this is the same error
> > returned if the access to userspace fails totally.
> >
> > There is no reason we cannot continue execution with the user string
> > truncated.
> >
> > Add a function strscpy_from_user() that guarantees the string written is
> > null-terminated.  If user string is longer than destination buffer
> > truncates the string.  Returns the number of characters written
> > excluding the null-terminator.
> >
> > Signed-off-by: Tobin C. Harding <tobin@kernel.org>
> > ---
> >  lib/strncpy_from_user.c | 43 +++++++++++++++++++++++++++++++++++++++++
> >  1 file changed, 43 insertions(+)
> >
> > diff --git a/lib/strncpy_from_user.c b/lib/strncpy_from_user.c
> > index 11fe9a4a00fd..6bd603ccec7a 100644
> > --- a/lib/strncpy_from_user.c
> > +++ b/lib/strncpy_from_user.c
> 
> This file is only built when CONFIG_GENERIC_STRNCPY_FROM_USER is set.
> Some architectures have their own versions of strncpy_from_user() and
> don't set that, so on those architectures, your code wouldn't be built
> into the kernel.

thanks!  Dropping *_from_user() stuff from set.

	Tobin
diff mbox series

Patch

diff --git a/lib/strncpy_from_user.c b/lib/strncpy_from_user.c
index 11fe9a4a00fd..6bd603ccec7a 100644
--- a/lib/strncpy_from_user.c
+++ b/lib/strncpy_from_user.c
@@ -120,3 +120,46 @@  long strncpy_from_user(char *dst, const char __user *src, long count)
 	return -EFAULT;
 }
 EXPORT_SYMBOL(strncpy_from_user);
+
+/**
+ * strscpy_from_user() - Copy a NUL terminated string from userspace.
+ * @dst:   Destination address, in kernel space.  This buffer must be at
+ *         least @count bytes long.
+ * @src:   Source address, in user space.
+ * @count: Maximum number of bytes to copy, including the trailing %NUL.
+ *
+ * Copies a NUL-terminated string from userspace to kernel space.  When
+ * the function returns @dst is guaranteed to be null terminated.
+ *
+ * Return: If access to userspace fails, returns -EFAULT.  Otherwise,
+ *         return the number of characters copied excluding the trailing
+ *         %NUL.
+ */
+long strscpy_from_user(char *dst, const char __user *src, long count)
+{
+	unsigned long max_addr, src_addr;
+
+	if (unlikely(count <= 0))
+		return 0;
+
+	max_addr = user_addr_max();
+	src_addr = (unsigned long)src;
+	if (likely(src_addr < max_addr)) {
+		unsigned long max = max_addr - src_addr;
+		long retval;
+
+		kasan_check_write(dst, count);
+		check_object_size(dst, count, false);
+		if (user_access_begin(src, max)) {
+			retval = do_strncpy_from_user(dst, src, count, max);
+			user_access_end();
+			if (retval == -EFAULT) {
+				dst[count-1] = '\0';
+				return count - 1;
+			}
+			return retval;
+		}
+	}
+	return -EFAULT;
+}
+EXPORT_SYMBOL(strscpy_from_user);