Skip to content

Commit 704bb69

Browse files
[3.13] GH-134453: Fix subprocess memoryview input handling on POSIX (GH-134949) (#142063)
GH-134453: Fix subprocess memoryview input handling on POSIX (GH-134949) Fix inconsistent subprocess.Popen.communicate() behavior between Windows and POSIX when using memoryview objects with non-byte elements as input. On POSIX systems, the code was incorrectly comparing bytes written against element count instead of byte count, causing data truncation for large inputs with non-byte element types. Changes: - Cast memoryview inputs to byte view when input is already a memoryview - Fix progress tracking to use len(input_view) instead of len(self._input) - Add comprehensive test coverage for memoryview inputs 🤖 Generated with [Claude Code](https://claude.ai/code) * old-man-yells-at-ReST * Update 2025-05-30-18-37-44.gh-issue-134453.kxkA-o.rst * assertIsNone review feedback * fix memoryview_nonbytes test to fail without our fix on main, and have a nicer error. Thanks to Peter Bierma @ZeroIntensity for the code review. (cherry picked from commit cc6bc4c) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
1 parent b3a0101 commit 704bb69

File tree

3 files changed

+51
-2
lines changed

3 files changed

+51
-2
lines changed

Lib/subprocess.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2105,7 +2105,10 @@ def _communicate(self, input, endtime, orig_timeout):
21052105
self._save_input(input)
21062106

21072107
if self._input:
2108-
input_view = memoryview(self._input)
2108+
if not isinstance(self._input, memoryview):
2109+
input_view = memoryview(self._input)
2110+
else:
2111+
input_view = self._input.cast("b") # byte input required
21092112

21102113
with _PopenSelector() as selector:
21112114
if self.stdin and input:
@@ -2141,7 +2144,7 @@ def _communicate(self, input, endtime, orig_timeout):
21412144
selector.unregister(key.fileobj)
21422145
key.fileobj.close()
21432146
else:
2144-
if self._input_offset >= len(self._input):
2147+
if self._input_offset >= len(input_view):
21452148
selector.unregister(key.fileobj)
21462149
key.fileobj.close()
21472150
elif key.fileobj in (self.stdout, self.stderr):

Lib/test/test_subprocess.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,48 @@ def test_communicate(self):
956956
self.assertEqual(stdout, b"banana")
957957
self.assertEqual(stderr, b"pineapple")
958958

959+
def test_communicate_memoryview_input(self):
960+
# Test memoryview input with byte elements
961+
test_data = b"Hello, memoryview!"
962+
mv = memoryview(test_data)
963+
p = subprocess.Popen([sys.executable, "-c",
964+
'import sys; sys.stdout.write(sys.stdin.read())'],
965+
stdin=subprocess.PIPE,
966+
stdout=subprocess.PIPE)
967+
self.addCleanup(p.stdout.close)
968+
self.addCleanup(p.stdin.close)
969+
(stdout, stderr) = p.communicate(mv)
970+
self.assertEqual(stdout, test_data)
971+
self.assertIsNone(stderr)
972+
973+
def test_communicate_memoryview_input_nonbyte(self):
974+
# Test memoryview input with non-byte elements (e.g., int32)
975+
# This tests the fix for gh-134453 where non-byte memoryviews
976+
# had incorrect length tracking on POSIX
977+
import array
978+
# Create an array of 32-bit integers that's large enough to trigger
979+
# the chunked writing behavior (> PIPE_BUF)
980+
pipe_buf = getattr(select, 'PIPE_BUF', 512)
981+
# Each 'i' element is 4 bytes, so we need more than pipe_buf/4 elements
982+
# Add some extra to ensure we exceed the buffer size
983+
num_elements = pipe_buf + 1
984+
test_array = array.array('i', [0x64306f66 for _ in range(num_elements)])
985+
expected_bytes = test_array.tobytes()
986+
mv = memoryview(test_array)
987+
988+
p = subprocess.Popen([sys.executable, "-c",
989+
'import sys; '
990+
'data = sys.stdin.buffer.read(); '
991+
'sys.stdout.buffer.write(data)'],
992+
stdin=subprocess.PIPE,
993+
stdout=subprocess.PIPE)
994+
self.addCleanup(p.stdout.close)
995+
self.addCleanup(p.stdin.close)
996+
(stdout, stderr) = p.communicate(mv)
997+
self.assertEqual(stdout, expected_bytes,
998+
msg=f"{len(stdout)=} =? {len(expected_bytes)=}")
999+
self.assertIsNone(stderr)
1000+
9591001
def test_communicate_timeout(self):
9601002
p = subprocess.Popen([sys.executable, "-c",
9611003
'import sys,os,time;'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed :func:`subprocess.Popen.communicate` ``input=`` handling of :class:`memoryview`
2+
instances that were non-byte shaped on POSIX platforms. Those are now properly
3+
cast to a byte shaped view instead of truncating the input. Windows platforms
4+
did not have this bug.

0 commit comments

Comments
 (0)