`with lock`: can skip `__exit__` when a signal handler raises right after `__enter__`
還沒有人認領這個 Issue。
- 主要語言
- Python
- 星號
- 77.2k
- 分支
- 36k
- PR 合併指標
- PR 指標待擷取
描述
Bug report
A with X: statement can return without calling X.__exit__ when a Python signal handler raises between __enter__ returning and the body starting.
This leaks whatever resource __enter__ acquired (e.g. a threading.Lock stays locked forever).
Cause
In 3.14, the following changed how with X: compiles:
- GH-120507
Before 3.14:
LOAD X
BEFORE_WITH # single op: calls enter, sets up exit on stack
POP_TOP # <- exception table starts here
... body ...
BEFORE_WITH has no periodic/eval-breaker check, so there was no window for a signal handler to fire between __enter__ returning and the with-statement's exception handler being established.
From 3.14 onward:
LOAD X
COPY 1
LOAD_SPECIAL exit
SWAP 2 / SWAP 3
LOAD_SPECIAL enter
CALL 0 # ends with _CHECK_PERIODIC_AT_END
POP_TOP # <- exception table starts here
... body ...
CALL includes _CHECK_PERIODIC_AT_END, which runs the Python signal handler. If that handler raises, JUMP_TO_LABEL(error) fires with frame->instr_ptr still pointing at CALL (set before the micro-ops run).
Reproducer (via Claude Code)
test_with_signal_leak.py
import ctypes, ctypes.util, signal, sys, threading
# signal.pthread_kill / os.kill both call PyErr_CheckSignals after the syscall,
# which perturbs timing enough to suppress the race. Use libc.pthread_kill
# directly.
_pthread_kill = ctypes.CDLL(ctypes.util.find_library("c")).pthread_kill
_pthread_kill.argtypes = [ctypes.c_ulong, ctypes.c_int]
_pthread_kill.restype = ctypes.c_int
_MAIN_TID = threading.get_ident()
def _handler(signum, frame):
raise RuntimeError("signal")
def _send():
_pthread_kill(_MAIN_TID, signal.SIGUSR1)
def run_trial(lock, iterations=200):
t = threading.Thread(target=_send)
t.start()
try:
for _ in range(iterations):
with lock:
pass
except BaseException:
pass
try:
t.join()
except BaseException:
pass
if lock.locked():
lock.release()
return True
return False
signal.signal(signal.SIGUSR1, _handler)
lock = threading.Lock()
leaked = 0
for _ in range(2000):
try:
if run_trial(lock):
leaked += 1
except BaseException:
if lock.locked():
lock.release()
print(f"leaked={leaked}/2000")
cc @markshannon
Linked PRs
- gh-149019
- gh-150911
- gh-154830
貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
研究方向
從 issue 中回報的 with X: 位元組碼序列和以訊號為基礎的重現程式開始,然後檢查相關的 PR gh-149019、gh-150911 和 gh-154830,以了解已在進行的工作。完成的標準是:在 __enter__ 返回後引發的訊號無法繞過 __exit__,而且資源不會處於已取得狀態。
由索引模型根據 Issue 內容生成。
評估
- 技術堆疊
- python
- 領域
- compilers
- Issue 類型
- 缺陷
- 難度
- 4/5
- 預估耗時
- 3-5 天
- 活躍度
- 停滯
- 描述清晰度
- 基本清楚
- 新手友好度
- 25/100