]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-150818: Wire logger parent before publishing it in getLogger() (GH-150941)
authorBernát Gábor <gaborjbernat@gmail.com>
Fri, 5 Jun 2026 15:00:56 +0000 (08:00 -0700)
committerGitHub <noreply@github.com>
Fri, 5 Jun 2026 15:00:56 +0000 (16:00 +0100)
Lib/logging/__init__.py
Lib/test/test_logging.py

index 9febc50b1264ef42c608abbbb931fb0f9993c986..b4a5f5cc2f598f5b313a2a6d855aa9d0ec21c939 100644 (file)
@@ -1377,9 +1377,10 @@ class Manager(object):
             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 @@ class Manager(object):
                     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):
index d4fa78acfd11dee7b7835ccc64caa2a4009beb4c..9f29fe8a5b3c9bcc8561ee5b70c80ae455f31ff3 100644 (file)
@@ -4269,6 +4269,43 @@ class ManagerTest(BaseTest):
         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()