Skip to content

Commit efb46e5

Browse files
authored
[3.8] gh-108342: Make ssl TestPreHandshakeClose more reliable (GH-108370) (#108408)
* In preauth tests of test_ssl, explicitly break reference cycles invoving SingleConnectionTestServerThread to make sure that the thread is deleted. Otherwise, the test marks the environment as altered because the threading module sees a "dangling thread" (SingleConnectionTestServerThread). This test leak was introduced by the test added for the fix of issue gh-108310. * Use support.SHORT_TIMEOUT instead of hardcoded 1.0 or 2.0 seconds timeout. * SingleConnectionTestServerThread.run() catchs TimeoutError * Fix a race condition (missing synchronization) in test_preauth_data_to_tls_client(): the server now waits until the client connect() completed in call_after_accept(). * test_https_client_non_tls_response_ignored() calls server.join() explicitly. * Replace "localhost" with server.listener.getsockname()[0]. (cherry picked from commit 592bacb)
1 parent 6f2b64f commit efb46e5

File tree

2 files changed

+79
-31
lines changed

2 files changed

+79
-31
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,11 @@ Tools/ssl/win32
120120
# Ignore ./python binary on Unix but still look into ./Python/ directory.
121121
/python
122122
!/Python/
123+
124+
# Artifacts generated by 3.11 lying around when switching branches:
125+
/_bootstrap_python
126+
/Programs/_freeze_module
127+
/Modules/Setup.bootstrap
128+
/Modules/Setup.stdlib
129+
/Python/deepfreeze/
130+
/Python/frozen_modules/

Lib/test/test_ssl.py

+71-31
Original file line numberDiff line numberDiff line change
@@ -4828,12 +4828,16 @@ class TestPreHandshakeClose(unittest.TestCase):
48284828

48294829
class SingleConnectionTestServerThread(threading.Thread):
48304830

4831-
def __init__(self, *, name, call_after_accept):
4831+
def __init__(self, *, name, call_after_accept, timeout=None):
48324832
self.call_after_accept = call_after_accept
48334833
self.received_data = b'' # set by .run()
48344834
self.wrap_error = None # set by .run()
48354835
self.listener = None # set by .start()
48364836
self.port = None # set by .start()
4837+
if timeout is None:
4838+
self.timeout = support.SHORT_TIMEOUT
4839+
else:
4840+
self.timeout = timeout
48374841
super().__init__(name=name)
48384842

48394843
def __enter__(self):
@@ -4856,13 +4860,19 @@ def start(self):
48564860
self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
48574861
self.listener = socket.socket()
48584862
self.port = support.bind_port(self.listener)
4859-
self.listener.settimeout(2.0)
4863+
self.listener.settimeout(self.timeout)
48604864
self.listener.listen(1)
48614865
super().start()
48624866

48634867
def run(self):
4864-
conn, address = self.listener.accept()
4865-
self.listener.close()
4868+
try:
4869+
conn, address = self.listener.accept()
4870+
except TimeoutError:
4871+
# on timeout, just close the listener
4872+
return
4873+
finally:
4874+
self.listener.close()
4875+
48664876
with conn:
48674877
if self.call_after_accept(conn):
48684878
return
@@ -4890,8 +4900,13 @@ def non_linux_skip_if_other_okay_error(self, err):
48904900
# we're specifically trying to test. The way this test is written
48914901
# is known to work on Linux. We'll skip it anywhere else that it
48924902
# does not present as doing so.
4893-
self.skipTest(f"Could not recreate conditions on {sys.platform}:"
4894-
f" {err=}")
4903+
try:
4904+
self.skipTest(f"Could not recreate conditions on {sys.platform}:"
4905+
f" {err=}")
4906+
finally:
4907+
# gh-108342: Explicitly break the reference cycle
4908+
err = None
4909+
48954910
# If maintaining this conditional winds up being a problem.
48964911
# just turn this into an unconditional skip anything but Linux.
48974912
# The important thing is that our CI has the logic covered.
@@ -4902,7 +4917,7 @@ def test_preauth_data_to_tls_server(self):
49024917

49034918
def call_after_accept(unused):
49044919
server_accept_called.set()
4905-
if not ready_for_server_wrap_socket.wait(2.0):
4920+
if not ready_for_server_wrap_socket.wait(support.SHORT_TIMEOUT):
49064921
raise RuntimeError("wrap_socket event never set, test may fail.")
49074922
return False # Tell the server thread to continue.
49084923

@@ -4924,20 +4939,31 @@ def call_after_accept(unused):
49244939

49254940
ready_for_server_wrap_socket.set()
49264941
server.join()
4942+
49274943
wrap_error = server.wrap_error
4928-
self.assertEqual(b"", server.received_data)
4929-
self.assertIsInstance(wrap_error, OSError) # All platforms.
4930-
self.non_linux_skip_if_other_okay_error(wrap_error)
4931-
self.assertIsInstance(wrap_error, ssl.SSLError)
4932-
self.assertIn("before TLS handshake with data", wrap_error.args[1])
4933-
self.assertIn("before TLS handshake with data", wrap_error.reason)
4934-
self.assertNotEqual(0, wrap_error.args[0])
4935-
self.assertIsNone(wrap_error.library, msg="attr must exist")
4944+
server.wrap_error = None
4945+
try:
4946+
self.assertEqual(b"", server.received_data)
4947+
self.assertIsInstance(wrap_error, OSError) # All platforms.
4948+
self.non_linux_skip_if_other_okay_error(wrap_error)
4949+
self.assertIsInstance(wrap_error, ssl.SSLError)
4950+
self.assertIn("before TLS handshake with data", wrap_error.args[1])
4951+
self.assertIn("before TLS handshake with data", wrap_error.reason)
4952+
self.assertNotEqual(0, wrap_error.args[0])
4953+
self.assertIsNone(wrap_error.library, msg="attr must exist")
4954+
finally:
4955+
# gh-108342: Explicitly break the reference cycle
4956+
wrap_error = None
4957+
server = None
49364958

49374959
def test_preauth_data_to_tls_client(self):
4960+
server_can_continue_with_wrap_socket = threading.Event()
49384961
client_can_continue_with_wrap_socket = threading.Event()
49394962

49404963
def call_after_accept(conn_to_client):
4964+
if not server_can_continue_with_wrap_socket.wait(support.SHORT_TIMEOUT):
4965+
print("ERROR: test client took too long")
4966+
49414967
# This forces an immediate connection close via RST on .close().
49424968
set_socket_so_linger_on_with_zero_timeout(conn_to_client)
49434969
conn_to_client.send(
@@ -4959,8 +4985,10 @@ def call_after_accept(conn_to_client):
49594985

49604986
with socket.socket() as client:
49614987
client.connect(server.listener.getsockname())
4962-
if not client_can_continue_with_wrap_socket.wait(2.0):
4963-
self.fail("test server took too long.")
4988+
server_can_continue_with_wrap_socket.set()
4989+
4990+
if not client_can_continue_with_wrap_socket.wait(support.SHORT_TIMEOUT):
4991+
self.fail("test server took too long")
49644992
ssl_ctx = ssl.create_default_context()
49654993
try:
49664994
tls_client = ssl_ctx.wrap_socket(
@@ -4974,24 +5002,31 @@ def call_after_accept(conn_to_client):
49745002
tls_client.close()
49755003

49765004
server.join()
4977-
self.assertEqual(b"", received_data)
4978-
self.assertIsInstance(wrap_error, OSError) # All platforms.
4979-
self.non_linux_skip_if_other_okay_error(wrap_error)
4980-
self.assertIsInstance(wrap_error, ssl.SSLError)
4981-
self.assertIn("before TLS handshake with data", wrap_error.args[1])
4982-
self.assertIn("before TLS handshake with data", wrap_error.reason)
4983-
self.assertNotEqual(0, wrap_error.args[0])
4984-
self.assertIsNone(wrap_error.library, msg="attr must exist")
5005+
try:
5006+
self.assertEqual(b"", received_data)
5007+
self.assertIsInstance(wrap_error, OSError) # All platforms.
5008+
self.non_linux_skip_if_other_okay_error(wrap_error)
5009+
self.assertIsInstance(wrap_error, ssl.SSLError)
5010+
self.assertIn("before TLS handshake with data", wrap_error.args[1])
5011+
self.assertIn("before TLS handshake with data", wrap_error.reason)
5012+
self.assertNotEqual(0, wrap_error.args[0])
5013+
self.assertIsNone(wrap_error.library, msg="attr must exist")
5014+
finally:
5015+
# gh-108342: Explicitly break the reference cycle
5016+
wrap_error = None
5017+
server = None
49855018

49865019
def test_https_client_non_tls_response_ignored(self):
4987-
49885020
server_responding = threading.Event()
49895021

49905022
class SynchronizedHTTPSConnection(http.client.HTTPSConnection):
49915023
def connect(self):
5024+
# Call clear text HTTP connect(), not the encrypted HTTPS (TLS)
5025+
# connect(): wrap_socket() is called manually below.
49925026
http.client.HTTPConnection.connect(self)
5027+
49935028
# Wait for our fault injection server to have done its thing.
4994-
if not server_responding.wait(1.0) and support.verbose:
5029+
if not server_responding.wait(support.SHORT_TIMEOUT) and support.verbose:
49955030
sys.stdout.write("server_responding event never set.")
49965031
self.sock = self._context.wrap_socket(
49975032
self.sock, server_hostname=self.host)
@@ -5006,29 +5041,34 @@ def call_after_accept(conn_to_client):
50065041
server_responding.set()
50075042
return True # Tell the server to stop.
50085043

5044+
timeout = 2.0
50095045
server = self.SingleConnectionTestServerThread(
50105046
call_after_accept=call_after_accept,
5011-
name="non_tls_http_RST_responder")
5047+
name="non_tls_http_RST_responder",
5048+
timeout=timeout)
50125049
server.__enter__() # starts it
50135050
self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it.
50145051
# Redundant; call_after_accept sets SO_LINGER on the accepted conn.
50155052
set_socket_so_linger_on_with_zero_timeout(server.listener)
50165053

50175054
connection = SynchronizedHTTPSConnection(
5018-
f"localhost",
5055+
server.listener.getsockname()[0],
50195056
port=server.port,
50205057
context=ssl.create_default_context(),
5021-
timeout=2.0,
5058+
timeout=timeout,
50225059
)
5060+
50235061
# There are lots of reasons this raises as desired, long before this
50245062
# test was added. Sending the request requires a successful TLS wrapped
50255063
# socket; that fails if the connection is broken. It may seem pointless
50265064
# to test this. It serves as an illustration of something that we never
50275065
# want to happen... properly not happening.
5028-
with self.assertRaises(OSError) as err_ctx:
5066+
with self.assertRaises(OSError):
50295067
connection.request("HEAD", "/test", headers={"Host": "localhost"})
50305068
response = connection.getresponse()
50315069

5070+
server.join()
5071+
50325072

50335073
def test_main(verbose=False):
50345074
if support.verbose:

0 commit comments

Comments
 (0)