From: Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com> Date: Mon, 29 Jun 2026 02:26:36 +0000 (+0200) Subject: [3.13] gh-151416: fix a borrowed ref potential use after free via fspath in os.spawnv... X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e041cdc3122102862297031282eac9bce5bc5b98;p=thirdparty%2FPython%2Fcpython.git [3.13] gh-151416: fix a borrowed ref potential use after free via fspath in os.spawnv/spawnve (GH-151417) (GH-152536) (#152539) [3.14] gh-151416: fix a borrowed ref potential use after free via fspath in os.spawnv/spawnve (GH-151417) (GH-152536) gh-151416: fix a borrowed ref potential use after free via fspath in os.spawnv/spawnve (GH-151417) * 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(). * 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. (cherry picked from commit 11a2482b8e5c3427f7f405049e6d42ef78739c14) (cherry picked from commit f57d3d6db39ea0bd39743f1a614b46cbefbfdab6) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> --- diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 9ee4fe564486..9facf47b4166 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -98,6 +98,15 @@ def create_file(filename, content=b'content'): fp.write(content) +# 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') @@ -3596,6 +3605,25 @@ class SpawnTests(unittest.TestCase): 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 new file mode 100644 index 000000000000..fd034d9885a8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst @@ -0,0 +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. :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 a633c5f86937..54718f07379c 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -7624,18 +7624,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, @@ -7653,14 +7650,29 @@ 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); - PyErr_SetString( - PyExc_TypeError, - "spawnv() arg 2 must contain only strings"); return NULL; } + if (!fsconvert_strdup(item, &argvlist[i])) { + Py_DECREF(item); + free_string_array(argvlist, i); + // 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); if (i == 0 && !argvlist[0][0]) { free_string_array(argvlist, i + 1); PyErr_SetString( @@ -7731,7 +7743,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 @@ -7740,11 +7751,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, @@ -7768,12 +7777,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(