python / python/cpython

On macOS, `socket.close()` after `shutdown(SHUT_RDWR)` does not reliably interrupt a blocked `recv()` with a timeout

Ouverte
#154,224 6 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

stdlib topic-socket 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 description:

Thread A is blocked in sock.recv() with a timeout set (sock.settimeout(...)).
Thread B closes the socket by calling sock.shutdown(socket.SHUT_RDWR) immediately followed by sock.close()

In this situation, sometimes thread A's recv() call is interrupted promptly, and sometimes it is not interrupted at all: it blocks for the full timeout duration and only then raises TimeoutError, as if shutdown()/close() had no effect on the already-blocked call.

The flakiness is easy to reproduce with the script below, if you have a Mac.

I have not observed this issue when:

  • recv() has no timeout set
  • The socket's peer closes its end — but the whole point is I want to close the socket quickly, without depending on the peer's behavior.
  • A small delay i.e. time.sleep(0.001)is inserted between sock.shutdown(socket.SHUT_RDWR) and sock.close() (but time.sleep(0) is not enough)

Discovered while investigating https://github.com/python-websockets/websockets/issues/1596.

Reproduction
import socket
import threading
import time

RECV_TIMEOUT = 1  # seconds
TRIALS = 50


def run_trial():
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listener.bind(("127.0.0.1", 0))
    listener.listen(1)
    a = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    a.connect(listener.getsockname())
    b, _ = listener.accept()
    listener.close()

    a.settimeout(RECV_TIMEOUT)
    result = {}

    def blocked_recv():
        t0 = time.monotonic()
        try:
            data = a.recv(4096)
            result["outcome"] = f"returned {data!r}"
        except OSError as exc:
            result["outcome"] = f"raised {exc!r}"
        result["elapsed"] = time.monotonic() - t0

    thread = threading.Thread(target=blocked_recv)
    thread.start()
    time.sleep(0.15)  # let the thread actually enter the recv() syscall

    a.shutdown(socket.SHUT_RDWR)
    a.close()

    thread.join()
    b.close()

    return result["elapsed"], result["outcome"]


if __name__ == "__main__":
    hangs = 0
    for i in range(TRIALS):
        elapsed, outcome = run_trial()
        hung = elapsed >= RECV_TIMEOUT * 0.5
        hangs += hung
        print(f"trial {i}: {outcome} after {elapsed:.3f}s {'HUNG' if hung else ''}")
    print(f"\n{hangs}/{TRIALS} trials failed to interrupt recv() promptly")
Expected result

recv() is interrupted within a few milliseconds in every trial, since the socket has been explicitly shut down and closed before the timeout can elapse.

Actual result

A representative run:

27/50 trials failed to interrupt recv() promptly

Individual "HUNG" trials show recv() raising TimeoutError('timed out') after the full RECV_TIMEOUT (1.0s), rather than being interrupted immediately after shutdown()/close() run (~0.15s in, as seen in the non-hung trials).

CPython versions tested on:

3.14

Specifically Python 3.14.4 (main, Apr 18 2026, 07:57:29) [Clang 17.0.0]

Operating systems tested on:

macOS

Specifically macOS 26.5.2 (Darwin 25.5.0), arm64

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 exécuter la reproduction fournie sur macOS et suivre les points d’entrée socket.recv(), socket.shutdown() et socket.close() dans CPython ; l’issue ne mentionne aucun fichier source ni test de régression. C’est terminé lorsque le recv() bloqué est interrompu rapidement et de manière fiable après shutdown() suivi de close(), avec une couverture de tests pour le comportement signalé.

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

Évaluation

Stack technique
macos, python
Domaine
networking, operating-systems
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
Active
Clarté
Plutôt claire
Accessibilité débutants
52/100

Recevez les nouvelles issues par e-mail

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