diff options
author | Chris Markiewicz <effigies@gmail.com> | 2024-02-24 00:02:16 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-24 00:02:16 (GMT) |
commit | 200271c61db44d90759f8a8934949aefd72d5724 (patch) | |
tree | 9fddf73bc8d17f6d71c8ac26591911c98a445a9c /Lib/test/test_importlib/test_lazy.py | |
parent | ef6074b352a95706f44a592ffe31baace690cc1c (diff) | |
download | cpython-200271c61db44d90759f8a8934949aefd72d5724.zip cpython-200271c61db44d90759f8a8934949aefd72d5724.tar.gz cpython-200271c61db44d90759f8a8934949aefd72d5724.tar.bz2 |
gh-114763: Protect lazy loading modules from attribute access races (GH-114781)
Setting the __class__ attribute of a lazy-loading module to ModuleType enables other threads to attempt to access attributes before the loading is complete. Now that is protected by a lock.
Diffstat (limited to 'Lib/test/test_importlib/test_lazy.py')
-rw-r--r-- | Lib/test/test_importlib/test_lazy.py | 42 |
1 files changed, 40 insertions, 2 deletions
diff --git a/Lib/test/test_importlib/test_lazy.py b/Lib/test/test_importlib/test_lazy.py index cc993f3..38ab219 100644 --- a/Lib/test/test_importlib/test_lazy.py +++ b/Lib/test/test_importlib/test_lazy.py @@ -2,9 +2,12 @@ import importlib 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 @@ -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): @@ -48,8 +52,10 @@ class TestingImporter(abc.MetaPathFinder, abc.Loader): 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): @@ -59,8 +65,9 @@ class LazyLoaderTests(unittest.TestCase): # 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, @@ -140,6 +147,37 @@ class LazyLoaderTests(unittest.TestCase): # 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() |