Skip to content

Commit 8b58cfb

Browse files
committed
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.
1 parent ece561e commit 8b58cfb

6 files changed

Lines changed: 169 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import errno
2+
import os
3+
import sysconfig
4+
import unittest
5+
6+
from test.support import threading_helper
7+
from test.support.threading_helper import run_concurrently
8+
9+
10+
NTHREADS = 10
11+
12+
13+
@threading_helper.requires_working_threading()
14+
class TestOs(unittest.TestCase):
15+
@unittest.skipUnless(sysconfig.get_config_var('_Py_HAVE_STRERROR_R'),
16+
'need _Py_HAVE_STRERROR_R macro')
17+
def test_strerror(self):
18+
# gh-158893: os.strerror() is implemented with strerror_r() which is
19+
# thread safe. Well, check if it's actually the case.
20+
last_error = max([getattr(errno, name) for name in dir(errno)
21+
if name.startswith('E')])
22+
test_errors = tuple(range(1, last_error + 1))
23+
loops = 20
24+
25+
def worker():
26+
for _ in range(loops):
27+
for i in test_errors:
28+
os.strerror(i)
29+
30+
run_concurrently(
31+
worker_func=worker, nthreads=NTHREADS
32+
)
33+
34+
35+
if __name__ == "__main__":
36+
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Make :func:`os.strerror` thread-safe: use the reentrant ``strerror_r()``
2+
function if available. Patch by Victor Stinner.

‎Modules/posixmodule.c‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13831,13 +13831,86 @@ static PyObject *
1383113831
os_strerror_impl(PyObject *module, int code)
1383213832
/*[clinic end generated code: output=baebf09fa02a78f2 input=75a8673d97915a91]*/
1383313833
{
13834+
#ifdef _Py_HAVE_STRERROR_R
13835+
// Check which strerror_r() API is used
13836+
# if defined(__GLIBC__) && !((_POSIX_C_SOURCE >= 200112L) && !defined(_GNU_SOURCE))
13837+
# define Py_STRERROR_R_GNU
13838+
# elif defined(__ANDROID__) && defined(_GNU_SOURCE)
13839+
# define Py_STRERROR_R_GNU
13840+
# endif
13841+
#endif
13842+
13843+
#ifdef Py_STRERROR_R_GNU
13844+
// Implementation for the GNU flavor of strerror_r()
13845+
13846+
// On Linux, the longest translated strerror() message is 86 bytes
13847+
// (including the NUL byte).
13848+
char buffer[100];
13849+
char *message = strerror_r(code, buffer, Py_ARRAY_LENGTH(buffer));
13850+
// The strerror_r() GNU flavor doesn't provide a way to check if the error
13851+
// message was truncated or not.
13852+
//
13853+
// When the buffer is used, a trailing NUL byte is always written.
13854+
assert(message != buffer || memchr(buffer, 0, Py_ARRAY_LENGTH(buffer)) != NULL);
13855+
return PyUnicode_DecodeLocale(message, "surrogateescape");
13856+
13857+
#elif defined(_Py_HAVE_STRERROR_R)
13858+
// Implementation for the XSI-compliant flavor of strerror_r()
13859+
13860+
// On Linux and FreeBSD, the longest translated strerror() message is 86
13861+
// bytes (including the NUL byte).
13862+
char small_buffer[100];
13863+
size_t buflen = Py_ARRAY_LENGTH(small_buffer);
13864+
char *buffer = NULL;
13865+
#ifndef NDEBUG
13866+
// Make sure that strerror_r() writes a trailing null byte
13867+
small_buffer[buflen - 1] = '#';
13868+
#endif
13869+
int len = strerror_r(code, small_buffer, buflen);
13870+
if (len == ERANGE) {
13871+
while (len == ERANGE) {
13872+
if (buflen > (size_t)PY_SSIZE_T_MAX / 2) {
13873+
PyMem_Free(buffer);
13874+
PyErr_NoMemory();
13875+
return NULL;
13876+
}
13877+
buflen = buflen * 2;
13878+
13879+
char *new_buffer = PyMem_Realloc(buffer, buflen);
13880+
if (new_buffer == NULL) {
13881+
PyMem_Free(buffer);
13882+
PyErr_NoMemory();
13883+
return NULL;
13884+
}
13885+
buffer = new_buffer;
13886+
#ifndef NDEBUG
13887+
buffer[buflen - 1] = '#';
13888+
#endif
13889+
len = strerror_r(code, buffer, buflen);
13890+
}
13891+
}
13892+
else {
13893+
buffer = small_buffer;
13894+
}
13895+
13896+
// strerror_r() always writes a trailing NUL byte
13897+
assert(memchr(buffer, 0, buflen) != NULL);
13898+
PyObject *result = PyUnicode_DecodeLocale(buffer, "surrogateescape");
13899+
if (buffer != small_buffer) {
13900+
PyMem_Free(buffer);
13901+
}
13902+
return result;
13903+
13904+
#else
13905+
// strerror() implementation
1383413906
char *message = strerror(code);
1383513907
if (message == NULL) {
1383613908
PyErr_SetString(PyExc_ValueError,
1383713909
"strerror() argument out of range");
1383813910
return NULL;
1383913911
}
1384013912
return PyUnicode_DecodeLocale(message, "surrogateescape");
13913+
#endif
1384113914
}
1384213915

1384313916

‎configure‎

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎configure.ac‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ AC_DEFUN([PY_CHECK_FUNC],
7373
AS_VAR_POPDEF([py_define])
7474
])
7575

76+
dnl PY_CHECK_FUNC_PRIVATE(FUNCTION, [INCLUDES], [AC_DEFINE-VAR])
77+
dnl Similar to PY_CHECK_FUNC but define macro _Py_HAVE_xxx instead of HAVE_xxx
78+
AC_DEFUN([PY_CHECK_FUNC_PRIVATE],
79+
[ PY_CHECK_FUNC([$1], [$2], m4_ifblank([$3], [[_Py_HAVE_]m4_toupper($1)], [$3]))
80+
])
81+
7682
dnl PY_CHECK_LIB(LIBRARY, FUNCTION, [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND], [OTHER-LIBRARIES])
7783
dnl Like AC_CHECK_LIB() but does not modify LIBS
7884
AC_DEFUN([PY_CHECK_LIB],
@@ -5516,6 +5522,9 @@ AC_CHECK_FUNCS([ \
55165522
wait wait3 wait4 waitid waitpid wcscoll wcsftime wcsxfrm wmemcmp writev \
55175523
])
55185524

5525+
PY_CHECK_FUNC_PRIVATE([strerror_r], [@%:@include <string.h>])
5526+
5527+
55195528
# os.statx uses Linux's statx function. AIX also has a function named statx,
55205529
# but it's unrelated. Check only on Linux (including Android).
55215530
AS_CASE([$ac_sys_system],

‎pyconfig.h.in‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2094,6 +2094,9 @@
20942094
/* Define if you have the 'PR_SET_VMA_ANON_NAME' constant. */
20952095
#undef _Py_HAVE_PR_SET_VMA_ANON_NAME
20962096

2097+
/* Define if you have the 'strerror_r' function. */
2098+
#undef _Py_HAVE_STRERROR_R
2099+
20972100
/* Thread stack size set by the linker (in bytes). */
20982101
#undef _Py_LINKER_THREAD_STACK_SIZE
20992102

0 commit comments

Comments
 (0)