diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index f13d42f664880b..4e8d4e4f6433f9 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -3032,6 +3032,67 @@ def check_array(arr): # 2-D, non-contiguous check_array(arr[::2]) + def test_evil_class_mutating_dict(self): + # https://github.com/python/cpython/issues/92930 + from random import getrandbits + + global Bad + class Bad: + def __eq__(self, other): + return ENABLED + def __hash__(self): + return 42 + def __reduce__(self): + if getrandbits(6) == 0: + collection.clear() + return (Bad, ()) + + for proto in protocols: + for _ in range(20): + ENABLED = False + collection = {Bad(): Bad() for _ in range(20)} + for bad in collection: + bad.bad = bad + bad.collection = collection + ENABLED = True + try: + data = self.dumps(collection, proto) + self.loads(data) + except RuntimeError as e: + expected = "changed size during iteration" + self.assertIn(expected, str(e)) + + def test_evil_pickler_mutating_collection(self): + # https://github.com/python/cpython/issues/92930 + if not hasattr(self, "pickler"): + raise self.skipTest(f"{type(self)} has no associated pickler type") + + global Clearer + class Clearer: + pass + + def check(collection): + class EvilPickler(self.pickler): + def persistent_id(self, obj): + if isinstance(obj, Clearer): + collection.clear() + return None + pickler = EvilPickler(io.BytesIO(), proto) + try: + pickler.dump(collection) + except RuntimeError as e: + expected = "changed size during iteration" + self.assertIn(expected, str(e)) + + for proto in protocols: + check([Clearer()]) + check([Clearer(), Clearer()]) + check({Clearer()}) + check({Clearer(), Clearer()}) + check({Clearer(): 1}) + check({Clearer(): 1, Clearer(): 2}) + check({1: Clearer(), 2: Clearer()}) + class BigmemPickleTests: diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-05-18-18-34-45.gh-issue-92930.kpYPOb.rst b/Misc/NEWS.d/next/Core and Builtins/2022-05-18-18-34-45.gh-issue-92930.kpYPOb.rst new file mode 100644 index 00000000000000..cd5d7b3214ea43 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-05-18-18-34-45.gh-issue-92930.kpYPOb.rst @@ -0,0 +1 @@ +Fixed a crash in ``_pickle.c`` from mutating collections during ``__reduce__`` or ``persistent_id``. diff --git a/Modules/_pickle.c b/Modules/_pickle.c index 919490c88f7d09..d05069a0d436d6 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -3005,7 +3005,10 @@ batch_list_exact(PicklerObject *self, PyObject *obj) if (PyList_GET_SIZE(obj) == 1) { item = PyList_GET_ITEM(obj, 0); - if (save(self, item, 0) < 0) + Py_INCREF(item); + int err = save(self, item, 0); + Py_DECREF(item); + if (err < 0) return -1; if (_Pickler_Write(self, &append_op, 1) < 0) return -1; @@ -3020,7 +3023,10 @@ batch_list_exact(PicklerObject *self, PyObject *obj) return -1; while (total < PyList_GET_SIZE(obj)) { item = PyList_GET_ITEM(obj, total); - if (save(self, item, 0) < 0) + Py_INCREF(item); + int err = save(self, item, 0); + Py_DECREF(item); + if (err < 0) return -1; total++; if (++this_batch == BATCHSIZE) @@ -3258,10 +3264,16 @@ batch_dict_exact(PicklerObject *self, PyObject *obj) /* Special-case len(d) == 1 to save space. */ if (dict_size == 1) { PyDict_Next(obj, &ppos, &key, &value); - if (save(self, key, 0) < 0) - return -1; - if (save(self, value, 0) < 0) - return -1; + Py_INCREF(key); + Py_INCREF(value); + if (save(self, key, 0) < 0) { + goto error; + } + if (save(self, value, 0) < 0) { + goto error; + } + Py_CLEAR(key); + Py_CLEAR(value); if (_Pickler_Write(self, &setitem_op, 1) < 0) return -1; return 0; @@ -3273,10 +3285,16 @@ batch_dict_exact(PicklerObject *self, PyObject *obj) if (_Pickler_Write(self, &mark_op, 1) < 0) return -1; while (PyDict_Next(obj, &ppos, &key, &value)) { - if (save(self, key, 0) < 0) - return -1; - if (save(self, value, 0) < 0) - return -1; + Py_INCREF(key); + Py_INCREF(value); + if (save(self, key, 0) < 0) { + goto error; + } + if (save(self, value, 0) < 0) { + goto error; + } + Py_CLEAR(key); + Py_CLEAR(value); if (++i == BATCHSIZE) break; } @@ -3291,6 +3309,10 @@ batch_dict_exact(PicklerObject *self, PyObject *obj) } while (i == BATCHSIZE); return 0; +error: + Py_XDECREF(key); + Py_XDECREF(value); + return -1; } static int @@ -3410,7 +3432,10 @@ save_set(PicklerObject *self, PyObject *obj) if (_Pickler_Write(self, &mark_op, 1) < 0) return -1; while (_PySet_NextEntry(obj, &ppos, &item, &hash)) { - if (save(self, item, 0) < 0) + Py_INCREF(item); + int err = save(self, item, 0); + Py_CLEAR(item); + if (err < 0) return -1; if (++i == BATCHSIZE) break;