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 3cf0199e88661a49162f0e2dfff9241874d5d7b0 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Fri, 12 Jun 2026 17:29:15 +0000 Subject: [PATCH 2/2] gh-151416: Document the __fspath__ reentrancy hazard of the FS path C APIs PyUnicode_FSConverter(), PyUnicode_FSDecoder(), and PyOS_FSPath() call an object's __fspath__() method during conversion, so the caller must protect the input object with a strong reference that this method cannot release. The docs previously described only the result reference semantics and gave no hint that passing a reference borrowed from a mutable container is unsafe. --- Doc/c-api/sys.rst | 5 +++++ Doc/c-api/unicode.rst | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst index ee73c1c8adaa7b..f10ef008790309 100644 --- a/Doc/c-api/sys.rst +++ b/Doc/c-api/sys.rst @@ -16,6 +16,11 @@ Operating System Utilities :class:`str` or :class:`bytes` object. Otherwise :exc:`TypeError` is raised and ``NULL`` is returned. + .. note:: + + The caller must hold a :term:`strong reference` to *path* that + its :meth:`~os.PathLike.__fspath__` method cannot release. + .. versionadded:: 3.6 diff --git a/Doc/c-api/unicode.rst b/Doc/c-api/unicode.rst index 634dcbce7a5791..a66cf8eed75a49 100644 --- a/Doc/c-api/unicode.rst +++ b/Doc/c-api/unicode.rst @@ -925,6 +925,13 @@ conversion function: If *obj* is ``NULL``, the function releases a strong reference stored in the variable referred by *result* and returns ``1``. + .. note:: + + If *obj* is not a :class:`str` or :class:`bytes` instance, the + conversion calls its :meth:`~os.PathLike.__fspath__` method. The + caller must hold a :term:`strong reference` to *obj* that this + method cannot release. + .. versionadded:: 3.1 .. versionchanged:: 3.6 @@ -952,6 +959,13 @@ conversion function: If *obj* is ``NULL``, release the strong reference to the object referred to by *result* and return ``1``. + .. note:: + + If *obj* is not a :class:`str` or :class:`bytes` instance, the + conversion calls its :meth:`~os.PathLike.__fspath__` method. The + caller must hold a :term:`strong reference` to *obj* that this + method cannot release. + .. versionadded:: 3.2 .. versionchanged:: 3.6