Skip to content

Commit 32b2550

Browse files
[3.15] gh-160499: Pause asyncio TLS reads when SSL replies pile up (GH-160500) (#160893)
gh-160499: Pause asyncio TLS reads when SSL replies pile up (GH-160500) (cherry picked from commit 8a5e24a) Co-authored-by: Gregory P. Smith <[email protected]>
1 parent f36fdb1 commit 32b2550

3 files changed

Lines changed: 61 additions & 1 deletion

File tree

‎Lib/asyncio/sslproto.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,9 @@ def __init__(self, loop, app_protocol, sslcontext, waiter,
308308
# App data write buffering
309309
self._write_backlog = collections.deque()
310310
self._write_buffer_size = 0
311+
# How much of self._outgoing was produced by _do_write(): encrypted
312+
# app data, plus anything else OpenSSL flushed along with it
313+
self._outgoing_app_size = 0
311314

312315
self._waiter = waiter
313316
self._loop = loop
@@ -710,6 +713,7 @@ def _write_appdata(self, list_of_data):
710713
self._fatal_error(ex, 'Fatal error on SSL protocol')
711714

712715
def _do_write(self):
716+
pending = self._outgoing.pending
713717
try:
714718
while self._write_backlog:
715719
data = self._write_backlog[0]
@@ -723,11 +727,13 @@ def _do_write(self):
723727
self._write_buffer_size -= data_len
724728
except SSLAgainErrors:
725729
pass
730+
self._outgoing_app_size += self._outgoing.pending - pending
726731
self._process_outgoing()
727732

728733
def _process_outgoing(self):
729734
if not self._ssl_writing_paused:
730735
data = self._outgoing.read()
736+
self._outgoing_app_size = 0
731737
if len(data):
732738
self._transport.write(data)
733739
self._control_app_writing()
@@ -889,7 +895,15 @@ def resume():
889895
# Flow control for reads from SSL socket
890896

891897
def _control_ssl_reading(self):
892-
size = self._get_read_buffer_size()
898+
# Also count the records OpenSSL writes on its own while reading
899+
# (alerts, KeyUpdate replies, ...) which the peer does not read:
900+
# the app is not involved in producing them, so pausing it does
901+
# not bound them; only pausing reads from the peer does. Whatever
902+
# was produced while writing app data (_outgoing_app_size) is left
903+
# out so that two peers which both write before they read cannot
904+
# deadlock.
905+
size = (self._get_read_buffer_size() +
906+
self._outgoing.pending - self._outgoing_app_size)
893907
if size >= self._incoming_high_water and not self._ssl_reading_paused:
894908
self._ssl_reading_paused = True
895909
self._transport.pause_reading()
@@ -922,6 +936,7 @@ def resume_writing(self):
922936
assert self._ssl_writing_paused
923937
self._ssl_writing_paused = False
924938
self._process_outgoing()
939+
self._control_ssl_reading()
925940

926941
def _fatal_error(self, exc, message='Fatal error on transport'):
927942
if self._transport:

‎Lib/test/test_asyncio/test_sslproto.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,46 @@ def test_write_after_closing(self):
289289
# should not raise
290290
self.assertIsNone(transp.write(b'data'))
291291

292+
def test_pause_reading_if_ssl_replies_are_not_read(self):
293+
# Output which OpenSSL produces on its own while reading (alerts,
294+
# KeyUpdate, ...) must not pile up if the peer does not read it.
295+
ssl_proto = self.ssl_protocol()
296+
transport = self.connection_made(ssl_proto, do_handshake=mock.Mock())
297+
reply = b'x' * (ssl_proto._incoming_high_water // 2)
298+
299+
def read(*args):
300+
ssl_proto._outgoing.write(reply)
301+
raise ssl.SSLWantReadError
302+
ssl_proto._sslobj.read.side_effect = read
303+
304+
ssl_proto.pause_writing()
305+
ssl_proto.buffer_updated(0)
306+
transport.pause_reading.assert_not_called()
307+
ssl_proto.buffer_updated(0)
308+
transport.pause_reading.assert_called_once_with()
309+
transport.resume_reading.assert_not_called()
310+
transport.write.assert_not_called()
311+
312+
ssl_proto.resume_writing()
313+
transport.write.assert_called_once_with(reply * 2)
314+
transport.resume_reading.assert_called_once_with()
315+
316+
def test_no_pause_reading_if_app_data_is_not_read(self):
317+
# Peers which both write a lot before they read must not deadlock.
318+
ssl_proto = self.ssl_protocol()
319+
transport = self.connection_made(ssl_proto, do_handshake=mock.Mock())
320+
ssl_proto._sslobj.write.side_effect = ssl_proto._outgoing.write
321+
data = b'x' * ssl_proto._incoming_high_water
322+
323+
ssl_proto.pause_writing()
324+
ssl_proto._app_transport.write(data)
325+
ssl_proto.buffer_updated(0)
326+
transport.pause_reading.assert_not_called()
327+
328+
ssl_proto.resume_writing()
329+
transport.write.assert_called_once_with(data)
330+
transport.resume_reading.assert_not_called()
331+
292332

293333
##############################################################################
294334
# Start TLS Tests
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix unbounded memory growth in :mod:`asyncio` TLS connections when a peer
2+
stops reading but keeps sending records that make OpenSSL generate its own
3+
replies (such as TLS 1.2 renegotiation alerts). Reading from the peer is now
4+
paused when this output accumulates, instead of only when application data
5+
does.

0 commit comments

Comments
 (0)