8000 gh-135862: fix asyncio socket partial writes of non-1d-binary arrays by duaneg · Pull Request #135974 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-135862: fix asyncio socket partial writes of non-1d-binary arrays #135974

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
gh-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.
  • Loading branch information
duaneg committed Jun 26, 2025
commit 55ca7788a98fe8ae9e43e66a30a02385c8cb7b31
6 changes: 5 additions & 1 deletion Lib/asyncio/selector_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,11 @@ def write(self, data):
self._fatal_error(exc, 'Fatal write error on socket transport')
return
else:
data = memoryview(data)[n:]
if isinstance(data, memoryview):
data = data.cast('c')[n:]
else:
data = memoryview(data)[n:]
assert(data.itemsize == 1)
if not data:
return
# Not all was written; register write handler.
Expand Down
40 changes: 39 additions & 1 deletion Lib/test/test_asyncio/test_selector_events.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for selector_events.py"""

import array
import collections
import selectors
import socket
Expand All @@ -13,6 +14,11 @@
except ImportError:
ssl = None

try:
from _testbuffer import *
except ImportError:
ndarray = None

import asyncio
from asyncio.selector_events import (BaseSelectorEventLoop,
_SelectorDatagramTransport,
Expand Down Expand Up @@ -768,7 +774,39 @@ def test_write_partial_memoryview(self):
transport.write(data)

self.loop.assert_writer(7, transport._write_ready)
self.assertEqual(list_to_buffer([b'ta']), transport._buffer)
self.assertEqualBufferContents(b'ta', transport._buffer)

def test_write_partial_nonbyte_array_memview_write(self):
arr = array.array('l', [-1, 1])
data = memoryview(arr)

self.sock.send.return_value = 8

transport = self.socket_transport()
transport.write(data)

self.loop.assert_writer(7, transport._write_ready)
remainder = memoryview(array.array('l', [1]))
self.assertEqualBufferContents(remainder.tobytes(), transport._buffer)

@unittest.skipUnless(ndarray, 'ndarray object required for this test')
def test_write_partial_ndarray_memview_write(self):
items = (-2, -1, 1, 2)
arr = ndarray(items, format='l', shape=(1, 2, 2))
data = memoryview(arr)

self.sock.send.return_value = 16

transport = self.socket_transport()
transport.write(data)

self.loop.assert_writer(7, transport._write_ready)
remainder = memoryview(array.array('l', (1, 2)))
self.assertEqualBufferContents(remainder.tobytes(), transport._buffer)

def assertEqualBufferContents(self, expected, buffer):
self.assertEqual(len(buffer), 1)
self.assertEqual(expected, buffer[0].tobytes())

def test_write_partial_none(self):
data = b'data'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix bug where partial writes of :class:`memoryview` s of non-byte or 2+
dimensional arrays would write remaining binary data incorrectly.
Loading
0