| import pickle | |
| import threading | |
| import unittest | |
| from test.support import threading_helper | |
| @threading_helper.requires_working_threading() | |
| class TestPickleFreeThreading(unittest.TestCase): | |
| def test_pickle_dumps_with_concurrent_dict_mutation(self): | |
| # gh-146452: Pickling a dict while another thread mutates it | |
| # used to segfault. batch_dict_exact() iterated dict items via | |
| # PyDict_Next() which returns borrowed references, and a | |
| # concurrent pop/replace could free the value before Py_INCREF | |
| # got to it. | |
| shared = {str(i): list(range(20)) for i in range(50)} | |
| def dumper(): | |
| for _ in range(1000): | |
| try: | |
| pickle.dumps(shared) | |
| except RuntimeError: | |
| # "dictionary changed size during iteration" is expected | |
| pass | |
| def mutator(): | |
| for j in range(1000): | |
| key = str(j % 50) | |
| shared[key] = list(range(j % 20)) | |
| if j % 10 == 0: | |
| shared.pop(key, None) | |
| shared[key] = [j] | |
| threads = [] | |
| for _ in range(10): | |
| threads.append(threading.Thread(target=dumper)) | |
| threads.append(threading.Thread(target=mutator)) | |
| with threading_helper.start_threads(threads): | |
| pass | |
| def test_pickle_dumps_with_concurrent_list_mutations(self): | |
| # gh-149816: Pickling a list while another thread mutates it | |
| # used to be a UAF in free-threaded mode. batch_list_exact() | |
| # used PyList_GET_ITEM (borrowed) followed by Py_INCREF, and a | |
| # concurrent replace/pop could free the item between those two | |
| # operations. | |
| shared = [list(range(20)) for _ in range(50)] | |
| def dumper(): | |
| for _ in range(1000): | |
| try: | |
| pickle.dumps(shared) | |
| except (RuntimeError, IndexError): | |
| pass | |
| def mutator(): | |
| for i in range(1000): | |
| idx = i % 50 | |
| shared[idx] = list(range(i % 20)) | |
| if i % 10 == 0: | |
| try: | |
| shared.pop() | |
| except IndexError: | |
| pass | |
| shared.append([i]) | |
| threads = [] | |
| for _ in range(10): | |
| threads.append(threading.Thread(target=dumper)) | |
| threads.append(threading.Thread(target=mutator)) | |
| with threading_helper.start_threads(threads): | |
| pass | |
| if __name__ == "__main__": | |
| unittest.main() |