Skip to content

Commit cc6bc4c

Browse files
authored
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) Co-Authored-By: Claude <noreply@anthropic.com> * 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.
1 parent 526d7a8 commit cc6bc4c

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
@@ -2102,7 +2102,10 @@ def _communicate(self, input, endtime, orig_timeout):
21022102
self._save_input(input)
21032103

21042104
if self._input:
2105-
input_view = memoryview(self._input)
2105+
if not isinstance(self._input, memoryview):
2106+
input_view = memoryview(self._input)
2107+
else:
2108+
input_view = self._input.cast("b") # byte input required
21062109

21072110
with _PopenSelector() as selector:
21082111
if self.stdin and not self.stdin.closed and self._input:
@@ -2138,7 +2141,7 @@ def _communicate(self, input, endtime, orig_timeout):
21382141
selector.unregister(key.fileobj)
21392142
key.fileobj.close()
21402143
else:
2141-
if self._input_offset >= len(self._input):
2144+
if self._input_offset >= len(input_view):
21422145
selector.unregister(key.fileobj)
21432146
key.fileobj.close()
21442147
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
@@ -957,6 +957,48 @@ def test_communicate(self):
957957
self.assertEqual(stdout, b"banana")
958958
self.assertEqual(stderr, b"pineapple")
959959

960+
def test_communicate_memoryview_input(self):
961+
# Test memoryview input with byte elements
962+
test_data = b"Hello, memoryview!"
963+
mv = memoryview(test_data)
964+
p = subprocess.Popen([sys.executable, "-c",
965+
'import sys; sys.stdout.write(sys.stdin.read())'],
966+
stdin=subprocess.PIPE,
967+
stdout=subprocess.PIPE)
968+
self.addCleanup(p.stdout.close)
969+
self.addCleanup(p.stdin.close)
970+
(stdout, stderr) = p.communicate(mv)
971+
self.assertEqual(stdout, test_data)
972+
self.assertIsNone(stderr)
973+
974+
def test_communicate_memoryview_input_nonbyte(self):
975+
# Test memoryview input with non-byte elements (e.g., int32)
976+
# This tests the fix for gh-134453 where non-byte memoryviews
977+
# had incorrect length tracking on POSIX
978+
import array
979+
# Create an array of 32-bit integers that's large enough to trigger
980+
# the chunked writing behavior (> PIPE_BUF)
981+
pipe_buf = getattr(select, 'PIPE_BUF', 512)
982+
# Each 'i' element is 4 bytes, so we need more than pipe_buf/4 elements
983+
# Add some extra to ensure we exceed the buffer size
984+
num_elements = pipe_buf + 1
985+
test_array = array.array('i', [0x64306f66 for _ in range(num_elements)])
986+
expected_bytes = test_array.tobytes()
987+
mv = memoryview(test_array)
988+
989+
p = subprocess.Popen([sys.executable, "-c",
990+
'import sys; '
991+
'data = sys.stdin.buffer.read(); '
992+
'sys.stdout.buffer.write(data)'],
993+
stdin=subprocess.PIPE,
994+
stdout=subprocess.PIPE)
995+
self.addCleanup(p.stdout.close)
996+
self.addCleanup(p.stdin.close)
997+
(stdout, stderr) = p.communicate(mv)
998+
self.assertEqual(stdout, expected_bytes,
999+
msg=f"{len(stdout)=} =? {len(expected_bytes)=}")
1000+
self.assertIsNone(stderr)
1001+
9601002
def test_communicate_timeout(self):
9611003
p = subprocess.Popen([sys.executable, "-c",
9621004
'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)