Skip to content

Commit 1190b63

Browse files
gh-92930: _pickle.c: Acquire strong references before calling save() (GH-92931)
(cherry picked from commit 4c496f1) Co-authored-by: Dennis Sweeney <36520290+sweeneyde@users.noreply.github.com>
1 parent 65e2a94 commit 1190b63

File tree

3 files changed

+98
-11
lines changed

3 files changed

+98
-11
lines changed

Lib/test/pickletester.py

+61
Original file line numberDiff line numberDiff line change
@@ -3032,6 +3032,67 @@ def check_array(arr):
30323032
# 2-D, non-contiguous
30333033
check_array(arr[::2])
30343034

3035+
def test_evil_class_mutating_dict(self):
3036+
# https://github.com/python/cpython/issues/92930
3037+
from random import getrandbits
3038+
3039+
global Bad
3040+
class Bad:
3041+
def __eq__(self, other):
3042+
return ENABLED
3043+
def __hash__(self):
3044+
return 42
3045+
def __reduce__(self):
3046+
if getrandbits(6) == 0:
3047+
collection.clear()
3048+
return (Bad, ())
3049+
3050+
for proto in protocols:
3051+
for _ in range(20):
3052+
ENABLED = False
3053+
collection = {Bad(): Bad() for _ in range(20)}
3054+
for bad in collection:
3055+
bad.bad = bad
3056+
bad.collection = collection
3057+
ENABLED = True
3058+
try:
3059+
data = self.dumps(collection, proto)
3060+
self.loads(data)
3061+
except RuntimeError as e:
3062+
expected = "changed size during iteration"
3063+
self.assertIn(expected, str(e))
3064+
3065+
def test_evil_pickler_mutating_collection(self):
3066+
# https://github.com/python/cpython/issues/92930
3067+
if not hasattr(self, "pickler"):
3068+
raise self.skipTest(f"{type(self)} has no associated pickler type")
3069+
3070+
global Clearer
3071+
class Clearer:
3072+
pass
3073+
3074+
def check(collection):
3075+
class EvilPickler(self.pickler):
3076+
def persistent_id(self, obj):
3077+
if isinstance(obj, Clearer):
3078+
collection.clear()
3079+
return None
3080+
pickler = EvilPickler(io.BytesIO(), proto)
3081+
try:
3082+
pickler.dump(collection)
3083+
except RuntimeError as e:
3084+
expected = "changed size during iteration"
3085+
self.assertIn(expected, str(e))
3086+
3087+
for proto in protocols:
3088+
check([Clearer()])
3089+
check([Clearer(), Clearer()])
3090+
check({Clearer()})
3091+
check({Clearer(), Clearer()})
3092+
check({Clearer(): 1})
3093+
check({Clearer(): 1, Clearer(): 2})
3094+
check({1: Clearer(), 2: Clearer()})
3095+
30353096

30363097
class BigmemPickleTests:
30373098

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed a crash in ``_pickle.c`` from mutating collections during ``__reduce__`` or ``persistent_id``.

Modules/_pickle.c

+36-11
Original file line numberDiff line numberDiff line change
@@ -3005,7 +3005,10 @@ batch_list_exact(PicklerObject *self, PyObject *obj)
30053005

30063006
if (PyList_GET_SIZE(obj) == 1) {
30073007
item = PyList_GET_ITEM(obj, 0);
3008-
if (save(self, item, 0) < 0)
3008+
Py_INCREF(item);
3009+
int err = save(self, item, 0);
3010+
Py_DECREF(item);
3011+
if (err < 0)
30093012
return -1;
30103013
if (_Pickler_Write(self, &append_op, 1) < 0)
30113014
return -1;
@@ -3020,7 +3023,10 @@ batch_list_exact(PicklerObject *self, PyObject *obj)
30203023
return -1;
30213024
while (total < PyList_GET_SIZE(obj)) {
30223025
item = PyList_GET_ITEM(obj, total);
3023-
if (save(self, item, 0) < 0)
3026+
Py_INCREF(item);
3027+
int err = save(self, item, 0);
3028+
Py_DECREF(item);
3029+
if (err < 0)
30243030
return -1;
30253031
total++;
30263032
if (++this_batch == BATCHSIZE)
@@ -3258,10 +3264,16 @@ batch_dict_exact(PicklerObject *self, PyObject *obj)
32583264
/* Special-case len(d) == 1 to save space. */
32593265
if (dict_size == 1) {
32603266
PyDict_Next(obj, &ppos, &key, &value);
3261-
if (save(self, key, 0) < 0)
3262-
return -1;
3263-
if (save(self, value, 0) < 0)
3264-
return -1;
3267+
Py_INCREF(key);
3268+
Py_INCREF(value);
3269+
if (save(self, key, 0) < 0) {
3270+
goto error;
3271+
}
3272+
if (save(self, value, 0) < 0) {
3273+
goto error;
3274+
}
3275+
Py_CLEAR(key);
3276+
Py_CLEAR(value);
32653277
if (_Pickler_Write(self, &setitem_op, 1) < 0)
32663278
return -1;
32673279
return 0;
@@ -3273,10 +3285,16 @@ batch_dict_exact(PicklerObject *self, PyObject *obj)
32733285
if (_Pickler_Write(self, &mark_op, 1) < 0)
32743286
return -1;
32753287
while (PyDict_Next(obj, &ppos, &key, &value)) {
3276-
if (save(self, key, 0) < 0)
3277-
return -1;
3278-
if (save(self, value, 0) < 0)
3279-
return -1;
3288+
Py_INCREF(key);
3289+
Py_INCREF(value);
3290+
if (save(self, key, 0) < 0) {
3291+
goto error;
3292+
}
3293+
if (save(self, value, 0) < 0) {
3294+
goto error;
3295+
}
3296+
Py_CLEAR(key);
3297+
Py_CLEAR(value);
32803298
if (++i == BATCHSIZE)
32813299
break;
32823300
}
@@ -3291,6 +3309,10 @@ batch_dict_exact(PicklerObject *self, PyObject *obj)
32913309

32923310
} while (i == BATCHSIZE);
32933311
return 0;
3312+
error:
3313+
Py_XDECREF(key);
3314+
Py_XDECREF(value);
3315+
return -1;
32943316
}
32953317

32963318
static int
@@ -3410,7 +3432,10 @@ save_set(PicklerObject *self, PyObject *obj)
34103432
if (_Pickler_Write(self, &mark_op, 1) < 0)
34113433
return -1;
34123434
while (_PySet_NextEntry(obj, &ppos, &item, &hash)) {
3413-
if (save(self, item, 0) < 0)
3435+
Py_INCREF(item);
3436+
int err = save(self, item, 0);
3437+
Py_CLEAR(item);
3438+
if (err < 0)
34143439
return -1;
34153440
if (++i == BATCHSIZE)
34163441
break;

0 commit comments

Comments
 (0)