Repository navigation
gh-103443: Make the TLS shutdown of the FTP data connection best-effort - #158958
Open
AliReza7222 wants to merge 2 commits into
Open
AliReza7222 wants to merge 2 commits into
AliReza7222 wants to merge 2 commits into
Conversation
Documentation build overview
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
gh-103443:
ftplib: make the TLS shutdown of the data connection best-effortFixes: gh-77303 ·
gh-78738 ·
gh-103443 ·
gh-124850
All four are open, labelled
type-bug, and have zero linked PRs.Suggested branch name:
gh-103443-ftplib-data-shutdownSuggested NEWS file:
Misc/NEWS.d/next/Library/2026-10-06-12-00-00.gh-issue-103443.<6-char>.rstSummary
retrbinary(),retrlines(),storbinary()andstorlines()end theirwith self.transfercmd(...) as conn:block withThat runs after the payload has been fully exchanged, on a socket which
exists for exactly one transfer and is closed a few lines later anyway.
Servers are under no obligation to complete a TLS shutdown handshake there,
and a large number of them do not. The result is that a transfer which
actually succeeded is reported as a failure:
ConnectionResetError: [Errno 104] Connection reset by peerout ofunwrap()ssl.SSLEOFError: EOF occurred in violation of protocolunwrap()blocks waiting for aclose_notifythat never comesIn every case the error or the hang happens after
sendall()/recv()hasfinished. The bytes are already on the wire; there is nothing left to
report.
Why not simply delete
unwrap()?That is the obvious fix and it is wrong.
unwrap()does two things: itflushes the record layer and it sends
close_notify, which is how the peerlearns that the transfer is over rather than merely stalled.
Measured with CPython's own
Lib/test/test_ftplib.pydummy server(
DummyTLS_FTPServer),TestTLS_FTPClassMixin, on CPython 3.13.3, threeruns each:
Lib/ftplib.pyunwrap())Ran 96 testsunwrap()deleted everywheretest_storlines:AssertionError: 14739 != 1700014739 of 17000 bytes — a silently truncated upload. Without
close_notifythe peer stops draining the socket while data is still inflight. Any fix that keeps
unwrap()(this PR) or that changes how therecord layer is flushed needs to be re-checked against that measurement;
simply removing the call is a data-loss regression.
Proposed fix
Keep the handshake, bound it, and never let it escape:
and each of the four methods calls it in place of
conn.unwrap():_shutdown_data_channellives in theelse:branch of thetry: import sslblock, exactly like_SSLSocketandFTP_TLSdo, soimport ftplibstill works whensslis unavailable. The four call sitesare inside class
FTP, which is defined before that block — the name isresolved at call time, which is how
_SSLSocketalready works today.Full patch:
patch.diff—1 file changed, 31 insertions(+), 8 deletions(-).Design notes
settimeout()beforeunwrap()is what fixes the hang. Verifiedempirically against a server that accepts the handshake and then never
sends
close_notify:unwrap()outcomeNone(blocking)2.0TimeoutErrorafter2.00 s1.0TimeoutErrorafter1.00 s0.5TimeoutErrorafter0.50 sTimeoutErroris anOSErrorsubclass, so theexceptswallows it. Thetimeout is deliberately not restored: the socket is closed unconditionally
a few lines later by the
withstatement.The
exceptis(OSError, ValueError), not a bareexcept. Itcovers
ConnectionResetError,SSLEOFError,TimeoutError,ssl.SSLError(allOSError) plus theValueErrorthatunwrap()raises if called twice. Programming errors still propagate.
unwrap()is still attempted. See the table above — removing ittruncates data.
FTP_TLS.ccc()keeps its unconditionalself.sock = self.sock.unwrap().That is the control connection, which is long-lived and whose shutdown
the caller explicitly asked for. Only the per-transfer data channel is
made best-effort.
_DATA_SHUTDOWN_TIMEOUTis read at call time (thetimeout=Nonesentinel), not baked in as a default argument, so it can be adjusted —
and monkeypatched by tests — after import.
5 seconds only ever costs time against a peer that is already broken;
against a healthy peer
unwrap()completes in the time it takes to sendone TLS record.
Alternatives considered
unwrap()blocks. — Rejected; truncates data(measured above).
try: conn.unwrap() except ssl.SSLError: passwithout a timeout. —Fixes ftplib: FTP_TLS seems to have problems with sites that close the encrypted channel themselfes #77303 / ftplib: FTP_TLS storbinary is hanging/never completing #103443 / ftplib storbinary #124850 but not When sending binary file to a Microsoft FTP server over FTP TLS, the SSL unwind method hangs #78738: a silent peer
still hangs the caller forever.
conn.shutdown(socket.SHUT_WR)instead ofunwrap(). — Emits a TCPFIN without
close_notify, i.e. exactly the ragged EOF that causes thetruncation above. Rejected for the same reason as (1).
storbinary/retrbinary. —retrlines()andstorlines()have the same four lines, andnlst(),dir(),mlsd()all funnel through
retrlines().Tests
Two new methods in
TestTLS_FTPClass(
tests.py), plus two existing tests that act as guards.New — fail without the patch, pass with it:
test_data_shutdown_error_is_ignoreddrives all five failure modes from thefour issues through both
retrbinary()andstorbinary().test_data_shutdown_timeout_is_ignoredfails the momentunwrap()iscalled without the socket timeout being lowered first, so it catches any
regression that drops the
settimeout()— the half of the fix that is easyto lose.
Existing — guard against "just delete it":
test_storbinaryandtest_storlines(inherited byTestTLS_FTPClassMixin) fail with a truncated payload ifunwrap()isremoved. No new test was written for that case on purpose; the existing
ones already cover it and were used to measure the truncation above.
Full matrix (CPython 3.13.3 system interpreter,
Lib/frommain@82c62ab, no C build required becauseftplibandtest_ftplibare pure Python):Lib/ftplib.pytest_ftplibwith the 2 new testsTest certificate/data:
Lib/test/certdata/keycert3.pem— required, withoutit every TLS test fails with a handshake timeout rather than a clean skip.
PR checklist
gh-103443: ftplib: make the TLS shutdown of the data connection best-effort— thegh-NNNNNN:prefix is mandatory.already exist and are open. Parent-issue discussion belongs on GitHub,
not in the PR.
blurb add -i 103443 -s Library(or the manual file below)—
Misc/NEWS.d/next/Library/...gh-issue-103443....rst(news.rst).Keep it to one paragraph, ≤80 columns, no leading
-, no issue numberin the body (it is in the file name), and no extra
gh-numbers —sampled
Misc/NEWS.d/next/Library/entries never cite them. Optionaltrailing
Patch by <your name>.(used by ~1 in 6 entries; never inventan attribution).
Doc/library/ftplib.rstupdated (docs.rst), andmake -C Docbuilds without warnings.tests.pyinserted intoTestTLS_FTPClass;./python -m test test_ftplibgreen, ideally./python -m test -j0../python Tools/patchcheck/patchcheck.pyclean (NEWS, whitespace,docs presence).
pre-commit run --all-filesclean.cpython-cla-botleaves a commentwith a "click to sign" button — log in with GitHub, Authorize Python
CLA Bot, Sign. One-time only, but it must cover every email
address used in the commits, so set
git config user.emailbeforehand.
squash-merges in the end anyway.
Out of scope
ftplibhas no implicit-mode support and thisis not the place to add it. A standalone reference implementation that
also works around this bug on unpatched
ftplibis kept next to this PRas
ftplib_implicit.py— it is not meant to bemerged into
Lib/.FTP_TLS.ccc()and the control-connection shutdown.FTP_TLS.transfercmd()or thePROT Phandling.