| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 795dd3b commit 77cc442
3 files changed
| Original file line number | Diff line number | Diff 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() | ||
| Original file line number | Diff line number | Diff 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. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3452,7 +3452,7 @@ batch_dict(PickleState *state, PicklerObject *self, PyObject *iter, PyObject *or | |||
| 3452 | 3452 | * Note that this currently doesn't work for protocol 0. | |
| 3453 | 3453 | */ | |
| 3454 | 3454 | static int | |
| 3455 | - batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj) | ||
| 3455 | + batch_dict_exact_impl(PickleState *state, PicklerObject *self, PyObject *obj) | ||
| 3456 | 3456 | { | |
| 3457 | 3457 | PyObject *key = NULL, *value = NULL; | |
| 3458 | 3458 | int i; | |
@@ -3525,6 +3525,18 @@ batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj) | |||
| 3525 | 3525 | return -1; | |
| 3526 | 3526 | } | |
| 3527 | 3527 | ||
| 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 | + | ||
| 3528 | 3540 | static int | |
| 3529 | 3541 | save_dict(PickleState *state, PicklerObject *self, PyObject *obj) | |
| 3530 | 3542 | { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments