Skip to content

Commit e76cb43

Browse files
authored
[3.6] bpo-30121: Fix debug assert in subprocess on Windows (#1224) (#3173)
* bpo-30121: Fix debug assert in subprocess on Windows (#1224) * bpo-30121: Fix debug assert in subprocess on Windows This is caused by closing HANDLEs using os.close which is for CRT file descriptors and not for HANDLEs. * bpo-30121: Suppress debug assertion in test_subprocess when ran directly (cherry picked from commit 4d38517) * Add test_subprocess.test_nonexisting_with_pipes() (#3133) bpo-30121: Test the Popen failure when Popen was created with pipes. Create also NONEXISTING_CMD variable in test_subprocess.py. (cherry picked from commit 9a83f65)
1 parent 12a3e34 commit e76cb43

File tree

2 files changed

+59
-9
lines changed

2 files changed

+59
-9
lines changed

Lib/subprocess.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,10 @@ def __init__(self, args, bufsize=-1, executable=None,
725725
to_close.append(self._devnull)
726726
for fd in to_close:
727727
try:
728-
os.close(fd)
728+
if _mswindows and isinstance(fd, Handle):
729+
fd.Close()
730+
else:
731+
os.close(fd)
729732
except OSError:
730733
pass
731734

@@ -1005,6 +1008,9 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
10051008
errwrite.Close()
10061009
if hasattr(self, '_devnull'):
10071010
os.close(self._devnull)
1011+
# Prevent a double close of these handles/fds from __init__
1012+
# on error.
1013+
self._closed_child_pipe_fds = True
10081014

10091015
# Retain the process handle, but close the thread handle
10101016
self._child_created = True

Lib/test/test_subprocess.py

+52-8
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
else:
5050
SETBINARY = ''
5151

52+
NONEXISTING_CMD = ('nonexisting_i_hope',)
53+
5254

5355
class BaseTestCase(unittest.TestCase):
5456
def setUp(self):
@@ -1120,10 +1122,11 @@ def _test_bufsize_equal_one(self, line, expected, universal_newlines):
11201122
p.stdin.write(line) # expect that it flushes the line in text mode
11211123
os.close(p.stdin.fileno()) # close it without flushing the buffer
11221124
read_line = p.stdout.readline()
1123-
try:
1124-
p.stdin.close()
1125-
except OSError:
1126-
pass
1125+
with support.SuppressCrashReport():
1126+
try:
1127+
p.stdin.close()
1128+
except OSError:
1129+
pass
11271130
p.stdin = None
11281131
self.assertEqual(p.returncode, 0)
11291132
self.assertEqual(read_line, expected)
@@ -1148,13 +1151,54 @@ def test_leaking_fds_on_error(self):
11481151
# 1024 times (each call leaked two fds).
11491152
for i in range(1024):
11501153
with self.assertRaises(OSError) as c:
1151-
subprocess.Popen(['nonexisting_i_hope'],
1154+
subprocess.Popen(NONEXISTING_CMD,
11521155
stdout=subprocess.PIPE,
11531156
stderr=subprocess.PIPE)
11541157
# ignore errors that indicate the command was not found
11551158
if c.exception.errno not in (errno.ENOENT, errno.EACCES):
11561159
raise c.exception
11571160

1161+
def test_nonexisting_with_pipes(self):
1162+
# bpo-30121: Popen with pipes must close properly pipes on error.
1163+
# Previously, os.close() was called with a Windows handle which is not
1164+
# a valid file descriptor.
1165+
#
1166+
# Run the test in a subprocess to control how the CRT reports errors
1167+
# and to get stderr content.
1168+
try:
1169+
import msvcrt
1170+
msvcrt.CrtSetReportMode
1171+
except (AttributeError, ImportError):
1172+
self.skipTest("need msvcrt.CrtSetReportMode")
1173+
1174+
code = textwrap.dedent(f"""
1175+
import msvcrt
1176+
import subprocess
1177+
1178+
cmd = {NONEXISTING_CMD!r}
1179+
1180+
for report_type in [msvcrt.CRT_WARN,
1181+
msvcrt.CRT_ERROR,
1182+
msvcrt.CRT_ASSERT]:
1183+
msvcrt.CrtSetReportMode(report_type, msvcrt.CRTDBG_MODE_FILE)
1184+
msvcrt.CrtSetReportFile(report_type, msvcrt.CRTDBG_FILE_STDERR)
1185+
1186+
try:
1187+
subprocess.Popen([cmd],
1188+
stdout=subprocess.PIPE,
1189+
stderr=subprocess.PIPE)
1190+
except OSError:
1191+
pass
1192+
""")
1193+
cmd = [sys.executable, "-c", code]
1194+
proc = subprocess.Popen(cmd,
1195+
stderr=subprocess.PIPE,
1196+
universal_newlines=True)
1197+
with proc:
1198+
stderr = proc.communicate()[1]
1199+
self.assertEqual(stderr, "")
1200+
self.assertEqual(proc.returncode, 0)
1201+
11581202
@unittest.skipIf(threading is None, "threading required")
11591203
def test_double_close_on_error(self):
11601204
# Issue #18851
@@ -1167,7 +1211,7 @@ def open_fds():
11671211
t.start()
11681212
try:
11691213
with self.assertRaises(EnvironmentError):
1170-
subprocess.Popen(['nonexisting_i_hope'],
1214+
subprocess.Popen(NONEXISTING_CMD,
11711215
stdin=subprocess.PIPE,
11721216
stdout=subprocess.PIPE,
11731217
stderr=subprocess.PIPE)
@@ -2433,7 +2477,7 @@ def test_leak_fast_process_del_killed(self):
24332477
# should trigger the wait() of p
24342478
time.sleep(0.2)
24352479
with self.assertRaises(OSError) as c:
2436-
with subprocess.Popen(['nonexisting_i_hope'],
2480+
with subprocess.Popen(NONEXISTING_CMD,
24372481
stdout=subprocess.PIPE,
24382482
stderr=subprocess.PIPE) as proc:
24392483
pass
@@ -2863,7 +2907,7 @@ def test_communicate_stdin(self):
28632907

28642908
def test_invalid_args(self):
28652909
with self.assertRaises((FileNotFoundError, PermissionError)) as c:
2866-
with subprocess.Popen(['nonexisting_i_hope'],
2910+
with subprocess.Popen(NONEXISTING_CMD,
28672911
stdout=subprocess.PIPE,
28682912
stderr=subprocess.PIPE) as proc:
28692913
pass

0 commit comments

Comments
 (0)