[3.11] gh-114763: Protect lazy loading modules from attribute access … · python/cpython@46f821d · GitHub
Skip to content

Commit 46f821d

Browse files
[3.11] gh-114763: Protect lazy loading modules from attribute access races (GH-114781) (GH-115871)
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. (cherry picked from commit 200271c) Co-authored-by: Chris Markiewicz <effigies@gmail.com>
1 parent a30a1e7 commit 46f821d

3 files changed

Lines changed: 94 additions & 32 deletions

File tree

Lib/importlib/util.py

Lines changed: 51 additions & 30 deletions

Lib/test/test_importlib/test_lazy.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
from importlib import abc
33
from importlib import util
44
import sys
5+
import time
6+
import threading
57
import types
68
import unittest
79

10+
from test.support import threading_helper
811
from test.test_importlib import util as test_util
912

1013

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

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

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

5460

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

62-
def new_module(self, source_code=None):
63-
loader = TestingImporter()
68+
def new_module(self, source_code=None, loader=None):
69+
if loader is None:
70+
loader = TestingImporter()
6471
if source_code is not None:
6572
loader.source_code = source_code
6673
spec = util.spec_from_loader(TestingImporter.module_name,
@@ -140,6 +147,37 @@ def test_module_already_in_sys(self):
140147
# Force the load; just care that no exception is raised.
141148
module.__name__
142149

150+
@threading_helper.requires_working_threading()
151+
def test_module_load_race(self):
152+
with test_util.uncache(TestingImporter.module_name):
153+
loader = TestingImporter()
154+
module = self.new_module(loader=loader)
155+
self.assertEqual(loader.load_count, 0)
156+
157+
class RaisingThread(threading.Thread):
158+
exc = None
159+
def run(self):
160+
try:
161+
super().run()
162+
except Exception as exc:
163+
self.exc = exc
164+
165+
def access_module():
166+
return module.attr
167+
168+
threads = []
169+
for _ in range(2):
170+
threads.append(thread := RaisingThread(target=access_module))
171+
thread.start()
172+
173+
# Races could cause errors
174+
for thread in threads:
175+
thread.join()
176+
self.assertIsNone(thread.exc)
177+
178+
# Or multiple load attempts
179+
self.assertEqual(loader.load_count, 1)
180+
143181

144182
if __name__ == '__main__':
145183
unittest.main()
Lines changed: 3 additions & 0 deletions

0 commit comments

Comments
 (0)