chash.last is not reset when the tree in use switches from the backup
servers back to the active ones. The next call then starts walking the
active tree while keeping a backup node as the stop condition, which it can
never reach, so it cycles over the active tree forever as soon as no server
can be picked: all of them saturated (maxconn reached with a queue, or
served >= the dynamic maxconn), or the only one left being the server to
avoid on a redispatch. In this case, if chash.last (assigned to <stop>)
isn't in the current tree, the loop will run forever.
Let's just count the number of times we wrap and stop at the second,
which indicates that the stop server is not in the tree.
Note that this is more theoretical than practical: this needs
"hash-type consistent" with requests not carrying the hash key so that
this fallback is used at all, backup servers with "option allbackups"
(without it lbprm.fbck is returned directly and the backup tree is never
walked), and all active servers going down then one coming back while
the remaining ones are saturated. In practice it has been there since
consistent hash was introduced in 1.4 by commit
6b2e11be1 ("[MEDIUM]
backend: implement consistent hashing variation") and was never
reported. It may be backported to all stable versions.
Reported-by: Claude (ANT-2026-98TCHHRD)
struct server *srv, *avoided;
struct eb32_node *node, *stop, *avoided_node;
struct eb_root *root;
+ int wrapped = 0;
srv = avoided = NULL;
avoided_node = NULL;
if (node)
node = eb32_next(node);
- if (!node)
+ if (!node) {
+ /* Reaching the end of the tree twice means that <stop>
+ * does not belong to <root> (e.g. active servers found
+ * from .last when only backup usable). So let's count
+ * wraps to avoid looping forever.
+ */
+ if (wrapped++)
+ break;
node = eb32_first(root);
+ }
p->lbprm.chash.last = node;
if (!node) {