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

This commit is contained in:
Bernát Gábor 2026-06-05 08:00:56 -07:00 committed by GitHub
parent 0036565e81
commit 3835fca3f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 47 additions and 5 deletions

View file

@ -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):

View file

@ -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()