https://github.com/python/cpython/commit/3835fca3f5ce0512ef7df10c7188daf04e07e409
commit: 3835fca3f5ce0512ef7df10c7188daf04e07e409
branch: main
author: Bernát Gábor <[email protected]>
committer: vsajip <[email protected]>
date: 2026-06-05T16:00:56+01:00
summary:

gh-150818: Wire logger parent before publishing it in getLogger() (GH-150941)

files:
M Lib/logging/__init__.py
M Lib/test/test_logging.py

diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py
index 9febc50b1264ef..b4a5f5cc2f598f 100644
--- a/Lib/logging/__init__.py
+++ b/Lib/logging/__init__.py
@@ -1377,9 +1377,10 @@ def getLogger(self, name):
             raise TypeError('A logger name must be a string')
         # Fast path: an already-registered, non-placeholder logger can be
         # returned without taking the lock. dict.get() is atomic under both
-        # the GIL and free threading, and a Logger is fully initialised before
-        # being inserted into loggerDict under the lock, so this never sees a
-        # partially-constructed object.
+        # the GIL and free threading. A Logger is inserted into loggerDict only
+        # after it is fully wired up (parent/child references fixed) under the
+        # lock, so the fast path never observes a logger whose parent is not 
yet
+        # set.
         rv = self.loggerDict.get(name)
         if rv is not None and not isinstance(rv, PlaceHolder):
             return rv
@@ -1390,14 +1391,18 @@ def getLogger(self, name):
                     ph = rv
                     rv = (self.loggerClass or _loggerClass)(name)
                     rv.manager = self
-                    self.loggerDict[name] = rv
                     self._fixupChildren(ph, rv)
                     self._fixupParents(rv)
+                    # Publish only after rv is fully wired: the fast path reads
+                    # loggerDict without the lock.
+                    self.loggerDict[name] = rv
             else:
                 rv = (self.loggerClass or _loggerClass)(name)
                 rv.manager = self
-                self.loggerDict[name] = rv
                 self._fixupParents(rv)
+                # Publish only after rv is fully wired: the fast path reads
+                # loggerDict without the lock.
+                self.loggerDict[name] = rv
         return rv
 
     def setLoggerClass(self, klass):
diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py
index d4fa78acfd11de..9f29fe8a5b3c9b 100644
--- a/Lib/test/test_logging.py
+++ b/Lib/test/test_logging.py
@@ -4269,6 +4269,43 @@ def test_set_log_record_factory(self):
         man.setLogRecordFactory(expected)
         self.assertEqual(man.logRecordFactory, expected)
 
+    @threading_helper.requires_working_threading()
+    def test_getLogger_fast_path_never_returns_unwired_logger(self):
+        # getLogger()'s lock-free fast path returns a logger straight out of
+        # loggerDict, so a logger must be published there only after
+        # _fixupParents() has set its parent; otherwise a concurrent caller
+        # observes it detached from the hierarchy (gh-150818 follow-up).
+        manager = logging.Manager(logging.RootLogger(logging.WARNING))
+        name = 'a.b.c'
+
+        paused = threading.Event()
+        seen = []
+        real_fixup = manager._fixupParents
+
+        # Pause the creating thread between publishing rv and wiring its
+        # parent, then read loggerDict the way the fast path does and snapshot
+        # the parent at that instant (rv is wired in place soon after).
+        def fixup(alogger):
+            paused.set()
+            reader.join()
+            real_fixup(alogger)
+
+        def read():
+            paused.wait()
+            rv = manager.loggerDict.get(name)
+            if rv is not None and not isinstance(rv, logging.PlaceHolder):
+                seen.append(rv.parent)
+
+        reader = threading.Thread(target=read)
+        manager._fixupParents = fixup
+        try:
+            reader.start()
+            manager.getLogger(name)
+        finally:
+            manager._fixupParents = real_fixup
+
+        self.assertNotIn(None, seen)
+
 class ChildLoggerTest(BaseTest):
     def test_child_loggers(self):
         r = logging.getLogger()

_______________________________________________
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