On Mon 02-11-26 15:21:34, Christian Brauner wrote:
> proc_readdir_de() counts the entries of a directory and resumes a
> listing at the number getdents64() stopped at. The entries are sorted
> by name, so one that is added or removed in between shifts everything
> behind it by one. Listing /proc/net/dev_snmp6 with a buffer that holds
> ".", ".." and "lo" and adding a device whose name sorts before "lo"
> makes the next call return "lo" a second time. Removing one that sorted
> before the position makes the next call skip an entry instead.
> 
> Since commit 895b4c0c79b0 ("fs/proc: fix uaf in proc_readdir_de()") an
> entry that is removed while it is being emitted also ends the listing
> right there. pde_subdir_next() finds nothing after an erased entry, so
> proc_readdir_de() reports the end of the directory and the next call
> resumes by number, skipping another entry. For /proc itself
> proc_root_readdir() takes that as the end of the static entries and
> moves on to the pids, so the rest of the static entries is missing.
> And if the position is behind the last entry when a call starts
> proc_readdir_de() returns 0, so /proc then lists no pids at all.
> 
> So remember the name of the last entry a call emitted in the file and
> resume at the first entry after it, like kernfs resumes after the node
> it stopped at. An entry that was removed while it was emitted is
> continued after by its name as well. If the position was moved by a
> seek in between the listing counts from the start like before.
> 
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Fixes: 895b4c0c79b0 ("fs/proc: fix uaf in proc_readdir_de()")
> Cc: [email protected]
> Signed-off-by: Christian Brauner (Amutable) <[email protected]>

Looks good. Feel free to add:

Reviewed-by: Jan Kara <[email protected]>

                                                                Honza

> ---
>  fs/proc/generic.c  | 89 
> ++++++++++++++++++++++++++++++++++++++++++++++--------
>  fs/proc/internal.h |  1 +
>  fs/proc/proc_net.c |  1 +
>  fs/proc/root.c     |  1 +
>  4 files changed, 80 insertions(+), 12 deletions(-)
> 
> diff --git a/fs/proc/generic.c b/fs/proc/generic.c
> index ef804cd3ec45..47830d6af255 100644
> --- a/fs/proc/generic.c
> +++ b/fs/proc/generic.c
> @@ -87,6 +87,28 @@ static struct proc_dir_entry *pde_subdir_find(struct 
> proc_dir_entry *dir,
>       return NULL;
>  }
>  
> +/* The first entry of @dir that sorts after @name. */
> +static struct proc_dir_entry *pde_subdir_after(struct proc_dir_entry *dir,
> +                                            const char *name,
> +                                            unsigned int len)
> +{
> +     struct rb_node *node = dir->subdir.rb_node, *after = NULL;
> +
> +     while (node) {
> +             struct proc_dir_entry *de = rb_entry(node,
> +                                                  struct proc_dir_entry,
> +                                                  subdir_node);
> +
> +             if (proc_match(name, de, len) < 0) {
> +                     after = node;
> +                     node = node->rb_left;
> +             } else {
> +                     node = node->rb_right;
> +             }
> +     }
> +     return rb_entry_safe(after, struct proc_dir_entry, subdir_node);
> +}
> +
>  static bool pde_subdir_insert(struct proc_dir_entry *dir,
>                             struct proc_dir_entry *de)
>  {
> @@ -277,6 +299,40 @@ struct dentry *proc_lookup(struct inode *dir, struct 
> dentry *dentry,
>       return proc_lookup_de(dir, dentry, PDE(dir));
>  }
>  
> +/*
> + * Where the next readdir resumes. The entries are counted, so one that is
> + * added or removed in front of the position would shift what the next call
> + * returns. The name of the last entry emitted doesn't move.
> + */
> +struct proc_dir_cursor {
> +     loff_t pos;                     /* the position behind the entry */
> +     unsigned int len;
> +     char name[NAME_MAX + 1];
> +};
> +
> +/* If this can't remember the entry the next call counts from the start. */
> +static void proc_dir_cursor_set(struct file *file, struct proc_dir_entry *de,
> +                             loff_t pos)
> +{
> +     struct proc_dir_cursor *cursor = file->private_data;
> +
> +     if (!cursor) {
> +             cursor = kmalloc(sizeof(*cursor), GFP_KERNEL);
> +             if (!cursor)
> +                     return;
> +             file->private_data = cursor;
> +     }
> +     cursor->pos = pos;
> +     cursor->len = de->namelen;
> +     memcpy(cursor->name, de->name, de->namelen);
> +}
> +
> +int proc_dir_release(struct inode *inode, struct file *file)
> +{
> +     kfree(file->private_data);
> +     return 0;
> +}
> +
>  /*
>   * This returns non-zero if at EOF, so that the /proc
>   * root directory can use this and check if it should
> @@ -289,23 +345,26 @@ struct dentry *proc_lookup(struct inode *dir, struct 
> dentry *dentry,
>  int proc_readdir_de(struct file *file, struct dir_context *ctx,
>                   struct proc_dir_entry *de)
>  {
> +     struct proc_dir_cursor *cursor = file->private_data;
> +     struct proc_dir_entry *dir = de;
>       int i;
>  
>       if (!dir_emit_dots(file, ctx))
>               return 0;
>  
> -     i = ctx->pos - 2;
>       read_lock(&proc_subdir_lock);
> -     de = pde_subdir_first(de);
> -     for (;;) {
> -             if (!de) {
> -                     read_unlock(&proc_subdir_lock);
> -                     return 0;
> -             }
> -             if (!i)
> -                     break;
> -             de = pde_subdir_next(de);
> -             i--;
> +     if (cursor && cursor->pos == ctx->pos) {
> +             /* Resume after the last entry emitted, wherever it is now. */
> +             de = pde_subdir_after(dir, cursor->name, cursor->len);
> +     } else {
> +             /* Moved by a seek, count from the start like always. */
> +             de = pde_subdir_first(dir);
> +             for (i = ctx->pos - 2; de && i; i--)
> +                     de = pde_subdir_next(de);
> +     }
> +     if (!de) {
> +             read_unlock(&proc_subdir_lock);
> +             return 1;
>       }
>  
>       do {
> @@ -318,8 +377,13 @@ int proc_readdir_de(struct file *file, struct 
> dir_context *ctx,
>                       return 0;
>               }
>               ctx->pos++;
> +             proc_dir_cursor_set(file, de, ctx->pos);
>               read_lock(&proc_subdir_lock);
> -             next = pde_subdir_next(de);
> +             /* Removed in the meantime, go on after its name. */
> +             if (RB_EMPTY_NODE(&de->subdir_node))
> +                     next = pde_subdir_after(dir, de->name, de->namelen);
> +             else
> +                     next = pde_subdir_next(de);
>               pde_put(de);
>               de = next;
>       } while (de);
> @@ -347,6 +411,7 @@ static const struct file_operations proc_dir_operations = 
> {
>       .llseek                 = generic_file_llseek,
>       .read                   = generic_read_dir,
>       .iterate_shared         = proc_readdir,
> +     .release                = proc_dir_release,
>  };
>  
>  static int proc_net_d_revalidate(struct inode *dir, const struct qstr *name,
> diff --git a/fs/proc/internal.h b/fs/proc/internal.h
> index df873df9afc5..04f7ce2b24ed 100644
> --- a/fs/proc/internal.h
> +++ b/fs/proc/internal.h
> @@ -287,6 +287,7 @@ extern struct dentry *proc_lookup(struct inode *, struct 
> dentry *, unsigned int)
>  struct dentry *proc_lookup_de(struct inode *, struct dentry *, struct 
> proc_dir_entry *);
>  extern int proc_readdir(struct file *, struct dir_context *);
>  int proc_readdir_de(struct file *, struct dir_context *, struct 
> proc_dir_entry *);
> +int proc_dir_release(struct inode *, struct file *);
>  
>  static inline void pde_get(struct proc_dir_entry *pde)
>  {
> diff --git a/fs/proc/proc_net.c b/fs/proc/proc_net.c
> index b1f5eafb069a..dbcf87a257e1 100644
> --- a/fs/proc/proc_net.c
> +++ b/fs/proc/proc_net.c
> @@ -351,6 +351,7 @@ const struct file_operations proc_net_operations = {
>       .llseek         = generic_file_llseek,
>       .read           = generic_read_dir,
>       .iterate_shared = proc_tgid_net_readdir,
> +     .release        = proc_dir_release,
>  };
>  
>  static __net_init int proc_net_ns_init(struct net *net)
> diff --git a/fs/proc/root.c b/fs/proc/root.c
> index 7fbbe92bf73a..dfee1caa26f7 100644
> --- a/fs/proc/root.c
> +++ b/fs/proc/root.c
> @@ -441,6 +441,7 @@ static const struct file_operations proc_root_operations 
> = {
>       .read            = generic_read_dir,
>       .iterate_shared  = proc_root_readdir,
>       .llseek         = generic_file_llseek,
> +     .release        = proc_dir_release,
>  };
>  
>  /*
> 
> -- 
> 2.53.0
> 
-- 
Jan Kara <[email protected]>
SUSE Labs, CR

Reply via email to