bpo-36046: Add user and group parameters to subprocess (GH-11950)
* subprocess: Add user, group and extra_groups paremeters to subprocess.Popen This adds a `user` parameter to the Popen constructor that will call setreuid() in the child before calling exec(). This allows processes running as root to safely drop privileges before running the subprocess without having to use a preexec_fn. This also adds a `group` parameter that will call setregid() in the child process before calling exec(). Finally an `extra_groups` parameter was added that will call setgroups() to set the supplimental groups.
This commit is contained in:
parent
57b7dbc46e
commit
2b2ead7438
|
@ -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,
|
||||
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -53,6 +53,14 @@ import warnings
|
|||
import contextlib
|
||||
from time import monotonic as _time
|
||||
|
||||
try:
|
||||
import pwd
|
||||
except ImportError:
|
||||
pwd = None
|
||||
try:
|
||||
import grp
|
||||
except ImportError:
|
||||
grp = None
|
||||
|
||||
__all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call", "getstatusoutput",
|
||||
"getoutput", "check_output", "run", "CalledProcessError", "DEVNULL",
|
||||
|
@ -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
|
||||
|
@ -735,7 +749,8 @@ class Popen(object):
|
|||
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
|
||||
|
@ -833,6 +848,78 @@ class Popen(object):
|
|||
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:
|
||||
raise ValueError(f"User ID cannot be negative, got {uid}")
|
||||
|
||||
try:
|
||||
if p2cwrite != -1:
|
||||
self.stdin = io.open(p2cwrite, 'wb', bufsize)
|
||||
|
@ -857,7 +944,9 @@ class Popen(object):
|
|||
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)):
|
||||
|
@ -1227,7 +1316,9 @@ class Popen(object):
|
|||
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."
|
||||
|
@ -1553,7 +1644,9 @@ class Popen(object):
|
|||
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)):
|
||||
|
@ -1641,7 +1734,9 @@ class Popen(object):
|
|||
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
|
||||
|
|
|
@ -96,7 +96,7 @@ class CAPITest(unittest.TestCase):
|
|||
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):
|
||||
|
@ -104,7 +104,7 @@ class CAPITest(unittest.TestCase):
|
|||
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):
|
||||
|
@ -114,7 +114,7 @@ class CAPITest(unittest.TestCase):
|
|||
|
||||
# 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")
|
||||
|
|
|
@ -18,6 +18,7 @@ import shutil
|
|||
import threading
|
||||
import gc
|
||||
import textwrap
|
||||
import json
|
||||
from test.support import FakePath
|
||||
|
||||
try:
|
||||
|
@ -25,6 +26,14 @@ try:
|
|||
except ImportError:
|
||||
_testcapi = None
|
||||
|
||||
try:
|
||||
import pwd
|
||||
except ImportError:
|
||||
pwd = None
|
||||
try:
|
||||
import grp
|
||||
except ImportError:
|
||||
grp = None
|
||||
|
||||
if support.PGO:
|
||||
raise unittest.SkipTest("test is not helpful for PGO")
|
||||
|
@ -1733,6 +1742,139 @@ class POSIXProcessTestCase(BaseTestCase):
|
|||
child_sid = int(output)
|
||||
self.assertNotEqual(parent_sid, child_sid)
|
||||
|
||||
@unittest.skipUnless(hasattr(os, 'setreuid'), 'no setreuid on platform')
|
||||
def test_user(self):
|
||||
# For code coverage of the user parameter. We don't care if we get an
|
||||
# EPERM error from it depending on the test execution environment, that
|
||||
# still indicates that it was called.
|
||||
|
||||
uid = os.geteuid()
|
||||
test_users = [65534 if uid != 65534 else 65533, uid]
|
||||
name_uid = "nobody" if sys.platform != 'darwin' else "unknown"
|
||||
|
||||
if pwd is not None:
|
||||
test_users.append(name_uid)
|
||||
|
||||
for user in test_users:
|
||||
with self.subTest(user=user):
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
[sys.executable, "-c",
|
||||
"import os; print(os.getuid())"],
|
||||
user=user)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EPERM:
|
||||
raise
|
||||
else:
|
||||
if isinstance(user, str):
|
||||
user_uid = pwd.getpwnam(user).pw_uid
|
||||
else:
|
||||
user_uid = user
|
||||
child_user = int(output)
|
||||
self.assertEqual(child_user, user_uid)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
subprocess.check_call([sys.executable, "-c", "pass"], user=-1)
|
||||
|
||||
if pwd is None:
|
||||
with self.assertRaises(ValueError):
|
||||
subprocess.check_call([sys.executable, "-c", "pass"], user=name_uid)
|
||||
|
||||
@unittest.skipIf(hasattr(os, 'setreuid'), 'setreuid() available on platform')
|
||||
def test_user_error(self):
|
||||
with self.assertRaises(ValueError):
|
||||
subprocess.check_call([sys.executable, "-c", "pass"], user=65535)
|
||||
|
||||
@unittest.skipUnless(hasattr(os, 'setregid'), 'no setregid() on platform')
|
||||
def test_group(self):
|
||||
gid = os.getegid()
|
||||
group_list = [65534 if gid != 65534 else 65533]
|
||||
name_group = "nogroup" if sys.platform != 'darwin' else "staff"
|
||||
|
||||
if grp is not None:
|
||||
group_list.append(name_group)
|
||||
|
||||
for group in group_list + [gid]:
|
||||
with self.subTest(group=group):
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
[sys.executable, "-c",
|
||||
"import os; print(os.getgid())"],
|
||||
group=group)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EPERM:
|
||||
raise
|
||||
else:
|
||||
if isinstance(group, str):
|
||||
group_gid = grp.getgrnam(group).gr_gid
|
||||
else:
|
||||
group_gid = group
|
||||
|
||||
child_group = int(output)
|
||||
self.assertEqual(child_group, group_gid)
|
||||
|
||||
# make sure we bomb on negative values
|
||||
with self.assertRaises(ValueError):
|
||||
subprocess.check_call([sys.executable, "-c", "pass"], group=-1)
|
||||
|
||||
if grp is None:
|
||||
with self.assertRaises(ValueError):
|
||||
subprocess.check_call([sys.executable, "-c", "pass"], group=name_group)
|
||||
|
||||
@unittest.skipIf(hasattr(os, 'setregid'), 'setregid() available on platform')
|
||||
def test_group_error(self):
|
||||
with self.assertRaises(ValueError):
|
||||
subprocess.check_call([sys.executable, "-c", "pass"], group=65535)
|
||||
|
||||
@unittest.skipUnless(hasattr(os, 'setgroups'), 'no setgroups() on platform')
|
||||
def test_extra_groups(self):
|
||||
gid = os.getegid()
|
||||
group_list = [65534 if gid != 65534 else 65533]
|
||||
name_group = "nogroup" if sys.platform != 'darwin' else "staff"
|
||||
perm_error = False
|
||||
|
||||
if grp is not None:
|
||||
group_list.append(name_group)
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
[sys.executable, "-c",
|
||||
"import os, sys, json; json.dump(os.getgroups(), sys.stdout)"],
|
||||
extra_groups=group_list)
|
||||
except OSError as ex:
|
||||
if ex.errno != errno.EPERM:
|
||||
raise
|
||||
perm_error = True
|
||||
|
||||
else:
|
||||
parent_groups = os.getgroups()
|
||||
child_groups = json.loads(output)
|
||||
|
||||
if grp is not None:
|
||||
desired_gids = [grp.getgrnam(g).gr_gid if isinstance(g, str) else g
|
||||
for g in group_list]
|
||||
else:
|
||||
desired_gids = group_list
|
||||
|
||||
if perm_error:
|
||||
self.assertEqual(set(child_groups), set(parent_groups))
|
||||
else:
|
||||
self.assertEqual(set(desired_gids), set(child_groups))
|
||||
|
||||
# make sure we bomb on negative values
|
||||
with self.assertRaises(ValueError):
|
||||
subprocess.check_call([sys.executable, "-c", "pass"], extra_groups=[-1])
|
||||
|
||||
if grp is None:
|
||||
with self.assertRaises(ValueError):
|
||||
subprocess.check_call([sys.executable, "-c", "pass"],
|
||||
extra_groups=[name_group])
|
||||
|
||||
@unittest.skipIf(hasattr(os, 'setgroups'), 'setgroups() available on platform')
|
||||
def test_extra_groups_error(self):
|
||||
with self.assertRaises(ValueError):
|
||||
subprocess.check_call([sys.executable, "-c", "pass"], extra_groups=[])
|
||||
|
||||
def test_run_abort(self):
|
||||
# returncode handles signal termination
|
||||
with support.SuppressCrashReport():
|
||||
|
@ -2782,13 +2924,23 @@ class POSIXProcessTestCase(BaseTestCase):
|
|||
([b"arg"], [b"exe"], 123, [b"env"]),
|
||||
([b"arg"], [b"exe"], None, 123),
|
||||
):
|
||||
with self.assertRaises(TypeError):
|
||||
with self.assertRaises(TypeError) as err:
|
||||
_posixsubprocess.fork_exec(
|
||||
args, exe_list,
|
||||
True, (), cwd, env_list,
|
||||
-1, -1, -1, -1,
|
||||
1, 2, 3, 4,
|
||||
True, True, func)
|
||||
True, True,
|
||||
False, [], 0,
|
||||
func)
|
||||
# Attempt to prevent
|
||||
# "TypeError: fork_exec() takes exactly N arguments (M given)"
|
||||
# from passing the test. More refactoring to have us start
|
||||
# with a valid *args list, confirm a good call with that works
|
||||
# before mutating it in various ways to ensure that bad calls
|
||||
# with individual arg type errors raise a typeerror would be
|
||||
# ideal. Saving that for a future PR...
|
||||
self.assertNotIn('takes exactly', str(err.exception))
|
||||
finally:
|
||||
if not gc_enabled:
|
||||
gc.disable()
|
||||
|
@ -2827,7 +2979,9 @@ class POSIXProcessTestCase(BaseTestCase):
|
|||
True, fds_to_keep, None, [b"env"],
|
||||
-1, -1, -1, -1,
|
||||
1, 2, 3, 4,
|
||||
True, True, None)
|
||||
True, True,
|
||||
None, None, None,
|
||||
None)
|
||||
self.assertIn('fds_to_keep', str(c.exception))
|
||||
finally:
|
||||
if not gc_enabled:
|
||||
|
@ -3239,7 +3393,7 @@ class MiscTests(unittest.TestCase):
|
|||
|
||||
def test__all__(self):
|
||||
"""Ensure that __all__ is populated properly."""
|
||||
intentionally_excluded = {"list2cmdline", "Handle"}
|
||||
intentionally_excluded = {"list2cmdline", "Handle", "pwd", "grp"}
|
||||
exported = set(subprocess.__all__)
|
||||
possible_exports = set()
|
||||
import types
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Added ``user``, ``group`` and ``extra_groups`` parameters to the
|
||||
subprocess.Popen constructor. Patch by Patrick McLean.
|
|
@ -20,6 +20,11 @@
|
|||
#ifdef HAVE_DIRENT_H
|
||||
#include <dirent.h>
|
||||
#endif
|
||||
#ifdef HAVE_GRP_H
|
||||
#include <grp.h>
|
||||
#endif /* HAVE_GRP_H */
|
||||
|
||||
#include "posixmodule.h"
|
||||
|
||||
#ifdef _Py_MEMORY_SANITIZER
|
||||
# include <sanitizer/msan_interface.h>
|
||||
|
@ -47,6 +52,12 @@
|
|||
# define FD_DIR "/proc/self/fd"
|
||||
#endif
|
||||
|
||||
#ifdef NGROUPS_MAX
|
||||
#define MAX_GROUPS NGROUPS_MAX
|
||||
#else
|
||||
#define MAX_GROUPS 64
|
||||
#endif
|
||||
|
||||
#define POSIX_CALL(call) do { if ((call) == -1) goto error; } while (0)
|
||||
|
||||
typedef struct {
|
||||
|
@ -415,6 +426,9 @@ child_exec(char *const exec_array[],
|
|||
int errpipe_read, int errpipe_write,
|
||||
int close_fds, int restore_signals,
|
||||
int call_setsid,
|
||||
int call_setgid, gid_t gid,
|
||||
int call_setgroups, size_t groups_size, const gid_t *groups,
|
||||
int call_setuid, uid_t uid,
|
||||
PyObject *py_fds_to_keep,
|
||||
PyObject *preexec_fn,
|
||||
PyObject *preexec_fn_args_tuple)
|
||||
|
@ -492,6 +506,22 @@ child_exec(char *const exec_array[],
|
|||
POSIX_CALL(setsid());
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_SETGROUPS
|
||||
if (call_setgroups)
|
||||
POSIX_CALL(setgroups(groups_size, groups));
|
||||
#endif /* HAVE_SETGROUPS */
|
||||
|
||||
#ifdef HAVE_SETREGID
|
||||
if (call_setgid)
|
||||
POSIX_CALL(setregid(gid, gid));
|
||||
#endif /* HAVE_SETREGID */
|
||||
|
||||
#ifdef HAVE_SETREUID
|
||||
if (call_setuid)
|
||||
POSIX_CALL(setreuid(uid, uid));
|
||||
#endif /* HAVE_SETREUID */
|
||||
|
||||
|
||||
reached_preexec = 1;
|
||||
if (preexec_fn != Py_None && preexec_fn_args_tuple) {
|
||||
/* This is where the user has asked us to deadlock their program. */
|
||||
|
@ -571,26 +601,33 @@ subprocess_fork_exec(PyObject* self, PyObject *args)
|
|||
PyObject *env_list, *preexec_fn;
|
||||
PyObject *process_args, *converted_args = NULL, *fast_args = NULL;
|
||||
PyObject *preexec_fn_args_tuple = NULL;
|
||||
PyObject *groups_list;
|
||||
PyObject *uid_object, *gid_object;
|
||||
int p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite;
|
||||
int errpipe_read, errpipe_write, close_fds, restore_signals;
|
||||
int call_setsid;
|
||||
int call_setgid = 0, call_setgroups = 0, call_setuid = 0;
|
||||
uid_t uid;
|
||||
gid_t gid, *groups = NULL;
|
||||
PyObject *cwd_obj, *cwd_obj2;
|
||||
const char *cwd;
|
||||
pid_t pid;
|
||||
int need_to_reenable_gc = 0;
|
||||
char *const *exec_array, *const *argv = NULL, *const *envp = NULL;
|
||||
Py_ssize_t arg_num;
|
||||
Py_ssize_t arg_num, num_groups = 0;
|
||||
int need_after_fork = 0;
|
||||
int saved_errno = 0;
|
||||
|
||||
if (!PyArg_ParseTuple(
|
||||
args, "OOpO!OOiiiiiiiiiiO:fork_exec",
|
||||
args, "OOpO!OOiiiiiiiiiiOOOO:fork_exec",
|
||||
&process_args, &executable_list,
|
||||
&close_fds, &PyTuple_Type, &py_fds_to_keep,
|
||||
&cwd_obj, &env_list,
|
||||
&p2cread, &p2cwrite, &c2pread, &c2pwrite,
|
||||
&errread, &errwrite, &errpipe_read, &errpipe_write,
|
||||
&restore_signals, &call_setsid, &preexec_fn))
|
||||
&restore_signals, &call_setsid,
|
||||
&gid_object, &groups_list, &uid_object,
|
||||
&preexec_fn))
|
||||
return NULL;
|
||||
|
||||
if ((preexec_fn != Py_None) &&
|
||||
|
@ -689,6 +726,90 @@ subprocess_fork_exec(PyObject* self, PyObject *args)
|
|||
cwd_obj2 = NULL;
|
||||
}
|
||||
|
||||
if (groups_list != Py_None) {
|
||||
#ifdef HAVE_SETGROUPS
|
||||
Py_ssize_t i;
|
||||
unsigned long gid;
|
||||
|
||||
if (!PyList_Check(groups_list)) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"setgroups argument must be a list");
|
||||
goto cleanup;
|
||||
}
|
||||
num_groups = PySequence_Size(groups_list);
|
||||
|
||||
if (num_groups < 0)
|
||||
goto cleanup;
|
||||
|
||||
if (num_groups > MAX_GROUPS) {
|
||||
PyErr_SetString(PyExc_ValueError, "too many groups");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if ((groups = PyMem_RawMalloc(num_groups * sizeof(gid_t))) == NULL) {
|
||||
PyErr_SetString(PyExc_MemoryError,
|
||||
"failed to allocate memory for group list");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
for (i = 0; i < num_groups; i++) {
|
||||
PyObject *elem;
|
||||
elem = PySequence_GetItem(groups_list, i);
|
||||
if (!elem)
|
||||
goto cleanup;
|
||||
if (!PyLong_Check(elem)) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"groups must be integers");
|
||||
Py_DECREF(elem);
|
||||
goto cleanup;
|
||||
} else {
|
||||
/* In posixmodule.c UnsignedLong is used as a fallback value
|
||||
* if the value provided does not fit in a Long. Since we are
|
||||
* already doing the bounds checking on the Python side, we
|
||||
* can go directly to an UnsignedLong here. */
|
||||
if (!_Py_Gid_Converter(elem, &gid)) {
|
||||
Py_DECREF(elem);
|
||||
PyErr_SetString(PyExc_ValueError, "invalid group id");
|
||||
goto cleanup;
|
||||
}
|
||||
groups[i] = gid;
|
||||
}
|
||||
Py_DECREF(elem);
|
||||
}
|
||||
call_setgroups = 1;
|
||||
|
||||
#else /* HAVE_SETGROUPS */
|
||||
PyErr_BadInternalCall();
|
||||
goto cleanup;
|
||||
#endif /* HAVE_SETGROUPS */
|
||||
}
|
||||
|
||||
if (gid_object != Py_None) {
|
||||
#ifdef HAVE_SETREGID
|
||||
if (!_Py_Gid_Converter(gid_object, &gid))
|
||||
goto cleanup;
|
||||
|
||||
call_setgid = 1;
|
||||
|
||||
#else /* HAVE_SETREGID */
|
||||
PyErr_BadInternalCall();
|
||||
goto cleanup;
|
||||
#endif /* HAVE_SETREUID */
|
||||
}
|
||||
|
||||
if (uid_object != Py_None) {
|
||||
#ifdef HAVE_SETREUID
|
||||
if (!_Py_Uid_Converter(uid_object, &uid))
|
||||
goto cleanup;
|
||||
|
||||
call_setuid = 1;
|
||||
|
||||
#else /* HAVE_SETREUID */
|
||||
PyErr_BadInternalCall();
|
||||
goto cleanup;
|
||||
#endif /* HAVE_SETREUID */
|
||||
}
|
||||
|
||||
/* This must be the last thing done before fork() because we do not
|
||||
* want to call PyOS_BeforeFork() if there is any chance of another
|
||||
* error leading to the cleanup: code without calling fork(). */
|
||||
|
@ -721,6 +842,8 @@ subprocess_fork_exec(PyObject* self, PyObject *args)
|
|||
p2cread, p2cwrite, c2pread, c2pwrite,
|
||||
errread, errwrite, errpipe_read, errpipe_write,
|
||||
close_fds, restore_signals, call_setsid,
|
||||
call_setgid, gid, call_setgroups, num_groups, groups,
|
||||
call_setuid, uid,
|
||||
py_fds_to_keep, preexec_fn, preexec_fn_args_tuple);
|
||||
_exit(255);
|
||||
return NULL; /* Dead code to avoid a potential compiler warning. */
|
||||
|
@ -765,6 +888,8 @@ cleanup:
|
|||
_Py_FreeCharPArray(argv);
|
||||
if (exec_array)
|
||||
_Py_FreeCharPArray(exec_array);
|
||||
|
||||
PyMem_RawFree(groups);
|
||||
Py_XDECREF(converted_args);
|
||||
Py_XDECREF(fast_args);
|
||||
Py_XDECREF(preexec_fn_args_tuple);
|
||||
|
@ -778,7 +903,10 @@ PyDoc_STRVAR(subprocess_fork_exec_doc,
|
|||
"fork_exec(args, executable_list, close_fds, cwd, env,\n\
|
||||
p2cread, p2cwrite, c2pread, c2pwrite,\n\
|
||||
errread, errwrite, errpipe_read, errpipe_write,\n\
|
||||
restore_signals, call_setsid, preexec_fn)\n\
|
||||
restore_signals, call_setsid,\n\
|
||||
call_setgid, gid, groups_size, gids,\n\
|
||||
call_setuid, uid,\n\
|
||||
preexec_fn)\n\
|
||||
\n\
|
||||
Forks a child process, closes parent file descriptors as appropriate in the\n\
|
||||
child and dups the few that are needed before calling exec() in the child\n\
|
||||
|
|
Loading…
Reference in New Issue