8000 GH-91079: Implement C stack limits using addresses, not counters. by markshannon · Pull Request #130007 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

GH-91079: Implement C stack limits using addresses, not counters. #130007

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 46 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
fc910a3
Hide that C recursion protection is implemented with a counter. There…
markshannon Feb 10, 2025
afeb866
Implement C recursion protection with limit pointers
markshannon Feb 11, 2025
22ca169
Use uintptr_t instead of char * to avoid warnings and UB
markshannon Feb 11, 2025
e8d8c4b
Merge branch 'main' into c-recursion-limit
markshannon Feb 11, 2025
b3638a5
Fix typo and update stable ABI
markshannon Feb 11, 2025
774efb5
Tweak AST test numbers
markshannon Feb 11, 2025
151c88f
Improve logic handling trial C stack overflow
markshannon Feb 11, 2025
350f8ec
Remove calls to PyOS_CheckStack
markshannon Feb 11, 2025
428c46a
Up the limits for recursion tests
markshannon Feb 11, 2025
a9be141
Use deeper stack for test
markshannon Feb 12, 2025
03fc52e
Remove exceeds_recursion_limit and get_c_recursion_limit. Use platfor…
markshannon Feb 12, 2025
afac1e6
Do fewer probes when growing stack limit
markshannon Feb 12, 2025
9da904d
Tweak depths
markshannon Feb 12, 2025
a802ff6
Merge branch 'main' into c-recursion-limit
markshannon Feb 12, 2025
dbcf6f0
Perform lazy initialization of c recursion check
markshannon Feb 12, 2025
2cc3287
Post merge fixup
markshannon Feb 12, 2025
e697926
Up depth again
markshannon Feb 12, 2025
f8a9143
8000 Drop 'failing' depth
markshannon Feb 12, 2025
7d6d77f
Add news
markshannon Feb 12, 2025
31a83dc
Increase headroom
markshannon Feb 12, 2025
47c50aa
Update test
markshannon Feb 12, 2025
495c4ea
Tweak some more thresholds and tests
markshannon Feb 12, 2025
9e0cc67
Add stack protection to parser
markshannon Feb 13, 2025
857a7bb
Make tests more robust to low stacks
markshannon Feb 13, 2025
9c9326a
Improve error messages for stack overflow
markshannon Feb 13, 2025
75d3219
Merge branch 'main' into c-recursion-limit
markshannon Feb 13, 2025
3e41b46
Fix formatting
markshannon Feb 13, 2025
158401a
Halve size of WASI stack
markshannon Feb 13, 2025
c1eb229
Reduce webassembly 'stack size' by 10
markshannon Feb 13, 2025
7407d2b
Halve size of WASI stack, again
markshannon Feb 13, 2025
978b5e7
Change WASI stack back to 100k
markshannon Feb 13, 2025
7b36f59
Add many skip tests for WASI due to stack issues
markshannon Feb 13, 2025
5e5db03
Probe all pages when extending stack limits
markshannon Feb 14, 2025
82173ed
Fix compiler warnings
markshannon Feb 14, 2025
64cfd86
Use GetCurrentThreadStackLimits instead of probing with alloca
markshannon Feb 14, 2025
e52137f
Refactor a bit
markshannon Feb 14, 2025
704c336
Merge branch 'main' into c-recursion-limit
markshannon Feb 14, 2025
21366c3
Fix logic error in test
markshannon Feb 17, 2025
7761d31
Make ABI function needed for Py_TRASHCAN_BEGIN private
markshannon Feb 17, 2025
b067e3e
Move new fields to _PyThreadStateImpl to avoid any potential API brea…
markshannon Feb 17, 2025
b0e695f
Fix missing cast
markshannon Feb 17, 2025
c4cd68f
yet another missing _
markshannon Feb 17, 2025
9654790
Tidy up a bit
markshannon Feb 18, 2025
2cef96f
Restore use of exceeds_recursion_limit
markshannon Feb 18, 2025
c5d8a40
Address remaining review comments
markshannon Feb 18, 2025
33e11c8
Merge branch 'main' into c-recursion-limit
markshannon Feb 19, 2025
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
Prev Previous commit
Next Next commit
Probe all pages when extending stack limits
  • Loading branch information
markshannon committed Feb 14, 2025
commit 5e5db03e7f72497cccfbe15bf16c16b2d73f15cc
6 changes: 3 additions & 3 deletions Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ static inline void _Py_LeaveRecursiveCallTstate(PyThreadState *tstate) {
(void)tstate;
}

PyAPI_FUNC(void) _Py_InitializeRecursionCheck(PyThreadState *tstate);
PyAPI_FUNC(void) _Py_UpdateRecursionLimits(PyThreadState *tstate);

static inline int _Py_ReachedRecursionLimit(PyThreadState *tstate, int margin_count) {
char here;
Expand All @@ -231,7 +231,7 @@ static inline int _Py_ReachedRecursionLimit(PyThreadState *tstate, int margin_co
return 0;
}
if (tstate->c_stack_hard_limit == 0) {
_Py_InitializeRecursionCheck(tstate);
_Py_UpdateRecursionLimits(tstate);
}
return here_addr <= tstate->c_stack_soft_limit + margin_count * PYOS_STACK_MARGIN_BYTES;
}
Expand Down Expand Up @@ -327,7 +327,7 @@ void _Py_unset_eval_breaker_bit_all(PyInterpreterState *interp, uintptr_t bit);

PyAPI_FUNC(PyObject *) _PyFloat_FromDouble_ConsumeInputs(_PyStackRef left, _PyStackRef right, double value);

extern int _PyOS_CheckStack(int words);
extern void _Py_StackProbe(uintptr_t from, int bytes, int *probed);

#ifdef __cplusplus
}
Expand Down
15 changes: 10 additions & 5 deletions Include/pythonrun.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ PyAPI_FUNC(void) PyErr_DisplayException(PyObject *);
/* Stuff with no proper home (yet) */
PyAPI_DATA(int) (*PyOS_InputHook)(void);

/* Stack size, in "pointers" (so we get extra safety margins
on 64-bit platforms). On a 32-bit platform, this translates
to an 16k margin. */
#define PYOS_STACK_MARGIN 4096
/* Stack size, in "pointers". This must be large enough, so
* no two calls to check recursion depth are more than this far
* apart. In practice, that means it must be larger than the C
* stack consumption of PyEval_EvalDefault */
#if defined(Py_DEBUG) && defined(WIN32)
# define PYOS_STACK_MARGIN 3072
#else
# define PYOS_STACK_MARGIN 2048
#endif
#define PYOS_STACK_MARGIN_BYTES (PYOS_STACK_MARGIN * sizeof(void *))

#if defined(WIN32) && !defined(MS_WIN64) && !defined(_M_ARM) && defined(_MSC_VER) && _MSC_VER >= 1300
#if defined(WIN32)
/* Enable stack checking under Microsoft C */
// When changing the platforms, ensure PyOS_CheckStack() docs are still correct
#define USE_STACKCHECK
Expand Down
48 changes: 22 additions & 26 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,8 @@

#if defined(__s390x__)
# define Py_C_STACK_SIZE 320000
#elif defined(_WIN32) && defined(_M_ARM64)
# define Py_C_STACK_SIZE 400000
#elif defined(_WIN32)
# define Py_C_STACK_SIZE 1200000
// Don't define Py_C_STACK_SIZE, use probing instead
#elif defined(__ANDROID__)
# define Py_C_STACK_SIZE 1200000
#elif defined(__sparc__)
Expand All @@ -342,32 +340,30 @@
#endif

void
_Py_InitializeRecursionCheck(PyThreadState *tstate)
_Py_UpdateRecursionLimits(PyThreadState *tstate)
{
char here;
uintptr_t here_addr = (uintptr_t)&here;
tstate->c_stack_top = here_addr;
int to_probe = PYOS_STACK_MARGIN_BYTES * 2;

Check warning on line 347 in Python/ceval.c

View workflow job for this annotation

GitHub Actions / Address sanitizer (ubuntu-24.04)

unused variable ‘to_probe’ [-Wunused-variable]

Check warning on line 347 in Python/ceval.c

View workflow job for this annotation

GitHub Actions / Hypothesis tests on Ubuntu

unused variable ‘to_probe’ [-Wunused-variable]

Check warning on line 347 in Python/ceval.c

View workflow job for this annotation

GitHub Actions / Cross build Linux

unused variable ‘to_probe’ [-Wunused-variable]

Check warning on line 347 in Python/ceval.c

View workflow job for this annotation

GitHub Actions / Ubuntu / build and test (ubuntu-22.04-arm)

unused variable ‘to_probe’ [-Wunused-variable]

Check warning on line 347 in Python/ceval.c

View workflow job for this annotation

GitHub Actions / Ubuntu / build and test (ubuntu-24.04)

unused variable ‘to_probe’ [-Wunused-variable]

Check warning on line 347 in Python/ceval.c

View workflow job for this annotation

GitHub Actions / Ubuntu (free-threading) / build and test (ubuntu-24.04)

unused variable ‘to_probe’ [-Wunused-variable]

Check warning on line 347 in Python/ceval.c

View workflow job for this annotation

GitHub Actions / Ubuntu (free-threading) / build and test (ubuntu-22.04-arm)

unused variable ‘to_probe’ [-Wunused-variable]
#ifdef USE_STACKCHECK
if (_PyOS_CheckStack(PYOS_STACK_MARGIN * 2) == 0) {
tstate->c_stack_soft_limit = here_addr - PYOS_STACK_MARGIN_BYTES;
return;
}
int margin = PYOS_STACK_MARGIN;
assert(tstate->c_stack_soft_limit != UINTPTR_MAX);
if (_PyOS_CheckStack(margin)) {
margin = PYOS_STACK_MARGIN/2;
}
else {
if (_PyOS_CheckStack(PYOS_STACK_MARGIN*3/2) == 0) {
margin = PYOS_STACK_MARGIN*3/2;
}
if (tstate->c_stack_top == 0) {
assert(tstate->c_stack_soft_limit == UINTPTR_MAX);
tstate->c_stack_top = _Py_SIZE_ROUND_UP(here_addr, 4096);
tstate->c_stack_soft_limit = tstate->c_stack_top;
to_probe = PYOS_STACK_MARGIN_BYTES * 4;
}
int depth;
uintptr_t implicit_hard_limit = tstate->c_stack_soft_limit - PYOS_STACK_MARGIN_BYTES;
_Py_StackProbe(implicit_hard_limit, to_probe, &depth);
tstate->c_stack_soft_limit = implicit_hard_limit - depth + PYOS_STACK_MARGIN_BYTES * 2;
if (depth != to_probe) {
tstate->c_stack_hard_limit = tstate->c_stack_soft_limit - PYOS_STACK_MARGIN_BYTES;
}
tstate->c_stack_hard_limit = here_addr - margin * sizeof(void *);
tstate->c_stack_soft_limit = tstate->c_stack_hard_limit + PYOS_STACK_MARGIN_BYTES;
#else
assert(tstate->c_stack_soft_limit == UINTPTR_MAX);
tstate->c_stack_soft_limit = here_addr - Py_C_STACK_SIZE;
tstate->c_stack_hard_limit = here_addr - (Py_C_STACK_SIZE + PYOS_STACK_MARGIN_BYTES);
assert(tstate->c_stack_top == 0);
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);
#endif
}

Expand All @@ -380,15 +376,15 @@
uintptr_t here_addr = (uintptr_t)&here;
assert(tstate->c_stack_soft_limit != 0);
if (tstate->c_stack_hard_limit == 0) {
_Py_InitializeRecursionCheck(tstate);
_Py_UpdateRecursionLimits(tstate);
}
if (here_addr >= tstate->c_stack_soft_limit) {
return 0;
}
assert(tstate->c_stack_hard_limit != 0);
if (here_addr < tstate->c_stack_hard_limit) {
/* Overflowing while handling an overflow. Give up. */
int kbytes_used = (tstate->c_stack_top - here_addr)/1024;
int kbytes_used = (int)(tstate->c_stack_top - here_addr)/1024;
char buffer[80];
snprintf(buffer, 80, "Unrecoverable stack overflow (used %d kB)%s", kbytes_used, where);
Py_FatalError(buffer);
Expand All @@ -397,7 +393,7 @@
return 0;
}
else {
int kbytes_used = (tstate->c_stack_top - here_addr)/1024;
int kbytes_used = (int)(tstate->c_stack_top - here_addr)/1024;
tstate->recursion_headroom++;
_PyErr_Format(tstate, PyExc_RecursionError,
"Stack overflow (used %d kB)%s",
Expand Down
1 change: 1 addition & 0 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,7 @@ init_threadstate(_PyThreadStateImpl *_tstate,
tstate->py_recursion_limit = interp->ceval.recursion_limit;
tstate->py_recursion_remaining = interp->ceval.recursion_limit;
tstate->c_stack_soft_limit = UINTPTR_MAX;
tstate->c_stack_top = 0;
tstate->c_stack_hard_limit = 0;

tstate->exc_info = &tstate->exc_state;
Expand Down
34 changes: 27 additions & 7 deletions Python/pythonrun.c
Original file line number Diff line number Diff line change
Expand Up @@ -1552,14 +1552,24 @@ _Py_SourceAsString(PyObject *cmd, const char *funcname, const char *what, PyComp
#include <malloc.h>
#include <excpt.h>

int _PyOS_CheckStack(int words)
void _Py_StackProbe(uintptr_t from, int bytes, int *probed)
{
assert((bytes & 4095) == 0);
int depth = 4096;
char here;
uintptr_t here_addr = (uintptr_t)&here;
__try
{
/* alloca throws a stack overflow exception if there's
not enough space left on the stack */
alloca(words * sizeof(void *));
return 0;
while (depth <= bytes) {
uintptr_t probe_point = from - depth + 64;
if (probe_point < here_addr) {
/* alloca throws a stack overflow exception if there's
* not enough space left on the stack */
alloca(here_addr - probe_point);
}
*probed = depth;
depth += 4096;
}
}
__except (GetExceptionCode() == STATUS_STACK_OVERFLOW ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
Expand All @@ -1569,15 +1579,25 @@ int _PyOS_CheckStack(int words)
Py_FatalError("Could not reset the stack!");
}
}
return 1;
}

/*
* Return non-zero when we run out of memory on the stack; zero otherwise.
*/
int PyOS_CheckStack(void)
{
return _PyOS_CheckStack(PYOS_STACK_MARGIN);
char here;
uintptr_t here_addr = (uintptr_t)&here;
uintptr_t from = _Py_SIZE_ROUND_UP(here_addr, 4096);
int depth;
_Py_StackProbe(from, PYOS_STACK_MARGIN_BYTES*2, &depth);
assert(depth <= PYOS_STACK_MARGIN_BYTES*2);
if (depth == PYOS_STACK_MARGIN_BYTES*2) {
return 0;
}
else {
return -1;
}
}

#endif /* WIN32 && _MSC_VER */
Expand Down
Loading
0