Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 85bbfa8

Browse files
miss-islingtongpsheadserhiy-storchaka
authored
[3.12] gh-112334: Restore subprocess's use of vfork() & fix extra_groups=[] behavior (GH-112617) (#112731)
Restore `subprocess`'s intended use of `vfork()` by default for performance on Linux; also fixes the behavior of `extra_groups=[]` which was unintentionally broken in 3.12.0: Fixed a performance regression in 3.12's :mod:`subprocess` on Linux where it would no longer use the fast-path ``vfork()`` system call when it could have due to a logic bug, instead falling back to the safe but slower ``fork()``. Also fixed a security bug introduced in 3.12.0. If a value of ``extra_groups=[]`` was passed to :mod:`subprocess.Popen` or related APIs, the underlying ``setgroups(0, NULL)`` system call to clear the groups list would not be made in the child process prior to ``exec()``. The security issue was identified via code inspection in the process of fixing the first bug. Thanks to @vain for the detailed report and analysis in the initial bug on Github. (cherry picked from commit 9fe7655) + Reword NEWS for the bugfix/security release. (mentions the assigned CVE number) Co-authored-by: Gregory P. Smith [Google LLC] <[email protected]> Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 494cd50 commit 85bbfa8

File tree

3 files changed

+37
-23
lines changed

3 files changed

+37
-23
lines changed

Lib/test/test_subprocess.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,8 +2066,14 @@ def test_group_error(self):
20662066
def test_extra_groups(self):
20672067
gid = os.getegid()
20682068
group_list = [65534 if gid != 65534 else 65533]
2069+
self._test_extra_groups_impl(gid=gid, group_list=group_list)
2070+
2071+
@unittest.skipUnless(hasattr(os, 'setgroups'), 'no setgroups() on platform')
2072+
def test_extra_groups_empty_list(self):
2073+
self._test_extra_groups_impl(gid=os.getegid(), group_list=[])
2074+
2075+
def _test_extra_groups_impl(self, *, gid, group_list):
20692076
name_group = _get_test_grp_name()
2070-
perm_error = False
20712077

20722078
if grp is not None:
20732079
group_list.append(name_group)
@@ -2077,11 +2083,8 @@ def test_extra_groups(self):
20772083
[sys.executable, "-c",
20782084
"import os, sys, json; json.dump(os.getgroups(), sys.stdout)"],
20792085
extra_groups=group_list)
2080-
except OSError as ex:
2081-
if ex.errno != errno.EPERM:
2082-
raise
2083-
perm_error = True
2084-
2086+
except PermissionError:
2087+
self.skipTest("setgroup() EPERM; this test may require root.")
20852088
else:
20862089
parent_groups = os.getgroups()
20872090
child_groups = json.loads(output)
@@ -2092,12 +2095,15 @@ def test_extra_groups(self):
20922095
else:
20932096
desired_gids = group_list
20942097

2095-
if perm_error:
2096-
self.assertEqual(set(child_groups), set(parent_groups))
2097-
else:
2098-
self.assertEqual(set(desired_gids), set(child_groups))
2098+
self.assertEqual(set(desired_gids), set(child_groups))
20992099

2100-
# make sure we bomb on negative values
2100+
if grp is None:
2101+
with self.assertRaises(ValueError):
2102+
subprocess.check_call(ZERO_RETURN_CMD,
2103+
extra_groups=[name_group])
2104+
2105+
# No skip necessary, this test won't make it to a setgroup() call.
2106+
def test_extra_groups_invalid_gid_t_values(self):
21012107
with self.assertRaises(ValueError):
21022108
subprocess.check_call(ZERO_RETURN_CMD, extra_groups=[-1])
21032109

@@ -2106,16 +2112,6 @@ def test_extra_groups(self):
21062112
cwd=os.curdir, env=os.environ,
21072113
extra_groups=[2**64])
21082114

2109-
if grp is None:
2110-
with self.assertRaises(ValueError):
2111-
subprocess.check_call(ZERO_RETURN_CMD,
2112-
extra_groups=[name_group])
2113-
2114-
@unittest.skipIf(hasattr(os, 'setgroups'), 'setgroups() available on platform')
2115-
def test_extra_groups_error(self):
2116-
with self.assertRaises(ValueError):
2117-
subprocess.check_call(ZERO_RETURN_CMD, extra_groups=[])
2118-
21192115
@unittest.skipIf(mswindows or not hasattr(os, 'umask'),
21202116
'POSIX umask() is not available.')
21212117
def test_umask(self):
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Fixed a performance regression in 3.12's :mod:`subprocess` on Linux where it
2+
would no longer use the fast-path ``vfork()`` system call when it should have
3+
due to a logic bug, instead always falling back to the safe but slower ``fork()``.
4+
5+
Also fixed a related 3.12 security regression: If a value of ``extra_groups=[]``
6+
was passed to :mod:`subprocess.Popen` or related APIs, the underlying
7+
``setgroups(0, NULL)`` system call to clear the groups list would not be made
8+
in the child process prior to ``exec()``. This has been assigned CVE-2023-6507.
9+
10+
This was identified via code inspection in the process of fixing the first bug.

Modules/_posixsubprocess.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -682,8 +682,10 @@ child_exec(char *const exec_array[],
682682
#endif
683683

684684
#ifdef HAVE_SETGROUPS
685-
if (extra_group_size > 0)
685+
if (extra_group_size >= 0) {
686+
assert((extra_group_size == 0) == (extra_groups == NULL));
686687
POSIX_CALL(setgroups(extra_group_size, extra_groups));
688+
}
687689
#endif /* HAVE_SETGROUPS */
688690

689691
#ifdef HAVE_SETREGID
@@ -937,7 +939,6 @@ subprocess_fork_exec_impl(PyObject *module, PyObject *process_args,
937939
pid_t pid = -1;
938940
int need_to_reenable_gc = 0;
939941
char *const *argv = NULL, *const *envp = NULL;
940-
Py_ssize_t extra_group_size = 0;
941942
int need_after_fork = 0;
942943
int saved_errno = 0;
943944
int *c_fds_to_keep = NULL;
@@ -1018,6 +1019,13 @@ subprocess_fork_exec_impl(PyObject *module, PyObject *process_args,
10181019
cwd = PyBytes_AsString(cwd_obj2);
10191020
}
10201021

1022+
// Special initial value meaning that subprocess API was called with
1023+
// extra_groups=None leading to _posixsubprocess.fork_exec(gids=None).
1024+
// We use this to differentiate between code desiring a setgroups(0, NULL)
1025+
// call vs no call at all. The fast vfork() code path could be used when
1026+
// there is no setgroups call.
1027+
Py_ssize_t extra_group_size = -2;
1028+
10211029
if (extra_groups_packed != Py_None) {
10221030
#ifdef HAVE_SETGROUPS
10231031
if (!PyList_Check(extra_groups_packed)) {

0 commit comments

Comments
 (0)