python / python/cpython

`with lock`: can skip `__exit__` when a signal handler raises right after `__enter__`

Offen
#148,874 3 Kommentare 3 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

3.14 3.15 interpreter-core type-bug
Vorherrschende Sprache
Python
Sterne
77.2k
Forks
35.9k
PR-Merge-Kennzahlen
PR-Kennzahlen ausstehend

Beschreibung

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

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit den gemeldeten with X:-Bytecode-Sequenzen und dem signalbasierten Reproducer im Issue und untersuche anschließend die verknüpften PRs gh-149019, gh-150911 und gh-154830, um die bereits laufenden Arbeiten zu verstehen. Als abgeschlossen gilt, wenn ein Signal, das nach der Rückkehr von __enter__ ausgelöst wird, __exit__ nicht umgehen kann und die Ressource nicht erworben bleibt.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
compilers
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Veraltet
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
25/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.