Skip to content

Commit b0a43d4

Browse files
authored
[3.15] gh-155978: Fix leak in update_slot_after_setattr() (GH-155979) (#155984)
(cherry picked from commit f381d16)
1 parent 5159338 commit b0a43d4

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,6 +181,27 @@ 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
def test_concurrent_setattr_deadlock(self):
185206
# gh-155400: two threads assigning to a special method of the same
186207
# 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
@@ -6760,24 +6760,29 @@ static int
67606760
update_slot_after_setattr(PyTypeObject *type, PyObject *name)
67616761
{
67626762
#ifdef Py_GIL_DISABLED
6763-
// stack allocate one chunk since that's all we need
67646763
assert(SLOT_UPDATE_CHUNK_SIZE >= MAX_EQUIV);
67656764
slot_update_chunk_t chunk = {0};
6765+
// Stack allocate the first chunk. It is usually the only one needed but
6766+
// updates are queued for subclasses as well, so more chunks are needed if
6767+
// the type has more than SLOT_UPDATE_CHUNK_SIZE subclasses.
67666768
slot_update_t queued_updates = {&chunk};
67676769

6768-
if (update_slot(type, name, &queued_updates) < 0) {
6769-
return -1;
6770-
}
6771-
if (queued_updates.head->n > 0) {
6770+
int res = update_slot(type, name, &queued_updates);
6771+
if (res == 0 && queued_updates.head->n > 0) {
67726772
apply_type_slot_updates(&queued_updates);
67736773
ASSERT_TYPE_LOCK_HELD();
6774-
// should never allocate another chunk
6775-
assert(chunk.prev == NULL);
67766774
}
6775+
slot_update_chunk_t *cur = queued_updates.head;
6776+
while (cur != &chunk) {
6777+
slot_update_chunk_t *prev = cur->prev;
6778+
PyMem_Free(cur);
6779+
cur = prev;
6780+
}
6781+
return res;
67776782
#else
67786783
update_slot(type, name, NULL);
6779-
#endif
67806784
return 0;
6785+
#endif
67816786
}
67826787

67836788
static int

0 commit comments

Comments
 (0)