Skip to content

Commit b99db92

Browse files
vstinnerencukou
andauthored
pythongh-139653: Add PyUnstable_ThreadState_SetStackProtection() (python#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 <[email protected]>
1 parent d7862e9 commit b99db92

File tree

10 files changed

+199
-7
lines changed

10 files changed

+199
-7
lines changed

Doc/c-api/exceptions.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,9 @@ because the :ref:`call protocol <call>` takes care of recursion handling.
976976
be concatenated to the :exc:`RecursionError` message caused by the recursion
977977
depth limit.
978978
979+
.. seealso::
980+
The :c:func:`PyUnstable_ThreadState_SetStackProtection` function.
981+
979982
.. versionchanged:: 3.9
980983
This function is now also available in the :ref:`limited API <limited-c-api>`.
981984

Doc/c-api/init.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,43 @@ All of the following functions must be called after :c:func:`Py_Initialize`.
13661366
.. versionadded:: 3.11
13671367
13681368
1369+
.. c:function:: int PyUnstable_ThreadState_SetStackProtection(PyThreadState *tstate, void *stack_start_addr, size_t stack_size)
1370+
1371+
Set the stack protection start address and stack protection size
1372+
of a Python thread state.
1373+
1374+
On success, return ``0``.
1375+
On failure, set an exception and return ``-1``.
1376+
1377+
CPython implements :ref:`recursion control <recursion>` for C code by raising
1378+
:py:exc:`RecursionError` when it notices that the machine execution stack is close
1379+
to overflow. See for example the :c:func:`Py_EnterRecursiveCall` function.
1380+
For this, it needs to know the location of the current thread's stack, which it
1381+
normally gets from the operating system.
1382+
When the stack is changed, for example using context switching techniques like the
1383+
Boost library's ``boost::context``, you must call
1384+
:c:func:`~PyUnstable_ThreadState_SetStackProtection` to inform CPython of the change.
1385+
1386+
Call :c:func:`~PyUnstable_ThreadState_SetStackProtection` either before
1387+
or after changing the stack.
1388+
Do not call any other Python C API between the call and the stack
1389+
change.
1390+
1391+
See :c:func:`PyUnstable_ThreadState_ResetStackProtection` for undoing this operation.
1392+
1393+
.. versionadded:: next
1394+
1395+
1396+
.. c:function:: void PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate)
1397+
1398+
Reset the stack protection start address and stack protection size
1399+
of a Python thread state to the operating system defaults.
1400+
1401+
See :c:func:`PyUnstable_ThreadState_SetStackProtection` for an explanation.
1402+
1403+
.. versionadded:: next
1404+
1405+
13691406
.. c:function:: PyInterpreterState* PyInterpreterState_Get(void)
13701407
13711408
Get the current interpreter.

Doc/whatsnew/3.15.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,12 @@ New features
10661066
* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
10671067
(Contributed by Victor Stinner in :gh:`111489`.)
10681068

1069+
* Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and
1070+
:c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set
1071+
the stack protection base address and stack protection size of a Python
1072+
thread state.
1073+
(Contributed by Victor Stinner in :gh:`139653`.)
1074+
10691075

10701076
Changed C APIs
10711077
--------------

Include/cpython/pystate.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,18 @@ PyAPI_FUNC(int) PyGILState_Check(void);
276276
*/
277277
PyAPI_FUNC(PyObject*) _PyThread_CurrentFrames(void);
278278

279+
// Set the stack protection start address and stack protection size
280+
// of a Python thread state
281+
PyAPI_FUNC(int) PyUnstable_ThreadState_SetStackProtection(
282+
PyThreadState *tstate,
283+
void *stack_start_addr, // Stack start address
284+
size_t stack_size); // Stack size (in bytes)
285+
286+
// Reset the stack protection start address and stack protection size
287+
// of a Python thread state
288+
PyAPI_FUNC(void) PyUnstable_ThreadState_ResetStackProtection(
289+
PyThreadState *tstate);
290+
279291
/* Routines for advanced debuggers, requested by David Beazley.
280292
Don't use unless you know what you are doing! */
281293
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
@@ -60,6 +60,12 @@ extern PyObject * _Py_CompileStringObjectWithModule(
6060
# define _PyOS_STACK_MARGIN_SHIFT (_PyOS_LOG2_STACK_MARGIN + 2)
6161
#endif
6262

63+
#ifdef _Py_THREAD_SANITIZER
64+
# define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 6)
65+
#else
66+
# define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 3)
67+
#endif
68+
6369

6470
#ifdef __cplusplus
6571
}

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
@@ -2446,6 +2446,58 @@ module_get_gc_hooks(PyObject *self, PyObject *arg)
24462446
return result;
24472447
}
24482448

2449+
2450+
static void
2451+
check_threadstate_set_stack_protection(PyThreadState *tstate,
2452+
void *start, size_t size)
2453+
{
2454+
assert(PyUnstable_ThreadState_SetStackProtection(tstate, start, size) == 0);
2455+
assert(!PyErr_Occurred());
2456+
2457+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
2458+
assert(ts->c_stack_top == (uintptr_t)start + size);
2459+
assert(ts->c_stack_hard_limit <= ts->c_stack_soft_limit);
2460+
assert(ts->c_stack_soft_limit < ts->c_stack_top);
2461+
}
2462+
2463+
2464+
static PyObject *
2465+
test_threadstate_set_stack_protection(PyObject *self, PyObject *Py_UNUSED(args))
2466+
{
2467+
PyThreadState *tstate = PyThreadState_GET();
2468+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
2469+
assert(!PyErr_Occurred());
2470+
2471+
uintptr_t init_base = ts->c_stack_init_base;
2472+
size_t init_top = ts->c_stack_init_top;
2473+
2474+
// Test the minimum stack size
2475+
size_t size = _PyOS_MIN_STACK_SIZE;
2476+
void *start = (void*)(_Py_get_machine_stack_pointer() - size);
2477+
check_threadstate_set_stack_protection(tstate, start, size);
2478+
2479+
// Test a larger size
2480+
size = 7654321;
2481+
assert(size > _PyOS_MIN_STACK_SIZE);
2482+
start = (void*)(_Py_get_machine_stack_pointer() - size);
2483+
check_threadstate_set_stack_protection(tstate, start, size);
2484+
2485+
// Test invalid size (too small)
2486+
size = 5;
2487+
start = (void*)(_Py_get_machine_stack_pointer() - size);
2488+
assert(PyUnstable_ThreadState_SetStackProtection(tstate, start, size) == -1);
2489+
assert(PyErr_ExceptionMatches(PyExc_ValueError));
2490+
PyErr_Clear();
2491+
2492+
// Test PyUnstable_ThreadState_ResetStackProtection()
2493+
PyUnstable_ThreadState_ResetStackProtection(tstate);
2494+
assert(ts->c_stack_init_base == init_base);
2495+
assert(ts->c_stack_init_top == init_top);
2496+
2497+
Py_RETURN_NONE;
2498+
}
2499+
2500+
24492501
static PyMethodDef module_functions[] = {
24502502
{"get_configs", get_configs, METH_NOARGS},
24512503
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2556,6 +2608,8 @@ static PyMethodDef module_functions[] = {
25562608
{"simple_pending_call", simple_pending_call, METH_O},
25572609
{"set_vectorcall_nop", set_vectorcall_nop, METH_O},
25582610
{"module_get_gc_hooks", module_get_gc_hooks, METH_O},
2611+
{"test_threadstate_set_stack_protection",
2612+
test_threadstate_set_stack_protection, METH_NOARGS},
25592613
{NULL, NULL} /* sentinel */
25602614
};
25612615

Python/ceval.c

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

445445
static void
446-
hardware_stack_limits(uintptr_t *top, uintptr_t *base)
446+
hardware_stack_limits(uintptr_t *base, uintptr_t *top)
447447
{
448448
#ifdef WIN32
449449
ULONG_PTR low, high;
@@ -486,23 +486,86 @@ hardware_stack_limits(uintptr_t *top, uintptr_t *base)
486486
#endif
487487
}
488488

489-
void
490-
_Py_InitializeRecursionLimits(PyThreadState *tstate)
489+
static void
490+
tstate_set_stack(PyThreadState *tstate,
491+
uintptr_t base, uintptr_t top)
491492
{
492-
uintptr_t top;
493-
uintptr_t base;
494-
hardware_stack_limits(&top, &base);
493+
assert(base < top);
494+
assert((top - base) >= _PyOS_MIN_STACK_SIZE);
495+
495496
#ifdef _Py_THREAD_SANITIZER
496497
// Thread sanitizer crashes if we use more than half the stack.
497498
uintptr_t stacksize = top - base;
498-
base += stacksize/2;
499+
base += stacksize / 2;
499500
#endif
500501
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
501502
_tstate->c_stack_top = top;
502503
_tstate->c_stack_hard_limit = base + _PyOS_STACK_MARGIN_BYTES;
503504
_tstate->c_stack_soft_limit = base + _PyOS_STACK_MARGIN_BYTES * 2;
505+
506+
#ifndef NDEBUG
507+
// Sanity checks
508+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
509+
assert(ts->c_stack_hard_limit <= ts->c_stack_soft_limit);
510+
assert(ts->c_stack_soft_limit < ts->c_stack_top);
511+
#endif
512+
}
513+
514+
515+
void
516+
_Py_InitializeRecursionLimits(PyThreadState *tstate)
517+
{
518+
uintptr_t base, top;
519+
hardware_stack_limits(&base, &top);
520+
assert(top != 0);
521+
522+
tstate_set_stack(tstate, base, top);
523+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
524+
ts->c_stack_init_base = base;
525+
ts->c_stack_init_top = top;
526+
527+
// Test the stack pointer
528+
#if !defined(NDEBUG) && !defined(__wasi__)
529+
uintptr_t here_addr = _Py_get_machine_stack_pointer();
530+
assert(ts->c_stack_soft_limit < here_addr);
531+
assert(here_addr < ts->c_stack_top);
532+
#endif
533+
}
534+
535+
536+
int
537+
PyUnstable_ThreadState_SetStackProtection(PyThreadState *tstate,
538+
void *stack_start_addr, size_t stack_size)
539+
{
540+
if (stack_size < _PyOS_MIN_STACK_SIZE) {
541+
PyErr_Format(PyExc_ValueError,
542+
"stack_size must be at least %zu bytes",
543+
_PyOS_MIN_STACK_SIZE);
544+
return -1;
545+
}
546+
547+
uintptr_t base = (uintptr_t)stack_start_addr;
548+
uintptr_t top = base + stack_size;
549+
tstate_set_stack(tstate, base, top);
550+
return 0;
504551
}
505552

553+
554+
void
555+
PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate)
556+
{
557+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
558+
if (ts->c_stack_init_top != 0) {
559+
tstate_set_stack(tstate,
560+
ts->c_stack_init_base,
561+
ts->c_stack_init_top);
562+
return;
563+
}
564+
565+
_Py_InitializeRecursionLimits(tstate);
566+
}
567+
568+
506569
/* The function _Py_EnterRecursiveCallTstate() only calls _Py_CheckRecursiveCall()
507570
if the recursion_depth reaches recursion_limit. */
508571
int

Python/pystate.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,9 @@ init_threadstate(_PyThreadStateImpl *_tstate,
14951495
_tstate->c_stack_top = 0;
14961496
_tstate->c_stack_hard_limit = 0;
14971497

1498+
_tstate->c_stack_init_base = 0;
1499+
_tstate->c_stack_init_top = 0;
1500+
14981501
_tstate->asyncio_running_loop = NULL;
14991502
_tstate->asyncio_running_task = NULL;
15001503

0 commit comments

Comments
 (0)