Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,69 @@ def test_hash_collision_remove_add(self):
self.assertEqual(len(d), len(items), d)
self.assertEqual(d, dict(items))

def test_clear_reentrant_embedded(self):
# gh-130555: dict.clear() must be safe when values are embedded
# in an object and a destructor mutates the dict.
class MyObj: pass
class ClearOnDelete:
def __del__(self):
nonlocal x
del x

x = MyObj()
x.a = ClearOnDelete()

d = x.__dict__
d.clear()

def test_clear_reentrant_cycle(self):
# gh-130555: dict.clear() must be safe for embedded dicts when the
# object is part of a reference cycle and the last reference to the
# dict is via the cycle.
class MyObj: pass
obj = MyObj()
obj.f = obj
obj.attr = "attr"

d = obj.__dict__
del obj

d.clear()

def test_clear_reentrant_force_combined(self):
# gh-130555: dict.clear() must be safe when a destructor forces the
# dict from embedded/split to combined (setting ma_values to NULL).
class MyObj: pass
class ForceConvert:
def __del__(self):
d[1] = "trigger"

x = MyObj()
x.a = ForceConvert()
x.b = "other"

d = x.__dict__
d.clear()

def test_clear_reentrant_delete(self):
# gh-130555: dict.clear() must be safe when a destructor deletes
# a key from the same embedded dict.
class MyObj: pass
class DelKey:
def __del__(self):
try:
del d['b']
except KeyError:
pass

x = MyObj()
x.a = DelKey()
x.b = "value_b"
x.c = "value_c"

d = x.__dict__
d.clear()


class CAPITest(unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix use-after-free in :meth:`dict.clear` when the dictionary values are
embedded in an object and a destructor causes re-entrant mutation of the
dictionary.
31 changes: 22 additions & 9 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2969,6 +2969,21 @@ _PyDict_DelItemIf(PyObject *op, PyObject *key,
return res;
}

static void
clear_embedded_values(PyDictValues *values, Py_ssize_t nentries)
{
PyObject *refs[SHARED_KEYS_MAX_SIZE];
assert(nentries <= SHARED_KEYS_MAX_SIZE);
for (Py_ssize_t i = 0; i < nentries; i++) {
refs[i] = values->values[i];
values->values[i] = NULL;
}
values->size = 0;
for (Py_ssize_t i = 0; i < nentries; i++) {
Py_XDECREF(refs[i]);
}
}

static void
clear_lock_held(PyObject *op)
{
Expand Down Expand Up @@ -2997,20 +3012,18 @@ clear_lock_held(PyObject *op)
assert(oldkeys->dk_refcnt == 1);
dictkeys_decref(oldkeys, IS_DICT_SHARED(mp));
}
else if (oldvalues->embedded) {
clear_embedded_values(oldvalues, oldkeys->dk_nentries);
}
else {
set_values(mp, NULL);
set_keys(mp, Py_EMPTY_KEYS);
n = oldkeys->dk_nentries;
for (i = 0; i < n; i++) {
Py_CLEAR(oldvalues->values[i]);
}
if (oldvalues->embedded) {
oldvalues->size = 0;
}
else {
set_values(mp, NULL);
set_keys(mp, Py_EMPTY_KEYS);
free_values(oldvalues, IS_DICT_SHARED(mp));
dictkeys_decref(oldkeys, false);
}
free_values(oldvalues, IS_DICT_SHARED(mp));
dictkeys_decref(oldkeys, false);
}
ASSERT_CONSISTENT(mp);
}
Expand Down
Loading