Skip to content

Commit d2cd0a3

Browse files
ambvvstinner
andauthored
[3.9] gh-108342: Make ssl TestPreHandshakeClose more reliable (GH-108370) (#108407)
* 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) Co-authored-by: Victor Stinner <[email protected]>
1 parent b8058b3 commit d2cd0a3

File tree

1 file changed

+71
-31
lines changed

1 file changed

+71
-31
lines changed

Lib/test/test_ssl.py

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

48564856
class SingleConnectionTestServerThread(threading.Thread):
48574857

4858-
def __init__(self, *, name, call_after_accept):
4858+
def __init__(self, *, name, call_after_accept, timeout=None):
48594859
self.call_after_accept = call_after_accept
48604860
self.received_data = b'' # set by .run()
48614861
self.wrap_error = None # set by .run()
48624862
self.listener = None # set by .start()
48634863
self.port = None # set by .start()
4864+
if timeout is None:
4865+
self.timeout = support.SHORT_TIMEOUT
4866+
else:
4867+
self.timeout = timeout
48644868
super().__init__(name=name)
48654869

48664870
def __enter__(self):
@@ -4883,13 +4887,19 @@ def start(self):
48834887
self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
48844888
self.listener = socket.socket()
48854889
self.port = socket_helper.bind_port(self.listener)
4886-
self.listener.settimeout(2.0)
4890+
self.listener.settimeout(self.timeout)
48874891
self.listener.listen(1)
48884892
super().start()
48894893

48904894
def run(self):
4891-
conn, address = self.listener.accept()
4892-
self.listener.close()
4895+
try:
4896+
conn, address = self.listener.accept()
4897+
except TimeoutError:
4898+
# on timeout, just close the listener
4899+
return
4900+
finally:
4901+
self.listener.close()
4902+
48934903
with conn:
48944904
if self.call_after_accept(conn):
48954905
return
@@ -4917,8 +4927,13 @@ def non_linux_skip_if_other_okay_error(self, err):
49174927
# we're specifically trying to test. The way this test is written
49184928
# is known to work on Linux. We'll skip it anywhere else that it
49194929
# does not present as doing so.
4920-
self.skipTest(f"Could not recreate conditions on {sys.platform}:"
4921-
f" {err=}")
4930+
try:
4931+
self.skipTest(f"Could not recreate conditions on {sys.platform}:"
4932+
f" {err=}")
4933+
finally:
4934+
# gh-108342: Explicitly break the reference cycle
4935+
err = None
4936+
49224937
# If maintaining this conditional winds up being a problem.
49234938
# just turn this into an unconditional skip anything but Linux.
49244939
# The important thing is that our CI has the logic covered.
@@ -4929,7 +4944,7 @@ def test_preauth_data_to_tls_server(self):
49294944

49304945
def call_after_accept(unused):
49314946
server_accept_called.set()
4932-
if not ready_for_server_wrap_socket.wait(2.0):
4947+
if not ready_for_server_wrap_socket.wait(support.SHORT_TIMEOUT):
49334948
raise RuntimeError("wrap_socket event never set, test may fail.")
49344949
return False # Tell the server thread to continue.
49354950

@@ -4951,20 +4966,31 @@ def call_after_accept(unused):
49514966

49524967
ready_for_server_wrap_socket.set()
49534968
server.join()
4969+
49544970
wrap_error = server.wrap_error
4955-
self.assertEqual(b"", server.received_data)
4956-
self.assertIsInstance(wrap_error, OSError) # All platforms.
4957-
self.non_linux_skip_if_other_okay_error(wrap_error)
4958-
self.assertIsInstance(wrap_error, ssl.SSLError)
4959-
self.assertIn("before TLS handshake with data", wrap_error.args[1])
4960-
self.assertIn("before TLS handshake with data", wrap_error.reason)
4961-
self.assertNotEqual(0, wrap_error.args[0])
4962-
self.assertIsNone(wrap_error.library, msg="attr must exist")
4971+
server.wrap_error = None
4972+
try:
4973+
self.assertEqual(b"", server.received_data)
4974+
self.assertIsInstance(wrap_error, OSError) # All platforms.
4975+
self.non_linux_skip_if_other_okay_error(wrap_error)
4976+
self.assertIsInstance(wrap_error, ssl.SSLError)
4977+
self.assertIn("before TLS handshake with data", wrap_error.args[1])
4978+
self.assertIn("before TLS handshake with data", wrap_error.reason)
4979+
self.assertNotEqual(0, wrap_error.args[0])
4980+
self.assertIsNone(wrap_error.library, msg="attr must exist")
4981+
finally:
4982+
# gh-108342: Explicitly break the reference cycle
4983+
wrap_error = None
4984+
server = None
49634985

49644986
def test_preauth_data_to_tls_client(self):
4987+
server_can_continue_with_wrap_socket = threading.Event()
49654988
client_can_continue_with_wrap_socket = threading.Event()
49664989

49674990
def call_after_accept(conn_to_client):
4991+
if not server_can_continue_with_wrap_socket.wait(support.SHORT_TIMEOUT):
4992+
print("ERROR: test client took too long")
4993+
49684994
# This forces an immediate connection close via RST on .close().
49694995
set_socket_so_linger_on_with_zero_timeout(conn_to_client)
49704996
conn_to_client.send(
@@ -4986,8 +5012,10 @@ def call_after_accept(conn_to_client):
49865012

49875013
with socket.socket() as client:
49885014
client.connect(server.listener.getsockname())
4989-
if not client_can_continue_with_wrap_socket.wait(2.0):
4990-
self.fail("test server took too long.")
5015+
server_can_continue_with_wrap_socket.set()
5016+
5017+
if not client_can_continue_with_wrap_socket.wait(support.SHORT_TIMEOUT):
5018+
self.fail("test server took too long")
49915019
ssl_ctx = ssl.create_default_context()
49925020
try:
49935021
tls_client = ssl_ctx.wrap_socket(
@@ -5001,24 +5029,31 @@ def call_after_accept(conn_to_client):
50015029
tls_client.close()
50025030

50035031
server.join()
5004-
self.assertEqual(b"", received_data)
5005-
self.assertIsInstance(wrap_error, OSError) # All platforms.
5006-
self.non_linux_skip_if_other_okay_error(wrap_error)
5007-
self.assertIsInstance(wrap_error, ssl.SSLError)
5008-
self.assertIn("before TLS handshake with data", wrap_error.args[1])
5009-
self.assertIn("before TLS handshake with data", wrap_error.reason)
5010-
self.assertNotEqual(0, wrap_error.args[0])
5011-
self.assertIsNone(wrap_error.library, msg="attr must exist")
5032+
try:
5033+
self.assertEqual(b"", received_data)
5034+
self.assertIsInstance(wrap_error, OSError) # All platforms.
5035+
self.non_linux_skip_if_other_okay_error(wrap_error)
5036+
self.assertIsInstance(wrap_error, ssl.SSLError)
5037+
self.assertIn("before TLS handshake with data", wrap_error.args[1])
5038+
self.assertIn("before TLS handshake with data", wrap_error.reason)
5039+
self.assertNotEqual(0, wrap_error.args[0])
5040+
self.assertIsNone(wrap_error.library, msg="attr must exist")
5041+
finally:
5042+
# gh-108342: Explicitly break the reference cycle
5043+
wrap_error = None
5044+
server = None
50125045

50135046
def test_https_client_non_tls_response_ignored(self):
5014-
50155047
server_responding = threading.Event()
50165048

50175049
class SynchronizedHTTPSConnection(http.client.HTTPSConnection):
50185050
def connect(self):
5051+
# Call clear text HTTP connect(), not the encrypted HTTPS (TLS)
5052+
# connect(): wrap_socket() is called manually below.
50195053
http.client.HTTPConnection.connect(self)
5054+
50205055
# Wait for our fault injection server to have done its thing.
5021-
if not server_responding.wait(1.0) and support.verbose:
5056+
if not server_responding.wait(support.SHORT_TIMEOUT) and support.verbose:
50225057
sys.stdout.write("server_responding event never set.")
50235058
self.sock = self._context.wrap_socket(
50245059
self.sock, server_hostname=self.host)
@@ -5033,29 +5068,34 @@ def call_after_accept(conn_to_client):
50335068
server_responding.set()
50345069
return True # Tell the server to stop.
50355070

5071+
timeout = 2.0
50365072
server = self.SingleConnectionTestServerThread(
50375073
call_after_accept=call_after_accept,
5038-
name="non_tls_http_RST_responder")
5074+
name="non_tls_http_RST_responder",
5075+
timeout=timeout)
50395076
server.__enter__() # starts it
50405077
self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it.
50415078
# Redundant; call_after_accept sets SO_LINGER on the accepted conn.
50425079
set_socket_so_linger_on_with_zero_timeout(server.listener)
50435080

50445081
connection = SynchronizedHTTPSConnection(
5045-
f"localhost",
5082+
server.listener.getsockname()[0],
50465083
port=server.port,
50475084
context=ssl.create_default_context(),
5048-
timeout=2.0,
5085+
timeout=timeout,
50495086
)
5087+
50505088
# There are lots of reasons this raises as desired, long before this
50515089
# test was added. Sending the request requires a successful TLS wrapped
50525090
# socket; that fails if the connection is broken. It may seem pointless
50535091
# to test this. It serves as an illustration of something that we never
50545092
# want to happen... properly not happening.
5055-
with self.assertRaises(OSError) as err_ctx:
5093+
with self.assertRaises(OSError):
50565094
connection.request("HEAD", "/test", headers={"Host": "localhost"})
50575095
response = connection.getresponse()
50585096

5097+
server.join()
5098+
50595099

50605100
def setUpModule():
50615101
if support.verbose:

0 commit comments

Comments
 (0)