Alexey Dobriyan <adobri...@gmail.com> writes:

> /proc/*/cmdline continues to cause problems:
>
>       https://lkml.org/lkml/2019/4/5/825
>       Subject get_mm_cmdline and userspace (Perl) changing argv0
>
>       https://marc.info/?l=linux-kernel&m=156294831308786&w=4
>       [PATCH] proc: Fix uninitialized byte read in get_mm_cmdline()
>
> This patch reverts implementation to the last known good state.
> Revert
>
>       commit f5b65348fd77839b50e79bc0a5e536832ea52d8d
>       proc: fix missing final NUL in get_mm_cmdline() rewrite
>
>       commit 5ab8271899658042fabc5ae7e6a99066a210bc0e
>       fs/proc: simplify and clarify get_mm_cmdline() function
>
> Signed-off-by: Alexey Dobriyan <adobri...@gmail.com>

Given that this fixes a regression and a bug.

Acked-by: "Eric W. Biederman" <ebied...@xmission.com>

> ---
>
>       Cc lists
>
>  fs/proc/base.c |  198 
> +++++++++++++++++++++++++++++++++------------------------
>  1 file changed, 118 insertions(+), 80 deletions(-)
>
> --- a/fs/proc/base.c
> +++ b/fs/proc/base.c
> @@ -210,16 +210,24 @@ static int proc_root_link(struct dentry *dentry, struct 
> path *path)
>  }
>  
>  static ssize_t get_mm_cmdline(struct mm_struct *mm, char __user *buf,
> -                           size_t count, loff_t *ppos)
> +                           size_t _count, loff_t *pos)
>  {
> +     unsigned long count = _count;
>       unsigned long arg_start, arg_end, env_start, env_end;
> -     unsigned long pos, len;
> +     unsigned long len1, len2, len;
> +     unsigned long p;
>       char *page;
> +     ssize_t rv;
> +     char c;
>  
>       /* Check if process spawned far enough to have cmdline. */
>       if (!mm->env_end)
>               return 0;
>  
> +     page = (char *)__get_free_page(GFP_KERNEL);
> +     if (!page)
> +             return -ENOMEM;
> +
>       spin_lock(&mm->arg_lock);
>       arg_start = mm->arg_start;
>       arg_end = mm->arg_end;
> @@ -227,96 +235,126 @@ static ssize_t get_mm_cmdline(struct mm_struct *mm, 
> char __user *buf,
>       env_end = mm->env_end;
>       spin_unlock(&mm->arg_lock);
>  
> -     if (arg_start >= arg_end)
> -             return 0;
> +     BUG_ON(arg_start > arg_end);
> +     BUG_ON(env_start > env_end);
> +
> +     len1 = arg_end - arg_start;
> +     len2 = env_end - env_start;
>  
> +     /* Empty ARGV. */
> +     if (len1 == 0) {
> +             rv = 0;
> +             goto out_free_page;
> +     }
>       /*
> -      * We have traditionally allowed the user to re-write
> -      * the argument strings and overflow the end result
> -      * into the environment section. But only do that if
> -      * the environment area is contiguous to the arguments.
> +      * Inherently racy -- command line shares address space
> +      * with code and data.
>        */
> -     if (env_start != arg_end || env_start >= env_end)
> -             env_start = env_end = arg_end;
> -
> -     /* .. and limit it to a maximum of one page of slop */
> -     if (env_end >= arg_end + PAGE_SIZE)
> -             env_end = arg_end + PAGE_SIZE - 1;
> -
> -     /* We're not going to care if "*ppos" has high bits set */
> -     pos = arg_start + *ppos;
> -
> -     /* .. but we do check the result is in the proper range */
> -     if (pos < arg_start || pos >= env_end)
> -             return 0;
> -
> -     /* .. and we never go past env_end */
> -     if (env_end - pos < count)
> -             count = env_end - pos;
> -
> -     page = (char *)__get_free_page(GFP_KERNEL);
> -     if (!page)
> -             return -ENOMEM;
> -
> -     len = 0;
> -     while (count) {
> -             int got;
> -             size_t size = min_t(size_t, PAGE_SIZE, count);
> -             long offset;
> +     rv = access_remote_vm(mm, arg_end - 1, &c, 1, FOLL_ANON);
> +     if (rv <= 0)
> +             goto out_free_page;
> +
> +     rv = 0;
> +
> +     if (c == '\0') {
> +             /* Command line (set of strings) occupies whole ARGV. */
> +             if (len1 <= *pos)
> +                     goto out_free_page;
> +
> +             p = arg_start + *pos;
> +             len = len1 - *pos;
> +             while (count > 0 && len > 0) {
> +                     unsigned int _count;
> +                     int nr_read;
> +
> +                     _count = min3(count, len, PAGE_SIZE);
> +                     nr_read = access_remote_vm(mm, p, page, _count, 
> FOLL_ANON);
> +                     if (nr_read < 0)
> +                             rv = nr_read;
> +                     if (nr_read <= 0)
> +                             goto out_free_page;
> +
> +                     if (copy_to_user(buf, page, nr_read)) {
> +                             rv = -EFAULT;
> +                             goto out_free_page;
> +                     }
>  
> +                     p       += nr_read;
> +                     len     -= nr_read;
> +                     buf     += nr_read;
> +                     count   -= nr_read;
> +                     rv      += nr_read;
> +             }
> +     } else {
>               /*
> -              * Are we already starting past the official end?
> -              * We always include the last byte that is *supposed*
> -              * to be NUL
> +              * Command line (1 string) occupies ARGV and
> +              * extends into ENVP.
>                */
> -             offset = (pos >= arg_end) ? pos - arg_end + 1 : 0;
> -
> -             got = access_remote_vm(mm, pos - offset, page, size + offset, 
> FOLL_ANON);
> -             if (got <= offset)
> -                     break;
> -             got -= offset;
> -
> -             /* Don't walk past a NUL character once you hit arg_end */
> -             if (pos + got >= arg_end) {
> -                     int n = 0;
> -
> -                     /*
> -                      * If we started before 'arg_end' but ended up
> -                      * at or after it, we start the NUL character
> -                      * check at arg_end-1 (where we expect the normal
> -                      * EOF to be).
> -                      *
> -                      * NOTE! This is smaller than 'got', because
> -                      * pos + got >= arg_end
> -                      */
> -                     if (pos < arg_end)
> -                             n = arg_end - pos - 1;
> -
> -                     /* Cut off at first NUL after 'n' */
> -                     got = n + strnlen(page+n, offset+got-n);
> -                     if (got < offset)
> -                             break;
> -                     got -= offset;
> -
> -                     /* Include the NUL if it existed */
> -                     if (got < size)
> -                             got++;
> +             struct {
> +                     unsigned long p;
> +                     unsigned long len;
> +             } cmdline[2] = {
> +                     { .p = arg_start, .len = len1 },
> +                     { .p = env_start, .len = len2 },
> +             };
> +             loff_t pos1 = *pos;
> +             unsigned int i;
> +
> +             i = 0;
> +             while (i < 2 && pos1 >= cmdline[i].len) {
> +                     pos1 -= cmdline[i].len;
> +                     i++;
>               }
> +             while (i < 2) {
> +                     p = cmdline[i].p + pos1;
> +                     len = cmdline[i].len - pos1;
> +                     while (count > 0 && len > 0) {
> +                             unsigned int _count, l;
> +                             int nr_read;
> +                             bool final;
> +
> +                             _count = min3(count, len, PAGE_SIZE);
> +                             nr_read = access_remote_vm(mm, p, page, _count, 
> FOLL_ANON);
> +                             if (nr_read < 0)
> +                                     rv = nr_read;
> +                             if (nr_read <= 0)
> +                                     goto out_free_page;
> +
> +                             /*
> +                              * Command line can be shorter than whole ARGV
> +                              * even if last "marker" byte says it is not.
> +                              */
> +                             final = false;
> +                             l = strnlen(page, nr_read);
> +                             if (l < nr_read) {
> +                                     nr_read = l;
> +                                     final = true;
> +                             }
> +
> +                             if (copy_to_user(buf, page, nr_read)) {
> +                                     rv = -EFAULT;
> +                                     goto out_free_page;
> +                             }
> +
> +                             p       += nr_read;
> +                             len     -= nr_read;
> +                             buf     += nr_read;
> +                             count   -= nr_read;
> +                             rv      += nr_read;
> +
> +                             if (final)
> +                                     goto out_free_page;
> +                     }
>  
> -             got -= copy_to_user(buf, page+offset, got);
> -             if (unlikely(!got)) {
> -                     if (!len)
> -                             len = -EFAULT;
> -                     break;
> +                     /* Only first chunk can be read partially. */
> +                     pos1 = 0;
> +                     i++;
>               }
> -             pos += got;
> -             buf += got;
> -             len += got;
> -             count -= got;
>       }
>  
> +out_free_page:
>       free_page((unsigned long)page);
> -     return len;
> +     return rv;
>  }
>  
>  static ssize_t get_task_cmdline(struct task_struct *tsk, char __user *buf,

Reply via email to