python / python/cpython

Free-threaded CPython 3.14: concurrent functools.partial.__setstate__ crashes with repr(), calls, or another __setstate__

未關閉
#157,841 0 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

還沒有人認領這個 Issue。

extension-modules topic-free-threading type-crash
主要語言
Python
星號
77.2k
分支
36k
PR 合併指標
PR 指標待擷取

描述

Prior report

gh-157099 (2026-09-07) reported the same missing synchronization in partial_setstate, reaching it through partial_new (a crash in tuple_concat under ASan, 3.14.7 @ 823f032). It was closed by its reporter after a request to consolidate a batch of reports (gh-157124), and the consolidated issues gh-157196 / gh-157197 / gh-157198 were then withdrawn by the reporter on 2026-09-11 to be resubmitted under the security policy. As far as I can see nothing tracks it now, and main @ 89c67a9 still crashes. This report was found independently (static analysis of partial_repr on 3.14.0rc1, then runtime confirmation) and adds a sanitizer-free 25-line reproducer covering repr(), calls and writer/writer, with a GIL control on the same binary. If you would rather reopen gh-157099, please close this one as a duplicate. Filed on the public tracker because triggering it requires running Python code in the process (the assessment given on gh-157196).

What happened?

On the free-threaded build, calling partial.__setstate__ on a functools.partial object while another thread uses the same object (repr(p) or p()), or while a second thread also calls __setstate__, crashes the interpreter within seconds. The read-only control completed all five four-second runs without a crash. The same binary with PYTHON_GIL=1 completed every run without a crash.

Standalone reproducer (repro_partial.py <mode>):

import functools, itertools, os, sys, threading
if os.environ.get("ALLOW_GIL") != "1":
    assert not sys._is_gil_enabled()
mode = sys.argv[1]
counter = itertools.count(10**12)          # fresh heap ints, never cached
def fresh(): return next(counter)
def f(*a, **k): return None
p = functools.partial(f, fresh(), fresh())
stop = threading.Event()
def reader_repr(): repr(p)
def reader_call(): p()
def writer(): p.__setstate__((f, (fresh(), fresh()), {}, None))
reader, nr, nw = {"ro": (reader_repr, 4, 0), "rw": (reader_repr, 4, 1),
                  "ww": (None, 0, 2), "cw": (reader_call, 4, 1)}[mode]
def loop(fn):
    while not stop.is_set():
        fn()
ts = [threading.Thread(target=loop, args=(reader,)) for _ in range(nr)] + \
     [threading.Thread(target=loop, args=(writer,)) for _ in range(nw)]
for t in ts: t.start()
stop.wait(4); stop.set()
for t in ts: t.join()
print("ok")

Results, 5 runs × 4 s per mode, run with -X faulthandler ("hang" = the process had not exited 30 s after start and was killed):

mode threads 3.14.6t 3.14.6t, PYTHONMALLOC=debug same binary, PYTHON_GIL=1 main @ 89c67a9 (--disable-gil) main, PYTHONMALLOC=debug
ro 4 × repr(p) 5/5 ok 5/5 ok 5/5 ok 5/5 ok
rw 4 × repr(p) + 1 × __setstate__ 0/5 (SIGILL, SIGABRT, 3 hangs) 0/5 (SIGSEGV ×5) 3/3 ok 0/5 (SIGABRT ×2, SIGILL, 2 hangs) 0/5 (SIGSEGV ×5)
ww 2 × __setstate__ 4/5 (SIGSEGV ×1) 1/5 (SIGSEGV ×4) 3/3 ok 2/5 (SIGSEGV ×3) 4/5 (SIGSEGV ×1)
cw 4 × p() + 1 × __setstate__ 0/5 (SIGILL ×2, SIGTRAP ×2, SIGSEGV) 0/5 (SIGSEGV ×5) 3/3 ok 0/5 (SIGSEGV ×3, SIGILL, 1 hang) 0/5 (SIGSEGV ×5)

ww is intermittent: over several runs of this script today it crashed in 1 to 4 of 5 runs per configuration. rw and cw failed in every run of every free-threaded configuration. The SIGABRT cases are

Fatal Python error: PyMutex_Unlock: unlocking mutex that is not locked

raised from the repr thread (faulthandler shows reader_repr). The repr/__setstate__ shape also crashes on 3.14.0rc1 free-threaded (5/5, PYTHONMALLOC=debug). Note that main already carries the partial_vectorcall snapshot from gh-154189, and cw still crashes there.

Analysis

partial_setstate replaces pto->fn, pto->args and pto->kw with Py_SETREF and holds no critical section on the partial object (3.13 branch Modules/_functoolsmodule.c:514; 3.14 branch :762; main :857). The readers take strong references from the fields — partial_repr since gh-144475 (3.14 branch :630), partial_vectorcall since gh-154189 (main only; not in 3.14). The source suggests an unsynchronized reference-acquisition gap: Py_NewRef(pto->args) does not protect the interval between loading the field and acquiring a strong reference while another thread replaces it. The observed crashes have not yet been attributed to a particular native access. Two concurrent __setstate__ writers also crash without a reader thread; the failing access in that case has not yet been identified. gh-144475 and gh-154189 addressed the re-entrant, same-thread shape of this; gh-157099 and this report are the cross-thread shape.

Expected behaviour

I expect concurrent use to avoid interpreter crashes; no particular ordering or consistent result is required by this reproducer. The free-threading HOWTO says the build "aims to provide similar thread-safety behavior at the Python level to the default GIL-enabled build" while noting that this "should be treated as a description of the current implementation, not a guarantee"; the same binary with the GIL enabled runs the reproducer without a crash.

Possible approach

A possible approach is to synchronize state publication and acquisition of coherent reader snapshots on the partial object, including associated placeholder and vectorcall state; the complete access and reentrancy audit remains to be done. Happy to test a patch.

CPython versions tested on

3.14.6 (free-threaded, python-build-standalone via uv), 3.14.0rc1 (free-threaded), CPython main branch @ 89c67a9 (2026-09-20, local --disable-gil build; Modules/_functoolsmodule.c identical to upstream at that commit)

Operating systems tested on

macOS (arm64, Darwin 25.6)

Output from running 'python -VV' on the command line
Python 3.14.6 free-threading build (main, Jul 18 2026, 17:07:52) [Clang 22.1.3 ]
Python 3.16.0a0 free-threading build (remotes/origin/HEAD-dirty:89c67a9, Sep 20 2026, 14:12:46) [Clang 21.0.0 (clang-2100.3.34.2)]
Related

gh-157099 (same root cause, closed not_planned), gh-144475, gh-154189, gh-140590

貢獻指南

開啟貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

研究方向

從 repro_partial.py 中的獨立重現開始,在 free-threaded 建置上執行其 ro、rw、ww 和 cw 模式,並使用 PYTHON_GIL=1 作為對照。接著檢查 Modules/_functoolsmodule.c 中的 partial_setstate、partial_repr 和 partial_vectorcall,包括報告中引用的相關變更。當並行使用不再導致直譯器崩潰,同時維持一致的狀態存取時,即視為完成。

由索引模型根據 Issue 內容生成。

評估

技術堆疊
python
領域
operating-systems
Issue 類型
缺陷
難度
5/5
預估耗時
一週以上
活躍度
活躍
描述清晰度
基本清楚
新手友好度
35/100

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。