From e6d260c5167de166bd3c2d78fe428a0e17ab333e Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 17 Aug 2026 19:25:08 -0700 Subject: [PATCH] [3.15] gh-155978: Fix leak in update_slot_after_setattr() (GH-155979) (cherry picked from commit f381d1634c1eddedddcb8ea5d4ae5a4dd7564822) Co-authored-by: Neil Schemenauer --- Lib/test/test_free_threading/test_type.py | 21 +++++++++++++++++++ ...-08-17-15-13-14.gh-issue-155978.4ztALD.rst | 2 ++ Objects/typeobject.c | 21 ++++++++++++------- 3 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-17-15-13-14.gh-issue-155978.4ztALD.rst diff --git a/Lib/test/test_free_threading/test_type.py b/Lib/test/test_free_threading/test_type.py index 1255d842dbff48f..9953ce9e284f899 100644 --- a/Lib/test/test_free_threading/test_type.py +++ b/Lib/test/test_free_threading/test_type.py @@ -181,5 +181,26 @@ def wrapper(): for reader in readers: reader.join() + def test_setattr_many_subclasses(self): + # gh-155978: Updating a special method queues a slot update for every + # affected subclass. Keep enough subclasses alive to require + # heap-allocated queue chunks in addition to the stack chunk. + class Base: + pass + + subclasses = [type(f"Sub{i}", (Base,), {}) for i in range(100)] + + def custom_repr(self): + return "custom repr" + + Base.__repr__ = custom_repr + self.assertTrue(all(repr(cls()) == "custom repr" + for cls in subclasses)) + + del Base.__repr__ + self.assertTrue(all(repr(cls()) != "custom repr" + for cls in subclasses)) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-17-15-13-14.gh-issue-155978.4ztALD.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-17-15-13-14.gh-issue-155978.4ztALD.rst new file mode 100644 index 000000000000000..8b807d763d604a2 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-17-15-13-14.gh-issue-155978.4ztALD.rst @@ -0,0 +1,2 @@ +Fix a memory leak in the free-threaded build when setting or deleting a +special method (such as ``__repr__``) on a class that has many subclasses. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 8108e8e92731dad..2831334068f2edf 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6726,24 +6726,29 @@ static int update_slot_after_setattr(PyTypeObject *type, PyObject *name) { #ifdef Py_GIL_DISABLED - // stack allocate one chunk since that's all we need assert(SLOT_UPDATE_CHUNK_SIZE >= MAX_EQUIV); slot_update_chunk_t chunk = {0}; + // Stack allocate the first chunk. It is usually the only one needed but + // updates are queued for subclasses as well, so more chunks are needed if + // the type has more than SLOT_UPDATE_CHUNK_SIZE subclasses. slot_update_t queued_updates = {&chunk}; - if (update_slot(type, name, &queued_updates) < 0) { - return -1; - } - if (queued_updates.head->n > 0) { + int res = update_slot(type, name, &queued_updates); + if (res == 0 && queued_updates.head->n > 0) { apply_type_slot_updates(&queued_updates); ASSERT_TYPE_LOCK_HELD(); - // should never allocate another chunk - assert(chunk.prev == NULL); } + slot_update_chunk_t *cur = queued_updates.head; + while (cur != &chunk) { + slot_update_chunk_t *prev = cur->prev; + PyMem_Free(cur); + cur = prev; + } + return res; #else update_slot(type, name, NULL); -#endif return 0; +#endif } static int