Skip to content

Commit 26a0dba

Browse files
[3.14] gh-130327: Always traverse managed dictionaries, even when inline values are available (GH-130469) (#145438)
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
1 parent 7f9fcba commit 26a0dba

File tree

3 files changed

+32
-7
lines changed

3 files changed

+32
-7
lines changed

Lib/test/test_dict.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,6 +1569,26 @@ def make_pairs():
15691569
self.assertEqual(d.get(key3_3), 44)
15701570
self.assertGreaterEqual(eq_count, 1)
15711571

1572+
def test_overwrite_managed_dict(self):
1573+
# GH-130327: Overwriting an object's managed dictionary with another object's
1574+
# skipped traversal in favor of inline values, causing the GC to believe that
1575+
# the __dict__ wasn't reachable.
1576+
import gc
1577+
1578+
class Shenanigans:
1579+
pass
1580+
1581+
to_be_deleted = Shenanigans()
1582+
to_be_deleted.attr = "whatever"
1583+
holds_reference = Shenanigans()
1584+
holds_reference.__dict__ = to_be_deleted.__dict__
1585+
holds_reference.ref = {"circular": to_be_deleted, "data": 42}
1586+
1587+
del to_be_deleted
1588+
gc.collect()
1589+
self.assertEqual(holds_reference.ref['data'], 42)
1590+
self.assertEqual(holds_reference.attr, "whatever")
1591+
15721592
def test_unhashable_key(self):
15731593
d = {'a': 1}
15741594
key = [1, 2, 3]
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
@@ -4624,10 +4624,8 @@ dict_traverse(PyObject *op, visitproc visit, void *arg)
46244624

46254625
if (DK_IS_UNICODE(keys)) {
46264626
if (_PyDict_HasSplitTable(mp)) {
4627-
if (!mp->ma_values->embedded) {
4628-
for (i = 0; i < n; i++) {
4629-
Py_VISIT(mp->ma_values->values[i]);
4630-
}
4627+
for (i = 0; i < n; i++) {
4628+
Py_VISIT(mp->ma_values->values[i]);
46314629
}
46324630
}
46334631
else {
@@ -7188,16 +7186,21 @@ PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg)
71887186
if((tp->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) {
71897187
return 0;
71907188
}
7191-
if (tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
7189+
PyDictObject *dict = _PyObject_ManagedDictPointer(obj)->dict;
7190+
if (dict != NULL) {
7191+
// GH-130327: If there's a managed dictionary available, we should
7192+
// *always* traverse it. The dict is responsible for traversing the
7193+
// inline values if it points to them.
7194+
Py_VISIT(dict);
7195+
}
7196+
else if (tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
71927197
PyDictValues *values = _PyObject_InlineValues(obj);
71937198
if (values->valid) {
71947199
for (Py_ssize_t i = 0; i < values->capacity; i++) {
71957200
Py_VISIT(values->values[i]);
71967201
}
7197-
return 0;
71987202
}
71997203
}
7200-
Py_VISIT(_PyObject_ManagedDictPointer(obj)->dict);
72017204
return 0;
72027205
}
72037206

0 commit comments

Comments
 (0)