Skip to content

Commit 702d085

Browse files
[3.13] gh-130327: Always traverse managed dictionaries, even when inline values are available (GH-130469) (#145440)
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
1 parent d567f45 commit 702d085

File tree

3 files changed

+31
-7
lines changed

3 files changed

+31
-7
lines changed

Lib/test/test_dict.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,25 @@ def make_pairs():
16631663
self.assertEqual(d.get(key3_3), 44)
16641664
self.assertGreaterEqual(eq_count, 1)
16651665

1666+
def test_overwrite_managed_dict(self):
1667+
# GH-130327: Overwriting an object's managed dictionary with another object's
1668+
# skipped traversal in favor of inline values, causing the GC to believe that
1669+
# the __dict__ wasn't reachable.
1670+
import gc
1671+
1672+
class Shenanigans:
1673+
pass
1674+
1675+
to_be_deleted = Shenanigans()
1676+
to_be_deleted.attr = "whatever"
1677+
holds_reference = Shenanigans()
1678+
holds_reference.__dict__ = to_be_deleted.__dict__
1679+
holds_reference.ref = {"circular": to_be_deleted, "data": 42}
1680+
1681+
del to_be_deleted
1682+
gc.collect()
1683+
self.assertEqual(holds_reference.ref['data'], 42)
1684+
self.assertEqual(holds_reference.attr, "whatever")
16661685
def test_clear_at_lookup(self):
16671686
# gh-140551 dict crash if clear is called at lookup stage
16681687
class X:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix erroneous clearing of an object's :attr:`~object.__dict__` if
2+
overwritten at runtime.

Objects/dictobject.c

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4557,10 +4557,8 @@ dict_traverse(PyObject *op, visitproc visit, void *arg)
45574557

45584558
if (DK_IS_UNICODE(keys)) {
45594559
if (_PyDict_HasSplitTable(mp)) {
4560-
if (!mp->ma_values->embedded) {
4561-
for (i = 0; i < n; i++) {
4562-
Py_VISIT(mp->ma_values->values[i]);
4563-
}
4560+
for (i = 0; i < n; i++) {
4561+
Py_VISIT(mp->ma_values->values[i]);
45644562
}
45654563
}
45664564
else {
@@ -7128,16 +7126,21 @@ PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg)
71287126
if((tp->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) {
71297127
return 0;
71307128
}
7131-
if (tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
7129+
PyDictObject *dict = _PyObject_ManagedDictPointer(obj)->dict;
7130+
if (dict != NULL) {
7131+
// GH-130327: If there's a managed dictionary available, we should
7132+
// *always* traverse it. The dict is responsible for traversing the
7133+
// inline values if it points to them.
7134+
Py_VISIT(dict);
7135+
}
7136+
else if (tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
71327137
PyDictValues *values = _PyObject_InlineValues(obj);
71337138
if (values->valid) {
71347139
for (Py_ssize_t i = 0; i < values->capacity; i++) {
71357140
Py_VISIT(values->values[i]);
71367141
}
7137-
return 0;
71387142
}
71397143
}
7140-
Py_VISIT(_PyObject_ManagedDictPointer(obj)->dict);
71417144
return 0;
71427145
}
71437146

0 commit comments

Comments
 (0)