Skip to content

Commit af3c994

Browse files
committed
gh-152298: Guard lazy resolve writebacks
1 parent 7cbf44c commit af3c994

9 files changed

Lines changed: 232 additions & 31 deletions

File tree

Include/internal/pycore_lazyimportobject.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import);
3333
PyAPI_FUNC(PyObject *) _PyLazyImport_GetResolved(PyObject *lazy_import);
3434
PyAPI_FUNC(int) _PyLazyImport_FinishResolve(
3535
PyObject *lazy_import, PyObject *resolved);
36+
PyAPI_FUNC(int) _PyLazyImport_ReplaceDictItemIfCurrent(
37+
PyObject *lazy_import, PyObject *dict, PyObject *name,
38+
PyObject *resolved);
3639
PyAPI_FUNC(int) _PyLazyImport_SetGlobalBindingAndDictItem(
3740
PyObject *lazy_import, PyObject *globals, PyObject *name);
3841
PyAPI_FUNC(PyObject *) _PyLazyImport_New(

Lib/test/test_lazy_import/__init__.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,127 @@ def test_resolve():
12441244
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
12451245
self.assertIn("OK", result.stdout)
12461246

1247+
def test_load_global_respects_rebound_global_during_reification(self):
1248+
"""LOAD_GLOBAL should not overwrite a global rebound by the import hook."""
1249+
code = textwrap.dedent("""
1250+
import builtins
1251+
import types
1252+
1253+
real_import = builtins.__import__
1254+
calls = []
1255+
sentinel = object()
1256+
1257+
lazy import target_module as target
1258+
1259+
def custom_import(name, globals=None, locals=None, fromlist=None, level=0):
1260+
if name == "target_module":
1261+
calls.append(name)
1262+
globals["target"] = sentinel
1263+
module = types.ModuleType(name)
1264+
module.VALUE = "resolved"
1265+
return module
1266+
return real_import(name, globals, locals, fromlist, level)
1267+
1268+
builtins.__import__ = custom_import
1269+
try:
1270+
def trigger_load_global():
1271+
return target
1272+
1273+
resolved = trigger_load_global()
1274+
assert resolved.VALUE == "resolved"
1275+
assert globals()["target"] is sentinel
1276+
assert calls == ["target_module"], calls
1277+
finally:
1278+
builtins.__import__ = real_import
1279+
print("OK")
1280+
""")
1281+
result = subprocess.run(
1282+
[sys.executable, "-c", code],
1283+
capture_output=True,
1284+
text=True
1285+
)
1286+
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
1287+
self.assertIn("OK", result.stdout)
1288+
1289+
def test_load_name_respects_deleted_global_during_reification(self):
1290+
"""LOAD_NAME should not recreate a global deleted by the import hook."""
1291+
code = textwrap.dedent("""
1292+
import builtins
1293+
import types
1294+
1295+
real_import = builtins.__import__
1296+
calls = []
1297+
1298+
lazy import target_module as target
1299+
1300+
def custom_import(name, globals=None, locals=None, fromlist=None, level=0):
1301+
if name == "target_module":
1302+
calls.append(name)
1303+
del globals["target"]
1304+
module = types.ModuleType(name)
1305+
module.VALUE = "resolved"
1306+
return module
1307+
return real_import(name, globals, locals, fromlist, level)
1308+
1309+
builtins.__import__ = custom_import
1310+
try:
1311+
resolved = target
1312+
assert resolved.VALUE == "resolved"
1313+
assert "target" not in globals()
1314+
assert calls == ["target_module"], calls
1315+
finally:
1316+
builtins.__import__ = real_import
1317+
print("OK")
1318+
""")
1319+
result = subprocess.run(
1320+
[sys.executable, "-c", code],
1321+
capture_output=True,
1322+
text=True
1323+
)
1324+
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
1325+
self.assertIn("OK", result.stdout)
1326+
1327+
def test_module_attr_respects_rebound_global_during_reification(self):
1328+
"""Module attribute access should not overwrite a rebound lazy binding."""
1329+
code = textwrap.dedent("""
1330+
import builtins
1331+
import types
1332+
1333+
real_import = builtins.__import__
1334+
calls = []
1335+
sentinel = object()
1336+
holder = types.ModuleType("holder")
1337+
holder.__dict__["__builtins__"] = builtins
1338+
1339+
exec("lazy import target_module as target", holder.__dict__)
1340+
1341+
def custom_import(name, globals=None, locals=None, fromlist=None, level=0):
1342+
if name == "target_module":
1343+
calls.append(name)
1344+
holder.__dict__["target"] = sentinel
1345+
module = types.ModuleType(name)
1346+
module.VALUE = "resolved"
1347+
return module
1348+
return real_import(name, globals, locals, fromlist, level)
1349+
1350+
builtins.__import__ = custom_import
1351+
try:
1352+
resolved = holder.target
1353+
assert resolved.VALUE == "resolved"
1354+
assert holder.__dict__["target"] is sentinel
1355+
assert calls == ["target_module"], calls
1356+
finally:
1357+
builtins.__import__ = real_import
1358+
print("OK")
1359+
""")
1360+
result = subprocess.run(
1361+
[sys.executable, "-c", code],
1362+
capture_output=True,
1363+
text=True
1364+
)
1365+
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
1366+
self.assertIn("OK", result.stdout)
1367+
12471368
def test_add_lazy_to_globals(self):
12481369
code = textwrap.dedent("""
12491370
import sys

Modules/_testinternalcapi/test_cases.c.h

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Objects/lazyimportobject.c

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,49 @@ _PyLazyImport_SetGlobalBindingAndDictItem(PyObject *op, PyObject *globals,
184184
return err;
185185
}
186186

187+
static int
188+
lazy_import_replace_dict_item_if_current(PyObject *op, PyObject *globals,
189+
PyObject *key, Py_hash_t key_hash,
190+
PyObject *resolved)
191+
{
192+
assert(PyLazyImport_CheckExact(op));
193+
assert(PyDict_CheckExact(globals));
194+
assert(!PyLazyImport_CheckExact(resolved));
195+
196+
PyObject *current = NULL;
197+
int err = 0;
198+
199+
Py_BEGIN_CRITICAL_SECTION(globals);
200+
int found = _PyDict_GetItemRef_KnownHash_LockHeld(
201+
(PyDictObject *)globals, key, key_hash, &current);
202+
if (found < 0) {
203+
err = -1;
204+
}
205+
else if (found && current == op) {
206+
err = _PyDict_SetItem_KnownHash_LockHeld(
207+
(PyDictObject *)globals, key, resolved, key_hash);
208+
}
209+
Py_END_CRITICAL_SECTION();
210+
211+
Py_XDECREF(current);
212+
return err;
213+
}
214+
215+
int
216+
_PyLazyImport_ReplaceDictItemIfCurrent(PyObject *op, PyObject *globals,
217+
PyObject *key, PyObject *resolved)
218+
{
219+
assert(PyLazyImport_CheckExact(op));
220+
assert(PyDict_CheckExact(globals));
221+
222+
Py_hash_t key_hash = PyObject_Hash(key);
223+
if (key_hash == -1) {
224+
return -1;
225+
}
226+
return lazy_import_replace_dict_item_if_current(
227+
op, globals, key, key_hash, resolved);
228+
}
229+
187230
int
188231
_PyLazyImport_FinishResolve(PyObject *op, PyObject *resolved)
189232
{
@@ -212,25 +255,12 @@ _PyLazyImport_FinishResolve(PyObject *op, PyObject *resolved)
212255
return 0;
213256
}
214257

215-
PyObject *current = NULL;
216-
217258
if (globals != NULL) {
218259
assert(key != NULL);
219260
assert(PyDict_CheckExact(globals));
220261

221-
Py_BEGIN_CRITICAL_SECTION(globals);
222-
int found = _PyDict_GetItemRef_KnownHash_LockHeld(
223-
(PyDictObject *)globals, key, key_hash, &current);
224-
if (found < 0) {
225-
err = -1;
226-
}
227-
else if (found && current == op) {
228-
err = _PyDict_SetItem_KnownHash_LockHeld(
229-
(PyDictObject *)globals, key, resolved, key_hash);
230-
}
231-
Py_END_CRITICAL_SECTION();
232-
233-
Py_XDECREF(current);
262+
err = lazy_import_replace_dict_item_if_current(
263+
op, globals, key, key_hash, resolved);
234264
if (err < 0) {
235265
Py_DECREF(globals);
236266
Py_DECREF(key);

Objects/moduleobject.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1368,7 +1368,9 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
13681368
return NULL;
13691369
}
13701370

1371-
if (PyDict_SetItem(m->md_dict, name, new_value) < 0) {
1371+
if (_PyLazyImport_ReplaceDictItemIfCurrent(
1372+
attr, m->md_dict, name, new_value) < 0)
1373+
{
13721374
Py_CLEAR(new_value);
13731375
}
13741376
Py_DECREF(attr);

Python/bytecodes.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2264,7 +2264,14 @@ dummy_func(
22642264
Py_DECREF(v_o);
22652265
ERROR_IF(true);
22662266
}
2267-
int err = PyDict_SetItem(GLOBALS(), name, l_v);
2267+
int err;
2268+
if (PyDict_CheckExact(GLOBALS())) {
2269+
err = _PyLazyImport_ReplaceDictItemIfCurrent(
2270+
v_o, GLOBALS(), name, l_v);
2271+
}
2272+
else {
2273+
err = PyDict_SetItem(GLOBALS(), name, l_v);
2274+
}
22682275
if (err < 0) {
22692276
Py_DECREF(v_o);
22702277
Py_DECREF(l_v);

Python/ceval.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3688,13 +3688,21 @@ _PyEval_LoadGlobalStackRef(PyObject *globals, PyObject *builtins, PyObject *name
36883688
PyObject *res_o = PyStackRef_AsPyObjectBorrow(*writeto);
36893689
if (res_o != NULL && PyLazyImport_CheckExact(res_o)) {
36903690
PyObject *l_v = _PyImport_LoadLazyImportTstate(PyThreadState_GET(), res_o);
3691-
PyStackRef_CLOSE(writeto[0]);
36923691
if (l_v == NULL) {
36933692
assert(PyErr_Occurred());
3693+
PyStackRef_CLOSE(writeto[0]);
36943694
*writeto = PyStackRef_NULL;
36953695
return;
36963696
}
3697-
int err = PyDict_SetItem(globals, name, l_v);
3697+
int err;
3698+
if (PyDict_CheckExact(globals)) {
3699+
err = _PyLazyImport_ReplaceDictItemIfCurrent(
3700+
res_o, globals, name, l_v);
3701+
}
3702+
else {
3703+
err = PyDict_SetItem(globals, name, l_v);
3704+
}
3705+
PyStackRef_CLOSE(writeto[0]);
36983706
if (err < 0) {
36993707
Py_DECREF(l_v);
37003708
*writeto = PyStackRef_NULL;

Python/executor_cases.c.h

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/generated_cases.c.h

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)