Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Lib/test/test_free_threading/test_os.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Make :func:`os.strerror` thread-safe: use the reentrant ``strerror_r()``
function if available. Patch by Victor Stinner.
73 changes: 73 additions & 0 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -13923,13 +13923,86 @@ 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
}


Expand Down
14 changes: 13 additions & 1 deletion Python/fileutils.c
Original file line number Diff line number Diff line change
Expand Up @@ -538,8 +538,14 @@ decode_current_locale(const char* arg, wchar_t **wstr, size_t *wlen,
// +1 to write also the trailing NUL character
size_t count = _Py_mbstowcs(res, arg, argsize + 1);
if (count != DECODE_ERROR) {
// Success
// String decoded successfully.

// gh-158893: This assertion can fail if the input string was
// mutated during this function call. For example, the assertion
// fails on decoding strerror() result if another thread mutated
// the string in-place by calling strerror() in parallel.
assert(count == argsize);

*wstr = res;
if (wlen != NULL) {
*wlen = count;
Expand Down Expand Up @@ -694,6 +700,7 @@ _Py_DecodeLocale(const char* arg, wchar_t **wstr, size_t *wlen,
assert(wstr != NULL);

#ifdef Py_DEBUG
size_t arglen = strlen(arg);
size_t wlen_canary = (size_t)-2;
if (wlen) {
*wlen = wlen_canary;
Expand All @@ -719,6 +726,11 @@ _Py_DecodeLocale(const char* arg, wchar_t **wstr, size_t *wlen,
// Success
assert(*wstr != NULL);
#ifdef Py_DEBUG
// gh-158893: Detect if the input string was mutated during the
// function call. For example, the assertion fails on decoding
// strerror() result if another thread mutated the string in-place by
// calling strerror() in parallel.
assert(strlen(arg) == arglen);
if (wlen != NULL) {
assert(*wlen == wcslen(*wstr));
}
Expand Down
44 changes: 44 additions & 0 deletions configure

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -5587,6 +5587,9 @@ AC_CHECK_FUNCS([ \
wait wait3 wait4 waitid waitpid wcscoll wcsftime wcsxfrm wmemcmp writev \
])

PY_CHECK_FUNC_PRIVATE([strerror_r], [@%:@include <string.h>])


# os.statx uses Linux's statx function. AIX also has a function named statx,
# but it's unrelated. Check only on Linux (including Android).
AS_CASE([$ac_sys_system],
Expand Down
3 changes: 3 additions & 0 deletions pyconfig.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -2214,6 +2214,9 @@
/* Define if you have the 'sinpi' function. */
#undef _Py_HAVE_SINPI

/* Define if you have the 'strerror_r' function. */
#undef _Py_HAVE_STRERROR_R

/* Define if you have the 'tanpi' function. */
#undef _Py_HAVE_TANPI

Expand Down
Loading