Skip to content
3 changes: 3 additions & 0 deletions Doc/c-api/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
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>`.

Expand Down
57 changes: 57 additions & 0 deletions Doc/c-api/init.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,63 @@ 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

.. 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)

Get the current interpreter.
Expand Down
18,729 changes: 9,408 additions & 9,321 deletions Doc/data/python3.14.abi

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3435,3 +3435,13 @@ Changes in the C API
functions on Python 3.13 and older.

.. _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`.)
12 changes: 12 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,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);
Expand Down
6 changes: 6 additions & 0 deletions Include/internal/pycore_pythonrun.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ extern const char* _Py_SourceAsString(
# 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
}
Expand Down
4 changes: 4 additions & 0 deletions Include/internal/pycore_tstate.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ typedef struct _PyThreadStateImpl {
Py_ssize_t reftotal; // this thread's total refcount operations
#endif

// PyUnstable_ThreadState_ResetStackProtection() values
uintptr_t c_stack_init_base;
uintptr_t c_stack_init_top;

} _PyThreadStateImpl;

#ifdef __cplusplus
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
54 changes: 54 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -2408,6 +2408,58 @@ set_vectorcall_nop(PyObject *self, PyObject *func)
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[] = {
{"get_configs", get_configs, METH_NOARGS},
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
Expand Down Expand Up @@ -2516,6 +2568,8 @@ static PyMethodDef module_functions[] = {
{"emscripten_set_up_async_input_device", emscripten_set_up_async_input_device, METH_NOARGS},
#endif
{"set_vectorcall_nop", set_vectorcall_nop, METH_O},
{"test_threadstate_set_stack_protection",
test_threadstate_set_stack_protection, METH_NOARGS},
{NULL, NULL} /* sentinel */
};

Expand Down
144 changes: 107 additions & 37 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -436,24 +436,26 @@ int pthread_attr_destroy(pthread_attr_t *a)

#endif


void
_Py_InitializeRecursionLimits(PyThreadState *tstate)
static void
hardware_stack_limits(uintptr_t *base, uintptr_t *top)
{
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
#ifdef WIN32
ULONG_PTR low, high;
GetCurrentThreadStackLimits(&low, &high);
_tstate->c_stack_top = (uintptr_t)high;
*top = (uintptr_t)high;
ULONG guarantee = 0;
SetThreadStackGuarantee(&guarantee);
_tstate->c_stack_hard_limit = ((uintptr_t)low) + guarantee + _PyOS_STACK_MARGIN_BYTES;
_tstate->c_stack_soft_limit = _tstate->c_stack_hard_limit + _PyOS_STACK_MARGIN_BYTES;
*base = (uintptr_t)low + guarantee;
#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
uintptr_t here_addr = _Py_get_machine_stack_pointer();
/// 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
/// compared to the old stack size estimation. (We assume musl is not glibc.)
/// 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
/// compared to the old stack size estimation. (We assume musl is not glibc.)
# if defined(HAVE_PTHREAD_GETATTR_NP) && !defined(_AIX) && \
!defined(__NetBSD__) && (defined(__GLIBC__) || !defined(__linux__))
size_t stack_size, guard_size;
Expand All @@ -466,38 +468,106 @@ _Py_InitializeRecursionLimits(PyThreadState *tstate)
err |= pthread_attr_destroy(&attr);
}
if (err == 0) {
uintptr_t base = ((uintptr_t)stack_addr) + guard_size;
uintptr_t top = base + 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
*base = ((uintptr_t)stack_addr) + guard_size;
*top = (uintptr_t)stack_addr + stack_size;
return;
}
# endif
_tstate->c_stack_top = _Py_SIZE_ROUND_UP(here_addr, 4096);
_tstate->c_stack_soft_limit = _tstate->c_stack_top - Py_C_STACK_SIZE;
_tstate->c_stack_hard_limit = _tstate->c_stack_top - (Py_C_STACK_SIZE + _PyOS_STACK_MARGIN_BYTES);
uintptr_t here_addr = _Py_get_machine_stack_pointer();
uintptr_t top_addr = _Py_SIZE_ROUND_UP(here_addr, 4096);
*top = top_addr;
*base = top_addr - Py_C_STACK_SIZE;
#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()
if the recursion_depth reaches recursion_limit. */
int
Expand Down
3 changes: 3 additions & 0 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -1583,6 +1583,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;

Expand Down
Loading