Skip to content

Commit 5301ae6

Browse files
committed
gh-152298: Require owner claim for explicit lazy resolve
1 parent c9c3441 commit 5301ae6

3 files changed

Lines changed: 85 additions & 36 deletions

File tree

Include/internal/pycore_lazyimportobject.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ typedef struct {
2121
PyObject *lz_attr;
2222
// Protected by the import lock. lz_owner_* records the first module
2323
// global that resolve() may replace. Pending owners are being stored;
24-
// pending and finalized may overlap during reentrant store resolution.
24+
// finalized owners cannot be claimed again.
2525
PyObject *lz_owner_globals;
2626
PyObject *lz_owner_key;
2727
PyObject *lz_resolved;

Lib/test/test_lazy_import/__init__.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ def check():
753753
assert_python_ok("-c", code)
754754

755755
@support.requires_subprocess()
756-
def test_reentrant_store_resolution_clears_pending_owner(self):
756+
def test_reentrant_store_resolution_waits_for_store_finish(self):
757757
code = textwrap.dedent("""
758758
import builtins
759759
import types
@@ -775,8 +775,11 @@ def custom_import(name, globals=None, locals=None, fromlist=None,
775775
class Reenter:
776776
def __del__(self):
777777
try:
778-
proxies.append(globals().copy()["target"])
779-
assert target.VALUE == "resolved"
778+
proxy = globals().copy()["target"]
779+
proxies.append(proxy)
780+
resolved = target
781+
assert resolved.VALUE == "resolved"
782+
assert globals()["target"] is proxy
780783
except BaseException as exc:
781784
errors.append((type(exc).__name__, str(exc)))
782785
@@ -787,9 +790,12 @@ def __del__(self):
787790
try:
788791
assert not errors, errors
789792
assert len(proxies) == 1, proxies
790-
resolved = globals()["target"]
791793
proxy = proxies[0]
794+
assert globals()["target"] is proxy
795+
796+
resolved = proxy.resolve()
792797
assert resolved.VALUE == "resolved"
798+
assert globals()["target"] is resolved
793799
794800
globals()["target"] = proxy
795801
assert proxy.resolve() is resolved

Objects/lazyimportobject.c

Lines changed: 74 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ clear_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m,
179179
_PyImport_AcquireLock(interp);
180180
if (owner_matches(m, globals, name)) {
181181
m->lz_owner_pending = 0;
182+
// Keep lz_owner_finalized set: this owner has been committed or
183+
// abandoned, so later stores of the same proxy are not canonical.
182184
Py_CLEAR(m->lz_owner_globals);
183185
Py_CLEAR(m->lz_owner_key);
184186
}
@@ -217,7 +219,10 @@ finalize_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m,
217219
int finalized = 0;
218220
PyInterpreterState *interp = tstate->interp;
219221
_PyImport_AcquireLock(interp);
220-
if (!m->lz_owner_finalized && owner_matches(m, globals, name)) {
222+
if (!m->lz_owner_pending &&
223+
!m->lz_owner_finalized &&
224+
owner_matches(m, globals, name))
225+
{
221226
m->lz_owner_finalized = 1;
222227
finalized = 1;
223228
}
@@ -237,21 +242,45 @@ restore_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m,
237242
_PyImport_ReleaseLock(interp);
238243
}
239244

240-
int
241-
_PyLazyImport_CommitIfCurrent(PyThreadState *tstate, PyObject *op,
242-
PyObject *globals, PyObject *name,
243-
PyObject *value)
245+
static int
246+
owner_is_pending(PyThreadState *tstate, PyLazyImportObject *m,
247+
PyObject *globals, PyObject *name)
244248
{
245-
if (!PyLazyImport_CheckExact(op) ||
246-
!PyDict_CheckExact(globals) ||
247-
!PyUnicode_CheckExact(name))
248-
{
249-
return 0;
249+
int pending = 0;
250+
PyInterpreterState *interp = tstate->interp;
251+
_PyImport_AcquireLock(interp);
252+
if (m->lz_owner_pending && owner_matches(m, globals, name)) {
253+
pending = 1;
250254
}
255+
_PyImport_ReleaseLock(interp);
256+
return pending;
257+
}
251258

252-
PyLazyImportObject *m = PyLazyImportObject_CAST(op);
253-
int finalized_owner = finalize_owner_if_matches(tstate, m, globals, name);
259+
static int
260+
claim_stored_owner(PyThreadState *tstate, PyLazyImportObject *m,
261+
PyObject **globals, PyObject **name)
262+
{
263+
int claimed = 0;
264+
PyInterpreterState *interp = tstate->interp;
265+
_PyImport_AcquireLock(interp);
266+
if (!m->lz_owner_pending &&
267+
!m->lz_owner_finalized &&
268+
m->lz_owner_globals != NULL &&
269+
m->lz_owner_key != NULL)
270+
{
271+
m->lz_owner_finalized = 1;
272+
*globals = Py_NewRef(m->lz_owner_globals);
273+
*name = Py_NewRef(m->lz_owner_key);
274+
claimed = 1;
275+
}
276+
_PyImport_ReleaseLock(interp);
277+
return claimed;
278+
}
254279

280+
static int
281+
commit_if_current(PyObject *op, PyObject *globals, PyObject *name,
282+
PyObject *value)
283+
{
255284
PyObject *current = NULL;
256285
int err = 0;
257286
Py_BEGIN_CRITICAL_SECTION(globals);
@@ -265,7 +294,28 @@ _PyLazyImport_CommitIfCurrent(PyThreadState *tstate, PyObject *op,
265294
}
266295
Py_END_CRITICAL_SECTION();
267296
Py_XDECREF(current);
297+
return err;
298+
}
268299

300+
int
301+
_PyLazyImport_CommitIfCurrent(PyThreadState *tstate, PyObject *op,
302+
PyObject *globals, PyObject *name,
303+
PyObject *value)
304+
{
305+
if (!PyLazyImport_CheckExact(op) ||
306+
!PyDict_CheckExact(globals) ||
307+
!PyUnicode_CheckExact(name))
308+
{
309+
return 0;
310+
}
311+
312+
PyLazyImportObject *m = PyLazyImportObject_CAST(op);
313+
if (owner_is_pending(tstate, m, globals, name)) {
314+
return 0;
315+
}
316+
317+
int finalized_owner = finalize_owner_if_matches(tstate, m, globals, name);
318+
int err = commit_if_current(op, globals, name, value);
269319
if (finalized_owner) {
270320
if (err < 0) {
271321
restore_owner_if_matches(tstate, m, globals, name);
@@ -289,27 +339,19 @@ _PyLazyImport_CommitStoredIfCurrent(PyThreadState *tstate, PyObject *op,
289339
PyObject *globals = NULL;
290340
PyObject *name = NULL;
291341

292-
PyInterpreterState *interp = tstate->interp;
293-
// Copy the owner under the import lock, then release it before entering a
294-
// dict critical section in _PyLazyImport_CommitIfCurrent().
295-
_PyImport_AcquireLock(interp);
296-
if (!m->lz_owner_pending &&
297-
!m->lz_owner_finalized &&
298-
m->lz_owner_globals != NULL &&
299-
m->lz_owner_key != NULL)
300-
{
301-
globals = Py_NewRef(m->lz_owner_globals);
302-
name = Py_NewRef(m->lz_owner_key);
342+
if (!claim_stored_owner(tstate, m, &globals, &name)) {
343+
return 0;
303344
}
304-
_PyImport_ReleaseLock(interp);
305345

306-
int err = 0;
307-
if (globals != NULL && name != NULL) {
308-
err = _PyLazyImport_CommitIfCurrent(
309-
tstate, op, globals, name, value);
346+
int err = commit_if_current(op, globals, name, value);
347+
if (err < 0) {
348+
restore_owner_if_matches(tstate, m, globals, name);
310349
}
311-
Py_XDECREF(globals);
312-
Py_XDECREF(name);
350+
else {
351+
clear_owner_if_matches(tstate, m, globals, name);
352+
}
353+
Py_DECREF(globals);
354+
Py_DECREF(name);
313355
return err;
314356
}
315357

@@ -345,7 +387,8 @@ PyDoc_STRVAR(lazy_import_doc,
345387
"\n"
346388
"A successful resolution is cached. Instances of this object accessed\n"
347389
"from the global scope will be automatically imported based upon their\n"
348-
"name and then replaced with the imported value.");
390+
"name. The original global is replaced only if it still refers to this\n"
391+
"lazy import object.");
349392

350393
PyTypeObject PyLazyImport_Type = {
351394
PyVarObject_HEAD_INIT(&PyType_Type, 0)

0 commit comments

Comments
 (0)