From 8da22afb0e1ae5e1e7b2127b20683cf8deef63b3 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Fri, 12 Jun 2026 17:29:10 +0000 Subject: [PATCH 1/2] gh-151416: Fix use-after-free in os.spawnv/spawnve when __fspath__ mutates argv The argv conversion loops passed references borrowed from the argv list into fsconvert_strdup(). An item's __fspath__() can mutate the list and release its reference to the item, leaving the converter operating on a freed object. A shrunk list could also make PyList_GetItem() return NULL, which PyUnicode_FS{Converter,Decoder}() treat as a request to release an uninitialized output variable. Hold a strong reference to each item across the conversion, matching parse_arglist() and parse_envlist(). --- ...06-12-00-00-00.gh-issue-151416.spawnUA.rst | 3 ++ Modules/posixmodule.c | 35 +++++++++++++------ 2 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst diff --git a/Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst b/Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst new file mode 100644 index 00000000000000..4d4df908874795 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst @@ -0,0 +1,3 @@ +Fix a crash in :func:`os.spawnv` and :func:`os.spawnve` when an *argv* +item's :meth:`~os.PathLike.__fspath__` method mutates the *argv* list +during argument conversion. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 1f1b7fa729c01c..a0cd6fd78c7a78 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -8192,18 +8192,15 @@ os_spawnv_impl(PyObject *module, int mode, path_t *path, PyObject *argv) int i; Py_ssize_t argc; intptr_t spawnval; - PyObject *(*getitem)(PyObject *, Py_ssize_t); /* spawnv has three arguments: (mode, path, argv), where argv is a list or tuple of strings. */ if (PyList_Check(argv)) { argc = PyList_Size(argv); - getitem = PyList_GetItem; } else if (PyTuple_Check(argv)) { argc = PyTuple_Size(argv); - getitem = PyTuple_GetItem; } else { PyErr_SetString(PyExc_TypeError, @@ -8221,14 +8218,24 @@ os_spawnv_impl(PyObject *module, int mode, path_t *path, PyObject *argv) return PyErr_NoMemory(); } for (i = 0; i < argc; i++) { - if (!fsconvert_strdup((*getitem)(argv, i), - &argvlist[i])) { + // The item must be a strong reference because of possible + // side-effects of PyUnicode_FS{Converter,Decoder}() in + // fsconvert_strdup(): an item's __fspath__() can mutate a list + // *argv*, releasing the list's reference to the item (gh-151416). + PyObject *item = PySequence_ITEM(argv, i); + if (item == NULL) { + free_string_array(argvlist, i); + return NULL; + } + if (!fsconvert_strdup(item, &argvlist[i])) { + Py_DECREF(item); free_string_array(argvlist, i); PyErr_SetString( PyExc_TypeError, "spawnv() arg 2 must contain only strings"); return NULL; } + Py_DECREF(item); if (i == 0 && !argvlist[0][0]) { free_string_array(argvlist, i + 1); PyErr_SetString( @@ -8299,7 +8306,6 @@ os_spawnve_impl(PyObject *module, int mode, path_t *path, PyObject *argv, PyObject *res = NULL; Py_ssize_t argc, i, envc; intptr_t spawnval; - PyObject *(*getitem)(PyObject *, Py_ssize_t); Py_ssize_t lastarg = 0; /* spawnve has four arguments: (mode, path, argv, env), where @@ -8308,11 +8314,9 @@ os_spawnve_impl(PyObject *module, int mode, path_t *path, PyObject *argv, if (PyList_Check(argv)) { argc = PyList_Size(argv); - getitem = PyList_GetItem; } else if (PyTuple_Check(argv)) { argc = PyTuple_Size(argv); - getitem = PyTuple_GetItem; } else { PyErr_SetString(PyExc_TypeError, @@ -8336,12 +8340,21 @@ os_spawnve_impl(PyObject *module, int mode, path_t *path, PyObject *argv, goto fail_0; } for (i = 0; i < argc; i++) { - if (!fsconvert_strdup((*getitem)(argv, i), - &argvlist[i])) - { + // The item must be a strong reference because of possible + // side-effects of PyUnicode_FS{Converter,Decoder}() in + // fsconvert_strdup(): an item's __fspath__() can mutate a list + // *argv*, releasing the list's reference to the item (gh-151416). + PyObject *item = PySequence_ITEM(argv, i); + if (item == NULL) { lastarg = i; goto fail_1; } + if (!fsconvert_strdup(item, &argvlist[i])) { + Py_DECREF(item); + lastarg = i; + goto fail_1; + } + Py_DECREF(item); if (i == 0 && !argvlist[0][0]) { lastarg = i + 1; PyErr_SetString( From daf288e4ae47deae0ec852113502a3401ccda819 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sun, 28 Jun 2026 21:08:30 +0000 Subject: [PATCH 2/2] gh-151416: Don't mask non-TypeError argv conversion errors in os.spawnv os.spawnv() replaced any error raised during argv item conversion, such as MemoryError, codec errors, or the embedded-null ValueError, with a generic TypeError. Only add the contextual message when the conversion actually raised TypeError, matching how os.spawnve() and the exec functions propagate these errors. The test is gated to the native C spawnv: the Python fallback used elsewhere reports conversion failures from the forked child as exit status 127 instead of raising. --- Lib/test/test_os/test_os.py | 28 +++++++++++++++++++ ...06-12-00-00-00.gh-issue-151416.spawnUA.rst | 4 ++- Modules/posixmodule.c | 11 ++++++-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index b88388e091312a..c2a1c15e9bc61b 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -86,6 +86,15 @@ def requires_os_func(name): return unittest.skipUnless(hasattr(os, name), 'requires os.%s' % name) +# On platforms without a native spawnv(), os.py provides a Python fallback +# built on fork()+exec*() that reports argument conversion failures from the +# child as exit status 127 instead of raising, so tests of the C +# implementation's error paths cannot run against it. +requires_native_spawnv = unittest.skipUnless( + isinstance(getattr(os, 'spawnv', None), types.BuiltinFunctionType), + 'requires the native C os.spawnv') + + # bpo-41625: On AIX, splice() only works with a socket, not with a pipe. requires_splice_pipe = unittest.skipIf(sys.platform.startswith("aix"), 'on AIX, splice() only accepts sockets') @@ -3502,6 +3511,25 @@ def test_spawnve_noargs(self): self.assertRaises(ValueError, os.spawnve, os.P_NOWAIT, program, ('',), {}) self.assertRaises(ValueError, os.spawnve, os.P_NOWAIT, program, [''], {}) + @requires_native_spawnv + def test_spawnv_arg_conversion_errors(self): + # A non-path argv item gets a TypeError naming the argument... + with self.assertRaisesRegex(TypeError, 'must contain only strings'): + os.spawnv(os.P_NOWAIT, sys.executable, [sys.executable, 123]) + # ...but other conversion errors must not be masked as TypeError + # (gh-151416). + with self.assertRaises(ValueError): + os.spawnv(os.P_NOWAIT, sys.executable, + [sys.executable, 'embedded\0null']) + + class RaisingPath: + def __fspath__(self): + raise RuntimeError('gotcha') + + with self.assertRaisesRegex(RuntimeError, 'gotcha'): + os.spawnv(os.P_NOWAIT, sys.executable, + [sys.executable, RaisingPath()]) + def _test_invalid_env(self, spawn): program = sys.executable args = self.quote_args([program, '-c', 'pass']) diff --git a/Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst b/Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst index 4d4df908874795..fd034d9885a830 100644 --- a/Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst +++ b/Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst @@ -1,3 +1,5 @@ Fix a crash in :func:`os.spawnv` and :func:`os.spawnve` when an *argv* item's :meth:`~os.PathLike.__fspath__` method mutates the *argv* list -during argument conversion. +during argument conversion. :func:`!os.spawnv` argument conversion errors +other than :exc:`TypeError`, such as the :exc:`ValueError` for an embedded +null, are no longer replaced with a generic :exc:`TypeError`. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index a0cd6fd78c7a78..66c07e1eb39bfd 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -8230,9 +8230,14 @@ os_spawnv_impl(PyObject *module, int mode, path_t *path, PyObject *argv) if (!fsconvert_strdup(item, &argvlist[i])) { Py_DECREF(item); free_string_array(argvlist, i); - PyErr_SetString( - PyExc_TypeError, - "spawnv() arg 2 must contain only strings"); + // Add argument context to the converter's terse TypeError, but + // let MemoryError, codec errors, embedded-null ValueError, etc. + // propagate unmasked. + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_SetString( + PyExc_TypeError, + "spawnv() arg 2 must contain only strings"); + } return NULL; } Py_DECREF(item);