gh-139653: Add PyUnstable_ThreadState_SetStackProtection() (#139668)

Add PyUnstable_ThreadState_SetStackProtection() and
PyUnstable_ThreadState_ResetStackProtection() functions
to set the stack base address and stack size of a Python
thread state.

Co-authored-by: Petr Viktorin <encukou@gmail.com>
This commit is contained in:
Victor Stinner
2025-11-13 17:30:50 +01:00
committed by GitHub
parent d7862e9b1b
commit b99db92dde
10 changed files with 199 additions and 7 deletions

View File

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

View File

@@ -1366,6 +1366,43 @@ All of the following functions must be called after :c:func:`Py_Initialize`.
.. 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
.. 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
.. c:function:: PyInterpreterState* PyInterpreterState_Get(void)
Get the current interpreter.

View File

@@ -1066,6 +1066,12 @@ New features
* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
(Contributed by Victor Stinner in :gh:`111489`.)
* 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`.)
Changed C APIs
--------------

View File

@@ -276,6 +276,18 @@ PyAPI_FUNC(int) PyGILState_Check(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.
Don't use unless you know what you are doing! */
PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Main(void);

View File

@@ -60,6 +60,12 @@ extern PyObject * _Py_CompileStringObjectWithModule(
# define _PyOS_STACK_MARGIN_SHIFT (_PyOS_LOG2_STACK_MARGIN + 2)
#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
}

View File

@@ -37,6 +37,10 @@ typedef struct _PyThreadStateImpl {
uintptr_t c_stack_soft_limit;
uintptr_t c_stack_hard_limit;
// PyUnstable_ThreadState_ResetStackProtection() values
uintptr_t c_stack_init_base;
uintptr_t c_stack_init_top;
PyObject *asyncio_running_loop; // Strong reference
PyObject *asyncio_running_task; // Strong reference

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

@@ -2446,6 +2446,58 @@ finally:
return result;
}
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[] = {
{"get_configs", get_configs, METH_NOARGS},
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2556,6 +2608,8 @@ static PyMethodDef module_functions[] = {
{"simple_pending_call", simple_pending_call, METH_O},
{"set_vectorcall_nop", set_vectorcall_nop, METH_O},
{"module_get_gc_hooks", module_get_gc_hooks, METH_O},
{"test_threadstate_set_stack_protection",
test_threadstate_set_stack_protection, METH_NOARGS},
{NULL, NULL} /* sentinel */
};

View File

@@ -443,7 +443,7 @@ int pthread_attr_destroy(pthread_attr_t *a)
#endif
static void
hardware_stack_limits(uintptr_t *top, uintptr_t *base)
hardware_stack_limits(uintptr_t *base, uintptr_t *top)
{
#ifdef WIN32
ULONG_PTR low, high;
@@ -486,23 +486,86 @@ hardware_stack_limits(uintptr_t *top, uintptr_t *base)
#endif
}
void
_Py_InitializeRecursionLimits(PyThreadState *tstate)
static void
tstate_set_stack(PyThreadState *tstate,
uintptr_t base, uintptr_t top)
{
uintptr_t top;
uintptr_t base;
hardware_stack_limits(&top, &base);
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;
base += stacksize/2;
base += stacksize / 2;
#endif
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
_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
}
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;
// Test the stack pointer
#if !defined(NDEBUG) && !defined(__wasi__)
uintptr_t here_addr = _Py_get_machine_stack_pointer();
assert(ts->c_stack_soft_limit < here_addr);
assert(here_addr < ts->c_stack_top);
#endif
}
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()
if the recursion_depth reaches recursion_limit. */
int

View File

@@ -1495,6 +1495,9 @@ init_threadstate(_PyThreadStateImpl *_tstate,
_tstate->c_stack_top = 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_task = NULL;