Skip to content

Commit 2cca51f

Browse files
committed
gh-139653: Add PyUnstable_ThreadState_SetStackProtection() (GH-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>
1 parent 0300367 commit 2cca51f

File tree

9 files changed

+193
-7
lines changed

9 files changed

+193
-7
lines changed

Doc/c-api/exceptions.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,9 @@ because the :ref:`call protocol <call>` takes care of recursion handling.
984984
be concatenated to the :exc:`RecursionError` message caused by the recursion
985985
depth limit.
986986
987+
.. seealso::
988+
The :c:func:`PyUnstable_ThreadState_SetStackProtection` function.
989+
987990
.. versionchanged:: 3.9
988991
This function is now also available in the :ref:`limited API <limited-c-api>`.
989992

Doc/c-api/init.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,6 +1531,43 @@ All of the following functions must be called after :c:func:`Py_Initialize`.
15311531
.. versionadded:: 3.11
15321532
15331533
1534+
.. c:function:: int PyUnstable_ThreadState_SetStackProtection(PyThreadState *tstate, void *stack_start_addr, size_t stack_size)
1535+
1536+
Set the stack protection start address and stack protection size
1537+
of a Python thread state.
1538+
1539+
On success, return ``0``.
1540+
On failure, set an exception and return ``-1``.
1541+
1542+
CPython implements :ref:`recursion control <recursion>` for C code by raising
1543+
:py:exc:`RecursionError` when it notices that the machine execution stack is close
1544+
to overflow. See for example the :c:func:`Py_EnterRecursiveCall` function.
1545+
For this, it needs to know the location of the current thread's stack, which it
1546+
normally gets from the operating system.
1547+
When the stack is changed, for example using context switching techniques like the
1548+
Boost library's ``boost::context``, you must call
1549+
:c:func:`~PyUnstable_ThreadState_SetStackProtection` to inform CPython of the change.
1550+
1551+
Call :c:func:`~PyUnstable_ThreadState_SetStackProtection` either before
1552+
or after changing the stack.
1553+
Do not call any other Python C API between the call and the stack
1554+
change.
1555+
1556+
See :c:func:`PyUnstable_ThreadState_ResetStackProtection` for undoing this operation.
1557+
1558+
.. versionadded:: next
1559+
1560+
1561+
.. c:function:: void PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate)
1562+
1563+
Reset the stack protection start address and stack protection size
1564+
of a Python thread state to the operating system defaults.
1565+
1566+
See :c:func:`PyUnstable_ThreadState_SetStackProtection` for an explanation.
1567+
1568+
.. versionadded:: next
1569+
1570+
15341571
.. c:function:: PyInterpreterState* PyInterpreterState_Get(void)
15351572
15361573
Get the current interpreter.

Include/cpython/pystate.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,18 @@ PyAPI_FUNC(int) PyGILState_Check(void);
243243
*/
244244
PyAPI_FUNC(PyObject*) _PyThread_CurrentFrames(void);
245245

246+
// Set the stack protection start address and stack protection size
247+
// of a Python thread state
248+
PyAPI_FUNC(int) PyUnstable_ThreadState_SetStackProtection(
249+
PyThreadState *tstate,
250+
void *stack_start_addr, // Stack start address
251+
size_t stack_size); // Stack size (in bytes)
252+
253+
// Reset the stack protection start address and stack protection size
254+
// of a Python thread state
255+
PyAPI_FUNC(void) PyUnstable_ThreadState_ResetStackProtection(
256+
PyThreadState *tstate);
257+
246258
/* Routines for advanced debuggers, requested by David Beazley.
247259
Don't use unless you know what you are doing! */
248260
PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Main(void);

Include/internal/pycore_pythonrun.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ extern const char* _Py_SourceAsString(
5454
# define _PyOS_STACK_MARGIN_SHIFT (_PyOS_LOG2_STACK_MARGIN + 2)
5555
#endif
5656

57+
#ifdef _Py_THREAD_SANITIZER
58+
# define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 6)
59+
#else
60+
# define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 3)
61+
#endif
62+
5763

5864
#ifdef __cplusplus
5965
}

Include/internal/pycore_tstate.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ typedef struct _PyThreadStateImpl {
3737
uintptr_t c_stack_soft_limit;
3838
uintptr_t c_stack_hard_limit;
3939

40+
// PyUnstable_ThreadState_ResetStackProtection() values
41+
uintptr_t c_stack_init_base;
42+
uintptr_t c_stack_init_top;
43+
4044
PyObject *asyncio_running_loop; // Strong reference
4145
PyObject *asyncio_running_task; // Strong reference
4246

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and
2+
:c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set the
3+
stack protection base address and stack protection size of a Python thread
4+
state. Patch by Victor Stinner.

Modules/_testinternalcapi.c

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2408,6 +2408,58 @@ set_vectorcall_nop(PyObject *self, PyObject *func)
24082408
Py_RETURN_NONE;
24092409
}
24102410

2411+
2412+
static void
2413+
check_threadstate_set_stack_protection(PyThreadState *tstate,
2414+
void *start, size_t size)
2415+
{
2416+
assert(PyUnstable_ThreadState_SetStackProtection(tstate, start, size) == 0);
2417+
assert(!PyErr_Occurred());
2418+
2419+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
2420+
assert(ts->c_stack_top == (uintptr_t)start + size);
2421+
assert(ts->c_stack_hard_limit <= ts->c_stack_soft_limit);
2422+
assert(ts->c_stack_soft_limit < ts->c_stack_top);
2423+
}
2424+
2425+
2426+
static PyObject *
2427+
test_threadstate_set_stack_protection(PyObject *self, PyObject *Py_UNUSED(args))
2428+
{
2429+
PyThreadState *tstate = PyThreadState_GET();
2430+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
2431+
assert(!PyErr_Occurred());
2432+
2433+
uintptr_t init_base = ts->c_stack_init_base;
2434+
size_t init_top = ts->c_stack_init_top;
2435+
2436+
// Test the minimum stack size
2437+
size_t size = _PyOS_MIN_STACK_SIZE;
2438+
void *start = (void*)(_Py_get_machine_stack_pointer() - size);
2439+
check_threadstate_set_stack_protection(tstate, start, size);
2440+
2441+
// Test a larger size
2442+
size = 7654321;
2443+
assert(size > _PyOS_MIN_STACK_SIZE);
2444+
start = (void*)(_Py_get_machine_stack_pointer() - size);
2445+
check_threadstate_set_stack_protection(tstate, start, size);
2446+
2447+
// Test invalid size (too small)
2448+
size = 5;
2449+
start = (void*)(_Py_get_machine_stack_pointer() - size);
2450+
assert(PyUnstable_ThreadState_SetStackProtection(tstate, start, size) == -1);
2451+
assert(PyErr_ExceptionMatches(PyExc_ValueError));
2452+
PyErr_Clear();
2453+
2454+
// Test PyUnstable_ThreadState_ResetStackProtection()
2455+
PyUnstable_ThreadState_ResetStackProtection(tstate);
2456+
assert(ts->c_stack_init_base == init_base);
2457+
assert(ts->c_stack_init_top == init_top);
2458+
2459+
Py_RETURN_NONE;
2460+
}
2461+
2462+
24112463
static PyMethodDef module_functions[] = {
24122464
{"get_configs", get_configs, METH_NOARGS},
24132465
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2516,6 +2568,8 @@ static PyMethodDef module_functions[] = {
25162568
{"emscripten_set_up_async_input_device", emscripten_set_up_async_input_device, METH_NOARGS},
25172569
#endif
25182570
{"set_vectorcall_nop", set_vectorcall_nop, METH_O},
2571+
{"test_threadstate_set_stack_protection",
2572+
test_threadstate_set_stack_protection, METH_NOARGS},
25192573
{NULL, NULL} /* sentinel */
25202574
};
25212575

Python/ceval.c

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ int pthread_attr_destroy(pthread_attr_t *a)
425425
#endif
426426

427427
static void
428-
hardware_stack_limits(uintptr_t *top, uintptr_t *base)
428+
hardware_stack_limits(uintptr_t *base, uintptr_t *top)
429429
{
430430
#ifdef WIN32
431431
ULONG_PTR low, high;
@@ -468,23 +468,86 @@ hardware_stack_limits(uintptr_t *top, uintptr_t *base)
468468
#endif
469469
}
470470

471-
void
472-
_Py_InitializeRecursionLimits(PyThreadState *tstate)
471+
static void
472+
tstate_set_stack(PyThreadState *tstate,
473+
uintptr_t base, uintptr_t top)
473474
{
474-
uintptr_t top;
475-
uintptr_t base;
476-
hardware_stack_limits(&top, &base);
475+
assert(base < top);
476+
assert((top - base) >= _PyOS_MIN_STACK_SIZE);
477+
477478
#ifdef _Py_THREAD_SANITIZER
478479
// Thread sanitizer crashes if we use more than half the stack.
479480
uintptr_t stacksize = top - base;
480-
base += stacksize/2;
481+
base += stacksize / 2;
481482
#endif
482483
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
483484
_tstate->c_stack_top = top;
484485
_tstate->c_stack_hard_limit = base + _PyOS_STACK_MARGIN_BYTES;
485486
_tstate->c_stack_soft_limit = base + _PyOS_STACK_MARGIN_BYTES * 2;
487+
488+
#ifndef NDEBUG
489+
// Sanity checks
490+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
491+
assert(ts->c_stack_hard_limit <= ts->c_stack_soft_limit);
492+
assert(ts->c_stack_soft_limit < ts->c_stack_top);
493+
#endif
494+
}
495+
496+
497+
void
498+
_Py_InitializeRecursionLimits(PyThreadState *tstate)
499+
{
500+
uintptr_t base, top;
501+
hardware_stack_limits(&base, &top);
502+
assert(top != 0);
503+
504+
tstate_set_stack(tstate, base, top);
505+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
506+
ts->c_stack_init_base = base;
507+
ts->c_stack_init_top = top;
508+
509+
// Test the stack pointer
510+
#if !defined(NDEBUG) && !defined(__wasi__)
511+
uintptr_t here_addr = _Py_get_machine_stack_pointer();
512+
assert(ts->c_stack_soft_limit < here_addr);
513+
assert(here_addr < ts->c_stack_top);
514+
#endif
515+
}
516+
517+
518+
int
519+
PyUnstable_ThreadState_SetStackProtection(PyThreadState *tstate,
520+
void *stack_start_addr, size_t stack_size)
521+
{
522+
if (stack_size < _PyOS_MIN_STACK_SIZE) {
523+
PyErr_Format(PyExc_ValueError,
524+
"stack_size must be at least %zu bytes",
525+
_PyOS_MIN_STACK_SIZE);
526+
return -1;
527+
}
528+
529+
uintptr_t base = (uintptr_t)stack_start_addr;
530+
uintptr_t top = base + stack_size;
531+
tstate_set_stack(tstate, base, top);
532+
return 0;
486533
}
487534

535+
536+
void
537+
PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate)
538+
{
539+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
540+
if (ts->c_stack_init_top != 0) {
541+
tstate_set_stack(tstate,
542+
ts->c_stack_init_base,
543+
ts->c_stack_init_top);
544+
return;
545+
}
546+
547+
_Py_InitializeRecursionLimits(tstate);
548+
}
549+
550+
488551
/* The function _Py_EnterRecursiveCallTstate() only calls _Py_CheckRecursiveCall()
489552
if the recursion_depth reaches recursion_limit. */
490553
int

Python/pystate.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,9 @@ init_threadstate(_PyThreadStateImpl *_tstate,
15831583
_tstate->c_stack_top = 0;
15841584
_tstate->c_stack_hard_limit = 0;
15851585

1586+
_tstate->c_stack_init_base = 0;
1587+
_tstate->c_stack_init_top = 0;
1588+
15861589
_tstate->asyncio_running_loop = NULL;
15871590
_tstate->asyncio_running_task = NULL;
15881591

0 commit comments

Comments
 (0)