This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: subprocess: add user, group and extra_groups parameters
Type: enhancement Stage: resolved
Components: Library (Lib) Versions: Python 3.9
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: gregory.p.smith Nosy List: desbma, giampaolo.rodola, gregory.p.smith, izbyshev, patrick.mclean, rhettinger, twouters, vstinner
Priority: normal Keywords: patch

Created on 2019-02-20 01:24 by patrick.mclean, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 11950 merged patrick.mclean, 2019-02-20 01:26
PR 16091 merged gregory.p.smith, 2019-09-13 11:56
PR 16384 merged vstinner, 2019-09-25 10:54
Messages (21)
msg336033 - (view) Author: Patrick McLean (patrick.mclean) * Date: 2019-02-20 01:24
Currently when using python to automate system administration tasks, it is useful to drop privileges sometimes. Currently the only way to do this is via a preexec_fn, which has well-documented problems. It would be useful to be able to pass a user and groups arguments to subprocess.popen.
msg336346 - (view) Author: Alexey Izbyshev (izbyshev) * (Python triager) Date: 2019-02-22 21:48
Patrick, could you provide more background that would explain your choice of setreuid/setregid functions and the desired handling of supplementary groups? I'm not a security expert, so I may not have sufficient expertise to judge on that, but maybe my questions could be helpful for others:

1) setreuid/setregid, as used in your PR, will set the real, effective and saved ids to the specified value [1]. So this precludes the use-case where a user wants to temporarily switch the effective id to the real id ("drop privileges") and then switch it back to the old effective (saved) id in the child. This use case is supported for non-root processes by POSIX_SPAWN_RESETIDS flag used with posix_spawn() (the flag is implemented by simply calling setuid(getuid()) in the child). Is it okay that the proposed API doesn't support this?

2) While setreuid/setregid on Linux permit setting the real id to either the effective or the real id, POSIX leaves it unspecified [2]. Are we okay with potential portability problems?

3) setgroups() always requires privileges on Linux [3]. So, if we always call setgroups() if subprocess.Popen(groups=...) is used, we preclude the use case where a non-privileged process wants to switch its gid but doesn't want to touch its supplementary groups. Is this a valid use case we want to care about?

The current workaround for the above is to call setgroups() only if geteuid() == 0, but I'm not sure it's a good one:
(a) ISTM we're basically guessing the intent here: what if a process really wants to change its supplementary groups, but a user run it without appropriate privileges by mistake? I think such process would like to get an exception instead of silently ignored call to setgroups().
(b) geteuid() == 0 test is not precise. The Linux man page documents that the caller needs the CAP_SETGID capability, which doesn't necessarily imply that the effective id is zero.

Another solution would be to split groups into two arguments: gid and supplementary groups. This way users can explicitly tell us whether they want to switch supplementary groups or not.

Overall, I'd really like to have security experts review this PR if possible.

[1] http://man7.org/linux/man-pages/man2/setreuid.2.html
[2] http://pubs.opengroup.org/onlinepubs/9699919799/functions/setreuid.html
[3] http://man7.org/linux/man-pages/man2/setgroups.2.html
msg336579 - (view) Author: Patrick McLean (patrick.mclean) * Date: 2019-02-25 23:41
Alexey, here are my responses to your points:

1) This is intentional, this is for dropping privileges before running some (possibly untrusted) command, we do not want to leave a path for the subprocess to gain root back. If there is a subprocess that needs root for some operations, it would presumably have the ability to drop privileges itself, and would not need the python script to do it before running it.

2) While POSIX leaves it unspecified what changes are permitted for an unprivileged process, these are permitted for a privileged process, which is the main use case for this functionality. In the case the OS does not support it for an unpriviliged process, the syscall would fail with EPERM, which can be handled from the calling python code.

I am fine removing the workaround, and allowing it to fail with EPERM. Perhaps we could find another way around this for running the tests in an unprivileged environment, or just leave the test only running the EPERM case...

I could change the API to have have group= and supp_groups=  if you prefer.
msg336713 - (view) Author: Alexey Izbyshev (izbyshev) * (Python triager) Date: 2019-02-26 21:21
> 1) This is intentional, this is for dropping privileges before running some (possibly untrusted) command, we do not want to leave a path for the subprocess to gain root back. If there is a subprocess that needs root for some operations, it would presumably have the ability to drop privileges itself, and would not need the python script to do it before running it.

> 2) While POSIX leaves it unspecified what changes are permitted for an unprivileged process, these are permitted for a privileged process, which is the main use case for this functionality. In the case the OS does not support it for an unpriviliged process, the syscall would fail with EPERM, which can be handled from the calling python code.

Thanks for your explanation. In case of a privileged process, the behavior of setreuid/setregid/setgroups does seem well-defined. But setuid/setgid change all ids (real, effective, saved) too in this case. Do you prefer setreuid/setregid because they provide stricter semantics in non-privileged processes compared to setuid/setgid? (The latter ones change the effective id only, potentially preserving the process ability to switch ids later).

> I am fine removing the workaround, and allowing it to fail with EPERM. Perhaps we could find another way around this for running the tests in an unprivileged environment, or just leave the test only running the EPERM case...

> I could change the API to have have group= and supp_groups=  if you prefer.

Personally, I do prefer separate arguments because subprocess is a relatively low-level module and tends to expose underlying operations, so it seems logical to have a knob that maps to setgid() and another one that maps to setgroups(). @gregory.p.smith: what do you think about it?
msg336720 - (view) Author: Patrick McLean (patrick.mclean) * Date: 2019-02-26 23:04
> Thanks for your explanation. In case of a privileged process, the behavior of setreuid/setregid/setgroups does seem well-defined. But setuid/setgid change all ids (real, effective, saved) too in this case. Do you prefer setreuid/setregid because they provide stricter semantics in non-privileged processes compared to setuid/setgid? (The latter ones change the effective id only, potentially preserving the process ability to switch ids later).

Yes, exactly. The stricter semantics provide stronger security guarantees. The idea is to run code in an unprivileged context in a way that the code has no way to regain privileges.
msg336794 - (view) Author: Patrick McLean (patrick.mclean) * Date: 2019-02-28 02:42
I have updated the pull request to include 'group' and 'extra_groups' as separate parameters.
msg337021 - (view) Author: Gregory P. Smith (gregory.p.smith) * (Python committer) Date: 2019-03-02 23:11
I like the separate parameters. :)
msg352216 - (view) Author: Gregory P. Smith (gregory.p.smith) * (Python committer) Date: 2019-09-12 17:15
New changeset 2b2ead74382513d0bb9ef34504e283a71e6a706f by Gregory P. Smith (Patrick McLean) in branch 'master':
bpo-36046: Add user and group parameters to subprocess (GH-11950)
https://github.com/python/cpython/commit/2b2ead74382513d0bb9ef34504e283a71e6a706f
msg352217 - (view) Author: Gregory P. Smith (gregory.p.smith) * (Python committer) Date: 2019-09-12 17:16
I'm curious to see what weird things the various buildbot platforms find for this one.  crossing my fingers.
msg352233 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2019-09-13 01:20
Thanks for working on this.  Least privilege is an important security consideration.  The world will be a better place if we limit harm from bad or broken actors.
msg352235 - (view) Author: Gregory P. Smith (gregory.p.smith) * (Python committer) Date: 2019-09-13 01:57
The 'Debian root' buildbot exposed a unittest issue to deal with:

https://buildbot.python.org/all/#/builders/27/builds/3702/steps/5/logs/stdio

======================================================================
ERROR: test_user (test.test_subprocess.POSIXProcessTestCase) (user=65534)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/test/test_subprocess.py", line 1761, in test_user
    output = subprocess.check_output(
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 419, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 497, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 941, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 1797, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
PermissionError: [Errno 13] Permission denied: '/root/buildarea/3.x.angelico-debian-amd64/build/python'
======================================================================
ERROR: test_user (test.test_subprocess.POSIXProcessTestCase) (user='nobody')
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/test/test_subprocess.py", line 1761, in test_user
    output = subprocess.check_output(
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 419, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 497, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 941, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 1797, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
PermissionError: [Errno 13] Permission denied: '/root/buildarea/3.x.angelico-debian-amd64/build/python'
msg352236 - (view) Author: Gregory P. Smith (gregory.p.smith) * (Python committer) Date: 2019-09-13 01:58
and some Fedora and RHEL bots are failing with:

======================================================================
ERROR: test_extra_groups (test.test_subprocess.POSIXProcessTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/shager/cpython-buildarea/3.x.edelsohn-fedora-ppc64/build/Lib/test/test_subprocess.py", line 1840, in test_extra_groups
    output = subprocess.check_output(
  File "/home/shager/cpython-buildarea/3.x.edelsohn-fedora-ppc64/build/Lib/subprocess.py", line 419, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/home/shager/cpython-buildarea/3.x.edelsohn-fedora-ppc64/build/Lib/subprocess.py", line 497, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/home/shager/cpython-buildarea/3.x.edelsohn-fedora-ppc64/build/Lib/subprocess.py", line 889, in __init__
    gids.append(grp.getgrnam(extra_group).gr_gid)
KeyError: "getgrnam(): name not found: 'nogroup'"
======================================================================
ERROR: test_group (test.test_subprocess.POSIXProcessTestCase) (group='nogroup')
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/shager/cpython-buildarea/3.x.edelsohn-fedora-ppc64/build/Lib/test/test_subprocess.py", line 1800, in test_group
    output = subprocess.check_output(
  File "/home/shager/cpython-buildarea/3.x.edelsohn-fedora-ppc64/build/Lib/subprocess.py", line 419, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/home/shager/cpython-buildarea/3.x.edelsohn-fedora-ppc64/build/Lib/subprocess.py", line 497, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/home/shager/cpython-buildarea/3.x.edelsohn-fedora-ppc64/build/Lib/subprocess.py", line 862, in __init__
    gid = grp.getgrnam(group).gr_gid
KeyError: "getgrnam(): name not found: 'nogroup'"
msg352237 - (view) Author: Gregory P. Smith (gregory.p.smith) * (Python committer) Date: 2019-09-13 02:01
AIX has the same error as RHEL and Fedora

https://buildbot.python.org/all/#/builders/161/builds/1615/steps/5/logs/stdio
msg352265 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-09-13 09:45
The test fails on Fedora:
https://buildbot.python.org/all/#/builders/185/builds/1028

vstinner@apu$ ./python -m test test_subprocess -m test_extra_groups -m test_group -v
== CPython 3.9.0a0 (heads/master:7cad53e6b0, Sep 13 2019, 11:42:25) [GCC 9.2.1 20190827 (Red Hat 9.2.1-1)]
== Linux-5.2.9-200.fc30.x86_64-x86_64-with-glibc2.29 little-endian
== cwd: /home/vstinner/python/master/build/test_python_14292
== CPU count: 8
== encodings: locale=UTF-8, FS=utf-8
Run tests sequentially
0:00:00 load avg: 0.38 [1/1] test_subprocess
test_extra_groups (test.test_subprocess.POSIXProcessTestCase) ... ERROR
test_group (test.test_subprocess.POSIXProcessTestCase) ... 
======================================================================
ERROR: test_extra_groups (test.test_subprocess.POSIXProcessTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vstinner/python/master/Lib/test/test_subprocess.py", line 1840, in test_extra_groups
    output = subprocess.check_output(
  File "/home/vstinner/python/master/Lib/subprocess.py", line 419, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/home/vstinner/python/master/Lib/subprocess.py", line 497, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/home/vstinner/python/master/Lib/subprocess.py", line 889, in __init__
    gids.append(grp.getgrnam(extra_group).gr_gid)
KeyError: "getgrnam(): name not found: 'nogroup'"

======================================================================
ERROR: test_group (test.test_subprocess.POSIXProcessTestCase) (group='nogroup')
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vstinner/python/master/Lib/test/test_subprocess.py", line 1800, in test_group
    output = subprocess.check_output(
  File "/home/vstinner/python/master/Lib/subprocess.py", line 419, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/home/vstinner/python/master/Lib/subprocess.py", line 497, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/home/vstinner/python/master/Lib/subprocess.py", line 862, in __init__
    gid = grp.getgrnam(group).gr_gid
KeyError: "getgrnam(): name not found: 'nogroup'"

----------------------------------------------------------------------

Ran 2 tests in 0.046s

FAILED (errors=2)
test test_subprocess failed
test_subprocess failed

== Tests result: FAILURE ==

1 test failed:
    test_subprocess

Total duration: 175 ms
Tests result: FAILURE
msg352268 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-09-13 09:46
Failures on Debian:
https://buildbot.python.org/all/#/builders/27/builds/3699

======================================================================
ERROR: test_user (test.test_subprocess.POSIXProcessTestCase) (user=65534)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/test/test_subprocess.py", line 1761, in test_user
    output = subprocess.check_output(
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 419, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 497, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 941, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 1797, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
PermissionError: [Errno 13] Permission denied: '/root/buildarea/3.x.angelico-debian-amd64/build/python'

======================================================================
ERROR: test_user (test.test_subprocess.POSIXProcessTestCase) (user='nobody')
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/test/test_subprocess.py", line 1761, in test_user
    output = subprocess.check_output(
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 419, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 497, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 941, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/root/buildarea/3.x.angelico-debian-amd64/build/Lib/subprocess.py", line 1797, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
PermissionError: [Errno 13] Permission denied: '/root/buildarea/3.x.angelico-debian-amd64/build/python'

----------------------------------------------------------------------

Ran 306 tests in 35.920s

FAILED (errors=2, skipped=32)
msg352333 - (view) Author: Thomas Wouters (twouters) * (Python committer) Date: 2019-09-13 13:43
New changeset 693aa80a434590ea7dcd35c000209e53d01b9425 by T. Wouters (Gregory P. Smith) in branch 'master':
bpo-36046: Fix buildbot failures (GH-16091)
https://github.com/python/cpython/commit/693aa80a434590ea7dcd35c000209e53d01b9425
msg353168 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-09-25 10:55
Hum, this change has a big (security?) issue:

import subprocess
subprocess.Popen(["/usr/bin/id"], user=1000, group=1000).wait()

gives:

uid=1000(vstinner) gid=1000(vstinner) groupes=1000(vstinner),0(root) contexte=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

But:

import subprocess
subprocess.Popen(["/usr/bin/id"], user=1000, group=1000, close_fds=False).wait()

gives:

uid=0(root) gid=0(root) groupes=0(root) contexte=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

=> user and group arguments are ignored when using close_fds=False: when subprocess uses posix_spawn().


Note: test_subprocess test_group() is skipped on my Fedora 30.


I wrote PR 16384 to fix the bug and enable test_group() on my Fedora (check also for "nobody" and "nfsnobody" groups).
msg353195 - (view) Author: Thomas Wouters (twouters) * (Python committer) Date: 2019-09-25 13:52
New changeset faca8553425c231d867dcabf6a69a9dd21118b6c by T. Wouters (Victor Stinner) in branch 'master':
bpo-36046: posix_spawn() doesn't support uid/gid (GH-16384)
https://github.com/python/cpython/commit/faca8553425c231d867dcabf6a69a9dd21118b6c
msg353197 - (view) Author: Thomas Wouters (twouters) * (Python committer) Date: 2019-09-25 13:55
As mentioned in the code review for Victor's fix, it feels like the big conditional on the use of self._posix_spawn in Popen._execute_child should perhaps be pulled into _posix_spawn, or at least refactored so it's closer to _posix_spawn, with some explanation about what is or isn't supported by _posix_spawn and why.
msg353198 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-09-25 13:57
> As mentioned in the code review for Victor's fix, it feels like the big conditional on the use of self._posix_spawn in Popen._execute_child should perhaps be pulled into _posix_spawn, or at least refactored so it's closer to _posix_spawn, with some explanation about what is or isn't supported by _posix_spawn and why.     

Maybe, yes. But having tests in the caller avoids to pass the very long list of arguments. Especially, it avoids to pass arguments which are not supposed by posix_spawn() :-)

_execute_child() has 21 arguments... is that really reasonable? Maybe we should put these stuff into a temporary object?
msg353199 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-09-25 13:57
The posix_spawn() bug is now fixed, I close again the issue.

Feel free to open a new issue if you want to follow-up on reworking the posix_spawn() code path ;-)
History
Date User Action Args
2022-04-11 14:59:11adminsetgithub: 80227
2019-09-25 13:57:50vstinnersetstatus: open -> closed
resolution: fixed
messages: + msg353199

stage: commit review -> resolved
2019-09-25 13:57:12vstinnersetmessages: + msg353198
2019-09-25 13:55:35twouterssetmessages: + msg353197
2019-09-25 13:52:53twouterssetmessages: + msg353195
2019-09-25 10:57:43vstinnersetstatus: closed -> open
resolution: fixed -> (no value)
title: support dropping privileges when running subprocesses -> subprocess: add user, group and extra_groups parameters
2019-09-25 10:55:38vstinnersetmessages: + msg353168
2019-09-25 10:54:28vstinnersetpull_requests: + pull_request15966
2019-09-25 05:40:24gregory.p.smithsetstatus: open -> closed
resolution: fixed
stage: patch review -> commit review
2019-09-17 16:55:23xtreaklinkissue38179 superseder
2019-09-13 13:43:38twouterssetnosy: + twouters
messages: + msg352333
2019-09-13 11:56:17gregory.p.smithsetpull_requests: + pull_request15712
2019-09-13 09:46:52vstinnersetmessages: + msg352268
2019-09-13 09:45:07vstinnersetnosy: + vstinner
messages: + msg352265
2019-09-13 02:01:22gregory.p.smithsetversions: + Python 3.9, - Python 3.8
2019-09-13 02:01:09gregory.p.smithsetmessages: + msg352237
2019-09-13 01:58:58gregory.p.smithsetmessages: + msg352236
2019-09-13 01:57:03gregory.p.smithsetmessages: + msg352235
2019-09-13 01:20:06rhettingersetnosy: + rhettinger
messages: + msg352233
2019-09-12 17:16:19gregory.p.smithsetmessages: + msg352217
2019-09-12 17:15:47gregory.p.smithsetmessages: + msg352216
2019-03-02 23:11:34gregory.p.smithsetmessages: + msg337021
2019-03-02 18:21:24desbmasetnosy: + desbma
2019-02-28 02:42:41patrick.mcleansetmessages: + msg336794
2019-02-26 23:04:20patrick.mcleansetmessages: + msg336720
2019-02-26 21:21:21izbyshevsetmessages: + msg336713
2019-02-25 23:41:01patrick.mcleansetmessages: + msg336579
2019-02-22 21:49:21izbyshevsetassignee: gregory.p.smith
2019-02-22 21:48:47izbyshevsetassignee: gregory.p.smith -> (no value)
messages: + msg336346
2019-02-21 23:45:43gregory.p.smithsetassignee: gregory.p.smith

nosy: + gregory.p.smith
2019-02-20 21:21:11izbyshevsetnosy: + izbyshev
2019-02-20 18:52:44SilentGhostsetnosy: + giampaolo.rodola
2019-02-20 01:26:35patrick.mcleansetkeywords: + patch
stage: patch review
pull_requests: + pull_request11974
2019-02-20 01:24:18patrick.mcleancreate