https://github.com/python/cpython/commit/11a2482b8e5c3427f7f405049e6d42ef78739c14
commit: 11a2482b8e5c3427f7f405049e6d42ef78739c14
branch: 3.14
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-06-28T19:03:03-07:00
summary:

[3.14] gh-151416: fix a borrowed ref potential use after free via fspath in 
os.spawnv/spawnve (GH-151417) (#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 f57d3d6db39ea0bd39743f1a614b46cbefbfdab6)

files:
A Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst
M Lib/test/test_os.py
M Modules/posixmodule.c

diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index f4953ef4faed35..0dcd62d205f632 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')
@@ -3762,6 +3771,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
new file mode 100644
index 00000000000000..fd034d9885a830
--- /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 b7aa5781f8ccd4..b73b9a8b728cd2 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -7729,18 +7729,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,
@@ -7758,14 +7755,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(
@@ -7836,7 +7848,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
@@ -7845,11 +7856,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,
@@ -7873,12 +7882,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(

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to