From a28f85f655c5882ad15a5281d8e69bb8a939bf5a Mon Sep 17 00:00:00 2001 From: Eryx-Z Date: Sat, 17 Oct 2026 08:09:07 +0800 Subject: [PATCH 1/2] Index send_all memoryviews by byte count, not element count SocketStream and FdStream treated send()/os.write() return values as element offsets into the memoryview. For contiguous views with itemsize > 1, a partial write skipped data and returned success early. Cast nonempty contiguous views to a 1-D bytes view before slicing. Fixes #3543. --- src/trio/_highlevel_socket.py | 5 +++++ src/trio/_tests/test_unix_pipes.py | 26 ++++++++++++++++++++++++++ src/trio/_unix_pipes.py | 7 ++++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/trio/_highlevel_socket.py b/src/trio/_highlevel_socket.py index 142ab11e0..8bcf12b86 100644 --- a/src/trio/_highlevel_socket.py +++ b/src/trio/_highlevel_socket.py @@ -117,6 +117,11 @@ async def send_all(self, data: bytes | bytearray | memoryview) -> None: raise trio.ClosedResourceError("socket was already closed") await trio.lowlevel.checkpoint() return + # send() returns a byte count; index memoryviews in bytes too. + # Contiguous multi-byte / multi-dim views must be cast first + # (#3543). Leave empty views alone (cast can fail on strided). + if data.contiguous: + data = data.cast("B") total_sent = 0 while total_sent < len(data): with data[total_sent:] as remaining: diff --git a/src/trio/_tests/test_unix_pipes.py b/src/trio/_tests/test_unix_pipes.py index f81004049..3499120bc 100644 --- a/src/trio/_tests/test_unix_pipes.py +++ b/src/trio/_tests/test_unix_pipes.py @@ -286,3 +286,29 @@ async def test_bizarro_OSError_from_receive() -> None: @skip_if_fbsd_pipes_broken async def test_pipe_fully() -> None: await check_one_way_stream(make_pipe, make_clogged_pipe) + + +async def test_send_all_memoryview_itemsize_gt_1() -> None: + """Partial writes must advance by byte count, not element count (#3543).""" + import os + + import trio + + payload = bytes(range(256)) * 64 # 16 KiB + read_fd, write_fd = os.pipe() + received = bytearray() + async with trio.lowlevel.FdStream(write_fd) as sender, trio.lowlevel.FdStream( + read_fd, + ) as receiver: + + async def send() -> None: + with memoryview(payload).cast("H") as data: + await sender.send_all(data) + await sender.aclose() + + async with trio.open_nursery() as nursery: + nursery.start_soon(send) + async for chunk in receiver: + received.extend(chunk) + + assert received == payload diff --git a/src/trio/_unix_pipes.py b/src/trio/_unix_pipes.py index 598feb61e..6a253a41a 100644 --- a/src/trio/_unix_pipes.py +++ b/src/trio/_unix_pipes.py @@ -134,9 +134,14 @@ 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: + # os.write returns a byte count; index memoryviews in bytes too. + # Contiguous multi-byte / multi-dim views must be cast first + # (#3543). Leave empty views alone (cast can fail on strided). + if view.contiguous and view: + view = view.cast("B") + length = len(view) sent = 0 while sent < length: with view[sent:] as remaining: From 0e9a80c1fdb9edfeb789d1024fa492660660f60a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:10:01 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/trio/_tests/test_unix_pipes.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/trio/_tests/test_unix_pipes.py b/src/trio/_tests/test_unix_pipes.py index 3499120bc..ded203d73 100644 --- a/src/trio/_tests/test_unix_pipes.py +++ b/src/trio/_tests/test_unix_pipes.py @@ -297,9 +297,12 @@ async def test_send_all_memoryview_itemsize_gt_1() -> None: payload = bytes(range(256)) * 64 # 16 KiB read_fd, write_fd = os.pipe() received = bytearray() - async with trio.lowlevel.FdStream(write_fd) as sender, trio.lowlevel.FdStream( - read_fd, - ) as receiver: + async with ( + trio.lowlevel.FdStream(write_fd) as sender, + trio.lowlevel.FdStream( + read_fd, + ) as receiver, + ): async def send() -> None: with memoryview(payload).cast("H") as data: