Skip to content

Commit b2a6083

Browse files
segevfinervstinner
authored andcommitted
bpo-19764: Implemented support for subprocess.Popen(close_fds=True) on Windows (#1218)
Even though Python marks any handles it opens as non-inheritable there is still a race when using `subprocess.Popen` since creating a process with redirected stdio requires temporarily creating inheritable handles. By implementing support for `subprocess.Popen(close_fds=True)` we fix this race. In order to implement this we use PROC_THREAD_ATTRIBUTE_HANDLE_LIST which is available since Windows Vista. Which allows to pass an explicit list of handles to inherit when creating a process. This commit also adds `STARTUPINFO.lpAttributeList["handle_list"]` which can be used to control PROC_THREAD_ATTRIBUTE_HANDLE_LIST directly.
1 parent 87010e8 commit b2a6083

File tree

8 files changed

+425
-54
lines changed

8 files changed

+425
-54
lines changed

Doc/library/subprocess.rst

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -452,17 +452,20 @@ functions.
452452
common use of *preexec_fn* to call os.setsid() in the child.
453453

454454
If *close_fds* is true, all file descriptors except :const:`0`, :const:`1` and
455-
:const:`2` will be closed before the child process is executed. (POSIX only).
456-
The default varies by platform: Always true on POSIX. On Windows it is
457-
true when *stdin*/*stdout*/*stderr* are :const:`None`, false otherwise.
455+
:const:`2` will be closed before the child process is executed.
458456
On Windows, if *close_fds* is true then no handles will be inherited by the
459-
child process. Note that on Windows, you cannot set *close_fds* to true and
460-
also redirect the standard handles by setting *stdin*, *stdout* or *stderr*.
457+
child process unless explicitly passed in the ``handle_list`` element of
458+
:attr:`STARTUPINFO.lpAttributeList`, or by standard handle redirection.
461459

462460
.. versionchanged:: 3.2
463461
The default for *close_fds* was changed from :const:`False` to
464462
what is described above.
465463

464+
.. versionchanged:: 3.7
465+
On Windows the default for *close_fds* was changed from :const:`False` to
466+
:const:`True` when redirecting the standard handles. It's now possible to
467+
set *close_fds* to :const:`True` when redirecting the standard handles.
468+
466469
*pass_fds* is an optional sequence of file descriptors to keep open
467470
between the parent and child. Providing any *pass_fds* forces
468471
*close_fds* to be :const:`True`. (POSIX only)
@@ -764,7 +767,7 @@ The :class:`STARTUPINFO` class and following constants are only available
764767
on Windows.
765768

766769
.. class:: STARTUPINFO(*, dwFlags=0, hStdInput=None, hStdOutput=None, \
767-
hStdError=None, wShowWindow=0)
770+
hStdError=None, wShowWindow=0, lpAttributeList=None)
768771

769772
Partial support of the Windows
770773
`STARTUPINFO <https://msdn.microsoft.com/en-us/library/ms686331(v=vs.85).aspx>`__
@@ -814,6 +817,33 @@ on Windows.
814817
:data:`SW_HIDE` is provided for this attribute. It is used when
815818
:class:`Popen` is called with ``shell=True``.
816819

820+
.. attribute:: lpAttributeList
821+
822+
A dictionary of additional attributes for process creation as given in
823+
``STARTUPINFOEX``, see
824+
`UpdateProcThreadAttribute <https://msdn.microsoft.com/en-us/library/windows/desktop/ms686880(v=vs.85).aspx>`__.
825+
826+
Supported attributes:
827+
828+
**handle_list**
829+
Sequence of handles that will be inherited. *close_fds* must be true if
830+
non-empty.
831+
832+
The handles must be temporarily made inheritable by
833+
:func:`os.set_handle_inheritable` when passed to the :class:`Popen`
834+
constructor, else :class:`OSError` will be raised with Windows error
835+
``ERROR_INVALID_PARAMETER`` (87).
836+
837+
.. warning::
838+
839+
In a multithreaded process, use caution to avoid leaking handles
840+
that are marked inheritable when combining this feature with
841+
concurrent calls to other process creation functions that inherit
842+
all handles such as :func:`os.system`. This also applies to
843+
standard handle redirection, which temporarily creates inheritable
844+
handles.
845+
846+
.. versionadded:: 3.7
817847

818848
Windows Constants
819849
^^^^^^^^^^^^^^^^^

Doc/whatsnew/3.7.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,17 @@ string
437437
expression pattern for braced placeholders and non-braced placeholders
438438
separately. (Contributed by Barry Warsaw in :issue:`1198569`.)
439439

440+
subprocess
441+
----------
442+
443+
On Windows the default for *close_fds* was changed from :const:`False` to
444+
:const:`True` when redirecting the standard handles. It's now possible to set
445+
*close_fds* to :const:`True` when redirecting the standard handles. See
446+
:class:`subprocess.Popen`.
447+
448+
This means that *close_fds* now defaults to :const:`True` on all supported
449+
platforms.
450+
440451
sys
441452
---
442453

@@ -883,6 +894,14 @@ Changes in the Python API
883894

884895
.. _Unicode Technical Standard #18: https://unicode.org/reports/tr18/
885896

897+
* On Windows the default for the *close_fds* argument of
898+
:class:`subprocess.Popen` was changed from :const:`False` to :const:`True`
899+
when redirecting the standard handles. If you previously depended on handles
900+
being inherited when using :class:`subprocess.Popen` with standard io
901+
redirection, you will have to pass ``close_fds=False`` to preserve the
902+
previous behaviour, or use
903+
:attr:`STARTUPINFO.lpAttributeList <subprocess.STARTUPINFO.lpAttributeList>`.
904+
886905

887906
Changes in the C API
888907
--------------------

Lib/subprocess.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,13 @@ def stdout(self, value):
128128
import _winapi
129129
class STARTUPINFO:
130130
def __init__(self, *, dwFlags=0, hStdInput=None, hStdOutput=None,
131-
hStdError=None, wShowWindow=0):
131+
hStdError=None, wShowWindow=0, lpAttributeList=None):
132132
self.dwFlags = dwFlags
133133
self.hStdInput = hStdInput
134134
self.hStdOutput = hStdOutput
135135
self.hStdError = hStdError
136136
self.wShowWindow = wShowWindow
137+
self.lpAttributeList = lpAttributeList or {"handle_list": []}
137138
else:
138139
import _posixsubprocess
139140
import select
@@ -577,9 +578,6 @@ def getoutput(cmd):
577578
return getstatusoutput(cmd)[1]
578579

579580

580-
_PLATFORM_DEFAULT_CLOSE_FDS = object()
581-
582-
583581
class Popen(object):
584582
""" Execute a child program in a new process.
585583
@@ -630,7 +628,7 @@ class Popen(object):
630628

631629
def __init__(self, args, bufsize=-1, executable=None,
632630
stdin=None, stdout=None, stderr=None,
633-
preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS,
631+
preexec_fn=None, close_fds=True,
634632
shell=False, cwd=None, env=None, universal_newlines=None,
635633
startupinfo=None, creationflags=0,
636634
restore_signals=True, start_new_session=False,
@@ -655,21 +653,8 @@ def __init__(self, args, bufsize=-1, executable=None,
655653
if preexec_fn is not None:
656654
raise ValueError("preexec_fn is not supported on Windows "
657655
"platforms")
658-
any_stdio_set = (stdin is not None or stdout is not None or
659-
stderr is not None)
660-
if close_fds is _PLATFORM_DEFAULT_CLOSE_FDS:
661-
if any_stdio_set:
662-
close_fds = False
663-
else:
664-
close_fds = True
665-
elif close_fds and any_stdio_set:
666-
raise ValueError(
667-
"close_fds is not supported on Windows platforms"
668-
" if you redirect stdin/stdout/stderr")
669656
else:
670657
# POSIX
671-
if close_fds is _PLATFORM_DEFAULT_CLOSE_FDS:
672-
close_fds = True
673658
if pass_fds and not close_fds:
674659
warnings.warn("pass_fds overriding close_fds.", RuntimeWarning)
675660
close_fds = True
@@ -1019,6 +1004,19 @@ def _make_inheritable(self, handle):
10191004
return Handle(h)
10201005

10211006

1007+
def _filter_handle_list(self, handle_list):
1008+
"""Filter out console handles that can't be used
1009+
in lpAttributeList["handle_list"] and make sure the list
1010+
isn't empty. This also removes duplicate handles."""
1011+
# An handle with it's lowest two bits set might be a special console
1012+
# handle that if passed in lpAttributeList["handle_list"], will
1013+
# cause it to fail.
1014+
return list({handle for handle in handle_list
1015+
if handle & 0x3 != 0x3
1016+
or _winapi.GetFileType(handle) !=
1017+
_winapi.FILE_TYPE_CHAR})
1018+
1019+
10221020
def _execute_child(self, args, executable, preexec_fn, close_fds,
10231021
pass_fds, cwd, env,
10241022
startupinfo, creationflags, shell,
@@ -1036,12 +1034,41 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
10361034
# Process startup details
10371035
if startupinfo is None:
10381036
startupinfo = STARTUPINFO()
1039-
if -1 not in (p2cread, c2pwrite, errwrite):
1037+
1038+
use_std_handles = -1 not in (p2cread, c2pwrite, errwrite)
1039+
if use_std_handles:
10401040
startupinfo.dwFlags |= _winapi.STARTF_USESTDHANDLES
10411041
startupinfo.hStdInput = p2cread
10421042
startupinfo.hStdOutput = c2pwrite
10431043
startupinfo.hStdError = errwrite
10441044

1045+
attribute_list = startupinfo.lpAttributeList
1046+
have_handle_list = bool(attribute_list and
1047+
"handle_list" in attribute_list and
1048+
attribute_list["handle_list"])
1049+
1050+
# If we were given an handle_list or need to create one
1051+
if have_handle_list or (use_std_handles and close_fds):
1052+
if attribute_list is None:
1053+
attribute_list = startupinfo.lpAttributeList = {}
1054+
handle_list = attribute_list["handle_list"] = \
1055+
list(attribute_list.get("handle_list", []))
1056+
1057+
if use_std_handles:
1058+
handle_list += [int(p2cread), int(c2pwrite), int(errwrite)]
1059+
1060+
handle_list[:] = self._filter_handle_list(handle_list)
1061+
1062+
if handle_list:
1063+
if not close_fds:
1064+
warnings.warn("startupinfo.lpAttributeList['handle_list'] "
1065+
"overriding close_fds", RuntimeWarning)
1066+
1067+
# When using the handle_list we always request to inherit
1068+
# handles but the only handles that will be inherited are
1069+
# the ones in the handle_list
1070+
close_fds = False
1071+
10451072
if shell:
10461073
startupinfo.dwFlags |= _winapi.STARTF_USESHOWWINDOW
10471074
startupinfo.wShowWindow = _winapi.SW_HIDE

Lib/test/test_subprocess.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2743,11 +2743,6 @@ def test_invalid_args(self):
27432743
[sys.executable, "-c",
27442744
"import sys; sys.exit(47)"],
27452745
preexec_fn=lambda: 1)
2746-
self.assertRaises(ValueError, subprocess.call,
2747-
[sys.executable, "-c",
2748-
"import sys; sys.exit(47)"],
2749-
stdout=subprocess.PIPE,
2750-
close_fds=True)
27512746

27522747
@support.cpython_only
27532748
def test_issue31471(self):
@@ -2765,6 +2760,67 @@ def test_close_fds(self):
27652760
close_fds=True)
27662761
self.assertEqual(rc, 47)
27672762

2763+
def test_close_fds_with_stdio(self):
2764+
import msvcrt
2765+
2766+
fds = os.pipe()
2767+
self.addCleanup(os.close, fds[0])
2768+
self.addCleanup(os.close, fds[1])
2769+
2770+
handles = []
2771+
for fd in fds:
2772+
os.set_inheritable(fd, True)
2773+
handles.append(msvcrt.get_osfhandle(fd))
2774+
2775+
p = subprocess.Popen([sys.executable, "-c",
2776+
"import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])],
2777+
stdout=subprocess.PIPE, close_fds=False)
2778+
stdout, stderr = p.communicate()
2779+
self.assertEqual(p.returncode, 0)
2780+
int(stdout.strip()) # Check that stdout is an integer
2781+
2782+
p = subprocess.Popen([sys.executable, "-c",
2783+
"import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])],
2784+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
2785+
stdout, stderr = p.communicate()
2786+
self.assertEqual(p.returncode, 1)
2787+
self.assertIn(b"OSError", stderr)
2788+
2789+
# The same as the previous call, but with an empty handle_list
2790+
handle_list = []
2791+
startupinfo = subprocess.STARTUPINFO()
2792+
startupinfo.lpAttributeList = {"handle_list": handle_list}
2793+
p = subprocess.Popen([sys.executable, "-c",
2794+
"import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])],
2795+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
2796+
startupinfo=startupinfo, close_fds=True)
2797+
stdout, stderr = p.communicate()
2798+
self.assertEqual(p.returncode, 1)
2799+
self.assertIn(b"OSError", stderr)
2800+
2801+
# Check for a warning due to using handle_list and close_fds=False
2802+
with support.check_warnings((".*overriding close_fds", RuntimeWarning)):
2803+
startupinfo = subprocess.STARTUPINFO()
2804+
startupinfo.lpAttributeList = {"handle_list": handles[:]}
2805+
p = subprocess.Popen([sys.executable, "-c",
2806+
"import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])],
2807+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
2808+
startupinfo=startupinfo, close_fds=False)
2809+
stdout, stderr = p.communicate()
2810+
self.assertEqual(p.returncode, 0)
2811+
2812+
def test_empty_attribute_list(self):
2813+
startupinfo = subprocess.STARTUPINFO()
2814+
startupinfo.lpAttributeList = {}
2815+
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
2816+
startupinfo=startupinfo)
2817+
2818+
def test_empty_handle_list(self):
2819+
startupinfo = subprocess.STARTUPINFO()
2820+
startupinfo.lpAttributeList = {"handle_list": []}
2821+
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
2822+
startupinfo=startupinfo)
2823+
27682824
def test_shell_sequence(self):
27692825
# Run command through the shell (sequence)
27702826
newenv = os.environ.copy()

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@ Carl Feynman
466466
Vincent Fiack
467467
Anastasia Filatova
468468
Tomer Filiba
469+
Segev Finer
469470
Jeffrey Finkelstein
470471
Russell Finn
471472
Dan Finnie
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implement support for `subprocess.Popen(close_fds=True)` on Windows. Patch
2+
by Segev Finer.

0 commit comments

Comments
 (0)