https://github.com/python/cpython/commit/b23d657d0cc029a83cfe8a1a122005b4601c8e7e
commit: b23d657d0cc029a83cfe8a1a122005b4601c8e7e
branch: 3.14
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-10-10T20:54:38Z
summary:
[3.14] gh-158893: Make os.strerror() thread-safe (#159120) (#159145)
[3.15] gh-158893: Make os.strerror() thread-safe (#159120)
* gh-157695: Parse also _Py_HAVE_xxx variables in sysconfig (#158926)
Add parse_config_h() tests to test_sysconfig.
(cherry picked from commit 978a8befa5ec9b28b161a56b16d326330aef4733)
* gh-158893: Make os.strerror() thread-safe (#158927)
Make os.strerror() thread-safe: use the reentrant strerror_r()
function if available.
* The configure script now checks if strerror_r() is supported.
* Add a stress test to test_free_threading.test_os (new module).
* Add a comment on decode_current_locale() assertion which fails if
the input string is mutated.
* Add an assertion to _Py_DecodeLocale() to detect if the input
string was mutated during the function call.
* gh-158893: Add internal _Py_strerror() function (#158982)
Add a new internal _Py_strerror() function to Python/fileutils.c. It
uses strerror_r() if available, or use strerror() otherwise.
Replace all strerror(code) calls with _Py_strerror(code).
(cherry picked from commit 15dd7356f300733b9c3fcc35f0d75e61a6ef3e05)
(cherry picked from commit c774d1526f32057c3a34e0ff0a217a1acc8bf43a)
files:
A Lib/test/test_free_threading/test_os.py
A Misc/NEWS.d/next/Library/2026-10-06-20-03-01.gh-issue-158893.B6A53q.rst
M Include/internal/pycore_fileutils.h
M Lib/sysconfig/__init__.py
M Lib/test/test_sysconfig.py
M Modules/posixmodule.c
M Python/errors.c
M Python/fileutils.c
M Python/remote_debugging.c
M configure
M configure.ac
M pyconfig.h.in
diff --git a/Include/internal/pycore_fileutils.h
b/Include/internal/pycore_fileutils.h
index 2c6d6daa01994ee..c295ed124ed183e 100644
--- a/Include/internal/pycore_fileutils.h
+++ b/Include/internal/pycore_fileutils.h
@@ -314,6 +314,9 @@ extern int _Py_GetTicksPerSecond(long *ticks_per_second);
// Export for '_testcapi' shared extension
PyAPI_FUNC(int) _Py_IsValidFD(int fd);
+// Export for '_remote_debugging' shared extension
+PyAPI_FUNC(PyObject*) _Py_strerror(int code);
+
#ifdef __cplusplus
}
#endif
diff --git a/Lib/sysconfig/__init__.py b/Lib/sysconfig/__init__.py
index faf8273bd0b31f3..a91c27f2e8f71b4 100644
--- a/Lib/sysconfig/__init__.py
+++ b/Lib/sysconfig/__init__.py
@@ -440,8 +440,9 @@ def parse_config_h(fp, vars=None):
if vars is None:
vars = {}
import re
- define_rx = re.compile("#define ([A-Z][A-Za-z0-9_]+) (.*)\n")
- undef_rx = re.compile("/[*] #undef ([A-Z][A-Za-z0-9_]+) [*]/\n")
+ name_rx = '(?:[A-Z]|_Py_)[A-Za-z0-9_]+'
+ define_rx = re.compile(fr"#define ({name_rx}) (.*)\n")
+ undef_rx = re.compile(fr"/[*] #undef ({name_rx}) [*]/\n")
while True:
line = fp.readline()
diff --git a/Lib/test/test_free_threading/test_os.py
b/Lib/test/test_free_threading/test_os.py
new file mode 100644
index 000000000000000..72582da29015e93
--- /dev/null
+++ b/Lib/test/test_free_threading/test_os.py
@@ -0,0 +1,36 @@
+import errno
+import os
+import sysconfig
+import unittest
+
+from test.support import threading_helper
+from test.support.threading_helper import run_concurrently
+
+
+NTHREADS = 10
+
+
+@threading_helper.requires_working_threading()
+class TestOs(unittest.TestCase):
+ @unittest.skipUnless(sysconfig.get_config_var('_Py_HAVE_STRERROR_R'),
+ 'need _Py_HAVE_STRERROR_R macro')
+ def test_strerror(self):
+ # gh-158893: os.strerror() is implemented with strerror_r() which is
+ # thread safe. Well, check if it's actually the case.
+ last_error = max([getattr(errno, name) for name in dir(errno)
+ if name.startswith('E')])
+ test_errors = tuple(range(1, last_error + 1))
+ loops = 20
+
+ def worker():
+ for _ in range(loops):
+ for i in test_errors:
+ os.strerror(i)
+
+ run_concurrently(
+ worker_func=worker, nthreads=NTHREADS
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py
index 1fe4b6849fc9194..5a5f48af1998b13 100644
--- a/Lib/test/test_sysconfig.py
+++ b/Lib/test/test_sysconfig.py
@@ -28,7 +28,7 @@
get_path, get_path_names, _INSTALL_SCHEMES,
get_default_scheme, get_scheme_names, get_config_var,
_expand_vars, _get_preferred_schemes,
- is_python_build, _PROJECT_BASE)
+ is_python_build, _PROJECT_BASE, parse_config_h)
from sysconfig.__main__ import _main, _parse_makefile, _get_pybuilddir,
_get_json_data_name
import _imp
import _osx_support
@@ -749,6 +749,72 @@ def test_sysconfig_config_vars_no_prefix_cache(self):
self.assertEqual(config_vars['exec_prefix'], sys.exec_prefix)
self.assertEqual(config_vars['platbase'], sys.exec_prefix)
+ def test_parse_config_h(self):
+ config = textwrap.dedent('''
+ #ifndef Py_PYCONFIG_H
+ #define Py_PYCONFIG_H
+
+ /* C comment */
+
+ #define ALIGNOF_LONG 8
+ #define HAVE_ACCEPT 1
+ #define _Py_HAVE_COSPI 1
+ #define INVALID_NUMBER abc
+ #define ALT_SOABI "cpython-316t-x86_64-linux-gnu"
+
+ // Undef macros must be written as "/* #undef NAME */":
+ // name must be valid and there is not value.
+ /* #undef ANDROID_API_LEVEL */
+ #undef IGNORE_UNDEF
+ /* #undef IGNORE_VALUE 1 */
+
+ # _ALWAYS_STR: don't convert values to an integer
+ #define IPHONEOS_DEPLOYMENT_TARGET "13.0"
+ #define MACOSX_DEPLOYMENT_TARGET 10
+
+ // Spaces are tolerated after the name, not before
+ #define SPACES_AFTER 1
+ #define IGNORED_SPACES_BEFORE 1
+
+ // Ignore macro without value
+ #define IGNORE_NO_VALUE
+
+ // Ignore macros with an invalid name
+ #define _PRIVATE_IGNORED 1
+ #define aLOWER_IGNORED 1
+ #define 123IGNORED 1
+ #define INVALID-NAME 1
+ #define INVALID#NAME 1
+ #define NONASCII_NAME_é 1
+
+ // Ignore single letter names
+ #define A 1
+ /* #undef A */
+
+ #endif /*Py_PYCONFIG_H*/
+ ''')
+
+ filename = TESTFN
+ self.addCleanup(unlink, filename)
+ with open(filename, "w", encoding="utf-8") as fp:
+ fp.write(config)
+ vars = {}
+ # In Python 3.14, quotes are not stripped
+ with open(filename, encoding="utf-8") as fp:
+ parse_config_h(fp, vars)
+ expected = {
+ 'ALIGNOF_LONG': 8,
+ 'HAVE_ACCEPT': 1,
+ '_Py_HAVE_COSPI': 1,
+ 'INVALID_NUMBER': 'abc',
+ 'ALT_SOABI': '"cpython-316t-x86_64-linux-gnu"',
+ 'ANDROID_API_LEVEL': 0,
+ 'IPHONEOS_DEPLOYMENT_TARGET': '"13.0"',
+ 'MACOSX_DEPLOYMENT_TARGET': '10', # str, not int
+ 'SPACES_AFTER': 1,
+ }
+ self.assertEqual(vars, expected)
+
class MakefileTests(unittest.TestCase):
diff --git
a/Misc/NEWS.d/next/Library/2026-10-06-20-03-01.gh-issue-158893.B6A53q.rst
b/Misc/NEWS.d/next/Library/2026-10-06-20-03-01.gh-issue-158893.B6A53q.rst
new file mode 100644
index 000000000000000..9345b46da0f4762
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-06-20-03-01.gh-issue-158893.B6A53q.rst
@@ -0,0 +1,2 @@
+Make :func:`os.strerror` thread-safe: use the reentrant ``strerror_r()``
+function if available. Patch by Victor Stinner.
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index 7354082ea0490e6..09ad975b259e672 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -13276,13 +13276,7 @@ static PyObject *
os_strerror_impl(PyObject *module, int code)
/*[clinic end generated code: output=baebf09fa02a78f2 input=75a8673d97915a91]*/
{
- char *message = strerror(code);
- if (message == NULL) {
- PyErr_SetString(PyExc_ValueError,
- "strerror() argument out of range");
- return NULL;
- }
- return PyUnicode_DecodeLocale(message, "surrogateescape");
+ return _Py_strerror(code);
}
diff --git a/Python/errors.c b/Python/errors.c
index 1549b5055f2df4f..d33fdba63cf110d 100644
--- a/Python/errors.c
+++ b/Python/errors.c
@@ -841,8 +841,7 @@ PyErr_SetFromErrnoWithFilenameObjects(PyObject *exc,
PyObject *filenameObject, P
#ifndef MS_WINDOWS
if (i != 0) {
- const char *s = strerror(i);
- message = PyUnicode_DecodeLocale(s, "surrogateescape");
+ message = _Py_strerror(i);
}
else {
/* Sometimes errno didn't get set */
diff --git a/Python/fileutils.c b/Python/fileutils.c
index 3eb682b2a3b46de..d5495ecf34d0a33 100644
--- a/Python/fileutils.c
+++ b/Python/fileutils.c
@@ -3136,3 +3136,97 @@ _Py_IsValidFD(int fd)
return (fstat(fd, &st) == 0);
#endif
}
+
+
+// Call strerror_r(code) if available, or use strerror() otherwise. Decode the
+// result from the locale encoding using surrogateescape error handler.
+//
+// On success, return a Unicode string. On error, set an exception and return
+// NULL.
+PyObject*
+_Py_strerror(int code)
+/*[clinic end generated code: output=baebf09fa02a78f2 input=75a8673d97915a91]*/
+{
+ const char *errors = "surrogateescape";
+
+#ifdef _Py_HAVE_STRERROR_R
+ // Check which strerror_r() API is used
+# if defined(__GLIBC__) && !((_POSIX_C_SOURCE >= 200112L) &&
!defined(_GNU_SOURCE))
+# define Py_STRERROR_R_GNU
+# elif defined(__ANDROID__) && defined(_GNU_SOURCE)
+# define Py_STRERROR_R_GNU
+# endif
+#endif
+
+#ifdef Py_STRERROR_R_GNU
+ // Implementation for the GNU flavor of strerror_r()
+
+ // On Linux, the longest translated strerror() message is 86 bytes
+ // (including the NUL byte).
+ char buffer[100];
+ char *message = strerror_r(code, buffer, Py_ARRAY_LENGTH(buffer));
+ // The strerror_r() GNU flavor doesn't provide a way to check if the error
+ // message was truncated or not.
+ //
+ // When the buffer is used, a trailing NUL byte is always written.
+ assert(message != buffer || memchr(buffer, 0, Py_ARRAY_LENGTH(buffer)) !=
NULL);
+ return PyUnicode_DecodeLocale(message, errors);
+
+#elif defined(_Py_HAVE_STRERROR_R)
+ // Implementation for the XSI-compliant flavor of strerror_r()
+
+ // On Linux and FreeBSD, the longest translated strerror() message is 86
+ // bytes (including the NUL byte).
+ char small_buffer[100];
+ size_t buflen = Py_ARRAY_LENGTH(small_buffer);
+ char *buffer = NULL;
+#ifndef NDEBUG
+ // Make sure that strerror_r() writes a trailing null byte
+ small_buffer[buflen - 1] = '#';
+#endif
+ int len = strerror_r(code, small_buffer, buflen);
+ if (len == ERANGE) {
+ while (len == ERANGE) {
+ if (buflen > (size_t)PY_SSIZE_T_MAX / 2) {
+ PyMem_Free(buffer);
+ PyErr_NoMemory();
+ return NULL;
+ }
+ buflen = buflen * 2;
+
+ char *new_buffer = PyMem_Realloc(buffer, buflen);
+ if (new_buffer == NULL) {
+ PyMem_Free(buffer);
+ PyErr_NoMemory();
+ return NULL;
+ }
+ buffer = new_buffer;
+#ifndef NDEBUG
+ buffer[buflen - 1] = '#';
+#endif
+ len = strerror_r(code, buffer, buflen);
+ }
+ }
+ else {
+ buffer = small_buffer;
+ }
+
+ // strerror_r() always writes a trailing NUL byte
+ assert(memchr(buffer, 0, buflen) != NULL);
+ PyObject *result = PyUnicode_DecodeLocale(buffer, errors);
+ if (buffer != small_buffer) {
+ PyMem_Free(buffer);
+ }
+ return result;
+
+#else
+ // strerror() implementation (usually not thread-safe)
+ char *message = strerror(code);
+ if (message == NULL) {
+ PyErr_SetString(PyExc_ValueError,
+ "strerror() argument out of range");
+ return NULL;
+ }
+ return PyUnicode_DecodeLocale(message, errors);
+#endif
+}
diff --git a/Python/remote_debugging.c b/Python/remote_debugging.c
index 82e58f03966de30..b77596c4b6cb263 100644
--- a/Python/remote_debugging.c
+++ b/Python/remote_debugging.c
@@ -2,8 +2,9 @@
#include "pyconfig.h"
#include "Python.h"
-#include "internal/pycore_runtime.h"
-#include "internal/pycore_ceval.h"
+#include "pycore_runtime.h"
+#include "pycore_ceval.h"
+#include "pycore_fileutils.h" // _Py_strerror()
#if defined(Py_REMOTE_DEBUG) && defined(Py_SUPPORTS_REMOTE_DEBUG)
#include "remote_debug.h"
@@ -121,10 +122,14 @@ write_memory(proc_handle_t *handle, uintptr_t
remote_address, size_t len, const
}
errno = err;
PyErr_SetFromErrno(PyExc_OSError);
- _set_debug_exception_cause(PyExc_OSError,
- "process_vm_writev failed for PID %d at address 0x%lx "
- "(size %zu, partial write %zd bytes): %s",
- handle->pid, remote_address + result, len - result, result,
strerror(err));
+ PyObject *message = _Py_strerror(err);
+ if (message != NULL) {
+ _set_debug_exception_cause(PyExc_OSError,
+ "process_vm_writev failed for PID %d at address 0x%lx "
+ "(size %zu, partial write %zd bytes): %s",
+ handle->pid, remote_address + result, len - result,
result, message);
+ Py_DECREF(message);
+ }
return -1;
}
diff --git a/configure b/configure
index 9b0c81ac6f96a77..22a2639db6958d9 100755
--- a/configure
+++ b/configure
@@ -3568,6 +3568,8 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+
if test "$srcdir" != . -a "$srcdir" != "$(pwd)"; then
# If we're building out-of-tree, we need to make sure the following
# resources get picked up before their $srcdir counterparts.
@@ -20562,6 +20564,48 @@ then :
fi
+
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for strerror_r" >&5
+printf %s "checking for strerror_r... " >&6; }
+if test ${ac_cv_func_strerror_r+y}
+then :
+ printf %s "(cached) " >&6
+else case e in #(
+ e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+#include <string.h>
+int
+main (void)
+{
+void *x=strerror_r
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_func_strerror_r=yes
+else case e in #(
+ e) ac_cv_func_strerror_r=no ;;
+esac
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+ ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_func_strerror_r"
>&5
+printf "%s\n" "$ac_cv_func_strerror_r" >&6; }
+ if test "x$ac_cv_func_strerror_r" = xyes
+then :
+
+printf "%s\n" "#define _Py_HAVE_STRERROR_R 1" >>confdefs.h
+
+fi
+
+
+
+
# Force lchmod off for Linux. Linux disallows changing the mode of symbolic
# links. Some libc implementations have a stub lchmod implementation that
always
# returns an error.
diff --git a/configure.ac b/configure.ac
index 645ecfccc4846da..963e6ccee0d0542 100644
--- a/configure.ac
+++ b/configure.ac
@@ -73,6 +73,12 @@ AC_DEFUN([PY_CHECK_FUNC],
AS_VAR_POPDEF([py_define])
])
+dnl PY_CHECK_FUNC_PRIVATE(FUNCTION, [INCLUDES], [AC_DEFINE-VAR])
+dnl Similar to PY_CHECK_FUNC but define macro _Py_HAVE_xxx instead of HAVE_xxx
+AC_DEFUN([PY_CHECK_FUNC_PRIVATE],
+[ PY_CHECK_FUNC([$1], [$2], m4_ifblank([$3], [[_Py_HAVE_]m4_toupper($1)],
[$3]))
+])
+
dnl PY_CHECK_LIB(LIBRARY, FUNCTION, [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND],
[OTHER-LIBRARIES])
dnl Like AC_CHECK_LIB() but does not modify LIBS
AC_DEFUN([PY_CHECK_LIB],
@@ -5353,6 +5359,7 @@ AC_CHECK_FUNCS([ \
wait wait3 wait4 waitid waitpid wcscoll wcsftime wcsxfrm wmemcmp writev \
])
+PY_CHECK_FUNC_PRIVATE([strerror_r], [@%:@include <string.h>])
# Force lchmod off for Linux. Linux disallows changing the mode of symbolic
# links. Some libc implementations have a stub lchmod implementation that
always
# returns an error.
diff --git a/pyconfig.h.in b/pyconfig.h.in
index bb6bc2b8caf8b44..cfff481bae3f21b 100644
--- a/pyconfig.h.in
+++ b/pyconfig.h.in
@@ -2047,6 +2047,9 @@
/* HACL* library can compile SIMD256 implementations */
#undef _Py_HACL_CAN_COMPILE_VEC256
+/* Define if you have the 'strerror_r' function. */
+#undef _Py_HAVE_STRERROR_R
+
/* Thread stack size set by the linker (in bytes). */
#undef _Py_LINKER_THREAD_STACK_SIZE
_______________________________________________
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]