From 3a46d7e4f10324746bda3c635cb03adbffde011d Mon Sep 17 00:00:00 2001 From: 1fanwang <1fannnw@gmail.com> Date: Tue, 12 May 2026 07:02:30 -0700 Subject: [PATCH 1/2] gh-149509: Fix os.posix_spawn crash when environ is mutated mid-call When env is None, py_posix_spawn aliases the global C environ array into envlist without copying it, but envc is left uninitialised. The cleanup block decided whether to free envlist by comparing it against environ. If something running during the call (an LD_PRELOAD interposer such as gprofng, or an audit hook that ends up modifying the environment) replaces environ with a different pointer, the identity check would mis-classify the borrowed pointer as owned and free it with the uninitialised envc, corrupting the process environment and typically crashing. Track ownership explicitly with an envlist_owned flag instead of inferring it from a pointer identity check, and initialise envc to 0. The regression test simulates the interposer using a sys audit hook that swaps environ via ctypes between argument parsing and the cleanup path. --- Lib/test/test_os/test_posix.py | 46 +++++++++++++++++++ ...-05-12-07-01-29.gh-issue-149509.WwKxjE.rst | 5 ++ Modules/posixmodule.c | 10 +++- 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-12-07-01-29.gh-issue-149509.WwKxjE.rst diff --git a/Lib/test/test_os/test_posix.py b/Lib/test/test_os/test_posix.py index 0e8495a4eff2ed..ae50bd338a79a0 100644 --- a/Lib/test/test_os/test_posix.py +++ b/Lib/test/test_os/test_posix.py @@ -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() diff --git a/Misc/NEWS.d/next/Library/2026-05-12-07-01-29.gh-issue-149509.WwKxjE.rst b/Misc/NEWS.d/next/Library/2026-05-12-07-01-29.gh-issue-149509.WwKxjE.rst new file mode 100644 index 00000000000000..b57d06fb646ca6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-12-07-01-29.gh-issue-149509.WwKxjE.rst @@ -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. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 5bd53c2146a822..4ff94aa76b92e8 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -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; PyObject *result = NULL; PyObject *temp_buffer = NULL; pid_t pid; @@ -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) { @@ -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) { From cd69d59848748f1546b9a76ea8c0b46ec5578402 Mon Sep 17 00:00:00 2001 From: 1fanwang <1fannnw@gmail.com> Date: Tue, 12 May 2026 22:44:31 -0700 Subject: [PATCH 2/2] Retrigger CI