Skip to content
Open
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 Doc/c-api/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,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_SetStack` function.

.. versionchanged:: 3.9
This function is now also available in the :ref:`limited API <limited-c-api>`.

Expand Down
36 changes: 36 additions & 0 deletions Doc/c-api/init.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,42 @@ All of the following functions must be called after :c:func:`Py_Initialize`.
.. versionadded:: 3.11


.. c:function:: int PyUnstable_ThreadState_SetStack(PyThreadState *tstate, void *stack_start_addr, size_t stack_size)

Set the stack start address and stack 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_SetStack` to inform CPython of the change.

Call :c:func:`~PyUnstable_ThreadState_SetStack` 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_ResetStack` for undoing this operation.

.. versionadded:: next


.. c:function:: void PyUnstable_ThreadState_ResetStack(PyThreadState *tstate)

Reset the stack start address and stack size of a Python thread state to
the operating system defaults.

See :c:func:`PyUnstable_ThreadState_SetStack` for an explanation.

.. versionadded:: next


.. c:function:: PyInterpreterState* PyInterpreterState_Get(void)

Get the current interpreter.
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2994,6 +2994,11 @@ New features in the C API
as arguments to C API functions.
(Contributed by Sam Gross in :gh:`133164`.)

* Add :c:func:`PyUnstable_ThreadState_SetStack` and
:c:func:`PyUnstable_ThreadState_ResetStack` functions to set the stack base
address and stack size of a Python thread state.
(Contributed by Victor Stinner in :gh:`139653`.)


Limited C API changes
---------------------
Expand Down
10 changes: 10 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,16 @@ PyAPI_FUNC(int) PyGILState_Check(void);
*/
PyAPI_FUNC(PyObject*) _PyThread_CurrentFrames(void);

// Set the stack start address and stack size of a Python thread state
PyAPI_FUNC(int) PyUnstable_ThreadState_SetStack(
PyThreadState *tstate,
void *stack_start_addr, // Stack start address
size_t stack_size); // Stack size (in bytes)

// Reset the stack start address and stack size of a Python thread state
PyAPI_FUNC(void) PyUnstable_ThreadState_ResetStack(
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 @@ -37,6 +37,10 @@ typedef struct _PyThreadStateImpl {
uintptr_t c_stack_soft_limit;
uintptr_t c_stack_hard_limit;

// PyUnstable_ThreadState_ResetStack() 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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :c:func:`PyUnstable_ThreadState_SetStack` and
:c:func:`PyUnstable_ThreadState_ResetStack` functions to set the stack base
address and stack size of a Python thread state. Patch by Victor Stinner.
50 changes: 50 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -2418,6 +2418,55 @@ set_vectorcall_nop(PyObject *self, PyObject *func)
Py_RETURN_NONE;
}

static void
check_threadstate_set_stack(PyThreadState *tstate, void *start, size_t size)
{
assert(PyUnstable_ThreadState_SetStack(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(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(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(tstate, start, size);

// Test invalid size (too small)
size = 5;
start = (void*)(_Py_get_machine_stack_pointer() - size);
assert(PyUnstable_ThreadState_SetStack(tstate, start, size) == -1);
assert(PyErr_ExceptionMatches(PyExc_ValueError));
PyErr_Clear();

// Test PyUnstable_ThreadState_ResetStack()
PyUnstable_ThreadState_ResetStack(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 @@ -2527,6 +2576,7 @@ static PyMethodDef module_functions[] = {
#endif
{"simple_pending_call", simple_pending_call, METH_O},
{"set_vectorcall_nop", set_vectorcall_nop, METH_O},
{"test_threadstate_set_stack", test_threadstate_set_stack, METH_NOARGS},
{NULL, NULL} /* sentinel */
};

Expand Down
77 changes: 70 additions & 7 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,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;
Expand Down Expand Up @@ -482,23 +482,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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be aware that this is going to conflict with #140028

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_SetStack(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_ResetStack(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 @@ -1490,6 +1490,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