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
3 changes: 3 additions & 0 deletions Include/internal/pycore_fileutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions Lib/sysconfig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
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()
68 changes: 67 additions & 1 deletion Lib/test/test_sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):

Expand Down
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.
8 changes: 1 addition & 7 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}


Expand Down
3 changes: 1 addition & 2 deletions Python/errors.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
94 changes: 94 additions & 0 deletions Python/fileutils.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
17 changes: 11 additions & 6 deletions Python/remote_debugging.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
}

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.

7 changes: 7 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions pyconfig.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading