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 <vstinner@python.org>
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

Lines changed: 71 additions & 31 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy