Skip to content

Commit ff0cca0

Browse files
cornopaezclaude
andcommitted
gh-159875: Keep the SSL protocol of a closed transport after connection loss
_SSLProtocolTransport.close() dropped its _ssl_protocol when called a second time, and the transport is already marked closed once the connection is lost, so an application closing it from connection_lost() left a transport whose write(), set_protocol(), get_write_buffer_size(), ... raise AttributeError. That branch was also what released the SSLContext of a transport the application keeps. Keep the protocol, and release the TLS state from SSLProtocol.connection_lost() instead: one loop iteration later when the application's connection_lost() was scheduled, so that it can still read get_extra_info('ssl_object') and ('sslcontext'). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 455f39e commit ff0cca0

2 files changed

Lines changed: 24 additions & 2 deletions

File tree

‎Lib/asyncio/sslproto.py‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,6 @@ def close(self):
114114
if not self._closed:
115115
self._closed = True
116116
self._ssl_protocol._start_shutdown()
117-
else:
118-
self._ssl_protocol = None
119117

120118
def __del__(self, _warnings=warnings):
121119
if not self._closed:
@@ -406,13 +404,15 @@ def connection_lost(self, exc):
406404
if self._app_transport is not None:
407405
self._app_transport._closed = True
408406

407+
app_notified = False
409408
if self._state != SSLProtocolState.DO_HANDSHAKE:
410409
if (
411410
self._app_state == AppProtocolState.STATE_CON_MADE or
412411
self._app_state == AppProtocolState.STATE_EOF
413412
):
414413
self._app_state = AppProtocolState.STATE_CON_LOST
415414
self._loop.call_soon(self._app_protocol.connection_lost, exc)
415+
app_notified = True
416416
self._set_state(SSLProtocolState.UNWRAPPED)
417417
self._transport = None
418418
self._app_transport = None
@@ -425,6 +425,21 @@ def connection_lost(self, exc):
425425
if self._handshake_timeout_handle:
426426
self._handshake_timeout_handle.cancel()
427427
self._handshake_timeout_handle = None
428+
if app_notified:
429+
# After the app protocol's connection_lost() (scheduled above),
430+
# so that it can still read the TLS extra info.
431+
self._loop.call_soon(self._release_tls_state)
432+
else:
433+
self._release_tls_state()
434+
435+
def _release_tls_state(self):
436+
# The connection is gone: drop everything that refers to the
437+
# SSLContext, so that an application holding the transport does
438+
# not keep the context alive.
439+
self._sslobj = None
440+
self._sslcontext = None
441+
self._extra.pop('sslcontext', None)
442+
self._extra.pop('ssl_object', None)
428443

429444
def get_buffer(self, n):
430445
want = n
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fix :exc:`AttributeError` from the methods of an :mod:`asyncio` SSL transport,
2+
such as :meth:`~asyncio.WriteTransport.write` and
3+
:meth:`~asyncio.BaseTransport.set_protocol`, when the transport is closed after
4+
its connection was lost. This also affected the fallback of
5+
:meth:`loop.sendfile() <asyncio.loop.sendfile>` over SSL when the peer went
6+
away mid-transfer. The SSL context is still released once the connection is
7+
lost.

0 commit comments

Comments
 (0)