On Mon 02-11-26 15:21:36, Christian Brauner wrote:
> proc_remove() looks the entry up by its name in its parent and removes
> whatever it finds there:
> 
>   remove_proc_subtree(de->name, de->parent);
> 
> If the entry was removed already, say along with its parent, that is
> either nothing or a different entry that got the same name in the
> meantime, and de->parent may well be gone. Two removals of the same
> entry are a bug in the caller, but going by the name turns it into
> removing an entry that belongs to somebody else.
> 
> Since commit 895b4c0c79b0 ("fs/proc: fix uaf in proc_readdir_de()")
> an entry that was unlinked has an empty rb node. So check for that
> under proc_subdir_lock and otherwise remove the entry itself, with the
> loop of remove_proc_subtree() moved into a helper both of them use.
> Nothing can help a caller that passes an entry that has been freed
> already.
> 
> Reported-by: Chris Mason <[email protected]>
> Fixes: a8ca16ea7b0a ("proc: Supply a function to remove a proc entry by PDE")
> Signed-off-by: Christian Brauner (Amutable) <[email protected]>

One nitpick below. Otherwise feel free to add:

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

> -int remove_proc_subtree(const char *name, struct proc_dir_entry *parent)
> +/*
> + * Remove @root, already erased from its parent, and everything below it.
> + * Called with proc_subdir_lock held for writing, returns with it dropped.
> + */
> +static int __remove_proc_subtree(struct proc_dir_entry *root)
>  {
> -     struct proc_dir_entry *root = NULL, *de, *next;
> -     const char *fn = name;
> -     unsigned int len;
> -
> -     write_lock(&proc_subdir_lock);
> -     if (__xlate_proc_name(name, &parent, &fn) != 0) {
> -             write_unlock(&proc_subdir_lock);
> -             return -ENOENT;
> -     }
> -     len = strlen(fn);
> +     struct proc_dir_entry *de = root, *next;
>  
> -     root = pde_subdir_find(parent, fn, len);
> -     if (!root) {
> -             write_unlock(&proc_subdir_lock);
> -             return -ENOENT;
> -     }
> -     if (unlikely(pde_is_permanent(root))) {
> -             write_unlock(&proc_subdir_lock);
> -             WARN(1, "removing permanent /proc entry '%s/%s'",
> -                     root->parent->name, root->name);
> -             return -EINVAL;
> -     }
> -     pde_erase(root, parent);
> -
> -     de = root;
>       while (1) {
>               next = pde_subdir_first(de);
>               if (next) {
> @@ -871,6 +852,34 @@ int remove_proc_subtree(const char *name, struct 
> proc_dir_entry *parent)
>       pde_put(root);
>       return 0;
>  }
> +
> +int remove_proc_subtree(const char *name, struct proc_dir_entry *parent)
> +{
> +     struct proc_dir_entry *root = NULL;
> +     const char *fn = name;
> +     unsigned int len;
> +
> +     write_lock(&proc_subdir_lock);
> +     if (__xlate_proc_name(name, &parent, &fn) != 0) {
> +             write_unlock(&proc_subdir_lock);
> +             return -ENOENT;
> +     }
> +     len = strlen(fn);
> +
> +     root = pde_subdir_find(parent, fn, len);
> +     if (!root) {
> +             write_unlock(&proc_subdir_lock);
> +             return -ENOENT;
> +     }
> +     if (unlikely(pde_is_permanent(root))) {
> +             write_unlock(&proc_subdir_lock);
> +             WARN(1, "removing permanent /proc entry '%s/%s'",
> +                     root->parent->name, root->name);
> +             return -EINVAL;
> +     }
> +     pde_erase(root, parent);
> +     return __remove_proc_subtree(root);
> +}
>  EXPORT_SYMBOL(remove_proc_subtree);
>  
>  void *proc_get_parent_data(const struct inode *inode)
> @@ -882,8 +891,23 @@ EXPORT_SYMBOL_GPL(proc_get_parent_data);
>  
>  void proc_remove(struct proc_dir_entry *de)
>  {
> -     if (de)
> -             remove_proc_subtree(de->name, de->parent);
> +     if (!de)
> +             return;
> +
> +     write_lock(&proc_subdir_lock);
> +     /* Removed already, maybe along with its parent. */
> +     if (RB_EMPTY_NODE(&de->subdir_node)) {
> +             write_unlock(&proc_subdir_lock);
> +             return;
> +     }
> +     if (unlikely(pde_is_permanent(de))) {
> +             write_unlock(&proc_subdir_lock);
> +             WARN(1, "removing permanent /proc entry '%s/%s'",
> +                  de->parent->name, de->name);
> +             return;
> +     }
> +     pde_erase(de, de->parent);
> +     __remove_proc_subtree(de);

I'd move the RB_EMPTY_NODE() and pde_is_permanent() along with pde_erase()
call back to __remove_proc_subtree(). Both callers need them (so it is
unnecessary duplication) and also logically it makes sense there.

                                                                Honza
-- 
Jan Kara <[email protected]>
SUSE Labs, CR

Reply via email to