Skip to content

Commit 32a38a2

Browse files
encukourokmmarkshannonvstinner
authored
[3.14] gh-139653: Add PyUnstable_ThreadState_SetStackProtection() (GH-139668) (#141661)
Co-authored-by: Rok Mandeljc <rok.mandeljc@gmail.com> Co-authored-by: Mark Shannon <mark@hotpy.org> Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 11e3fc9 commit 32a38a2

File tree

11 files changed

+9668
-9358
lines changed

11 files changed

+9668
-9358
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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,6 +1531,63 @@ 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+
.. warning::
1561+
1562+
This function was added in a bugfix release, and
1563+
extensions that use it will be incompatible with Python 3.14.0.
1564+
Most packaging tools for Python are not able to handle this
1565+
incompatibility automatically, and will need explicit configuration.
1566+
When using PyPA standards (wheels and source distributions),
1567+
specify ``Requires-Python: != 3.14.0.*`` in
1568+
`core metadata <https://packaging.python.org/en/latest/specifications/core-metadata/#requires-python>`_.
1569+
1570+
1571+
.. c:function:: void PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate)
1572+
1573+
Reset the stack protection start address and stack protection size
1574+
of a Python thread state to the operating system defaults.
1575+
1576+
See :c:func:`PyUnstable_ThreadState_SetStackProtection` for an explanation.
1577+
1578+
.. versionadded:: next
1579+
1580+
.. warning::
1581+
1582+
This function was added in a bugfix release, and
1583+
extensions that use it will be incompatible with Python 3.14.0.
1584+
Most packaging tools for Python are not able to handle this
1585+
incompatibility automatically, and will need explicit configuration.
1586+
When using PyPA standards (wheels and source distributions),
1587+
specify ``Requires-Python: != 3.14.0.*`` in
1588+
`core metadata <https://packaging.python.org/en/latest/specifications/core-metadata/#requires-python>`_.
1589+
1590+
15341591
.. c:function:: PyInterpreterState* PyInterpreterState_Get(void)
15351592
15361593
Get the current interpreter.

Doc/data/python3.14.abi

Lines changed: 9408 additions & 9321 deletions
Large diffs are not rendered by default.

Doc/whatsnew/3.14.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3435,3 +3435,13 @@ Changes in the C API
34353435
functions on Python 3.13 and older.
34363436

34373437
.. _pythoncapi-compat project: https://github.com/python/pythoncapi-compat/
3438+
3439+
3440+
Notable changes in 3.14.1
3441+
=========================
3442+
3443+
* Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and
3444+
:c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set
3445+
the stack protection base address and stack protection size of a Python
3446+
thread state.
3447+
(Contributed by Victor Stinner in :gh:`139653`.)

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
@@ -76,6 +76,10 @@ typedef struct _PyThreadStateImpl {
7676
Py_ssize_t reftotal; // this thread's total refcount operations
7777
#endif
7878

79+
// PyUnstable_ThreadState_ResetStackProtection() values
80+
uintptr_t c_stack_init_base;
81+
uintptr_t c_stack_init_top;
82+
7983
} _PyThreadStateImpl;
8084

8185
#ifdef __cplusplus
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: 107 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -436,24 +436,26 @@ int pthread_attr_destroy(pthread_attr_t *a)
436436

437437
#endif
438438

439-
440-
void
441-
_Py_InitializeRecursionLimits(PyThreadState *tstate)
439+
static void
440+
hardware_stack_limits(uintptr_t *base, uintptr_t *top)
442441
{
443-
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
444442
#ifdef WIN32
445443
ULONG_PTR low, high;
446444
GetCurrentThreadStackLimits(&low, &high);
447-
_tstate->c_stack_top = (uintptr_t)high;
445+
*top = (uintptr_t)high;
448446
ULONG guarantee = 0;
449447
SetThreadStackGuarantee(&guarantee);
450-
_tstate->c_stack_hard_limit = ((uintptr_t)low) + guarantee + _PyOS_STACK_MARGIN_BYTES;
451-
_tstate->c_stack_soft_limit = _tstate->c_stack_hard_limit + _PyOS_STACK_MARGIN_BYTES;
448+
*base = (uintptr_t)low + guarantee;
449+
#elif defined(__APPLE__)
450+
pthread_t this_thread = pthread_self();
451+
void *stack_addr = pthread_get_stackaddr_np(this_thread); // top of the stack
452+
size_t stack_size = pthread_get_stacksize_np(this_thread);
453+
*top = (uintptr_t)stack_addr;
454+
*base = ((uintptr_t)stack_addr) - stack_size;
452455
#else
453-
uintptr_t here_addr = _Py_get_machine_stack_pointer();
454-
/// XXX musl supports HAVE_PTHRED_GETATTR_NP, but the resulting stack size
455-
/// (on alpine at least) is much smaller than expected and imposes undue limits
456-
/// compared to the old stack size estimation. (We assume musl is not glibc.)
456+
/// XXX musl supports HAVE_PTHRED_GETATTR_NP, but the resulting stack size
457+
/// (on alpine at least) is much smaller than expected and imposes undue limits
458+
/// compared to the old stack size estimation. (We assume musl is not glibc.)
457459
# if defined(HAVE_PTHREAD_GETATTR_NP) && !defined(_AIX) && \
458460
!defined(__NetBSD__) && (defined(__GLIBC__) || !defined(__linux__))
459461
size_t stack_size, guard_size;
@@ -466,38 +468,106 @@ _Py_InitializeRecursionLimits(PyThreadState *tstate)
466468
err |= pthread_attr_destroy(&attr);
467469
}
468470
if (err == 0) {
469-
uintptr_t base = ((uintptr_t)stack_addr) + guard_size;
470-
uintptr_t top = base + stack_size;
471-
# ifdef _Py_THREAD_SANITIZER
472-
// Thread sanitizer crashes if we use a bit more than half the stack.
473-
# if _Py_STACK_GROWS_DOWN
474-
base += stack_size / 2;
475-
# else
476-
top -= stack_size / 2;
477-
# endif
478-
# endif
479-
# if _Py_STACK_GROWS_DOWN
480-
_tstate->c_stack_top = top;
481-
_tstate->c_stack_hard_limit = base + _PyOS_STACK_MARGIN_BYTES;
482-
_tstate->c_stack_soft_limit = base + _PyOS_STACK_MARGIN_BYTES * 2;
483-
assert(_tstate->c_stack_soft_limit < here_addr);
484-
assert(here_addr < _tstate->c_stack_top);
485-
# else
486-
_tstate->c_stack_top = base;
487-
_tstate->c_stack_hard_limit = top - _PyOS_STACK_MARGIN_BYTES;
488-
_tstate->c_stack_soft_limit = top - _PyOS_STACK_MARGIN_BYTES * 2;
489-
assert(here_addr > base);
490-
assert(here_addr < _tstate->c_stack_soft_limit);
491-
# endif
471+
*base = ((uintptr_t)stack_addr) + guard_size;
472+
*top = (uintptr_t)stack_addr + stack_size;
492473
return;
493474
}
494475
# endif
495-
_tstate->c_stack_top = _Py_SIZE_ROUND_UP(here_addr, 4096);
496-
_tstate->c_stack_soft_limit = _tstate->c_stack_top - Py_C_STACK_SIZE;
497-
_tstate->c_stack_hard_limit = _tstate->c_stack_top - (Py_C_STACK_SIZE + _PyOS_STACK_MARGIN_BYTES);
476+
uintptr_t here_addr = _Py_get_machine_stack_pointer();
477+
uintptr_t top_addr = _Py_SIZE_ROUND_UP(here_addr, 4096);
478+
*top = top_addr;
479+
*base = top_addr - Py_C_STACK_SIZE;
480+
#endif
481+
}
482+
483+
static void
484+
tstate_set_stack(PyThreadState *tstate,
485+
uintptr_t base, uintptr_t top)
486+
{
487+
assert(base < top);
488+
assert((top - base) >= _PyOS_MIN_STACK_SIZE);
489+
490+
#ifdef _Py_THREAD_SANITIZER
491+
// Thread sanitizer crashes if we use more than half the stack.
492+
uintptr_t stacksize = top - base;
493+
# if _Py_STACK_GROWS_DOWN
494+
base += stacksize / 2;
495+
# else
496+
top -= stacksize / 2;
497+
# endif
498+
#endif
499+
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
500+
#if _Py_STACK_GROWS_DOWN
501+
_tstate->c_stack_top = top;
502+
_tstate->c_stack_hard_limit = base + _PyOS_STACK_MARGIN_BYTES;
503+
_tstate->c_stack_soft_limit = base + _PyOS_STACK_MARGIN_BYTES * 2;
504+
# ifndef NDEBUG
505+
// Sanity checks
506+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
507+
assert(ts->c_stack_hard_limit <= ts->c_stack_soft_limit);
508+
assert(ts->c_stack_soft_limit < ts->c_stack_top);
509+
# endif
510+
#else
511+
_tstate->c_stack_top = base;
512+
_tstate->c_stack_hard_limit = top - _PyOS_STACK_MARGIN_BYTES;
513+
_tstate->c_stack_soft_limit = top - _PyOS_STACK_MARGIN_BYTES * 2;
514+
# ifndef NDEBUG
515+
// Sanity checks
516+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
517+
assert(ts->c_stack_hard_limit >= ts->c_stack_soft_limit);
518+
assert(ts->c_stack_soft_limit > ts->c_stack_top);
519+
# endif
498520
#endif
499521
}
500522

523+
524+
void
525+
_Py_InitializeRecursionLimits(PyThreadState *tstate)
526+
{
527+
uintptr_t base, top;
528+
hardware_stack_limits(&base, &top);
529+
assert(top != 0);
530+
531+
tstate_set_stack(tstate, base, top);
532+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
533+
ts->c_stack_init_base = base;
534+
ts->c_stack_init_top = top;
535+
}
536+
537+
538+
int
539+
PyUnstable_ThreadState_SetStackProtection(PyThreadState *tstate,
540+
void *stack_start_addr, size_t stack_size)
541+
{
542+
if (stack_size < _PyOS_MIN_STACK_SIZE) {
543+
PyErr_Format(PyExc_ValueError,
544+
"stack_size must be at least %zu bytes",
545+
_PyOS_MIN_STACK_SIZE);
546+
return -1;
547+
}
548+
549+
uintptr_t base = (uintptr_t)stack_start_addr;
550+
uintptr_t top = base + stack_size;
551+
tstate_set_stack(tstate, base, top);
552+
return 0;
553+
}
554+
555+
556+
void
557+
PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate)
558+
{
559+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
560+
if (ts->c_stack_init_top != 0) {
561+
tstate_set_stack(tstate,
562+
ts->c_stack_init_base,
563+
ts->c_stack_init_top);
564+
return;
565+
}
566+
567+
_Py_InitializeRecursionLimits(tstate);
568+
}
569+
570+
501571
/* The function _Py_EnterRecursiveCallTstate() only calls _Py_CheckRecursiveCall()
502572
if the recursion_depth reaches recursion_limit. */
503573
int

0 commit comments

Comments
 (0)