From aa3256ada3eb8b292e9d6ed03e34ac884176b04d Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:25:39 +0300 Subject: [PATCH] Fix _Once.ensure() to propagate handshake failure to concurrent waiters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When two tasks use an SSLStream concurrently (one sending, one receiving), both call _Once.ensure() to trigger the lazy handshake. If the first task starts the handshake and it fails (e.g. certificate error, connection reset), the exception propagates to that task but _done is never set. The second task, already waiting on _done.wait(), blocks indefinitely — the Event is never signalled and started is permanently True, so there is no recovery path. Store the failure exception and set _done even on error, so that all waiters wake up and receive a BrokenResourceError chained from the original handshake exception. --- src/trio/_ssl.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/trio/_ssl.py b/src/trio/_ssl.py index 52c5137ea1..a5ade68b46 100644 --- a/src/trio/_ssl.py +++ b/src/trio/_ssl.py @@ -223,7 +223,7 @@ class NeedHandshakeError(Exception): class _Once: - __slots__ = ("_afn", "_args", "_done", "started") + __slots__ = ("_afn", "_args", "_done", "_failure", "started") def __init__( self, @@ -234,16 +234,26 @@ def __init__( self._args = args self.started = False self._done = _sync.Event() + self._failure: BaseException | None = None async def ensure(self, *, checkpoint: bool) -> None: if not self.started: self.started = True - await self._afn(*self._args) + try: + await self._afn(*self._args) + except BaseException as exc: + self._failure = exc + self._done.set() + raise self._done.set() elif not checkpoint and self._done.is_set(): + if self._failure is not None: + raise trio.BrokenResourceError from self._failure return else: await self._done.wait() + if self._failure is not None: + raise trio.BrokenResourceError from self._failure @property def done(self) -> bool: