]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
eventpoll: Fix semi-unbounded recursion
authorJann Horn <jannh@google.com>
Fri, 11 Jul 2025 16:33:36 +0000 (18:33 +0200)
committerChristian Brauner <brauner@kernel.org>
Wed, 16 Jul 2025 12:48:03 +0000 (14:48 +0200)
Ensure that epoll instances can never form a graph deeper than
EP_MAX_NESTS+1 links.

Currently, ep_loop_check_proc() ensures that the graph is loop-free and
does some recursion depth checks, but those recursion depth checks don't
limit the depth of the resulting tree for two reasons:

 - They don't look upwards in the tree.
 - If there are multiple downwards paths of different lengths, only one of
   the paths is actually considered for the depth check since commit
   28d82dc1c4ed ("epoll: limit paths").

Essentially, the current recursion depth check in ep_loop_check_proc() just
serves to prevent it from recursing too deeply while checking for loops.

A more thorough check is done in reverse_path_check() after the new graph
edge has already been created; this checks, among other things, that no
paths going upwards from any non-epoll file with a length of more than 5
edges exist. However, this check does not apply to non-epoll files.

As a result, it is possible to recurse to a depth of at least roughly 500,
tested on v6.15. (I am unsure if deeper recursion is possible; and this may
have changed with commit 8c44dac8add7 ("eventpoll: Fix priority inversion
problem").)

To fix it:

1. In ep_loop_check_proc(), note the subtree depth of each visited node,
and use subtree depths for the total depth calculation even when a subtree
has already been visited.
2. Add ep_get_upwards_depth_proc() for similarly determining the maximum
depth of an upwards walk.
3. In ep_loop_check(), use these values to limit the total path length
between epoll nodes to EP_MAX_NESTS edges.

Fixes: 22bacca48a17 ("epoll: prevent creating circular epoll structures")
Cc: stable@vger.kernel.org
Signed-off-by: Jann Horn <jannh@google.com>
Link: https://lore.kernel.org/20250711-epoll-recursion-fix-v1-1-fb2457c33292@google.com
Signed-off-by: Christian Brauner <brauner@kernel.org>
fs/eventpoll.c

index d4dbffdedd08e36a661e559b857a25349561ae62..44648cc09250ece23212b0717121c5ac3b19ea09 100644 (file)
@@ -218,6 +218,7 @@ struct eventpoll {
        /* used to optimize loop detection check */
        u64 gen;
        struct hlist_head refs;
+       u8 loop_check_depth;
 
        /*
         * usage count, used together with epitem->dying to
@@ -2142,23 +2143,24 @@ static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
 }
 
 /**
- * ep_loop_check_proc - verify that adding an epoll file inside another
- *                      epoll structure does not violate the constraints, in
- *                      terms of closed loops, or too deep chains (which can
- *                      result in excessive stack usage).
+ * ep_loop_check_proc - verify that adding an epoll file @ep inside another
+ *                      epoll file does not create closed loops, and
+ *                      determine the depth of the subtree starting at @ep
  *
  * @ep: the &struct eventpoll to be currently checked.
  * @depth: Current depth of the path being checked.
  *
- * Return: %zero if adding the epoll @file inside current epoll
- *          structure @ep does not violate the constraints, or %-1 otherwise.
+ * Return: depth of the subtree, or INT_MAX if we found a loop or went too deep.
  */
 static int ep_loop_check_proc(struct eventpoll *ep, int depth)
 {
-       int error = 0;
+       int result = 0;
        struct rb_node *rbp;
        struct epitem *epi;
 
+       if (ep->gen == loop_check_gen)
+               return ep->loop_check_depth;
+
        mutex_lock_nested(&ep->mtx, depth + 1);
        ep->gen = loop_check_gen;
        for (rbp = rb_first_cached(&ep->rbr); rbp; rbp = rb_next(rbp)) {
@@ -2166,13 +2168,11 @@ static int ep_loop_check_proc(struct eventpoll *ep, int depth)
                if (unlikely(is_file_epoll(epi->ffd.file))) {
                        struct eventpoll *ep_tovisit;
                        ep_tovisit = epi->ffd.file->private_data;
-                       if (ep_tovisit->gen == loop_check_gen)
-                               continue;
                        if (ep_tovisit == inserting_into || depth > EP_MAX_NESTS)
-                               error = -1;
+                               result = INT_MAX;
                        else
-                               error = ep_loop_check_proc(ep_tovisit, depth + 1);
-                       if (error != 0)
+                               result = max(result, ep_loop_check_proc(ep_tovisit, depth + 1) + 1);
+                       if (result > EP_MAX_NESTS)
                                break;
                } else {
                        /*
@@ -2186,9 +2186,27 @@ static int ep_loop_check_proc(struct eventpoll *ep, int depth)
                        list_file(epi->ffd.file);
                }
        }
+       ep->loop_check_depth = result;
        mutex_unlock(&ep->mtx);
 
-       return error;
+       return result;
+}
+
+/**
+ * ep_get_upwards_depth_proc - determine depth of @ep when traversed upwards
+ */
+static int ep_get_upwards_depth_proc(struct eventpoll *ep, int depth)
+{
+       int result = 0;
+       struct epitem *epi;
+
+       if (ep->gen == loop_check_gen)
+               return ep->loop_check_depth;
+       hlist_for_each_entry_rcu(epi, &ep->refs, fllink)
+               result = max(result, ep_get_upwards_depth_proc(epi->ep, depth + 1) + 1);
+       ep->gen = loop_check_gen;
+       ep->loop_check_depth = result;
+       return result;
 }
 
 /**
@@ -2204,8 +2222,22 @@ static int ep_loop_check_proc(struct eventpoll *ep, int depth)
  */
 static int ep_loop_check(struct eventpoll *ep, struct eventpoll *to)
 {
+       int depth, upwards_depth;
+
        inserting_into = ep;
-       return ep_loop_check_proc(to, 0);
+       /*
+        * Check how deep down we can get from @to, and whether it is possible
+        * to loop up to @ep.
+        */
+       depth = ep_loop_check_proc(to, 0);
+       if (depth > EP_MAX_NESTS)
+               return -1;
+       /* Check how far up we can go from @ep. */
+       rcu_read_lock();
+       upwards_depth = ep_get_upwards_depth_proc(ep, 0);
+       rcu_read_unlock();
+
+       return (depth+1+upwards_depth > EP_MAX_NESTS) ? -1 : 0;
 }
 
 static void clear_tfile_check_list(void)