From 67545c6a637b080506528c7c96abba3c2a19b2d5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 7 Oct 2026 21:50:40 +0200 Subject: [PATCH] gh-158893: Add internal _Py_strerror() function 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). --- Include/internal/pycore_fileutils.h | 3 + Modules/_remote_debugging/subprocess.c | 21 ++++-- Modules/_remote_debugging/threads.c | 45 ++++++++---- Modules/posixmodule.c | 81 +--------------------- Python/errors.c | 3 +- Python/fileutils.c | 94 ++++++++++++++++++++++++++ 6 files changed, 147 insertions(+), 100 deletions(-) diff --git a/Include/internal/pycore_fileutils.h b/Include/internal/pycore_fileutils.h index a765eb5fe2d3228..a5fe5206477d056 100644 --- a/Include/internal/pycore_fileutils.h +++ b/Include/internal/pycore_fileutils.h @@ -321,6 +321,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/Modules/_remote_debugging/subprocess.c b/Modules/_remote_debugging/subprocess.c index 1388a75a95c47be..eeb9693cef9222e 100644 --- a/Modules/_remote_debugging/subprocess.c +++ b/Modules/_remote_debugging/subprocess.c @@ -6,6 +6,7 @@ ******************************************************************************/ #include "_remote_debugging.h" +#include "pycore_fileutils.h" // _Py_strerror() #ifndef MS_WINDOWS #include @@ -229,9 +230,13 @@ get_child_pids_platform(pid_t target_pid, int recursive, pid_array_t *result) if (entry == NULL) { if (errno != 0) { int err = errno; - _set_debug_oserror_from_errno_with_filename(err, "/proc", - "Failed to read process directory '/proc': %s", - strerror(err)); + PyObject *message = _Py_strerror(err); + if (message != NULL) { + _set_debug_oserror_from_errno_with_filename(err, "/proc", + "Failed to read process directory '/proc': %S", + message); + Py_DECREF(message); + } goto done; } break; @@ -259,9 +264,13 @@ get_child_pids_platform(pid_t target_pid, int recursive, pid_array_t *result) if (closedir(proc_dir) != 0) { int err = errno; proc_dir = NULL; - _set_debug_oserror_from_errno_with_filename(err, "/proc", - "Failed to close process directory '/proc': %s", - strerror(err)); + PyObject *message = _Py_strerror(err); + if (message != NULL) { + _set_debug_oserror_from_errno_with_filename(err, "/proc", + "Failed to close process directory '/proc': %S", + message); + Py_DECREF(message); + } goto done; } proc_dir = NULL; diff --git a/Modules/_remote_debugging/threads.c b/Modules/_remote_debugging/threads.c index 198134fe6cfbea7..120065022ddc8ee 100644 --- a/Modules/_remote_debugging/threads.c +++ b/Modules/_remote_debugging/threads.c @@ -6,6 +6,7 @@ ******************************************************************************/ #include "_remote_debugging.h" +#include "pycore_fileutils.h" // _Py_strerror() #ifndef MS_WINDOWS #include @@ -716,9 +717,13 @@ read_thread_ids(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st int err = errno; closedir(dir); _Py_RemoteDebug_InitThreadsState(unwinder, st); - _set_debug_oserror_from_errno_with_filename(err, task_path, - "Failed to read process task directory '%s': %s", - task_path, strerror(err)); + PyObject *message = _Py_strerror(err); + if (message != NULL) { + _set_debug_oserror_from_errno_with_filename(err, task_path, + "Failed to read process task directory '%s': %S", + task_path, message); + Py_DECREF(message); + } return -1; } break; @@ -749,9 +754,13 @@ read_thread_ids(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st if (closedir(dir) != 0) { int err = errno; _Py_RemoteDebug_InitThreadsState(unwinder, st); - _set_debug_oserror_from_errno_with_filename(err, task_path, - "Failed to close process task directory '%s': %s", - task_path, strerror(err)); + PyObject *message = _Py_strerror(err); + if (message != NULL) { + _set_debug_oserror_from_errno_with_filename(err, task_path, + "Failed to close process task directory '%s': %S", + task_path, message); + Py_DECREF(message); + } return -1; } st->tids = unwinder->thread_tids; @@ -816,8 +825,12 @@ _Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_T } if (ret < 0) { detach_threads(st, seized); - _set_debug_oserror_from_errno(err, - "Failed to seize thread %d: %s", tid, strerror(err)); + PyObject *message = _Py_strerror(err); + if (message != NULL) { + _set_debug_oserror_from_errno(err, + "Failed to seize thread %d: %S", tid, message); + Py_DECREF(message); + } _Py_RemoteDebug_InitThreadsState(unwinder, st); return -1; } @@ -827,8 +840,12 @@ _Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_T err = errno; if (err != ESRCH) { detach_threads(st, seized); - _set_debug_oserror_from_errno(err, - "Failed to interrupt thread %d: %s", tid, strerror(err)); + PyObject *message = _Py_strerror(err); + if (message != NULL) { + _set_debug_oserror_from_errno(err, + "Failed to interrupt thread %d: %S", tid, message); + Py_DECREF(message); + } _Py_RemoteDebug_InitThreadsState(unwinder, st); return -1; } @@ -839,8 +856,12 @@ _Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_T err = errno; if (err != ECHILD && err != ESRCH) { detach_threads(st, seized); - _set_debug_oserror_from_errno(err, - "waitpid failed for thread %d: %s", tid, strerror(err)); + PyObject *message = _Py_strerror(err); + if (message != NULL) { + _set_debug_oserror_from_errno(err, + "waitpid failed for thread %d: %S", tid, message); + Py_DECREF(message); + } _Py_RemoteDebug_InitThreadsState(unwinder, st); return -1; } diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 7eb171641137565..028270e7b841fda 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -13923,86 +13923,7 @@ static PyObject * os_strerror_impl(PyObject *module, int code) /*[clinic end generated code: output=baebf09fa02a78f2 input=75a8673d97915a91]*/ { -#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, "surrogateescape"); - -#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, "surrogateescape"); - if (buffer != small_buffer) { - PyMem_Free(buffer); - } - return result; - -#else - // strerror() implementation - char *message = strerror(code); - if (message == NULL) { - PyErr_SetString(PyExc_ValueError, - "strerror() argument out of range"); - return NULL; - } - return PyUnicode_DecodeLocale(message, "surrogateescape"); -#endif + return _Py_strerror(code); } diff --git a/Python/errors.c b/Python/errors.c index edb1557e23f63bc..0645c688a9af7ed 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -842,8 +842,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 9deb474820b55f8..3ea0f59f69ad1ef 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -3302,3 +3302,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 +}