Skip to content

Commit 0142236

Browse files
authored
GH-130396: Use computed stack limits on linux (GH-130398)
* Implement C recursion protection with limit pointers for Linux, MacOS and Windows * Remove calls to PyOS_CheckStack * Add stack protection to parser * Make tests more robust to low stacks * Improve error messages for stack overflow
1 parent 99088ab commit 0142236

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1295
-1482
lines changed

Doc/c-api/exceptions.rst

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -921,11 +921,7 @@ because the :ref:`call protocol <call>` takes care of recursion handling.
921921
922922
Marks a point where a recursive C-level call is about to be performed.
923923
924-
If :c:macro:`!USE_STACKCHECK` is defined, this function checks if the OS
925-
stack overflowed using :c:func:`PyOS_CheckStack`. If this is the case, it
926-
sets a :exc:`MemoryError` and returns a nonzero value.
927-
928-
The function then checks if the recursion limit is reached. If this is the
924+
The function then checks if the stack limit is reached. If this is the
929925
case, a :exc:`RecursionError` is set and a nonzero value is returned.
930926
Otherwise, zero is returned.
931927

Include/cpython/object.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -487,18 +487,19 @@ PyAPI_FUNC(void) _PyTrash_thread_destroy_chain(PyThreadState *tstate);
487487
* we have headroom above the trigger limit */
488488
#define Py_TRASHCAN_HEADROOM 50
489489

490+
/* Helper function for Py_TRASHCAN_BEGIN */
491+
PyAPI_FUNC(int) _Py_ReachedRecursionLimitWithMargin(PyThreadState *tstate, int margin_count);
492+
490493
#define Py_TRASHCAN_BEGIN(op, dealloc) \
491494
do { \
492495
PyThreadState *tstate = PyThreadState_Get(); \
493-
if (tstate->c_recursion_remaining <= Py_TRASHCAN_HEADROOM && Py_TYPE(op)->tp_dealloc == (destructor)dealloc) { \
496+
if (_Py_ReachedRecursionLimitWithMargin(tstate, 1) && Py_TYPE(op)->tp_dealloc == (destructor)dealloc) { \
494497
_PyTrash_thread_deposit_object(tstate, (PyObject *)op); \
495498
break; \
496-
} \
497-
tstate->c_recursion_remaining--;
499+
}
498500
/* The body of the deallocator is here. */
499501
#define Py_TRASHCAN_END \
500-
tstate->c_recursion_remaining++; \
501-
if (tstate->delete_later && tstate->c_recursion_remaining > (Py_TRASHCAN_HEADROOM*2)) { \
502+
if (tstate->delete_later && !_Py_ReachedRecursionLimitWithMargin(tstate, 2)) { \
502503
_PyTrash_thread_destroy_chain(tstate); \
503504
} \
504505
} while (0);

Include/cpython/pystate.h

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ struct _ts {
112112
int py_recursion_remaining;
113113
int py_recursion_limit;
114114

115-
int c_recursion_remaining;
115+
int c_recursion_remaining; /* Retained for backwards compatibility. Do not use */
116116
int recursion_headroom; /* Allow 50 more calls to handle any errors. */
117117

118118
/* 'tracing' keeps track of the execution depth when tracing/profiling.
@@ -202,36 +202,7 @@ struct _ts {
202202
PyObject *threading_local_sentinel;
203203
};
204204

205-
#ifdef Py_DEBUG
206-
// A debug build is likely built with low optimization level which implies
207-
// higher stack memory usage than a release build: use a lower limit.
208-
# define Py_C_RECURSION_LIMIT 500
209-
#elif defined(__s390x__)
210-
# define Py_C_RECURSION_LIMIT 800
211-
#elif defined(_WIN32) && defined(_M_ARM64)
212-
# define Py_C_RECURSION_LIMIT 1000
213-
#elif defined(_WIN32)
214-
# define Py_C_RECURSION_LIMIT 3000
215-
#elif defined(__ANDROID__)
216-
// On an ARM64 emulator, API level 34 was OK with 10000, but API level 21
217-
// crashed in test_compiler_recursion_limit.
218-
# define Py_C_RECURSION_LIMIT 3000
219-
#elif defined(_Py_ADDRESS_SANITIZER)
220-
# define Py_C_RECURSION_LIMIT 4000
221-
#elif defined(__sparc__)
222-
// test_descr crashed on sparc64 with >7000 but let's keep a margin of error.
223-
# define Py_C_RECURSION_LIMIT 4000
224-
#elif defined(__wasi__)
225-
// Based on wasmtime 16.
226-
# define Py_C_RECURSION_LIMIT 5000
227-
#elif defined(__hppa__) || defined(__powerpc64__)
228-
// test_descr crashed with >8000 but let's keep a margin of error.
229-
# define Py_C_RECURSION_LIMIT 5000
230-
#else
231-
// This value is duplicated in Lib/test/support/__init__.py
232-
# define Py_C_RECURSION_LIMIT 10000
233-
#endif
234-
205+
# define Py_C_RECURSION_LIMIT 5000
235206

236207
/* other API */
237208

@@ -246,7 +217,6 @@ _PyThreadState_UncheckedGet(void)
246217
return PyThreadState_GetUnchecked();
247218
}
248219

249-
250220
// Disable tracing and profiling.
251221
PyAPI_FUNC(void) PyThreadState_EnterTracing(PyThreadState *tstate);
252222

Include/internal/pycore_ceval.h

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -193,18 +193,28 @@ extern void _PyEval_DeactivateOpCache(void);
193193

194194
/* --- _Py_EnterRecursiveCall() ----------------------------------------- */
195195

196-
#ifdef USE_STACKCHECK
197-
/* With USE_STACKCHECK macro defined, trigger stack checks in
198-
_Py_CheckRecursiveCall() on every 64th call to _Py_EnterRecursiveCall. */
199-
static inline int _Py_MakeRecCheck(PyThreadState *tstate) {
200-
return (tstate->c_recursion_remaining-- < 0
201-
|| (tstate->c_recursion_remaining & 63) == 0);
196+
#if !_Py__has_builtin(__builtin_frame_address)
197+
static uintptr_t return_pointer_as_int(char* p) {
198+
return (uintptr_t)p;
202199
}
200+
#endif
201+
202+
static inline uintptr_t
203+
_Py_get_machine_stack_pointer(void) {
204+
#if _Py__has_builtin(__builtin_frame_address)
205+
return (uintptr_t)__builtin_frame_address(0);
203206
#else
204-
static inline int _Py_MakeRecCheck(PyThreadState *tstate) {
205-
return tstate->c_recursion_remaining-- < 0;
206-
}
207+
char here;
208+
/* Avoid compiler warning about returning stack address */
209+
return return_pointer_as_int(&here);
207210
#endif
211+
}
212+
213+
static inline int _Py_MakeRecCheck(PyThreadState *tstate) {
214+
uintptr_t here_addr = _Py_get_machine_stack_pointer();
215+
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
216+
return here_addr < _tstate->c_stack_soft_limit;
217+
}
208218

209219
// Export for '_json' shared extension, used via _Py_EnterRecursiveCall()
210220
// static inline function.
@@ -220,23 +230,30 @@ static inline int _Py_EnterRecursiveCallTstate(PyThreadState *tstate,
220230
return (_Py_MakeRecCheck(tstate) && _Py_CheckRecursiveCall(tstate, where));
221231
}
222232

223-
static inline void _Py_EnterRecursiveCallTstateUnchecked(PyThreadState *tstate) {
224-
assert(tstate->c_recursion_remaining > 0);
225-
tstate->c_recursion_remaining--;
226-
}
227-
228233
static inline int _Py_EnterRecursiveCall(const char *where) {
229234
PyThreadState *tstate = _PyThreadState_GET();
230235
return _Py_EnterRecursiveCallTstate(tstate, where);
231236
}
232237

233-
static inline void _Py_LeaveRecursiveCallTstate(PyThreadState *tstate) {
234-
tstate->c_recursion_remaining++;
238+
static inline void _Py_LeaveRecursiveCallTstate(PyThreadState *tstate) {
239+
(void)tstate;
240+
}
241+
242+
PyAPI_FUNC(void) _Py_InitializeRecursionLimits(PyThreadState *tstate);
243+
244+
static inline int _Py_ReachedRecursionLimit(PyThreadState *tstate) {
245+
uintptr_t here_addr = _Py_get_machine_stack_pointer();
246+
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
247+
if (here_addr > _tstate->c_stack_soft_limit) {
248+
return 0;
249+
}
250+
if (_tstate->c_stack_hard_limit == 0) {
251+
_Py_InitializeRecursionLimits(tstate);
252+
}
253+
return here_addr <= _tstate->c_stack_soft_limit;
235254
}
236255

237256
static inline void _Py_LeaveRecursiveCall(void) {
238-
PyThreadState *tstate = _PyThreadState_GET();
239-
_Py_LeaveRecursiveCallTstate(tstate);
240257
}
241258

242259
extern struct _PyInterpreterFrame* _PyEval_GetFrame(void);
@@ -327,7 +344,6 @@ void _Py_unset_eval_breaker_bit_all(PyInterpreterState *interp, uintptr_t bit);
327344

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

330-
331347
#ifdef __cplusplus
332348
}
333349
#endif

Include/internal/pycore_symtable.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,6 @@ struct symtable {
8282
PyObject *st_private; /* name of current class or NULL */
8383
_PyFutureFeatures *st_future; /* module's future features that affect
8484
the symbol table */
85-
int recursion_depth; /* current recursion depth */
86-
int recursion_limit; /* recursion limit */
8785
};
8886

8987
typedef struct _symtable_entry {

Include/internal/pycore_tstate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ typedef struct _PyThreadStateImpl {
2121
// semi-public fields are in PyThreadState.
2222
PyThreadState base;
2323

24+
// These are addresses, but we need to convert to ints to avoid UB.
25+
uintptr_t c_stack_top;
26+
uintptr_t c_stack_soft_limit;
27+
uintptr_t c_stack_hard_limit;
28+
2429
PyObject *asyncio_running_loop; // Strong reference
2530
PyObject *asyncio_running_task; // Strong reference
2631

Include/pythonrun.h

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,23 @@ PyAPI_FUNC(void) PyErr_DisplayException(PyObject *);
2121
/* Stuff with no proper home (yet) */
2222
PyAPI_DATA(int) (*PyOS_InputHook)(void);
2323

24-
/* Stack size, in "pointers" (so we get extra safety margins
25-
on 64-bit platforms). On a 32-bit platform, this translates
26-
to an 8k margin. */
27-
#define PYOS_STACK_MARGIN 2048
28-
29-
#if defined(WIN32) && !defined(MS_WIN64) && !defined(_M_ARM) && defined(_MSC_VER) && _MSC_VER >= 1300
30-
/* Enable stack checking under Microsoft C */
31-
// When changing the platforms, ensure PyOS_CheckStack() docs are still correct
24+
/* Stack size, in "pointers". This must be large enough, so
25+
* no two calls to check recursion depth are more than this far
26+
* apart. In practice, that means it must be larger than the C
27+
* stack consumption of PyEval_EvalDefault */
28+
#if defined(_Py_ADDRESS_SANITIZER) || defined(_Py_THREAD_SANITIZER)
29+
# define PYOS_STACK_MARGIN 4096
30+
#elif defined(Py_DEBUG) && defined(WIN32)
31+
# define PYOS_STACK_MARGIN 3072
32+
#elif defined(__wasi__)
33+
/* Web assembly has two stacks, so this isn't really a size */
34+
# define PYOS_STACK_MARGIN 500
35+
#else
36+
# define PYOS_STACK_MARGIN 2048
37+
#endif
38+
#define PYOS_STACK_MARGIN_BYTES (PYOS_STACK_MARGIN * sizeof(void *))
39+
40+
#if defined(WIN32)
3241
#define USE_STACKCHECK
3342
#endif
3443

Lib/test/list_tests.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from functools import cmp_to_key
77

88
from test import seq_tests
9-
from test.support import ALWAYS_EQ, NEVER_EQ, get_c_recursion_limit, skip_emscripten_stack_overflow
9+
from test.support import ALWAYS_EQ, NEVER_EQ
10+
from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow
1011

1112

1213
class CommonTest(seq_tests.CommonTest):
@@ -59,10 +60,11 @@ def test_repr(self):
5960
self.assertEqual(str(a2), "[0, 1, 2, [...], 3]")
6061
self.assertEqual(repr(a2), "[0, 1, 2, [...], 3]")
6162

63+
@skip_wasi_stack_overflow()
6264
@skip_emscripten_stack_overflow()
6365
def test_repr_deep(self):
6466
a = self.type2test([])
65-
for i in range(get_c_recursion_limit() + 1):
67+
for i in range(200_000):
6668
a = self.type2test([a])
6769
self.assertRaises(RecursionError, repr, a)
6870

Lib/test/mapping_tests.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# tests common to dict and UserDict
22
import unittest
33
import collections
4-
from test.support import get_c_recursion_limit, skip_emscripten_stack_overflow
4+
from test import support
55

66

77
class BasicTestMappingProtocol(unittest.TestCase):
@@ -622,10 +622,11 @@ def __repr__(self):
622622
d = self._full_mapping({1: BadRepr()})
623623
self.assertRaises(Exc, repr, d)
624624

625-
@skip_emscripten_stack_overflow()
625+
@support.skip_wasi_stack_overflow()
626+
@support.skip_emscripten_stack_overflow()
626627
def test_repr_deep(self):
627628
d = self._empty_mapping()
628-
for i in range(get_c_recursion_limit() + 1):
629+
for i in range(support.exceeds_recursion_limit()):
629630
d0 = d
630631
d = self._empty_mapping()
631632
d[1] = d0

Lib/test/pythoninfo.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,6 @@ def collect_testcapi(info_add):
684684
for name in (
685685
'LONG_MAX', # always 32-bit on Windows, 64-bit on 64-bit Unix
686686
'PY_SSIZE_T_MAX',
687-
'Py_C_RECURSION_LIMIT',
688687
'SIZEOF_TIME_T', # 32-bit or 64-bit depending on the platform
689688
'SIZEOF_WCHAR_T', # 16-bit or 32-bit depending on the platform
690689
):

0 commit comments

Comments
 (0)