When a module import fails, other threads can receive a stale module reference that is no longer in sys.modules.
Race condition
- Thread 1 starts importing module X, adds it to
sys.modules - Thread 2 checks
sys.modules, finds module X, takes the "skip lock" fast path - Thread 1's import fails, removes module X from
sys.modules - Thread 2 returns the stale module reference (not in
sys.modules)
Affected code paths
Lib/importlib/_bootstrap.py:_find_and_load() lines 1268-1282Python/import.c:PyImport_GetModule()Python/import.c:PyImport_ImportModuleLevelObject()
All three have an optimization that skips locking when a module appears already loaded, but none verify the module is still valid after checking _initializing.
Reproducer
importsysimportthreadingimporttempfileimportoswithtempfile.TemporaryDirectory() astmpdir:
sys.path.insert(0, tmpdir)
# Module that partially initializes then failswithopen(os.path.join(tmpdir, "failing_mod.py"), "w") asf:
f.write("import time; time.sleep(0.05); raise RuntimeError('fail')")
results= []
defdo_import(delay):
importtime; time.sleep(delay)
try:
mod=__import__("failing_mod")
if"failing_mod"notinsys.modules:
results.append("RACE: got module not in sys.modules")
exceptRuntimeError:
results.append("ok")
t1=threading.Thread(target=do_import, args=(0,))
t2=threading.Thread(target=do_import, args=(0.01,))
t1.start(); t2.start(); t1.join(); t2.join()
print(results) # May show "RACE" on affected buildsLinked PRs
When a module import fails, other threads can receive a stale module reference that is no longer in
sys.modules.Race condition
sys.modulessys.modules, finds module X, takes the "skip lock" fast pathsys.modulessys.modules)Affected code paths
Lib/importlib/_bootstrap.py:_find_and_load()lines 1268-1282Python/import.c:PyImport_GetModule()Python/import.c:PyImport_ImportModuleLevelObject()All three have an optimization that skips locking when a module appears already loaded, but none verify the module is still valid after checking
_initializing.Reproducer
Linked PRs