python / python/cpython

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

Ouverte
#148,874 3 commentaires 3 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

3.14 3.15 interpreter-core type-bug
Langage dominant
Python
Étoiles
77.2k
Forks
35.9k
Métriques de merge des PR
Métriques de PR en attente

Description

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

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez par les séquences de bytecode with X: signalées et le reproducteur basé sur les signaux dans l’issue, puis examinez les PR liés gh-149019, gh-150911 et gh-154830 afin de comprendre le travail déjà en cours. C’est terminé lorsqu’un signal déclenché après le retour de __enter__ ne peut pas contourner __exit__ et que la ressource ne reste pas acquise.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
python
Domaine
compilers
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
À l'abandon
Clarté
Plutôt claire
Accessibilité débutants
25/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.