diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index a0fea389b6502c..c404fc6e67d193 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -350,9 +350,10 @@ _PyEval_SpecialMethodCanSuggest(PyObject *self, int oparg); #define _PY_EVAL_PLEASE_STOP_BIT (1U << 5) #define _PY_EVAL_EXPLICIT_MERGE_BIT (1U << 6) #define _PY_EVAL_JIT_INVALIDATE_COLD_BIT (1U << 7) +#define _PY_EVAL_PROCESS_DELAYED_BIT (1U << 8) /* Reserve a few bits for future use */ -#define _PY_EVAL_EVENTS_BITS 8 +#define _PY_EVAL_EVENTS_BITS 9 #define _PY_EVAL_EVENTS_MASK ((1 << _PY_EVAL_EVENTS_BITS)-1) static inline void diff --git a/Lib/test/test_free_threading/test_qsbr.py b/Lib/test/test_free_threading/test_qsbr.py new file mode 100644 index 00000000000000..8ce37b73e80a04 --- /dev/null +++ b/Lib/test/test_free_threading/test_qsbr.py @@ -0,0 +1,72 @@ +import threading +import unittest + +from test.support import threading_helper + + +class Holder: + pass + + +def make_shared(func, *args): + t = threading.Thread(target=func, args=args) + t.start() + t.join() + + +@threading_helper.requires_working_threading() +class TestDelayedFreeReentrancy(unittest.TestCase): + ROUNDS = 1000 + + def queue_finalizers(self, callback): + class Finalizer: + def __del__(self): + callback() + + holders = [Holder() for _ in range(self.ROUNDS)] + for h in holders: + h.__dict__ = {"f": Finalizer()} + return holders + + def test_dict_resize(self): + d = {} + make_shared(d.get, 0) + holders = self.queue_finalizers(lambda: d.__setitem__(object(), 0)) + for n, h in enumerate(holders): + h.__dict__ = {} + if n % 2: + h.__dict__ = {} + for k in list(d): + del d[k] + for i in range(6): + d[i] = i + self.assertEqual(len(d), len(list(d))) + + def test_list_resize(self): + L = [] + make_shared(L.append, 0) + holders = self.queue_finalizers(L.clear) + for n, h in enumerate(holders): + h.__dict__ = {} + if n % 2: + h.__dict__ = {} + L.clear() + for i in range(9): + L.insert(len(L), i) + self.assertEqual(len(L), len(list(L))) + + def test_type_cache_invalidation(self): + class C: + x = None + + holders = self.queue_finalizers(lambda: C.x) + for h in holders: + h.__dict__ = {} + C.x + new = object() + C.x = new + self.assertIs(C.x, new) + + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-26-10-18-26.gh-issue-160697.qSbR1f.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-26-10-18-26.gh-issue-160697.qSbR1f.rst new file mode 100644 index 00000000000000..55649732e8f4b2 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-26-10-18-26.gh-issue-160697.qSbR1f.rst @@ -0,0 +1,4 @@ +Fix crashes and hangs in the :term:`free-threaded build` when a destructor +ran in the middle of a :class:`dict` resize, a :class:`list` resize or a type +cache update. The thread's queue of delayed frees is now processed only from +the eval breaker, instead of synchronously when the queue fills up. diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index 3ad485620ede41..0f3436cd4eb0fb 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -1,6 +1,7 @@ /* Python's malloc wrappers (see pymem.h) */ #include "Python.h" +#include "pycore_ceval.h" #include "pycore_interp.h" // _PyInterpreterState_HasFeature #include "pycore_mmap.h" // _PyAnnotateMemoryMap() #include "pycore_object.h" // _PyDebugAllocatorStats() definition @@ -1513,10 +1514,8 @@ free_delayed(uintptr_t ptr, size_t size) buf->wr_idx++; if (buf->wr_idx == WORK_ITEMS_PER_CHUNK) { - // Normally the processing of delayed items is done from the eval - // breaker. Processing here is a safety measure to ensure too much - // work does not accumulate. - _PyMem_ProcessDelayed((PyThreadState *)tstate); + _Py_set_eval_breaker_bit((PyThreadState *)tstate, + _PY_EVAL_PROCESS_DELAYED_BIT); } #endif } @@ -1626,6 +1625,7 @@ _PyMem_ProcessDelayed(PyThreadState *tstate) _PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate; tstate_impl->qsbr->should_process = false; + _Py_unset_eval_breaker_bit(tstate, _PY_EVAL_PROCESS_DELAYED_BIT); // Process thread-local work process_queue(&tstate_impl->mem_free_queue, tstate_impl, true, NULL, NULL); diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c index 49b2ede4ed3e60..bc86164f5ceab8 100644 --- a/Python/ceval_gil.c +++ b/Python/ceval_gil.c @@ -1389,7 +1389,9 @@ _Py_HandlePending(PyThreadState *tstate) _Py_brc_merge_refcounts(tstate); } /* Process deferred memory frees held by QSBR */ - if (_Py_qsbr_should_process(((_PyThreadStateImpl *)tstate)->qsbr)) { + if ((breaker & _PY_EVAL_PROCESS_DELAYED_BIT) != 0 || + _Py_qsbr_should_process(((_PyThreadStateImpl *)tstate)->qsbr)) + { _PyMem_ProcessDelayed(tstate); } #endif