From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: David Lanning Date: Mon, 13 Jul 2026 00:00:00 +0000 Subject: [PATCH] Fix anvil busy-loop on EPOLLHUP when a multiplex channel is full On busy servers a Dovecot config reload (SIGHUP) can force-SIGTERM auth and login processes before they exit cleanly. When such a process held a multiplexed anvil connection (login/auth processes register a command callback so anvil can push KICK-USER), its socket half-closes and anvil's fd goes into EPOLLIN|EPOLLHUP. The ioloop epoll backend is level-triggered and invokes the io callback on every loop iteration while EPOLLHUP is asserted. If one multiplex channel's istream buffer is full and its consumer is gone, reading a different channel calls i_stream_multiplex_add(), which returns 0 ("this channel can't take the bytes right now") even though the parent stream is already at EOF and will never produce anything more. i_stream_multiplex_ read_stream() propagated that 0 straight back to the caller, so the connection framework saw "nothing new read", never closed the dead connection, and left the fd registered. The result was anvil spinning at 100% CPU in a tight epoll_wait() loop with no other syscalls. Fix the two early return paths so that a 0 result from i_stream_multiplex_add() is converted into an EOF/error (-1) when the parent stream has hit EOF or has a stream error. Normal backpressure (parent not at EOF) and the req-channel's own buffer-full case (ret == -2) are unchanged. Case CPANEL-54224. --- SOURCES/dovecot/src/lib/istream-multiplex.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/SOURCES/dovecot/src/lib/istream-multiplex.c b/SOURCES/dovecot/src/lib/istream-multiplex.c index daba3d86..1bbf4fdf 100644 --- a/SOURCES/dovecot/src/lib/istream-multiplex.c +++ b/SOURCES/dovecot/src/lib/istream-multiplex.c @@ -286,6 +286,14 @@ i_stream_multiplex_read_stream(struct multiplex_istream *mstream, if (ret <= 0) { if (got > 0) break; + if (ret == 0 && (mstream->parent->eof || + mstream->parent->stream_errno != 0)) { + /* Parent is at EOF/error, so a full sibling + channel can never drain. Return EOF, not + 0, to avoid busy-looping on EPOLLHUP. */ + propagate_error(mstream); + return -1; + } return ret; } i_stream_skip(mstream->parent, ret); @@ -311,6 +319,13 @@ i_stream_multiplex_read_stream(struct multiplex_istream *mstream, if (ret <= 0) { if (got > 0) return got; + if (ret == 0 && (mstream->parent->eof || + mstream->parent->stream_errno != 0)) { + /* See above: parent EOF/error, channel + can't drain - close, don't busy-loop. */ + propagate_error(mstream); + return -1; + } return ret; } i_assert(ret == 1);