[3.12] gh-114763: Protect lazy loading modules from attribute access races (GH-114781) by miss-islington · Pull Request #115870 · python/cpython · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 51 additions & 30 deletions Lib/importlib/util.py
42 changes: 40 additions & 2 deletions Lib/test/test_importlib/test_lazy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
from importlib import abc
from importlib import util
import sys
import time
import threading
import types
import unittest

from test.support import threading_helper
from test.test_importlib import util as test_util


Expand Down Expand Up @@ -40,6 +43,7 @@ class TestingImporter(abc.MetaPathFinder, abc.Loader):
module_name = 'lazy_loader_test'
mutated_name = 'changed'
loaded = None
load_count = 0
source_code = 'attr = 42; __name__ = {!r}'.format(mutated_name)

def find_spec(self, name, path, target=None):
Expand All @@ -48,8 +52,10 @@ def find_spec(self, name, path, target=None):
return util.spec_from_loader(name, util.LazyLoader(self))

def exec_module(self, module):
time.sleep(0.01) # Simulate a slow load.
exec(self.source_code, module.__dict__)
self.loaded = module
self.load_count += 1


class LazyLoaderTests(unittest.TestCase):
Expand All @@ -59,8 +65,9 @@ def test_init(self):
# Classes that don't define exec_module() trigger TypeError.
util.LazyLoader(object)

def new_module(self, source_code=None):
loader = TestingImporter()
def new_module(self, source_code=None, loader=None):
if loader is None:
loader = TestingImporter()
if source_code is not None:
loader.source_code = source_code
spec = util.spec_from_loader(TestingImporter.module_name,
Expand Down Expand Up @@ -140,6 +147,37 @@ def test_module_already_in_sys(self):
# Force the load; just care that no exception is raised.
module.__name__

@threading_helper.requires_working_threading()
def test_module_load_race(self):
with test_util.uncache(TestingImporter.module_name):
loader = TestingImporter()
module = self.new_module(loader=loader)
self.assertEqual(loader.load_count, 0)

class RaisingThread(threading.Thread):
exc = None
def run(self):
try:
super().run()
except Exception as exc:
self.exc = exc

def access_module():
return module.attr

threads = []
for _ in range(2):
threads.append(thread := RaisingThread(target=access_module))
thread.start()

# Races could cause errors
for thread in threads:
thread.join()
self.assertIsNone(thread.exc)

# Or multiple load attempts
self.assertEqual(loader.load_count, 1)


if __name__ == '__main__':
unittest.main()