Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions Doc/library/subprocess.rst
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,9 @@ functions.
stderr=None, preexec_fn=None, close_fds=True, shell=False, \
cwd=None, env=None, universal_newlines=None, \
startupinfo=None, creationflags=0, restore_signals=True, \
start_new_session=False, pass_fds=(), *, \
encoding=None, errors=None, text=None)
start_new_session=False, pass_fds=(), *, group=None, \
extra_groups=None, user=None, encoding=None, errors=None, \
text=None)

Execute a child program in a new process. On POSIX, the class uses
:meth:`os.execvp`-like behavior to execute the child program. On Windows,
Expand Down Expand Up @@ -544,6 +545,33 @@ functions.
.. versionchanged:: 3.2
*start_new_session* was added.

If *group* is not ``None``, the setregid() system call will be made in the
child process prior to the execution of the subprocess. If the provided
value is a string, it will be looked up via :func:`grp.getgrnam()` and
the value in ``gr_gid`` will be used. If the value is an integer, it
will be passed verbatim. (POSIX only)

.. availability:: POSIX
.. versionadded:: 3.9

If *extra_groups* is not ``None``, the setgroups() system call will be
made in the child process prior to the execution of the subprocess.
Strings provided in *extra_groups* will be looked up via
:func:`grp.getgrnam()` and the values in ``gr_gid`` will be used.
Integer values will be passed verbatim. (POSIX only)

.. availability:: POSIX
.. versionadded:: 3.9

If *user* is not ``None``, the setreuid() system call will be made in the
child process prior to the execution of the subprocess. If the provided
value is a string, it will be looked up via :func:`pwd.getpwnam()` and
the value in ``pw_uid`` will be used. If the value is an integer, it will
be passed verbatim. (POSIX only)

.. availability:: POSIX
.. versionadded:: 3.9

If *env* is not ``None``, it must be a mapping that defines the environment
variables for the new process; these are used instead of the default
behavior of inheriting the current process' environment.
Expand Down
2 changes: 1 addition & 1 deletion Lib/multiprocessing/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ def spawnv_passfds(path, args, passfds):
return _posixsubprocess.fork_exec(
args, [os.fsencode(path)], True, passfds, None, None,
-1, -1, -1, -1, -1, -1, errpipe_read, errpipe_write,
False, False, None)
False, False, None, None, None, None)
finally:
os.close(errpipe_read)
os.close(errpipe_write)
Expand Down
105 changes: 100 additions & 5 deletions Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
import contextlib
from time import monotonic as _time

try:
import pwd
except ImportError:
pwd = None
try:
import grp
except ImportError:
grp = None
Comment thread
patrick-mclean marked this conversation as resolved.
Outdated

__all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call", "getstatusoutput",
"getoutput", "check_output", "run", "CalledProcessError", "DEVNULL",
Expand Down Expand Up @@ -719,6 +727,12 @@ class Popen(object):

start_new_session (POSIX only)

group (POSIX only)

extra_groups (POSIX only)

user (POSIX only)

pass_fds (POSIX only)

encoding and errors: Text mode encoding and error handling to use for
Expand All @@ -735,7 +749,8 @@ def __init__(self, args, bufsize=-1, executable=None,
shell=False, cwd=None, env=None, universal_newlines=None,
startupinfo=None, creationflags=0,
restore_signals=True, start_new_session=False,
pass_fds=(), *, encoding=None, errors=None, text=None):
pass_fds=(), *, user=None, group=None, extra_groups=None,
encoding=None, errors=None, text=None):
"""Create new Popen instance."""
_cleanup()
# Held while anything is calling waitpid before returncode has been
Expand Down Expand Up @@ -833,6 +848,78 @@ def __init__(self, args, bufsize=-1, executable=None,
else:
line_buffering = False

gid = None
if group is not None:
if not hasattr(os, 'setregid'):
raise ValueError("The 'group' parameter is not supported on the "
"current platform")

elif isinstance(group, str):
if grp is None:
raise ValueError("The group parameter cannot be a string "
"on systems without the grp module")

gid = grp.getgrnam(group).gr_gid
elif isinstance(group, int):
gid = group
else:
raise TypeError("Group must be a string or an integer, not {}"
.format(type(group)))

if gid < 0:
raise ValueError(f"Group ID cannot be negative, got {gid}")

gids = None
if extra_groups is not None:
if not hasattr(os, 'setgroups'):
raise ValueError("The 'extra_groups' parameter is not "
"supported on the current platform")

elif isinstance(extra_groups, str):
raise ValueError("Groups must be a list, not a string")

gids = []
for extra_group in extra_groups:
if isinstance(extra_group, str):
if grp is None:
raise ValueError("Items in extra_groups cannot be "
"strings on systems without the "
"grp module")

gids.append(grp.getgrnam(extra_group).gr_gid)
elif isinstance(extra_group, int):
gids.append(extra_group)
else:
raise TypeError("Items in extra_groups must be a string "
"or integer, not {}"
.format(type(extra_group)))

# make sure that the gids are all positive here so we can do less
# checking in the C code
for gid_check in gids:
if gid_check < 0:
raise ValueError(f"Group ID cannot be negative, got {gid_check}")

uid = None
if user is not None:
if not hasattr(os, 'setreuid'):
raise ValueError("The 'user' parameter is not supported on "
"the current platform")

elif isinstance(user, str):
if pwd is None:
raise ValueError("The user parameter cannot be a string "
"on systems without the pwd module")

uid = pwd.getpwnam(user).pw_uid
elif isinstance(user, int):
uid = user
else:
raise TypeError("User must be a string or an integer")

if uid < 0:
Comment thread
patrick-mclean marked this conversation as resolved.
Outdated
raise ValueError(f"User ID cannot be negative, got {uid}")

try:
if p2cwrite != -1:
self.stdin = io.open(p2cwrite, 'wb', bufsize)
Expand All @@ -857,7 +944,9 @@ def __init__(self, args, bufsize=-1, executable=None,
p2cread, p2cwrite,
c2pread, c2pwrite,
errread, errwrite,
restore_signals, start_new_session)
restore_signals,
gid, gids, uid,
start_new_session)
except:
# Cleanup if the child failed starting.
for f in filter(None, (self.stdin, self.stdout, self.stderr)):
Expand Down Expand Up @@ -1227,7 +1316,9 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
p2cread, p2cwrite,
c2pread, c2pwrite,
errread, errwrite,
unused_restore_signals, unused_start_new_session):
unused_restore_signals,
unused_gid, unused_gids, unused_uid,
unused_start_new_session):
"""Execute program (MS Windows version)"""

assert not pass_fds, "pass_fds not supported on Windows."
Expand Down Expand Up @@ -1553,7 +1644,9 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
p2cread, p2cwrite,
c2pread, c2pwrite,
errread, errwrite,
restore_signals, start_new_session):
restore_signals,
gid, gids, uid,
start_new_session):
"""Execute program (POSIX version)"""

if isinstance(args, (str, bytes)):
Expand Down Expand Up @@ -1641,7 +1734,9 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
p2cread, p2cwrite, c2pread, c2pwrite,
errread, errwrite,
errpipe_read, errpipe_write,
restore_signals, start_new_session, preexec_fn)
restore_signals, start_new_session,
gid, gids, uid,
preexec_fn)
self._child_created = True
finally:
# be sure the FD is closed no matter what
Expand Down
6 changes: 3 additions & 3 deletions Lib/test/test_capi.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,15 @@ class Z(object):
def __len__(self):
return 1
self.assertRaises(TypeError, _posixsubprocess.fork_exec,
1,Z(),3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17)
1,Z(),3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)
# Issue #15736: overflow in _PySequence_BytesToCharpArray()
class Z(object):
def __len__(self):
return sys.maxsize
def __getitem__(self, i):
return b'x'
self.assertRaises(MemoryError, _posixsubprocess.fork_exec,
1,Z(),3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17)
1,Z(),3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)

@unittest.skipUnless(_posixsubprocess, '_posixsubprocess required for this test.')
def test_subprocess_fork_exec(self):
Expand All @@ -114,7 +114,7 @@ def __len__(self):

# Issue #15738: crash in subprocess_fork_exec()
self.assertRaises(TypeError, _posixsubprocess.fork_exec,
Z(),[b'1'],3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17)
Z(),[b'1'],3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)

@unittest.skipIf(MISSING_C_DOCSTRINGS,
"Signature information for builtins requires docstrings")
Expand Down
Loading