diff --git a/Lib/test/test_interpreters/test_static_types.py b/Lib/test/test_interpreters/test_static_types.py new file mode 100644 index 000000000000000..968bf5c81c53457 --- /dev/null +++ b/Lib/test/test_interpreters/test_static_types.py @@ -0,0 +1,56 @@ +import unittest + +from test.support import import_helper +# Raise SkipTest if subinterpreters not supported. +import_helper.import_module('_interpreters') +from concurrent import interpreters +from .utils import TestBase + + +xxsubtype = import_helper.import_module('xxsubtype') + + +class TestStaticTypeCache(TestBase): + + def test_subinterp_no_crash(self): + # gh-158203: Static extension types registered via PyType_Ready() + # only have a globally shared _tp_cache field. Two interpreters + # writing to the same cache slot caused a use-after-free (SIGSEGV). + obj = xxsubtype.spamlist() + obj.append(1) + for name in dir(xxsubtype.spamlist): + getattr(xxsubtype.spamlist, name, None) + + interp = interpreters.create() + try: + interp.exec(""" +import xxsubtype +obj = xxsubtype.spamlist() +obj.append(42) +for name in dir(xxsubtype.spamlist): + getattr(xxsubtype.spamlist, name, None) +""") + finally: + interp.close() + + def test_multiple_subinterps_no_crash(self): + # Same as above but with several subinterpreters concurrently + # reading and populating the cache. + xxsubtype.spamlist().append(0) + + interps = [interpreters.create() for _ in range(3)] + try: + for interp in interps: + interp.exec(""" +import xxsubtype +for name in dir(xxsubtype.spamlist): + getattr(xxsubtype.spamlist, name, None) +""") + finally: + for interp in interps: + interp.close() + + +if __name__ == '__main__': + # Test needs to be a package, so we can do relative imports. + unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-00-10-44.gh-issue-158203.dvggOu.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-00-10-44.gh-issue-158203.dvggOu.rst new file mode 100644 index 000000000000000..057e59a973f1c8c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-00-10-44.gh-issue-158203.dvggOu.rst @@ -0,0 +1,4 @@ +Fix a crash when using a static extension type from a subinterpreter. The +type's ``_tp_cache`` field is globally shared; a second interpreter resizing +the cache caused a use-after-free (SIGSEGV). Fix by disabling the type-cache +for static non-heap types that lack per-interpreter state. diff --git a/Python/typecache.c b/Python/typecache.c index 26079de5ce341f7..54903e1e2423e46 100644 --- a/Python/typecache.c +++ b/Python/typecache.c @@ -87,25 +87,39 @@ cache_slot(PyTypeObject *type) assert(state != NULL); return &state->_tp_cache; } + if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) { + return NULL; + } return &type->_tp_cache; } static inline struct type_cache * cache_get(PyTypeObject *type) { - return (struct type_cache *)FT_ATOMIC_LOAD_PTR_ACQUIRE(*cache_slot(type)); + void **slot = cache_slot(type); + if (slot == NULL) { + return NULL; + } + return (struct type_cache *)FT_ATOMIC_LOAD_PTR_ACQUIRE(*slot); } static inline void cache_set(PyTypeObject *type, struct type_cache *cache) { - FT_ATOMIC_STORE_PTR_RELEASE(*cache_slot(type), cache); + void **slot = cache_slot(type); + if (slot == NULL) { + return; + } + FT_ATOMIC_STORE_PTR_RELEASE(*slot, cache); } void _PyTypeCache_InitType(PyTypeObject *type) { - *cache_slot(type) = &empty_cache; + void **slot = cache_slot(type); + if (slot != NULL) { + *slot = &empty_cache; + } } static inline void @@ -175,6 +189,9 @@ void _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value) { struct type_cache *cache = cache_get(type); + if (cache == NULL) { + return; + } // If the cache is full, resize it before inserting the new entry. // this also handles the case of empty cache where available is 0 but there are no entries. if (cache->available == 0) { @@ -241,6 +258,9 @@ _PyTypeCache_Invalidate(PyTypeObject *type) { OBJECT_STAT_INC(type_cache_invalidations); struct type_cache *cache = cache_get(type); + if (cache == NULL) { + return; + } cache_set(type, &empty_cache); cache_free_delayed(cache); }