From 3d9921b4899d4971df88b5a3d89fbd5345afbd0b Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 17 Aug 2026 14:54:31 -0700 Subject: [PATCH] gh-155978: Fix leak in update_slot_after_setattr() On free-threaded builds, update_slot_after_setattr() stack-allocates a single chunk to queue slot updates, assuming one setattr never needs more. However, update_subclasses() queues one update per affected subclass and so can require heap-allocated chunks. Free those chunks after the slots update. --- Lib/test/test_free_threading/test_type.py | 20 ++++++++++++++++++ ...-08-17-15-13-14.gh-issue-155978.4ztALD.rst | 2 ++ Objects/typeobject.c | 21 ++++++++++++------- 3 files changed, 35 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 d64a3f38f14830..a55c4815a03837 100644 --- a/Lib/test/test_free_threading/test_type.py +++ b/Lib/test/test_free_threading/test_type.py @@ -324,6 +324,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)) + def test_concurrent_setattr_deadlock(self): # gh-155400: two threads assigning to a special method of the same # class could deadlock. One thread held the type lock and waited for 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 00000000000000..8b807d763d604a --- /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 420eb855a35d16..572e302df8d80d 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6598,24 +6598,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