Skip to content

Segfault in _pickle.c:batch_list_exact when pickling list with concurrent mutation (free-threading) #146533

@overlorde

Description

@overlorde

Bug report

Bug description

Same class of bug as gh-146452 but in the list pickling path. batch_list_exact() uses PyList_GET_ITEM() which returns a borrowed reference, and a concurrent list.clear() or mutation can free the backing array before Py_INCREF gets to it.

Found while working on the dict fix in gh-146452 — checked the list path and it has the same pattern.

Reproducer

import pickle
import threading

shared = list(range(200))

for trial in range(2000):
    barrier = threading.Barrier(4)

    def dumper():
        try:
            barrier.wait()
            pickle.dumps(shared)
        except Exception:
            pass

    def mutator():
        try:
            barrier.wait()
            for j in range(500):
                shared.append([j] * 20)
                if len(shared) > 100:
                    shared.clear()
                    shared.extend(range(50))
        except Exception:
            pass

    threads = [
        threading.Thread(target=dumper),
        threading.Thread(target=dumper),
        threading.Thread(target=mutator),
        threading.Thread(target=mutator),
    ]
    for t in threads: t.start()
    for t in threads: t.join()

Tested on

3.14.0a4 free-threading (stock install, no sanitizers):

$ python3.14t reproducer.py
Segmentation fault (core dumped)

3.15.0a7+ free-threading (built with --with-address-sanitizer):

==2034815==ERROR: AddressSanitizer: SEGV on unknown address 0x00000000000c
    #0 _Py_atomic_load_uint32_relaxed Include/cpython/pyatomic_gcc.h:367
    #1 Py_INCREF Include/refcount.h:267
    #2 batch_list_exact Modules/_pickle.c:3213
    #3 save_list Modules/_pickle.c:3268
    #4 save Modules/_pickle.c:4595
    #5 dump Modules/_pickle.c:4768
    #6 _pickle_dumps_impl Modules/_pickle.c:7992

Root cause

batch_list_exact() iterates list items using PyList_GET_ITEM() (borrowed ref) at lines 3196 and 3212. A concurrent list mutation can invalidate the borrowed reference before Py_INCREF. Same pattern as the dict issue in gh-146452.

Possible fix

Same helper function approach as gh-146452 — wrap batch_list_exact in Py_BEGIN_CRITICAL_SECTION(obj) / Py_END_CRITICAL_SECTION(). Happy to send a PR.

CPython versions tested on

3.14, CPython main branch

Operating systems tested on

Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions