diff --git a/Lib/test/test_ordered_dict.py b/Lib/test/test_ordered_dict.py index 4204a6a47d2a81..6fae262bec1ac5 100644 --- a/Lib/test/test_ordered_dict.py +++ b/Lib/test/test_ordered_dict.py @@ -970,6 +970,87 @@ def test_weakref_list_is_not_traversed(self): gc.collect() + def test_copy_concurrent_clear_in__getitem__(self): + class MyOD(self.OrderedDict): + def __getitem__(self, key): + super().clear() + return None + + od = MyOD([(i, i) for i in range(4)]) + self.assertRaises(RuntimeError, od.copy) + + def test_copy_concurrent_insertion_in__getitem__(self): + class MyOD(self.OrderedDict): + def __getitem__(self, key): + self['new_key'] = 'new_value' + return super().__getitem__(key) + + od = MyOD([(1, 'one'), (2, 'two')]) + self.assertRaises(RuntimeError, od.copy) + + def test_copy_concurrent_deletion_by_del_in__getitem__(self): + class MyOD(self.OrderedDict): + call_count = 0 + def __getitem__(self, key): + self.call_count += 1 + if self.call_count == 1: + del self[3] + return super().__getitem__(key) + + od = MyOD([(1, 'one'), (2, 'two'), (3, 'three')]) + self.assertRaises(RuntimeError, od.copy) + + def test_copy_concurrent_deletion_by_pop_in__getitem__(self): + class MyOD(self.OrderedDict): + call_count = 0 + def __getitem__(self, key): + self.call_count += 1 + if self.call_count == 1: + self.pop(3) + return super().__getitem__(key) + + od = MyOD([(1, 'one'), (2, 'two'), (3, 'three')]) + self.assertRaises(RuntimeError, od.copy) + + def test_copy_concurrent_deletion_and_set_in__getitem__(self): + class MyOD(self.OrderedDict): + call_count = 0 + instance_count = 0 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + MyOD.instance_count += 1 + + def __getitem__(self, key): + self.call_count += 1 + if self.call_count == 1: + del self[4] + elif self.call_count == 2: + self['new_key'] = 'new_value' + return super().__getitem__(key) + + od = MyOD([(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]) + self.assertRaises(RuntimeError, od.copy) + self.assertEqual(MyOD.instance_count, 2) + + def test_copy_concurrent_mutation_in__setitem__(self): + od = None + class MyOD(self.OrderedDict): + instance_count = 0 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + MyOD.instance_count += 1 + + def __setitem__(self, key, value): + if self.instance_count == 2 and len(od) > 1: + del od[next(iter(od))] + return super().__setitem__(key, value) + + od = MyOD([(1, 'one'), (2, 'two'), (3, 'three')]) + self.assertRaises(RuntimeError, od.copy) + self.assertEqual(MyOD.instance_count, 2) + class PurePythonOrderedDictSubclassTests(PurePythonOrderedDictTests): diff --git a/Misc/NEWS.d/next/Library/2025-12-22-15-02-16.gh-issue-142734.IxVAQh.rst b/Misc/NEWS.d/next/Library/2025-12-22-15-02-16.gh-issue-142734.IxVAQh.rst new file mode 100644 index 00000000000000..67f697622378e8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-22-15-02-16.gh-issue-142734.IxVAQh.rst @@ -0,0 +1 @@ +:mod:`collections`: fix use-after-free crashes in :meth:`OrderedDict.copy ` when the dictionary to copy is concurrently mutated. diff --git a/Objects/odictobject.c b/Objects/odictobject.c index 25928028919c9c..2c0f78c68cef71 100644 --- a/Objects/odictobject.c +++ b/Objects/odictobject.c @@ -1266,21 +1266,39 @@ OrderedDict_copy_impl(PyObject *od) } } else { + PyODictObject *self = _PyODictObject_CAST(od); + size_t state = self->od_state; + _odict_FOREACH(od, node) { - int res; - PyObject *value = PyObject_GetItem((PyObject *)od, - _odictnode_KEY(node)); - if (value == NULL) + PyObject *key = Py_NewRef(_odictnode_KEY(node)); + PyObject *value = PyObject_GetItem(od, key); + if (value == NULL) { + Py_DECREF(key); goto fail; - res = PyObject_SetItem((PyObject *)od_copy, - _odictnode_KEY(node), value); + } + + if (self->od_state != state) { + Py_DECREF(key); + Py_DECREF(value); + goto invalid_state; // 成功获取值但状态改变 + } + + int rc = PyObject_SetItem(od_copy, key, value); + Py_DECREF(key); Py_DECREF(value); - if (res != 0) + if (rc != 0) { goto fail; + } + if (self->od_state != state) { + goto invalid_state; + } } } return od_copy; +invalid_state: + PyErr_SetString(PyExc_RuntimeError, + "OrderedDict mutated during iteration"); fail: Py_DECREF(od_copy); return NULL;