← 输入框|github.com|刷新
Skip to content

gh-159875: Keep the SSL protocol of a closed transport after connection loss - #160069

Open
cornopaez wants to merge 3 commits into
python:mainfrom
cornopaez:sendfile-ssl-restore-after-close
Open

cornopaez wants to merge 3 commits into
python:mainfrom
cornopaez:sendfile-ssl-restore-after-close

Conversation

@cornopaez

Copy link
Copy Markdown

When an SSL connection is lost and the application then closes the transport
(typically from its protocol's connection_lost(), as aiohttp does),
_SSLProtocolTransport.close() took the call for a second close and dropped
_ssl_protocol, leaving a transport whose write(), set_protocol(),
get_write_buffer_size(), ... raise AttributeError. loop.sendfile()'s
fallback meets this in restore() when the peer goes away mid-transfer.
Reported in gh-159875.

This implements the fix proposed in the issue (Proposed fix): close() no longer drops the
protocol, and the TLS state that branch was releasing (_sslobj,
_sslcontext, and the ssl_object/sslcontext extra info) is released by
SSLProtocol.connection_lost() instead. It is released one loop iteration
later when the application's connection_lost() has been scheduled, so that
the application can still read get_extra_info('ssl_object') and
('sslcontext') there, as it can today.

Simply removing the else branch fixes the AttributeError but keeps the
SSLContext alive for as long as the application holds the transport; the
issue's What the else branch is for and Alternatives considered
compare the other designs.

Tests

  • test_sslproto: test_transport_usable_after_connection_lost_and_close,
    the transport after connection_lost() then close().
  • test_sendfile: test_sendfile_ssl_close_peer_in_the_middle_closes_transport,
    the fallback with a protocol that closes its transport on connection loss.
    It reads the file synchronously so that the fallback is waiting in
    drain() when the connection is lost; with the executor read, the race
    is lost only occasionally and the test would mostly pass on main.
  • test_ssl: test_create_connection_memory_leak now keeps the transport
    past its check, which its comment says it covers. With the else branch
    merely removed it fails, 3 of 3 runs; on main and with this change it
    passes.

On macOS (kqueue, poll and select loops), the sendfile test fails in 60 of 60
runs on main with AttributeError from set_protocol(), and passes in 60
of 60 with this change. test_asyncio and test_ssl pass. I have not run
it on Windows or Linux; @junaidkhan80121 reported the related test_asyncio
cases passing on a Windows build
in the issue.

AI disclosure

Prepared with Claude Opus 5.5 (Anthropic), as in the issue: it wrote the
tests and the change, and ran them on my machine. I reviewed them and am
responsible for them.

🤖 Generated with Claude Code

cornopaez and others added 2 commits October 21, 2026 11:43
…on loss

An application that closes its transport from connection_lost(), as aiohttp
does, must be left with a transport whose write(), get_write_buffer_size()
and set_protocol() behave as after closing a live connection, and
loop.sendfile()'s fallback must then raise ConnectionError rather than
AttributeError from set_protocol().

The sendfile test reads the file synchronously, so that the fallback is
waiting in drain() when the connection is lost; a read in the executor that
completes in the same loop iteration as the reset lets the fallback finish
first and hides the bug.

test_create_connection_memory_leak now keeps the transport past its check,
as its comment says it covers, so that it would see an SSLContext kept alive
by a transport the application still holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nnection 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>
@python-cla-bot

python-cla-bot Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@cornopaez

Copy link
Copy Markdown
Author

The one failing job, Windows / Build and test (Win32), failed in
test_shutdown_corrupted_ssl_sends_close_notify on SelectorStartTLSTests.
The server thread's sock.unwrap() got ConnectionResetError: [WinError 10054], both in the main run and in the re-run. That test is not one this PR
adds or changes. Its traceback is the one in gh-158646, reported on two
unrelated PRs on 3 and 4 October. The other four Windows jobs passed.

Could someone with the rights re-run that job?

AI disclosure: Claude Opus 5.5 (Anthropic) read the job log and matched it
to gh-158646; I checked it and am responsible for the comment.

@Mariatta Mariatta added the AI-needs-explanation AI-assisted work that needs further explanation and follow up from the contributor label Oct 30, 2026
@bedevere-app

bedevere-app Bot commented Oct 30, 2026

Copy link
Copy Markdown

Thank you for your interest in contributing to Python. This is a community-driven project, and we want it to be a welcoming place to learn and collaborate.

Contributors are responsible for reviewing and understanding what they submit, checking its claims, and being able to explain changes in their own words. This helps reviewers focus on the contribution and leaves more time to support other contributors.

This contribution appears to have been authored with AI assistance. Please explain your contribution in your own words and describe how you checked its correctness.

Please read our guidelines for using AI tools.

@cornopaez

Copy link
Copy Markdown
Author

@Mariatta Thank you for reviewing my submission. Here's my own-words explanation of the issue and the proposed fix. But first, a disclaimer.

I am a regular Python user, as my day job is data analytics and data engineering, and my comfort zone is well downstream from CPython. In other words, it means I know how to leverage the language to solve analytical problems but the inner workings are opaque to me. This issue and proposed fix were discovered on a personal project that's not related to data analytics/engineering -- a project that deals with serving media to clients -- which is on-topic for SSL connection handling.

The way I understand the issue is this: when a client unexpectedly drops an SSL connection, the inner workings of CPython are dropping -- i.e. turning into None -- the entire _ssl_protocol object before the connection clean-up code has had a chance to run. This causes the cleanup code to find nothing leading to an exception that is not helpful to consumers. This is not an issue for non-SSL connections, which is likely why the issue has gone unnoticed for a while. The proposed fix suggests CPython revise the connection life cycle a bit and use an already existing function to treat unexpected connection drops as closed connections, so all the connection clean-up code can do what it does.

I detailed, with the help of AI, the findings here and was hoping for guidance and confirmation that my findings were, indeed, correct and not hallucinated. However, the evidence AI found seems to point to two important things: the finding appears valid and there's evidence of it happening as far back as Python 3.8 (based on issues raised in other projects); lastly, there's no clean way to fix this without changing a lot of things -- update lots of functions to deal with the None and ensure any new functions on the library do the same -- or change the lifecycle of the object slightly so it's more resilient to unexpected client behavior. I chose the latter approach as more sustainable and less intrusive.

As far as I can tell, the proposed solution makes sense to me, but my disclaimer then takes on a load-bearing role: that it makes sense to me does not mean that it is value added for the project. I understand if the mods close this PR due to not meeting AI guidelines -- the same way my initial issue was closed -- as I can compensate for this bug on my end without issues. However, I would much prefer a maintainer with knowledge in this domain take the time to review what the PR says and confirm whether or not it is added value and the finding is correct. This is the heart of collaborative work: communication and discussion. If it turns out to be valid and provides values, it will benefit the community at large and solve edge cases in lots of downstream projects. If not, there's no issue and I won't take it personally.

I'm happy to connect and work through any concerns anyone has.

@picnixz

picnixz commented Oct 30, 2026

Copy link
Copy Markdown
Member

I think a recent issue was opened for that specifically in asyncio: #160964

@cornopaez

cornopaez commented Oct 30, 2026 •

Copy link
Copy Markdown
Author

@picnixz Thank you for the assist! It is indeed the same issue I'm describing, except there's my evidence points to 3.8+ being affected instead of 3.11+.

It's nice to have someone independently arrive at the same conclusion.

EDIT: I misremembered the evidence for 3.8+ I had found. The error in those versions was similar in nature but did not stem from the same artifacts. To this end, please ignore the 3.8+ claim above, as the artifacts in question appeared in 3.11+ as stated by other contributors.

@asvetlov

Copy link
Copy Markdown
Contributor

@cornopaez Could you resolve merge conflicts?

Conflicts with pythongh-160513 (connection_lost() no longer skips the app
protocol during a start_tls() handshake) in SSLProtocol.connection_lost(),
and with pythongh-160499's tests in test_sslproto; both sides kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cornopaez

Copy link
Copy Markdown
Author

@cornopaez Could you resolve merge conflicts?

Merge conflicts fixed and a new force push has been sent; just waiting on the CI pipeline to complete as of 10:22am EDT.

I appreciate you looking at the proposal, even if you had your own fix lined up. I don't mind which fix goes in as they appear to do the same thing. However, one thing of note: both proposal differ in design but not correctness. Whichever you choose for a merge is OK with me, as the design decision is best left to a regular contributor instead of a newb like me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-needs-explanation AI-assisted work that needs further explanation and follow up from the contributor awaiting review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants