diff --git a/Lib/logging/config.py b/Lib/logging/config.py index f566de5750dbf55..599d7f706bdf64b 100644 --- a/Lib/logging/config.py +++ b/Lib/logging/config.py @@ -972,7 +972,12 @@ def handle(self): slen = struct.unpack(">L", chunk)[0] chunk = self.connection.recv(slen) while len(chunk) < slen: - chunk = chunk + conn.recv(slen - len(chunk)) + data = conn.recv(slen - len(chunk)) + if not data: + # The peer closed before sending the whole + # configuration, so there is nothing to apply. + return + chunk = chunk + data if self.server.verify is not None: chunk = self.server.verify(chunk) if chunk is not None: # verified, can process diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 7cd0df3ea0b62d2..a64c5f6caf4c054 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -3740,6 +3740,35 @@ def setup_via_listener(self, text, verify=None): logging.config.stopListening() threading_helper.join_thread(t) + @support.requires_working_socket() + def test_listen_truncated_request_does_not_spin(self): + # gh-156378: a client that declares a length and then closes without + # sending the body used to leave handle() calling recv() forever, + # burning a CPU core, because recv() keeps returning b'' at EOF. + t = logging.config.listen(0) + t.start() + self.assertTrue(t.ready.wait(support.LONG_TIMEOUT), + msg='the listener did not start') + port = t.port + t.ready.clear() + before = threading.active_count() + try: + sock = socket.create_connection(('localhost', port), timeout=2.0) + # Announce one byte of configuration, then hang up without it. + sock.sendall(struct.pack('>L', 1)) + sock.close() + # The handler must notice EOF and finish. Wait rather than assert + # immediately, so a slow machine does not make this flaky; only a + # handler that never exits fails here. + deadline = time.monotonic() + support.SHORT_TIMEOUT + while threading.active_count() > before: + if time.monotonic() > deadline: + self.fail('the request handler did not exit after EOF') + time.sleep(0.01) + finally: + logging.config.stopListening() + threading_helper.join_thread(t) + @support.requires_working_socket() def test_listen_config_10_ok(self): with support.captured_stdout() as output: diff --git a/Misc/NEWS.d/next/Library/2026-08-25-19-12-40.gh-issue-156378.Lk8Rw2.rst b/Misc/NEWS.d/next/Library/2026-08-25-19-12-40.gh-issue-156378.Lk8Rw2.rst new file mode 100644 index 000000000000000..ccfae1e33301b2f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-25-19-12-40.gh-issue-156378.Lk8Rw2.rst @@ -0,0 +1,3 @@ +Fix :func:`logging.config.listen` looping forever and consuming a CPU core +when a client announces a configuration length and then closes the connection +without sending the data.