@@ -4250,8 +4250,9 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name, PyObject *builtin
42504250 }
42514251 }
42524252
4253- // Release the lock - we only needed it for initialization
4254- // The dict operations below are thread-safe on their own
4253+ // Release the lock - we only needed it for initialization.
4254+ // The dict operations below use PyDict_SetDefaultRef which uses
4255+ // critical sections for thread-safety in free-threaded builds.
42554256 _PyImport_ReleaseLock (interp );
42564257
42574258 Py_INCREF (name );
@@ -4283,19 +4284,17 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name, PyObject *builtin
42834284 }
42844285
42854286 // Record the child to be added when the parent is imported.
4286- PyObject * lazy_submodules ;
4287- if (PyDict_GetItemRef (lazy_modules , parent , & lazy_submodules ) < 0 ) {
4287+ // Use PyDict_SetDefaultRef to atomically get-or-create the set,
4288+ // avoiding TOCTOU races in free-threaded builds.
4289+ PyObject * empty_set = PySet_New (NULL );
4290+ if (empty_set == NULL ) {
42884291 goto done ;
42894292 }
4290- if (lazy_submodules == NULL ) {
4291- lazy_submodules = PySet_New (NULL );
4292- if (lazy_submodules == NULL ) {
4293- goto done ;
4294- }
4295- if (PyDict_SetItem (lazy_modules , parent , lazy_submodules ) < 0 ) {
4296- Py_DECREF (lazy_submodules );
4297- goto done ;
4298- }
4293+ PyObject * lazy_submodules ;
4294+ int setdefault_result = PyDict_SetDefaultRef (lazy_modules , parent , empty_set , & lazy_submodules );
4295+ Py_DECREF (empty_set );
4296+ if (setdefault_result < 0 ) {
4297+ goto done ;
42994298 }
43004299 assert (PyAnySet_CheckExact (lazy_submodules ));
43014300 if (PySet_Add (lazy_submodules , child ) < 0 ) {
@@ -4357,11 +4356,15 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
43574356 _PyInterpreterFrame * frame = _PyEval_GetFrame ();
43584357 assert (frame != NULL && frame -> f_globals == frame -> f_locals ); // should only be called in global scope
43594358
4360- // Check if the filter disables the lazy import
4359+ // Check if the filter disables the lazy import.
4360+ // We must hold a reference to the filter while calling it to prevent
4361+ // use-after-free if another thread replaces it via PyImport_SetLazyImportsFilter.
43614362 PyObject * filter = FT_ATOMIC_LOAD_PTR_RELAXED (LAZY_IMPORTS_FILTER (interp ));
4363+ Py_XINCREF (filter );
43624364 if (filter != NULL ) {
43634365 PyObject * modname ;
43644366 if (PyDict_GetItemRef (globals , & _Py_ID (__name__ ), & modname ) < 0 ) {
4367+ Py_DECREF (filter );
43654368 Py_DECREF (abs_name );
43664369 return NULL ;
43674370 } else if (modname == NULL ) {
@@ -4376,6 +4379,7 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
43764379 );
43774380
43784381 Py_DECREF (modname );
4382+ Py_DECREF (filter );
43794383
43804384 if (res == NULL ) {
43814385 Py_DECREF (abs_name );
@@ -4408,15 +4412,20 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
44084412 return NULL ;
44094413 }
44104414
4411- // Add the module name to sys.lazy_modules set (PEP 810)
4412- PyObject * lazy_modules_set = interp -> imports .lazy_modules_set ;
4415+ // Add the module name to sys.lazy_modules set (PEP 810).
4416+ // We must hold a reference to the set while using it to prevent
4417+ // use-after-free if another thread clears it during interpreter shutdown.
4418+ PyObject * lazy_modules_set = FT_ATOMIC_LOAD_PTR_RELAXED (interp -> imports .lazy_modules_set );
4419+ Py_XINCREF (lazy_modules_set );
44134420 if (lazy_modules_set != NULL ) {
44144421 assert (PyAnySet_CheckExact (lazy_modules_set ));
44154422 if (PySet_Add (lazy_modules_set , abs_name ) < 0 ) {
4423+ Py_DECREF (lazy_modules_set );
44164424 Py_DECREF (res );
44174425 Py_DECREF (abs_name );
44184426 return NULL ;
44194427 }
4428+ Py_DECREF (lazy_modules_set );
44204429 }
44214430
44224431 Py_DECREF (abs_name );
@@ -4782,9 +4791,11 @@ PyImport_SetLazyImportsFilter(PyObject *filter)
47824791
47834792 PyInterpreterState * interp = _PyInterpreterState_GET ();
47844793#ifdef Py_GIL_DISABLED
4785- // exchange just in case another thread did same thing at same time
4794+ // Exchange the filter atomically. Use deferred DECREF to prevent
4795+ // use-after-free: another thread may have loaded the old filter
4796+ // and be about to INCREF it.
47864797 PyObject * old = _Py_atomic_exchange_ptr (& LAZY_IMPORTS_FILTER (interp ), Py_XNewRef (filter ));
4787- Py_XDECREF (old );
4798+ _PyObject_XDecRefDelayed (old );
47884799#else
47894800 Py_XSETREF (LAZY_IMPORTS_FILTER (interp ), Py_XNewRef (filter ));
47904801#endif
0 commit comments