From 2253df0f0eafdaad3b080cecc113bed4455ca4f1 Mon Sep 17 00:00:00 2001 From: AryanRG Date: Sat, 17 Oct 2026 04:18:27 +0530 Subject: [PATCH 1/2] Fix send_all byte offsets for typed memoryviews Partial writes return bytes rather than memoryview elements. Normalize nonempty buffers to byte views before retrying while retaining empty-view handling and explicit view cleanup. Fixes #3543. Developed and validated by an autonomous OpenAI Codex agent; independently reviewed by an AI agent. --- newsfragments/3543.bugfix.rst | 4 +++ src/trio/_highlevel_socket.py | 14 +++++---- src/trio/_tests/test_highlevel_socket.py | 40 ++++++++++++++++++++++++ src/trio/_tests/test_unix_pipes.py | 36 ++++++++++++++++++++- src/trio/_unix_pipes.py | 36 +++++++++++---------- 5 files changed, 107 insertions(+), 23 deletions(-) create mode 100644 newsfragments/3543.bugfix.rst diff --git a/newsfragments/3543.bugfix.rst b/newsfragments/3543.bugfix.rst new file mode 100644 index 000000000..bdd803548 --- /dev/null +++ b/newsfragments/3543.bugfix.rst @@ -0,0 +1,4 @@ +Fixed silent data loss in `trio.SocketStream.send_all` and +`trio.lowlevel.FdStream.send_all` when a partial write occurs while sending a +contiguous memoryview with multi-byte elements. Multidimensional and +zero-dimensional views are now also handled in byte units. diff --git a/src/trio/_highlevel_socket.py b/src/trio/_highlevel_socket.py index 142ab11e0..09ce9595f 100644 --- a/src/trio/_highlevel_socket.py +++ b/src/trio/_highlevel_socket.py @@ -112,16 +112,18 @@ async def send_all(self, data: bytes | bytearray | memoryview) -> None: with self._send_conflict_detector: with _translate_socket_errors_to_stream_errors(): with memoryview(data) as data: - if not data: + if not data.nbytes: if self.socket.fileno() == -1: raise trio.ClosedResourceError("socket was already closed") await trio.lowlevel.checkpoint() return - total_sent = 0 - while total_sent < len(data): - with data[total_sent:] as remaining: - sent = await self.socket.send(remaining) - total_sent += sent + # send() returns byte counts, so offsets must also be in bytes. + with data.cast("B") as data: + total_sent = 0 + while total_sent < len(data): + with data[total_sent:] as remaining: + sent = await self.socket.send(remaining) + total_sent += sent async def wait_send_all_might_not_block(self) -> None: with self._send_conflict_detector: diff --git a/src/trio/_tests/test_highlevel_socket.py b/src/trio/_tests/test_highlevel_socket.py index ff19e3367..e25805bba 100644 --- a/src/trio/_tests/test_highlevel_socket.py +++ b/src/trio/_tests/test_highlevel_socket.py @@ -9,6 +9,7 @@ from .. import _core, socket as tsocket from .._highlevel_socket import * +from .._socket import _SocketType from ..testing import ( assert_checkpoints, check_half_closeable_stream, @@ -119,6 +120,45 @@ async def receiver() -> None: assert await b.receive_some(10) == b"" +@pytest.mark.parametrize("shape", [(16,), (4, 4), ()]) +async def test_SocketStream_send_all_memoryview( + monkeypatch: pytest.MonkeyPatch, + shape: tuple[int, ...], +) -> None: + original_send = _SocketType.send + + async def partial_send( + self: _SocketType, + data: bytes | bytearray | memoryview, + flags: int = 0, + ) -> int: + # Use a real socket, but force partial writes independently of OS buffers. + with memoryview(data).cast("B") as view: + return await original_send(self, view[:3], flags) + + monkeypatch.setattr(_SocketType, "send", partial_send) + payload = bytes(range(32)) if shape else b"ab" + left, right = tsocket.socketpair() + async with SocketStream(left) as sender, SocketStream(right) as receiver: + with memoryview(payload).cast("H", shape=shape) as data: + await sender.send_all(data) + await sender.send_eof() + received = bytearray() + async for chunk in receiver: + received.extend(chunk) + assert received == payload + + +async def test_SocketStream_send_all_empty_noncontiguous_memoryview() -> None: + left, right = tsocket.socketpair() + async with SocketStream(left) as sender, SocketStream(right) as receiver: + with memoryview(b"abc")[0:0:2] as data: + with assert_checkpoints(): + await sender.send_all(data) + await sender.send_eof() + assert await receiver.receive_some() == b"" + + async def fill_stream(s: SocketStream) -> None: async def sender() -> None: while True: diff --git a/src/trio/_tests/test_unix_pipes.py b/src/trio/_tests/test_unix_pipes.py index f81004049..26bcfe23b 100644 --- a/src/trio/_tests/test_unix_pipes.py +++ b/src/trio/_tests/test_unix_pipes.py @@ -10,7 +10,7 @@ from .. import _core from .._core._tests.tutil import gc_collect_harder, skip_if_fbsd_pipes_broken -from ..testing import check_one_way_stream, wait_all_tasks_blocked +from ..testing import assert_checkpoints, check_one_way_stream, wait_all_tasks_blocked if TYPE_CHECKING: from .._file_io import _HasFileNo @@ -66,6 +66,40 @@ async def test_send_pipe() -> None: os.close(r) +@pytest.mark.parametrize("shape", [(16,), (4, 4), ()]) +async def test_send_pipe_memoryview( + monkeypatch: pytest.MonkeyPatch, + shape: tuple[int, ...], +) -> None: + original_write = os.write + + def partial_write(fd: int, data: bytes | bytearray | memoryview) -> int: + with memoryview(data).cast("B") as view: + return original_write(fd, view[:3]) + + monkeypatch.setattr(os, "write", partial_write) + payload = bytes(range(32)) if shape else b"ab" + send, receive = await make_pipe() + async with send, receive: + with memoryview(payload).cast("H", shape=shape) as data: + await send.send_all(data) + await send.aclose() + received = bytearray() + async for chunk in receive: + received.extend(chunk) + assert received == payload + + +async def test_send_pipe_empty_noncontiguous_memoryview() -> None: + send, receive = await make_pipe() + async with send, receive: + with memoryview(b"abc")[0:0:2] as data: + with assert_checkpoints(): + await send.send_all(data) + await send.aclose() + assert await receive.receive_some() == b"" + + async def test_receive_pipe() -> None: r, w = os.pipe() async with FdStream(r) as recv: diff --git a/src/trio/_unix_pipes.py b/src/trio/_unix_pipes.py index 598feb61e..d15614531 100644 --- a/src/trio/_unix_pipes.py +++ b/src/trio/_unix_pipes.py @@ -134,23 +134,27 @@ async def send_all(self, data: bytes | bytearray | memoryview) -> None: if self._fd_holder.closed: raise trio.ClosedResourceError("file was already closed") await trio.lowlevel.checkpoint() - length = len(data) # adapted from the SocketStream code - with memoryview(data) as view: - sent = 0 - while sent < length: - with view[sent:] as remaining: - try: - sent += os.write(self._fd_holder.fd, remaining) - except BlockingIOError: - await trio.lowlevel.wait_writable(self._fd_holder.fd) - except OSError as e: - if e.errno == errno.EBADF: - raise trio.ClosedResourceError( - "file was already closed", - ) from None - else: - raise trio.BrokenResourceError from e + with memoryview(data) as raw_view: + length = raw_view.nbytes + if not length: + return + # os.write() returns byte counts, regardless of the input format. + with raw_view.cast("B") as view: + sent = 0 + while sent < length: + with view[sent:] as remaining: + try: + sent += os.write(self._fd_holder.fd, remaining) + except BlockingIOError: + await trio.lowlevel.wait_writable(self._fd_holder.fd) + except OSError as e: + if e.errno == errno.EBADF: + raise trio.ClosedResourceError( + "file was already closed", + ) from None + else: + raise trio.BrokenResourceError from e async def wait_send_all_might_not_block(self) -> None: with self._send_conflict_detector: From 1c8419e83c8e9300726c1c6126ca75101b6f9e2f Mon Sep 17 00:00:00 2001 From: AryanRG Date: Sat, 17 Oct 2026 04:27:32 +0530 Subject: [PATCH 2/2] Fix release note references for memoryview correction Render inherited send_all method names as inline literals because their Sphinx object targets do not exist. The strict docs build reproduces the two warnings before this change and passes afterward; untouched base docs pass too. Developed and validated by an autonomous OpenAI Codex agent; independently reviewed by an AI agent. --- newsfragments/3543.bugfix.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/newsfragments/3543.bugfix.rst b/newsfragments/3543.bugfix.rst index bdd803548..3e724b9e2 100644 --- a/newsfragments/3543.bugfix.rst +++ b/newsfragments/3543.bugfix.rst @@ -1,4 +1,4 @@ -Fixed silent data loss in `trio.SocketStream.send_all` and -`trio.lowlevel.FdStream.send_all` when a partial write occurs while sending a +Fixed silent data loss in ``trio.SocketStream.send_all`` and +``trio.lowlevel.FdStream.send_all`` when a partial write occurs while sending a contiguous memoryview with multi-byte elements. Multidimensional and zero-dimensional views are now also handled in byte units.