Skip to content

Commit f381d16

Browse files
authored
gh-155978: Fix leak in update_slot_after_setattr() (#155979)
1 parent a7bb524 commit f381d16

3 files changed

Lines changed: 35 additions & 8 deletions

File tree

Lib/test/test_free_threading/test_type.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,26 @@ def wrapper():
324324
for reader in readers:
325325
reader.join()
326326

327+
def test_setattr_many_subclasses(self):
328+
# gh-155978: Updating a special method queues a slot update for every
329+
# affected subclass. Keep enough subclasses alive to require
330+
# heap-allocated queue chunks in addition to the stack chunk.
331+
class Base:
332+
pass
333+
334+
subclasses = [type(f"Sub{i}", (Base,), {}) for i in range(100)]
335+
336+
def custom_repr(self):
337+
return "custom repr"
338+
339+
Base.__repr__ = custom_repr
340+
self.assertTrue(all(repr(cls()) == "custom repr"
341+
for cls in subclasses))
342+
343+
del Base.__repr__
344+
self.assertTrue(all(repr(cls()) != "custom repr"
345+
for cls in subclasses))
346+
327347
def test_concurrent_setattr_deadlock(self):
328348
# gh-155400: two threads assigning to a special method of the same
329349
# class could deadlock. One thread held the type lock and waited for
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
@@ -6598,24 +6598,29 @@ static int
65986598
update_slot_after_setattr(PyTypeObject *type, PyObject *name)
65996599
{
66006600
#ifdef Py_GIL_DISABLED
6601-
// stack allocate one chunk since that's all we need
66026601
assert(SLOT_UPDATE_CHUNK_SIZE >= MAX_EQUIV);
66036602
slot_update_chunk_t chunk = {0};
6603+
// Stack allocate the first chunk. It is usually the only one needed but
6604+
// updates are queued for subclasses as well, so more chunks are needed if
6605+
// the type has more than SLOT_UPDATE_CHUNK_SIZE subclasses.
66046606
slot_update_t queued_updates = {&chunk};
66056607

6606-
if (update_slot(type, name, &queued_updates) < 0) {
6607-
return -1;
6608-
}
6609-
if (queued_updates.head->n > 0) {
6608+
int res = update_slot(type, name, &queued_updates);
6609+
if (res == 0 && queued_updates.head->n > 0) {
66106610
apply_type_slot_updates(&queued_updates);
66116611
ASSERT_TYPE_LOCK_HELD();
6612-
// should never allocate another chunk
6613-
assert(chunk.prev == NULL);
66146612
}
6613+
slot_update_chunk_t *cur = queued_updates.head;
6614+
while (cur != &chunk) {
6615+
slot_update_chunk_t *prev = cur->prev;
6616+
PyMem_Free(cur);
6617+
cur = prev;
6618+
}
6619+
return res;
66156620
#else
66166621
update_slot(type, name, NULL);
6617-
#endif
66186622
return 0;
6623+
#endif
66196624
}
66206625

66216626
static int

0 commit comments

Comments
 (0)