]> git.ipfire.org Git - thirdparty/kernel/stable.git/commitdiff
fs/proc: fix uaf in proc_readdir_de()
authorWei Yang <albinwyang@tencent.com>
Sat, 25 Oct 2025 02:42:33 +0000 (10:42 +0800)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Mon, 24 Nov 2025 09:36:00 +0000 (10:36 +0100)
commit 895b4c0c79b092d732544011c3cecaf7322c36a1 upstream.

Pde is erased from subdir rbtree through rb_erase(), but not set the node
to EMPTY, which may result in uaf access.  We should use RB_CLEAR_NODE()
set the erased node to EMPTY, then pde_subdir_next() will return NULL to
avoid uaf access.

We found an uaf issue while using stress-ng testing, need to run testcase
getdent and tun in the same time.  The steps of the issue is as follows:

1) use getdent to traverse dir /proc/pid/net/dev_snmp6/, and current
   pde is tun3;

2) in the [time windows] unregister netdevice tun3 and tun2, and erase
   them from rbtree.  erase tun3 first, and then erase tun2.  the
   pde(tun2) will be released to slab;

3) continue to getdent process, then pde_subdir_next() will return
   pde(tun2) which is released, it will case uaf access.

CPU 0                                      |    CPU 1
-------------------------------------------------------------------------
traverse dir /proc/pid/net/dev_snmp6/      |   unregister_netdevice(tun->dev)   //tun3 tun2
sys_getdents64()                           |
  iterate_dir()                            |
    proc_readdir()                         |
      proc_readdir_de()                    |     snmp6_unregister_dev()
        pde_get(de);                       |       proc_remove()
        read_unlock(&proc_subdir_lock);    |         remove_proc_subtree()
                                           |           write_lock(&proc_subdir_lock);
        [time window]                      |           rb_erase(&root->subdir_node, &parent->subdir);
                                           |           write_unlock(&proc_subdir_lock);
        read_lock(&proc_subdir_lock);      |
        next = pde_subdir_next(de);        |
        pde_put(de);                       |
        de = next;    //UAF                |

rbtree of dev_snmp6
                        |
                    pde(tun3)
                     /    \
                  NULL  pde(tun2)

Link: https://lkml.kernel.org/r/20251025024233.158363-1-albin_yang@163.com
Signed-off-by: Wei Yang <albinwyang@tencent.com>
Cc: Al Viro <viro@zeniv.linux.org.uk>
Cc: Christian Brauner <brauner@kernel.org>
Cc: wangzijie <wangzijie1@honor.com>
Cc: Alexey Dobriyan <adobriyan@gmail.com>
Cc: <stable@vger.kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
fs/proc/generic.c

index eb49beff69bcb8d8f8a59262eac975adf12e5d7a..69150974ad8762d19047fcfdf67617d8423e5317 100644 (file)
@@ -694,6 +694,12 @@ void pde_put(struct proc_dir_entry *pde)
        }
 }
 
+static void pde_erase(struct proc_dir_entry *pde, struct proc_dir_entry *parent)
+{
+       rb_erase(&pde->subdir_node, &parent->subdir);
+       RB_CLEAR_NODE(&pde->subdir_node);
+}
+
 /*
  * Remove a /proc entry and free it if it's not currently in use.
  */
@@ -716,7 +722,7 @@ void remove_proc_entry(const char *name, struct proc_dir_entry *parent)
                        WARN(1, "removing permanent /proc entry '%s'", de->name);
                        de = NULL;
                } else {
-                       rb_erase(&de->subdir_node, &parent->subdir);
+                       pde_erase(de, parent);
                        if (S_ISDIR(de->mode))
                                parent->nlink--;
                }
@@ -760,7 +766,7 @@ int remove_proc_subtree(const char *name, struct proc_dir_entry *parent)
                        root->parent->name, root->name);
                return -EINVAL;
        }
-       rb_erase(&root->subdir_node, &parent->subdir);
+       pde_erase(root, parent);
 
        de = root;
        while (1) {
@@ -772,7 +778,7 @@ int remove_proc_subtree(const char *name, struct proc_dir_entry *parent)
                                        next->parent->name, next->name);
                                return -EINVAL;
                        }
-                       rb_erase(&next->subdir_node, &de->subdir);
+                       pde_erase(next, de);
                        de = next;
                        continue;
                }