gh-155978: Fix leak in update_slot_after_setattr() (#155979)

kumaraditya303 <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/f381d1634c1eddedddcb8ea5d4ae5a4dd7564822
commit: f381d1634c1eddedddcb8ea5d4ae5a4dd7564822
branch: main
author: Neil Schemenauer <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-08-18T07:55:08+05:30
summary:

gh-155978: Fix leak in update_slot_after_setattr() (#155979)

files:
A Misc/NEWS.d/next/Core_and_Builtins/2026-08-17-15-13-14.gh-issue-155978.4ztALD.rst
M Lib/test/test_free_threading/test_type.py
M Objects/typeobject.c

diff --git a/Lib/test/test_free_threading/test_type.py b/Lib/test/test_free_threading/test_type.py
index d64a3f38f148305..a55c4815a038376 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 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 420eb855a35d16c..572e302df8d80d5 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

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.