https://github.com/python/cpython/commit/b0a43d4ec627f904e59fd4d2b7207bec01ffe25d
commit: b0a43d4ec627f904e59fd4d2b7207bec01ffe25d
branch: 3.15
author: Neil Schemenauer <[email protected]>
committer: hugovk <[email protected]>
date: 2026-08-25T08:24:31+09:00
summary:

[3.15] gh-155978: Fix leak in update_slot_after_setattr() (GH-155979) (#155984)

(cherry picked from commit f381d1634c1eddedddcb8ea5d4ae5a4dd7564822)

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 497c94170ae1387..6dac77f68829661 100644
--- a/Lib/test/test_free_threading/test_type.py
+++ b/Lib/test/test_free_threading/test_type.py
@@ -181,6 +181,27 @@ 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 b648b60cfc89dc5..0d3f98ff446aacb 100644
--- a/Objects/typeobject.c
+++ b/Objects/typeobject.c
@@ -6760,24 +6760,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]

Reply via email to