Skip to content

Commit 55ca778

Browse files
committed
pythongh-135862: fix asyncio socket partial writes of non-1d-binary arrays
The :mod:`asyncio` code is writing binary data to a :class:`socket.socket`, and advancing through the buffer after a partial write by doing ``memoryview(data)[n:]``, where ``n`` is the number of bytes written. This assumes the :class:`memoryview` is a one dimensional buffer of bytes, which is not the case e.g. for a :class:`memoryview` of an :class:`array.array` of non-bytes, or an ``ndarray`` with more than one dimension. Partial writes of such :class:`memoryview`s will corrupt the stream, repeating or omitting some data. When creating a :class:`memoryview` from a :class:`memoryview`, first convert it to a one-dimensional array of bytes before taking the remaining slice, thus avoiding these issues. Add an assertion that the remaining data is bytes, to guard against possible regressions if additional types of data are allowed in the future.
1 parent 6227662 commit 55ca778

3 files changed

Lines changed: 46 additions & 2 deletions

File tree

Lib/asyncio/selector_events.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1077,7 +1077,11 @@ def write(self, data):
10771077
self._fatal_error(exc, 'Fatal write error on socket transport')
10781078
return
10791079
else:
1080-
data = memoryview(data)[n:]
1080+
if isinstance(data, memoryview):
1081+
data = data.cast('c')[n:]
1082+
else:
1083+
data = memoryview(data)[n:]
1084+
assert(data.itemsize == 1)
10811085
if not data:
10821086
return
10831087
# Not all was written; register write handler.

Lib/test/test_asyncio/test_selector_events.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for selector_events.py"""
22

3+
import array
34
import collections
45
import selectors
56
import socket
@@ -13,6 +14,11 @@
1314
except ImportError:
1415
ssl = None
1516

17+
try:
18+
from _testbuffer import *
19+
except ImportError:
20+
ndarray = None
21+
1622
import asyncio
1723
from asyncio.selector_events import (BaseSelectorEventLoop,
1824
_SelectorDatagramTransport,
@@ -768,7 +774,39 @@ def test_write_partial_memoryview(self):
768774
transport.write(data)
769775

770776
self.loop.assert_writer(7, transport._write_ready)
771-
self.assertEqual(list_to_buffer([b'ta']), transport._buffer)
777+
self.assertEqualBufferContents(b'ta', transport._buffer)
778+
779+
def test_write_partial_nonbyte_array_memview_write(self):
780+
arr = array.array('l', [-1, 1])
781+
data = memoryview(arr)
782+
783+
self.sock.send.return_value = 8
784+
785+
transport = self.socket_transport()
786+
transport.write(data)
787+
788+
self.loop.assert_writer(7, transport._write_ready)
789+
remainder = memoryview(array.array('l', [1]))
790+
self.assertEqualBufferContents(remainder.tobytes(), transport._buffer)
791+
792+
@unittest.skipUnless(ndarray, 'ndarray object required for this test')
793+
def test_write_partial_ndarray_memview_write(self):
794+
items = (-2, -1, 1, 2)
795+
arr = ndarray(items, format='l', shape=(1, 2, 2))
796+
data = memoryview(arr)
797+
798+
self.sock.send.return_value = 16
799+
800+
transport = self.socket_transport()
801+
transport.write(data)
802+
803+
self.loop.assert_writer(7, transport._write_ready)
804+
remainder = memoryview(array.array('l', (1, 2)))
805+
self.assertEqualBufferContents(remainder.tobytes(), transport._buffer)
806+
807+
def assertEqualBufferContents(self, expected, buffer):
808+
self.assertEqual(len(buffer), 1)
809+
self.assertEqual(expected, buffer[0].tobytes())
772810

773811
def test_write_partial_none(self):
774812
data = b'data'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix bug where partial writes of :class:`memoryview` s of non-byte or 2+
2+
dimensional arrays would write remaining binary data incorrectly.

0 commit comments

Comments
 (0)