Skip to content

Commit 0e7144b

Browse files
izbyshevgpshead
authored andcommitted
bpo-32844: Fix a subprocess misredirection of a low fd (GH5689)
bpo-32844: subprocess: Fix a potential misredirection of a low fd to stderr. When redirecting, subprocess attempts to achieve the following state: each fd to be redirected to is less than or equal to the fd it is redirected from, which is necessary because redirection occurs in the ascending order of destination descriptors. It fails to do so in a couple of corner cases, for example, if 1 is redirected to 2 and 0 is closed in the parent.
1 parent de7a2f0 commit 0e7144b

File tree

3 files changed

+53
-1
lines changed

3 files changed

+53
-1
lines changed

Lib/test/test_subprocess.py

+50
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import platform
77
import signal
88
import io
9+
import itertools
910
import os
1011
import errno
1112
import tempfile
@@ -2134,6 +2135,55 @@ def test_swap_fds(self):
21342135
self.check_swap_fds(2, 0, 1)
21352136
self.check_swap_fds(2, 1, 0)
21362137

2138+
def _check_swap_std_fds_with_one_closed(self, from_fds, to_fds):
2139+
saved_fds = self._save_fds(range(3))
2140+
try:
2141+
for from_fd in from_fds:
2142+
with tempfile.TemporaryFile() as f:
2143+
os.dup2(f.fileno(), from_fd)
2144+
2145+
fd_to_close = (set(range(3)) - set(from_fds)).pop()
2146+
os.close(fd_to_close)
2147+
2148+
arg_names = ['stdin', 'stdout', 'stderr']
2149+
kwargs = {}
2150+
for from_fd, to_fd in zip(from_fds, to_fds):
2151+
kwargs[arg_names[to_fd]] = from_fd
2152+
2153+
code = textwrap.dedent(r'''
2154+
import os, sys
2155+
skipped_fd = int(sys.argv[1])
2156+
for fd in range(3):
2157+
if fd != skipped_fd:
2158+
os.write(fd, str(fd).encode('ascii'))
2159+
''')
2160+
2161+
skipped_fd = (set(range(3)) - set(to_fds)).pop()
2162+
2163+
rc = subprocess.call([sys.executable, '-c', code, str(skipped_fd)],
2164+
**kwargs)
2165+
self.assertEqual(rc, 0)
2166+
2167+
for from_fd, to_fd in zip(from_fds, to_fds):
2168+
os.lseek(from_fd, 0, os.SEEK_SET)
2169+
read_bytes = os.read(from_fd, 1024)
2170+
read_fds = list(map(int, read_bytes.decode('ascii')))
2171+
msg = textwrap.dedent(f"""
2172+
When testing {from_fds} to {to_fds} redirection,
2173+
parent descriptor {from_fd} got redirected
2174+
to descriptor(s) {read_fds} instead of descriptor {to_fd}.
2175+
""")
2176+
self.assertEqual([to_fd], read_fds, msg)
2177+
finally:
2178+
self._restore_fds(saved_fds)
2179+
2180+
# Check that subprocess can remap std fds correctly even
2181+
# if one of them is closed (#32844).
2182+
def test_swap_std_fds_with_one_closed(self):
2183+
for from_fds in itertools.combinations(range(3), 2):
2184+
for to_fds in itertools.permutations(range(3), 2):
2185+
self._check_swap_std_fds_with_one_closed(from_fds, to_fds)
2186+
21372187
def test_surrogates_error_message(self):
21382188
def prepare():
21392189
raise ValueError("surrogate:\uDCff")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix wrong redirection of a low descriptor (0 or 1) to stderr in subprocess
2+
if another low descriptor is closed.

Modules/_posixsubprocess.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ child_exec(char *const exec_array[],
424424
either 0, 1 or 2, it is possible that it is overwritten (#12607). */
425425
if (c2pwrite == 0)
426426
POSIX_CALL(c2pwrite = dup(c2pwrite));
427-
if (errwrite == 0 || errwrite == 1)
427+
while (errwrite == 0 || errwrite == 1)
428428
POSIX_CALL(errwrite = dup(errwrite));
429429

430430
/* Dup fds for child.

0 commit comments

Comments
 (0)