https://github.com/python/cpython/commit/624c704fa96630788d052a50aa329f7268947855
commit: 624c704fa96630788d052a50aa329f7268947855
branch: 3.13
author: Miss Islington (bot) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-24T15:26:19Z
summary:

[3.13] gh-73458: Fix logging.config.listen() on a host without an IPv4 address 
(GH-154491) (GH-154618)

The server is created in a thread which set the "ready" event only after a
successful start, so a failure to start left the caller waiting for that
event forever.  Set it also on failure.

The receiver always used AF_INET, which fails if the host has no IPv4
address, for example if "localhost" is only aliased to ::1.  Use the family
of the first resolved address in such case.

Also fixes gh-82076.
(cherry picked from commit 45a10f5d711ebe1bce9c6b93c10bbc5fe5bdf392)

Co-authored-by: Serhiy Storchaka <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst
M Lib/logging/config.py
M Lib/test/test_logging.py

diff --git a/Lib/logging/config.py b/Lib/logging/config.py
index 190b4f922590ca5..f7d277ba4a3993f 100644
--- a/Lib/logging/config.py
+++ b/Lib/logging/config.py
@@ -32,6 +32,7 @@
 import os
 import queue
 import re
+import socket
 import struct
 import threading
 import traceback
@@ -1010,6 +1011,15 @@ class ConfigSocketReceiver(ThreadingTCPServer):
 
         def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
                      handler=None, ready=None, verify=None):
+            # The host can have no IPv4 address, for example if "localhost"
+            # is only aliased to ::1.  Leave resolution errors to the server.
+            try:
+                infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
+            except OSError:
+                pass
+            else:
+                if not any(info[0] == socket.AF_INET for info in infos):
+                    self.address_family = infos[0][0]
             ThreadingTCPServer.__init__(self, (host, port), handler)
             with logging._lock:
                 self.abort = 0
@@ -1041,9 +1051,14 @@ def __init__(self, rcvr, hdlr, port, verify):
             self.ready = threading.Event()
 
         def run(self):
-            server = self.rcvr(port=self.port, handler=self.hdlr,
-                               ready=self.ready,
-                               verify=self.verify)
+            try:
+                server = self.rcvr(port=self.port, handler=self.hdlr,
+                                   ready=self.ready,
+                                   verify=self.verify)
+            except BaseException:
+                # Do not leave the caller waiting for ready forever.
+                self.ready.set()
+                raise
             if self.port == 0:
                 self.port = server.server_address[1]
             self.ready.set()
diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py
index 99ea474f7638852..50fba5dee97a2c9 100644
--- a/Lib/test/test_logging.py
+++ b/Lib/test/test_logging.py
@@ -3584,14 +3584,14 @@ def setup_via_listener(self, text, verify=None):
         # Ask for a randomly assigned port (by using port 0)
         t = logging.config.listen(0, verify)
         t.start()
-        t.ready.wait()
+        self.assertTrue(t.ready.wait(support.LONG_TIMEOUT),
+                        msg='the listener did not start')
         # Now get the port allocated
         port = t.port
         t.ready.clear()
         try:
-            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-            sock.settimeout(2.0)
-            sock.connect(('localhost', port))
+            # The server can listen on IPv6, so do not force a family.
+            sock = socket.create_connection(('localhost', port), timeout=2.0)
 
             slen = struct.pack('>L', len(text))
             s = slen + text
@@ -3706,6 +3706,18 @@ def verify_reverse(stuff):
             ('ERROR', '2'),
         ], pat=r"^[\w.]+ -> (\w+): (\d+)$")
 
+    @support.requires_working_socket()
+    def test_listen_server_error(self):
+        # The "ready" event should be set even if the server fails to start.
+        t = logging.config.listen(-1)
+        t.daemon = True
+        with threading_helper.catch_threading_exception() as cm:
+            t.start()
+            self.assertTrue(t.ready.wait(support.SHORT_TIMEOUT),
+                            msg='the listener did not report the failure')
+            threading_helper.join_thread(t)
+            self.assertIs(cm.exc_type, OverflowError)
+
     def test_bad_format(self):
         self.assertRaises(ValueError, self.apply_config, self.bad_format)
 
diff --git 
a/Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst 
b/Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst
new file mode 100644
index 000000000000000..27b6eeac5b520b2
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst
@@ -0,0 +1,5 @@
+Fix :func:`logging.config.listen`: it left the caller waiting for the
+``ready`` event forever if the server could not be started,
+for example if the port was invalid or already in use.
+It now also binds to an IPv6 address if the host has no IPv4 address,
+for example if ``localhost`` is only aliased to ``::1``.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to