python / python/cpython

concurrent.interpreters: Interpreter.call() crashes when the result fails to unpickle in the caller

オープン
#151,892 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

3.14 3.15 3.16 extension-modules topic-subinterpreters type-bug type-crash
主要言語
Python
スター
77.2k
フォーク
35.9k
PR マージ指標
PR 指標を取得中

説明

When Interpreter.call() returns a value, that value is pickled in the callee interpreter and reconstructed (unpickled) in the calling interpreter on the way out of the cross-interpreter session, inside _PyXI_Exit() (Python/crossinterp.c). If the reconstruction step raises — e.g. the returned object's __reduce__ produces a callable that raises during unpickling — the raised exception is left set while _PyXI_Exit() applies its "preserve failure" handling. On a debug build this trips assert(!_PyErr_Occurred(tstate)) and aborts the process (SIGABRT); on a release build the stale exception is clobbered and the resulting error state is wrong.

Reproduction (debug build)
import sys, os, tempfile, importlib
from textwrap import dedent
from concurrent import interpreters

d = tempfile.mkdtemp(); sys.path.insert(0, d)
with open(os.path.join(d, "b.py"), "w") as f:
    f.write(dedent('''
        def _boom():
            raise ValueError("unpickling failed")
        class Bomb:
            def __reduce__(self):
                return (_boom, ())
        def mk():
            return Bomb()
    '''))
m = importlib.import_module("b")
ip = interpreters.create()
ip.prepare_main(_d=d)
ip.exec("import sys; sys.path.insert(0, _d); import b")
ip.call(m.mk)   # debug build: Assertion failed in _PyXI_Exit -> SIGABRT

This affects only the call() path (and any other _PyXI_Enter/_PyXI_Exit-bracketed result-preserve path). Queue/channel get()/recv() and exec() do not go through this path and are unaffected. Since call() runs the caller's own code in the subinterpreter, this is a robustness / crash bug, not a security issue.

Expected: call() should raise a normal Python exception instead of aborting.

Fix

Have the reconstruction-failure branch consume the live exception and propagate it as the failure, mirroring how the active-session _pop_preserved path already routes errors through capture_session_error. With the fix, call() raises NotShareableError with the underlying error reachable as the cause. A self-contained regression test is added in Lib/test/test_interpreters/test_api.py.

Affected versions

main only. The result-preserve machinery (_PyXI_Preserve / _finish_preserved, gh-133484) and the xi_error_* error-handling rewrite (gh-135369) that this touches both landed after the 3.14.0b1 freeze, so the affected code path does not exist on 3.14 or earlier.

Related

A separate, distinct crossinterp NULL dereference on the queue/channel receive path is tracked in #151862.

Linked PR

A PR follows.

Linked PRs
  • gh-151893

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

Python/crossinterp.c の _PyXI_Exit() から始め、再構築失敗の処理をアクティブセッションの _pop_preserved パスおよび capture_session_error と比較します。次に Lib/test/test_interpreters/test_api.py のリグレッションテストを読み、そのテストを実行します。call() が中断する代わりに、基になるエラーを原因として NotShareableError を送出すれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
c, python
領域
compilers
issue の種類
バグ
難易度
3/5
見積もり時間
1〜2日
活発さ
停滞
明瞭さ
明確に書かれている
初心者へのやさしさ
25/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。