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()
batch_list_exact(PickleState *state, PicklerObject *self, PyObject *obj)
{
PyObject *item = NULL;
- Py_ssize_t this_batch, total;
+ Py_ssize_t this_batch, total, list_size;
const char append_op = APPEND;
const char appends_op = APPENDS;
assert(obj != NULL);
assert(self->proto > 0);
assert(PyList_CheckExact(obj));
- assert(PyList_GET_SIZE(obj));
+
+ list_size = PyList_GET_SIZE(obj);
/* Write in batches of BATCHSIZE. */
total = 0;
do {
- if (PyList_GET_SIZE(obj) - total == 1) {
- item = PyList_GET_ITEM(obj, total);
- Py_INCREF(item);
+ if (list_size - total == 1) {
+ item = PyList_GetItemRef(obj, total);
+ if (item == NULL) {
+ _PyErr_FormatNote("when serializing %T item %zd", obj, total);
+ return -1;
+ }
int err = save(state, self, item, 0);
Py_DECREF(item);
if (err < 0) {
if (_Pickler_Write(self, &mark_op, 1) < 0)
return -1;
while (total < PyList_GET_SIZE(obj)) {
- item = PyList_GET_ITEM(obj, total);
- Py_INCREF(item);
+ item = PyList_GetItemRef(obj, total);
+ if (item == NULL) {
+ _PyErr_FormatNote("when serializing %T item %zd", obj, total);
+ return -1;
+ }
int err = save(state, self, item, 0);
Py_DECREF(item);
if (err < 0) {
}
if (_Pickler_Write(self, &appends_op, 1) < 0)
return -1;
+ if (PyList_GET_SIZE(obj) != list_size) {
+ PyErr_Format(
+ PyExc_RuntimeError,
+ "list changed size during iteration");
+ return -1;
+ }
- } while (total < PyList_GET_SIZE(obj));
+ } while (total < list_size);
return 0;
}