https://github.com/python/cpython/commit/9a45ab1d654fd2c91f79c1f6bc8caa3b12c28d2b
commit: 9a45ab1d654fd2c91f79c1f6bc8caa3b12c28d2b
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: gpshead <[email protected]>
date: 2026-06-29T00:56:40Z
summary:
[3.15] gh-151416: fix a borrowed ref potential use after free via fspath in
os.spawnv/spawnve (GH-151417) (#152535)
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)
Co-authored-by: Gregory P. Smith <[email protected]>
files:
A Misc/NEWS.d/next/Library/2026-06-12-00-00-00.gh-issue-151416.spawnUA.rst
M Lib/test/test_os/test_os.py
M Modules/posixmodule.c
diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py
index fef4f495fa56799..930a06963ac0585 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')
@@ -3498,6 +3507,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 000000000000000..fd034d9885a8302
--- /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 ce8b6d3ff4c6892..27ee07389f125ff 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -8195,18 +8195,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,
@@ -8224,14 +8221,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(
@@ -8302,7 +8314,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
@@ -8311,11 +8322,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,
@@ -8339,12 +8348,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]