Skip to content
Open
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
46 changes: 46 additions & 0 deletions Lib/test/test_os/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -2271,6 +2271,52 @@ def test_dup2(self):
with open(dupfile, encoding="utf-8") as f:
self.assertEqual(f.read(), 'hello')

@support.requires_subprocess()
def test_env_none_with_environ_mutated_during_call(self):
# Regression test for gh-149509: when env is None the C-level
# ``environ`` array is borrowed, not copied. If anything mutates
# ``environ`` between argument parsing and the cleanup block
# (this is what an LD_PRELOAD interposer such as gprofng does),
# the previous identity check ``envlist != environ`` could
# mis-classify the borrowed pointer as owned and try to free it
# using an uninitialised count.
#
# The subprocess uses an audit hook to swap the global ``environ``
# pointer to a fresh array just before the spawn call. With the
# bug present this triggers a crash in the cleanup path; with the
# fix the borrowed pointer is left alone.
try:
import ctypes # noqa: F401
except ImportError:
self.skipTest("ctypes required")
spawn_name = self.spawn_func.__name__
code = textwrap.dedent(f"""
import ctypes
import os
import sys

libc = ctypes.CDLL(None)
environ_var = ctypes.c_void_p.in_dll(libc, 'environ')
saved = environ_var.value

# A fresh, empty environ array we substitute in.
replacement = (ctypes.c_char_p * 1)(None)
replacement_addr = ctypes.cast(replacement, ctypes.c_void_p).value

def hook(event, args):
if event == 'os.posix_spawn':
environ_var.value = replacement_addr

sys.addaudithook(hook)
try:
pid = os.{spawn_name}(sys.executable,
[sys.executable, '-c', 'pass'], None)
os.waitpid(pid, 0)
finally:
environ_var.value = saved
""")
assert_python_ok('-c', code)


@unittest.skipUnless(hasattr(os, 'posix_spawn'), "test needs os.posix_spawn")
@support.requires_subprocess()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix a crash in :func:`os.posix_spawn` and :func:`os.posix_spawnp` when
``env`` is ``None`` and the global ``environ`` array is mutated during
the call (for example, by an ``LD_PRELOAD`` interposer such as
``gprofng``). The cleanup path no longer attempts to free the borrowed
``environ`` pointer.
10 changes: 8 additions & 2 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -7903,11 +7903,12 @@ py_posix_spawn(int use_posix_spawnp, PyObject *module, path_t *path, PyObject *a
const char *func_name = use_posix_spawnp ? "posix_spawnp" : "posix_spawn";
EXECV_CHAR **argvlist = NULL;
EXECV_CHAR **envlist = NULL;
int envlist_owned = 0;
posix_spawn_file_actions_t file_actions_buf;
posix_spawn_file_actions_t *file_actionsp = NULL;
posix_spawnattr_t attr;
posix_spawnattr_t *attrp = NULL;
Py_ssize_t argc, envc;
Py_ssize_t argc, envc = 0;
Comment thread
picnixz marked this conversation as resolved.
PyObject *result = NULL;
PyObject *temp_buffer = NULL;
pid_t pid;
Expand Down Expand Up @@ -7966,6 +7967,7 @@ py_posix_spawn(int use_posix_spawnp, PyObject *module, path_t *path, PyObject *a
if (envlist == NULL) {
goto exit;
}
envlist_owned = 1;
}

if (file_actions != NULL && file_actions != Py_None) {
Expand Down Expand Up @@ -8028,7 +8030,11 @@ py_posix_spawn(int use_posix_spawnp, PyObject *module, path_t *path, PyObject *a
if (attrp) {
(void)posix_spawnattr_destroy(attrp);
}
if (envlist && envlist != environ) {
/* Only free envlist if we own it. Code that wraps posix_spawn (e.g.
gprofng) can mutate the global environ during the spawn call, which
would make `envlist != environ` true even for the borrowed case and
cause a free of process-owned memory with an uninitialized count. */
if (envlist_owned) {
free_string_array(envlist, envc);
}
if (argvlist) {
Expand Down
Loading