Skip to content

Commit e6d260c

Browse files
committed
[3.15] gh-155978: Fix leak in update_slot_after_setattr() (GH-155979)
(cherry picked from commit f381d16) Co-authored-by: Neil Schemenauer <nas-github@arctrix.com>
1 parent ac4e5d2 commit e6d260c

3 files changed

Lines changed: 36 additions & 8 deletions

File tree

Lib/test/test_free_threading/test_type.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,5 +181,26 @@ def wrapper():
181181
for reader in readers:
182182
reader.join()
183183

184+
def test_setattr_many_subclasses(self):
185+
# gh-155978: Updating a special method queues a slot update for every
186+
# affected subclass. Keep enough subclasses alive to require
187+
# heap-allocated queue chunks in addition to the stack chunk.
188+
class Base:
189+
pass
190+
191+
subclasses = [type(f"Sub{i}", (Base,), {}) for i in range(100)]
192+
193+
def custom_repr(self):
194+
return "custom repr"
195+
196+
Base.__repr__ = custom_repr
197+
self.assertTrue(all(repr(cls()) == "custom repr"
198+
for cls in subclasses))
199+
200+
del Base.__repr__
201+
self.assertTrue(all(repr(cls()) != "custom repr"
202+
for cls in subclasses))
203+
204+
184205
if __name__ == "__main__":
185206
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a memory leak in the free-threaded build when setting or deleting a
2+
special method (such as ``__repr__``) on a class that has many subclasses.

Objects/typeobject.c

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6726,24 +6726,29 @@ static int
67266726
update_slot_after_setattr(PyTypeObject *type, PyObject *name)
67276727
{
67286728
#ifdef Py_GIL_DISABLED
6729-
// stack allocate one chunk since that's all we need
67306729
assert(SLOT_UPDATE_CHUNK_SIZE >= MAX_EQUIV);
67316730
slot_update_chunk_t chunk = {0};
6731+
// Stack allocate the first chunk. It is usually the only one needed but
6732+
// updates are queued for subclasses as well, so more chunks are needed if
6733+
// the type has more than SLOT_UPDATE_CHUNK_SIZE subclasses.
67326734
slot_update_t queued_updates = {&chunk};
67336735

6734-
if (update_slot(type, name, &queued_updates) < 0) {
6735-
return -1;
6736-
}
6737-
if (queued_updates.head->n > 0) {
6736+
int res = update_slot(type, name, &queued_updates);
6737+
if (res == 0 && queued_updates.head->n > 0) {
67386738
apply_type_slot_updates(&queued_updates);
67396739
ASSERT_TYPE_LOCK_HELD();
6740-
// should never allocate another chunk
6741-
assert(chunk.prev == NULL);
67426740
}
6741+
slot_update_chunk_t *cur = queued_updates.head;
6742+
while (cur != &chunk) {
6743+
slot_update_chunk_t *prev = cur->prev;
6744+
PyMem_Free(cur);
6745+
cur = prev;
6746+
}
6747+
return res;
67436748
#else
67446749
update_slot(type, name, NULL);
6745-
#endif
67466750
return 0;
6751+
#endif
67476752
}
67486753

67496754
static int

0 commit comments

Comments
 (0)