Skip to content

Commit e62a611

Browse files
gh-146452: Fix pickle segfault on concurrent mutation of dict in pickle (#146470)
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
1 parent 1cbe035 commit e62a611

3 files changed

Lines changed: 59 additions & 1 deletion

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import pickle
2+
import threading
3+
import unittest
4+
5+
from test.support import threading_helper
6+
7+
8+
@threading_helper.requires_working_threading()
9+
class TestPickleFreeThreading(unittest.TestCase):
10+
11+
def test_pickle_dumps_with_concurrent_dict_mutation(self):
12+
# gh-146452: Pickling a dict while another thread mutates it
13+
# used to segfault. batch_dict_exact() iterated dict items via
14+
# PyDict_Next() which returns borrowed references, and a
15+
# concurrent pop/replace could free the value before Py_INCREF
16+
# got to it.
17+
shared = {str(i): list(range(20)) for i in range(50)}
18+
19+
def dumper():
20+
for _ in range(1000):
21+
try:
22+
pickle.dumps(shared)
23+
except RuntimeError:
24+
# "dictionary changed size during iteration" is expected
25+
pass
26+
27+
def mutator():
28+
for j in range(1000):
29+
key = str(j % 50)
30+
shared[key] = list(range(j % 20))
31+
if j % 10 == 0:
32+
shared.pop(key, None)
33+
shared[key] = [j]
34+
35+
threads = []
36+
for _ in range(10):
37+
threads.append(threading.Thread(target=dumper))
38+
threads.append(threading.Thread(target=mutator))
39+
40+
with threading_helper.start_threads(threads):
41+
pass
42+
43+
if __name__ == "__main__":
44+
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix segfault in :mod:`pickle` when pickling a dictionary concurrently
2+
mutated by another thread in the free-threaded build.

Modules/_pickle.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3452,7 +3452,7 @@ batch_dict(PickleState *state, PicklerObject *self, PyObject *iter, PyObject *or
34523452
* Note that this currently doesn't work for protocol 0.
34533453
*/
34543454
static int
3455-
batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
3455+
batch_dict_exact_impl(PickleState *state, PicklerObject *self, PyObject *obj)
34563456
{
34573457
PyObject *key = NULL, *value = NULL;
34583458
int i;
@@ -3525,6 +3525,18 @@ batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
35253525
return -1;
35263526
}
35273527

3528+
/* gh-146452: Wrap the dict iteration in a critical section to prevent
3529+
concurrent mutation from invalidating PyDict_Next() iteration state. */
3530+
static int
3531+
batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
3532+
{
3533+
int ret;
3534+
Py_BEGIN_CRITICAL_SECTION(obj);
3535+
ret = batch_dict_exact_impl(state, self, obj);
3536+
Py_END_CRITICAL_SECTION();
3537+
return ret;
3538+
}
3539+
35283540
static int
35293541
save_dict(PickleState *state, PicklerObject *self, PyObject *obj)
35303542
{

0 commit comments

Comments
 (0)