From: Bernát Gábor Date: Fri, 5 Jun 2026 15:00:56 +0000 (-0700) Subject: gh-150818: Wire logger parent before publishing it in getLogger() (GH-150941) X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=3835fca3f5ce0512ef7df10c7188daf04e07e409;p=thirdparty%2FPython%2Fcpython.git gh-150818: Wire logger parent before publishing it in getLogger() (GH-150941) --- diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 9febc50b1264..b4a5f5cc2f59 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -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): diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index d4fa78acfd11..9f29fe8a5b3c 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -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()