[3.14] gh-139653: Add PyUnstable_ThreadState_SetStackProtection() (GH-139668) (#141661)

Co-authored-by: Rok Mandeljc <rok.mandeljc@gmail.com>
Co-authored-by: Mark Shannon <mark@hotpy.org>
Co-authored-by: Victor Stinner <vstinner@python.org>
This commit is contained in:
Petr Viktorin 2025-11-25 14:21:53 +01:00 committed by GitHub
parent 11e3fc9636
commit 32a38a2523
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 9871 additions and 9561 deletions

View file

@ -984,6 +984,9 @@ because the :ref:`call protocol <call>` takes care of recursion handling.
be concatenated to the :exc:`RecursionError` message caused by the recursion be concatenated to the :exc:`RecursionError` message caused by the recursion
depth limit. depth limit.
.. seealso::
The :c:func:`PyUnstable_ThreadState_SetStackProtection` function.
.. versionchanged:: 3.9 .. versionchanged:: 3.9
This function is now also available in the :ref:`limited API <limited-c-api>`. This function is now also available in the :ref:`limited API <limited-c-api>`.

View file

@ -1531,6 +1531,63 @@ All of the following functions must be called after :c:func:`Py_Initialize`.
.. versionadded:: 3.11 .. versionadded:: 3.11
.. c:function:: int PyUnstable_ThreadState_SetStackProtection(PyThreadState *tstate, void *stack_start_addr, size_t stack_size)
Set the stack protection start address and stack protection size
of a Python thread state.
On success, return ``0``.
On failure, set an exception and return ``-1``.
CPython implements :ref:`recursion control <recursion>` for C code by raising
:py:exc:`RecursionError` when it notices that the machine execution stack is close
to overflow. See for example the :c:func:`Py_EnterRecursiveCall` function.
For this, it needs to know the location of the current thread's stack, which it
normally gets from the operating system.
When the stack is changed, for example using context switching techniques like the
Boost library's ``boost::context``, you must call
:c:func:`~PyUnstable_ThreadState_SetStackProtection` to inform CPython of the change.
Call :c:func:`~PyUnstable_ThreadState_SetStackProtection` either before
or after changing the stack.
Do not call any other Python C API between the call and the stack
change.
See :c:func:`PyUnstable_ThreadState_ResetStackProtection` for undoing this operation.
.. versionadded:: next
.. warning::
This function was added in a bugfix release, and
extensions that use it will be incompatible with Python 3.14.0.
Most packaging tools for Python are not able to handle this
incompatibility automatically, and will need explicit configuration.
When using PyPA standards (wheels and source distributions),
specify ``Requires-Python: != 3.14.0.*`` in
`core metadata <https://packaging.python.org/en/latest/specifications/core-metadata/#requires-python>`_.
.. c:function:: void PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate)
Reset the stack protection start address and stack protection size
of a Python thread state to the operating system defaults.
See :c:func:`PyUnstable_ThreadState_SetStackProtection` for an explanation.
.. versionadded:: next
.. warning::
This function was added in a bugfix release, and
extensions that use it will be incompatible with Python 3.14.0.
Most packaging tools for Python are not able to handle this
incompatibility automatically, and will need explicit configuration.
When using PyPA standards (wheels and source distributions),
specify ``Requires-Python: != 3.14.0.*`` in
`core metadata <https://packaging.python.org/en/latest/specifications/core-metadata/#requires-python>`_.
.. c:function:: PyInterpreterState* PyInterpreterState_Get(void) .. c:function:: PyInterpreterState* PyInterpreterState_Get(void)
Get the current interpreter. Get the current interpreter.

File diff suppressed because it is too large Load diff

View file

@ -3435,3 +3435,13 @@ Changes in the C API
functions on Python 3.13 and older. functions on Python 3.13 and older.
.. _pythoncapi-compat project: https://github.com/python/pythoncapi-compat/ .. _pythoncapi-compat project: https://github.com/python/pythoncapi-compat/
Notable changes in 3.14.1
=========================
* Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and
:c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set
the stack protection base address and stack protection size of a Python
thread state.
(Contributed by Victor Stinner in :gh:`139653`.)

View file

@ -243,6 +243,18 @@ PyAPI_FUNC(int) PyGILState_Check(void);
*/ */
PyAPI_FUNC(PyObject*) _PyThread_CurrentFrames(void); PyAPI_FUNC(PyObject*) _PyThread_CurrentFrames(void);
// Set the stack protection start address and stack protection size
// of a Python thread state
PyAPI_FUNC(int) PyUnstable_ThreadState_SetStackProtection(
PyThreadState *tstate,
void *stack_start_addr, // Stack start address
size_t stack_size); // Stack size (in bytes)
// Reset the stack protection start address and stack protection size
// of a Python thread state
PyAPI_FUNC(void) PyUnstable_ThreadState_ResetStackProtection(
PyThreadState *tstate);
/* Routines for advanced debuggers, requested by David Beazley. /* Routines for advanced debuggers, requested by David Beazley.
Don't use unless you know what you are doing! */ Don't use unless you know what you are doing! */
PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Main(void); PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Main(void);

View file

@ -54,6 +54,12 @@ extern const char* _Py_SourceAsString(
# define _PyOS_STACK_MARGIN_SHIFT (_PyOS_LOG2_STACK_MARGIN + 2) # define _PyOS_STACK_MARGIN_SHIFT (_PyOS_LOG2_STACK_MARGIN + 2)
#endif #endif
#ifdef _Py_THREAD_SANITIZER
# define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 6)
#else
# define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 3)
#endif
#ifdef __cplusplus #ifdef __cplusplus
} }

View file

@ -76,6 +76,10 @@ typedef struct _PyThreadStateImpl {
Py_ssize_t reftotal; // this thread's total refcount operations Py_ssize_t reftotal; // this thread's total refcount operations
#endif #endif
// PyUnstable_ThreadState_ResetStackProtection() values
uintptr_t c_stack_init_base;
uintptr_t c_stack_init_top;
} _PyThreadStateImpl; } _PyThreadStateImpl;
#ifdef __cplusplus #ifdef __cplusplus

View file

@ -0,0 +1,4 @@
Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and
:c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set the
stack protection base address and stack protection size of a Python thread
state. Patch by Victor Stinner.

View file

@ -2408,6 +2408,58 @@ set_vectorcall_nop(PyObject *self, PyObject *func)
Py_RETURN_NONE; Py_RETURN_NONE;
} }
static void
check_threadstate_set_stack_protection(PyThreadState *tstate,
void *start, size_t size)
{
assert(PyUnstable_ThreadState_SetStackProtection(tstate, start, size) == 0);
assert(!PyErr_Occurred());
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
assert(ts->c_stack_top == (uintptr_t)start + size);
assert(ts->c_stack_hard_limit <= ts->c_stack_soft_limit);
assert(ts->c_stack_soft_limit < ts->c_stack_top);
}
static PyObject *
test_threadstate_set_stack_protection(PyObject *self, PyObject *Py_UNUSED(args))
{
PyThreadState *tstate = PyThreadState_GET();
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
assert(!PyErr_Occurred());
uintptr_t init_base = ts->c_stack_init_base;
size_t init_top = ts->c_stack_init_top;
// Test the minimum stack size
size_t size = _PyOS_MIN_STACK_SIZE;
void *start = (void*)(_Py_get_machine_stack_pointer() - size);
check_threadstate_set_stack_protection(tstate, start, size);
// Test a larger size
size = 7654321;
assert(size > _PyOS_MIN_STACK_SIZE);
start = (void*)(_Py_get_machine_stack_pointer() - size);
check_threadstate_set_stack_protection(tstate, start, size);
// Test invalid size (too small)
size = 5;
start = (void*)(_Py_get_machine_stack_pointer() - size);
assert(PyUnstable_ThreadState_SetStackProtection(tstate, start, size) == -1);
assert(PyErr_ExceptionMatches(PyExc_ValueError));
PyErr_Clear();
// Test PyUnstable_ThreadState_ResetStackProtection()
PyUnstable_ThreadState_ResetStackProtection(tstate);
assert(ts->c_stack_init_base == init_base);
assert(ts->c_stack_init_top == init_top);
Py_RETURN_NONE;
}
static PyMethodDef module_functions[] = { static PyMethodDef module_functions[] = {
{"get_configs", get_configs, METH_NOARGS}, {"get_configs", get_configs, METH_NOARGS},
{"get_recursion_depth", get_recursion_depth, METH_NOARGS}, {"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@ -2516,6 +2568,8 @@ static PyMethodDef module_functions[] = {
{"emscripten_set_up_async_input_device", emscripten_set_up_async_input_device, METH_NOARGS}, {"emscripten_set_up_async_input_device", emscripten_set_up_async_input_device, METH_NOARGS},
#endif #endif
{"set_vectorcall_nop", set_vectorcall_nop, METH_O}, {"set_vectorcall_nop", set_vectorcall_nop, METH_O},
{"test_threadstate_set_stack_protection",
test_threadstate_set_stack_protection, METH_NOARGS},
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
}; };

View file

@ -436,24 +436,26 @@ int pthread_attr_destroy(pthread_attr_t *a)
#endif #endif
static void
void hardware_stack_limits(uintptr_t *base, uintptr_t *top)
_Py_InitializeRecursionLimits(PyThreadState *tstate)
{ {
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
#ifdef WIN32 #ifdef WIN32
ULONG_PTR low, high; ULONG_PTR low, high;
GetCurrentThreadStackLimits(&low, &high); GetCurrentThreadStackLimits(&low, &high);
_tstate->c_stack_top = (uintptr_t)high; *top = (uintptr_t)high;
ULONG guarantee = 0; ULONG guarantee = 0;
SetThreadStackGuarantee(&guarantee); SetThreadStackGuarantee(&guarantee);
_tstate->c_stack_hard_limit = ((uintptr_t)low) + guarantee + _PyOS_STACK_MARGIN_BYTES; *base = (uintptr_t)low + guarantee;
_tstate->c_stack_soft_limit = _tstate->c_stack_hard_limit + _PyOS_STACK_MARGIN_BYTES; #elif defined(__APPLE__)
pthread_t this_thread = pthread_self();
void *stack_addr = pthread_get_stackaddr_np(this_thread); // top of the stack
size_t stack_size = pthread_get_stacksize_np(this_thread);
*top = (uintptr_t)stack_addr;
*base = ((uintptr_t)stack_addr) - stack_size;
#else #else
uintptr_t here_addr = _Py_get_machine_stack_pointer(); /// XXX musl supports HAVE_PTHRED_GETATTR_NP, but the resulting stack size
/// XXX musl supports HAVE_PTHRED_GETATTR_NP, but the resulting stack size /// (on alpine at least) is much smaller than expected and imposes undue limits
/// (on alpine at least) is much smaller than expected and imposes undue limits /// compared to the old stack size estimation. (We assume musl is not glibc.)
/// compared to the old stack size estimation. (We assume musl is not glibc.)
# if defined(HAVE_PTHREAD_GETATTR_NP) && !defined(_AIX) && \ # if defined(HAVE_PTHREAD_GETATTR_NP) && !defined(_AIX) && \
!defined(__NetBSD__) && (defined(__GLIBC__) || !defined(__linux__)) !defined(__NetBSD__) && (defined(__GLIBC__) || !defined(__linux__))
size_t stack_size, guard_size; size_t stack_size, guard_size;
@ -466,38 +468,106 @@ _Py_InitializeRecursionLimits(PyThreadState *tstate)
err |= pthread_attr_destroy(&attr); err |= pthread_attr_destroy(&attr);
} }
if (err == 0) { if (err == 0) {
uintptr_t base = ((uintptr_t)stack_addr) + guard_size; *base = ((uintptr_t)stack_addr) + guard_size;
uintptr_t top = base + stack_size; *top = (uintptr_t)stack_addr + stack_size;
# ifdef _Py_THREAD_SANITIZER
// Thread sanitizer crashes if we use a bit more than half the stack.
# if _Py_STACK_GROWS_DOWN
base += stack_size / 2;
# else
top -= stack_size / 2;
# endif
# endif
# if _Py_STACK_GROWS_DOWN
_tstate->c_stack_top = top;
_tstate->c_stack_hard_limit = base + _PyOS_STACK_MARGIN_BYTES;
_tstate->c_stack_soft_limit = base + _PyOS_STACK_MARGIN_BYTES * 2;
assert(_tstate->c_stack_soft_limit < here_addr);
assert(here_addr < _tstate->c_stack_top);
# else
_tstate->c_stack_top = base;
_tstate->c_stack_hard_limit = top - _PyOS_STACK_MARGIN_BYTES;
_tstate->c_stack_soft_limit = top - _PyOS_STACK_MARGIN_BYTES * 2;
assert(here_addr > base);
assert(here_addr < _tstate->c_stack_soft_limit);
# endif
return; return;
} }
# endif # endif
_tstate->c_stack_top = _Py_SIZE_ROUND_UP(here_addr, 4096); uintptr_t here_addr = _Py_get_machine_stack_pointer();
_tstate->c_stack_soft_limit = _tstate->c_stack_top - Py_C_STACK_SIZE; uintptr_t top_addr = _Py_SIZE_ROUND_UP(here_addr, 4096);
_tstate->c_stack_hard_limit = _tstate->c_stack_top - (Py_C_STACK_SIZE + _PyOS_STACK_MARGIN_BYTES); *top = top_addr;
*base = top_addr - Py_C_STACK_SIZE;
#endif #endif
} }
static void
tstate_set_stack(PyThreadState *tstate,
uintptr_t base, uintptr_t top)
{
assert(base < top);
assert((top - base) >= _PyOS_MIN_STACK_SIZE);
#ifdef _Py_THREAD_SANITIZER
// Thread sanitizer crashes if we use more than half the stack.
uintptr_t stacksize = top - base;
# if _Py_STACK_GROWS_DOWN
base += stacksize / 2;
# else
top -= stacksize / 2;
# endif
#endif
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
#if _Py_STACK_GROWS_DOWN
_tstate->c_stack_top = top;
_tstate->c_stack_hard_limit = base + _PyOS_STACK_MARGIN_BYTES;
_tstate->c_stack_soft_limit = base + _PyOS_STACK_MARGIN_BYTES * 2;
# ifndef NDEBUG
// Sanity checks
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
assert(ts->c_stack_hard_limit <= ts->c_stack_soft_limit);
assert(ts->c_stack_soft_limit < ts->c_stack_top);
# endif
#else
_tstate->c_stack_top = base;
_tstate->c_stack_hard_limit = top - _PyOS_STACK_MARGIN_BYTES;
_tstate->c_stack_soft_limit = top - _PyOS_STACK_MARGIN_BYTES * 2;
# ifndef NDEBUG
// Sanity checks
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
assert(ts->c_stack_hard_limit >= ts->c_stack_soft_limit);
assert(ts->c_stack_soft_limit > ts->c_stack_top);
# endif
#endif
}
void
_Py_InitializeRecursionLimits(PyThreadState *tstate)
{
uintptr_t base, top;
hardware_stack_limits(&base, &top);
assert(top != 0);
tstate_set_stack(tstate, base, top);
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
ts->c_stack_init_base = base;
ts->c_stack_init_top = top;
}
int
PyUnstable_ThreadState_SetStackProtection(PyThreadState *tstate,
void *stack_start_addr, size_t stack_size)
{
if (stack_size < _PyOS_MIN_STACK_SIZE) {
PyErr_Format(PyExc_ValueError,
"stack_size must be at least %zu bytes",
_PyOS_MIN_STACK_SIZE);
return -1;
}
uintptr_t base = (uintptr_t)stack_start_addr;
uintptr_t top = base + stack_size;
tstate_set_stack(tstate, base, top);
return 0;
}
void
PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate)
{
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
if (ts->c_stack_init_top != 0) {
tstate_set_stack(tstate,
ts->c_stack_init_base,
ts->c_stack_init_top);
return;
}
_Py_InitializeRecursionLimits(tstate);
}
/* The function _Py_EnterRecursiveCallTstate() only calls _Py_CheckRecursiveCall() /* The function _Py_EnterRecursiveCallTstate() only calls _Py_CheckRecursiveCall()
if the recursion_depth reaches recursion_limit. */ if the recursion_depth reaches recursion_limit. */
int int

View file

@ -1583,6 +1583,9 @@ init_threadstate(_PyThreadStateImpl *_tstate,
_tstate->c_stack_top = 0; _tstate->c_stack_top = 0;
_tstate->c_stack_hard_limit = 0; _tstate->c_stack_hard_limit = 0;
_tstate->c_stack_init_base = 0;
_tstate->c_stack_init_top = 0;
_tstate->asyncio_running_loop = NULL; _tstate->asyncio_running_loop = NULL;
_tstate->asyncio_running_task = NULL; _tstate->asyncio_running_task = NULL;