Skip to content

Commit afeb866

Browse files
committed
Implement C recursion protection with limit pointers
1 parent fc910a3 commit afeb866

File tree

20 files changed

+87
-107
lines changed

20 files changed

+87
-107
lines changed

Include/ceval.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ PyAPI_FUNC(int) Py_EnterRecursiveCall(const char *where);
6161
PyAPI_FUNC(void) Py_LeaveRecursiveCall(void);
6262

6363
PyAPI_FUNC(int) Py_ReachedRecursionLimit(PyThreadState *tstate, int margin_count);
64-
PyAPI_FUNC(void) _Py_EnterRecursiveCallUnchecked(PyThreadState *tstate);
6564
PyAPI_FUNC(void) Py_LeaveRecursiveCallTstate(PyThreadState *tstate);
6665

6766
PyAPI_FUNC(const char *) PyEval_GetFuncName(PyObject *);

Include/cpython/object.h

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -493,11 +493,9 @@ do { \
493493
if (Py_ReachedRecursionLimit(tstate, 1) && Py_TYPE(op)->tp_dealloc == (destructor)dealloc) { \
494494
_PyTrash_thread_deposit_object(tstate, (PyObject *)op); \
495495
break; \
496-
} \
497-
_Py_EnterRecursiveCallUnchecked(tstate);
496+
}
498497
/* The body of the deallocator is here. */
499498
#define Py_TRASHCAN_END \
500-
Py_LeaveRecursiveCallTstate(tstate); \
501499
if (tstate->delete_later && !Py_ReachedRecursionLimit(tstate, 2)) { \
502500
_PyTrash_thread_destroy_chain(tstate); \
503501
} \

Include/cpython/pystate.h

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

115-
int c_recursion_remaining;
115+
char *c_stack_soft_limit;
116+
char *c_stack_hard_limit;
116117
int recursion_headroom; /* Allow 50 more calls to handle any errors. */
117118

118119
/* 'tracing' keeps track of the execution depth when tracing/profiling.
@@ -202,34 +203,45 @@ struct _ts {
202203
PyObject *threading_local_sentinel;
203204
};
204205

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
206+
207+
#if defined(__s390x__)
208+
# define Py_C_STACK_SIZE 320000
211209
#elif defined(_WIN32) && defined(_M_ARM64)
212-
# define Py_C_RECURSION_LIMIT 1000
210+
# define Py_C_STACK_SIZE 400000
213211
#elif defined(_WIN32)
214-
# define Py_C_RECURSION_LIMIT 3000
212+
# define Py_C_STACK_SIZE 1200000
215213
#elif defined(__ANDROID__)
216214
// On an ARM64 emulator, API level 34 was OK with 10000, but API level 21
217215
// 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
216+
# define Py_C_STACK_SIZE 1200000
221217
#elif defined(__sparc__)
222218
// test_descr crashed on sparc64 with >7000 but let's keep a margin of error.
223-
# define Py_C_RECURSION_LIMIT 4000
219+
# define Py_C_STACK_SIZE 1600000
224220
#elif defined(__wasi__)
225221
// Based on wasmtime 16.
226-
# define Py_C_RECURSION_LIMIT 5000
222+
# define Py_C_STACK_SIZE 2000000
227223
#elif defined(__hppa__) || defined(__powerpc64__)
228224
// test_descr crashed with >8000 but let's keep a margin of error.
229-
# define Py_C_RECURSION_LIMIT 5000
225+
# define Py_C_STACK_SIZE 2000000
230226
#else
231227
// This value is duplicated in Lib/test/support/__init__.py
232-
# define Py_C_RECURSION_LIMIT 10000
228+
# define Py_C_STACK_SIZE 5000000
229+
#endif
230+
231+
232+
#ifdef Py_DEBUG
233+
// A debug build is likely built with low optimization level which implies
234+
// higher stack memory usage than a release build: use a lower limit.
235+
# if defined(__has_feature) /* Clang */
236+
// Clang debug builds use a lot of stack space
237+
# define Py_C_RECURSION_LIMIT (Py_C_STACK_SIZE / 2000)
238+
# else
239+
# define Py_C_RECURSION_LIMIT (Py_C_STACK_SIZE / 1000)
240+
# endif
241+
#elif defined(_Py_ADDRESS_SANITIZER)
242+
# define Py_C_STACK_SIZE (Py_C_STACK_SIZE / 600)
243+
#else
244+
# define Py_C_RECURSION_LIMIT (Py_C_STACK_SIZE / 300)
233245
#endif
234246

235247

Include/internal/pycore_ceval.h

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -193,18 +193,10 @@ 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. */
199196
static inline int _Py_MakeRecCheck(PyThreadState *tstate) {
200-
return (tstate->c_recursion_remaining-- < 0
201-
|| (tstate->c_recursion_remaining & 63) == 0);
197+
char here;
198+
return &here < tstate->c_stack_soft_limit;
202199
}
203-
#else
204-
static inline int _Py_MakeRecCheck(PyThreadState *tstate) {
205-
return tstate->c_recursion_remaining-- < 0;
206-
}
207-
#endif
208200

209201
// Export for '_json' shared extension, used via _Py_EnterRecursiveCall()
210202
// static inline function.
@@ -220,29 +212,21 @@ static inline int _Py_EnterRecursiveCallTstate(PyThreadState *tstate,
220212
return (_Py_MakeRecCheck(tstate) && _Py_CheckRecursiveCall(tstate, where));
221213
}
222214

223-
static inline void _Py_EnterRecursiveCallTstateUnchecked(PyThreadState *tstate) {
224-
assert(tstate->c_recursion_remaining >= -2); // Allow a bit of wiggle room
225-
tstate->c_recursion_remaining--;
226-
}
227-
228215
static inline int _Py_EnterRecursiveCall(const char *where) {
229216
PyThreadState *tstate = _PyThreadState_GET();
230217
return _Py_EnterRecursiveCallTstate(tstate, where);
231218
}
232219

233220
static inline void _Py_LeaveRecursiveCallTstate(PyThreadState *tstate) {
234-
tstate->c_recursion_remaining++;
221+
(void)tstate;
235222
}
236223

237-
#define Py_RECURSION_LIMIT_MARGIN_MULTIPLIER 50
238-
239224
static inline int _Py_ReachedRecursionLimit(PyThreadState *tstate, int margin_count) {
240-
return tstate->c_recursion_remaining <= margin_count * Py_RECURSION_LIMIT_MARGIN_MULTIPLIER;
225+
char here;
226+
return &here <= tstate->c_stack_soft_limit + margin_count * PYOS_STACK_MARGIN_BYTES;
241227
}
242228

243229
static inline void _Py_LeaveRecursiveCall(void) {
244-
PyThreadState *tstate = _PyThreadState_GET();
245-
_Py_LeaveRecursiveCallTstate(tstate);
246230
}
247231

248232
extern struct _PyInterpreterFrame* _PyEval_GetFrame(void);

Include/pythonrun.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ PyAPI_DATA(int) (*PyOS_InputHook)(void);
2525
on 64-bit platforms). On a 32-bit platform, this translates
2626
to an 8k margin. */
2727
#define PYOS_STACK_MARGIN 2048
28+
#define PYOS_STACK_MARGIN_BYTES (PYOS_STACK_MARGIN * sizeof(void *))
2829

2930
#if defined(WIN32) && !defined(MS_WIN64) && !defined(_M_ARM) && defined(_MSC_VER) && _MSC_VER >= 1300
3031
/* Enable stack checking under Microsoft C */

Lib/test/list_tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def test_repr(self):
6262
@skip_emscripten_stack_overflow()
6363
def test_repr_deep(self):
6464
a = self.type2test([])
65-
for i in range(get_c_recursion_limit() + 1):
65+
for i in range(get_c_recursion_limit() * 10):
6666
a = self.type2test([a])
6767
self.assertRaises(RecursionError, repr, a)
6868

Lib/test/mapping_tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ def __repr__(self):
625625
@skip_emscripten_stack_overflow()
626626
def test_repr_deep(self):
627627
d = self._empty_mapping()
628-
for i in range(get_c_recursion_limit() + 1):
628+
for i in range(get_c_recursion_limit() * 5):
629629
d0 = d
630630
d = self._empty_mapping()
631631
d[1] = d0

Lib/test/support/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2634,7 +2634,7 @@ def get_c_recursion_limit():
26342634

26352635
def exceeds_recursion_limit():
26362636
"""For recursion tests, easily exceeds default recursion limit."""
2637-
return get_c_recursion_limit() * 3
2637+
return get_c_recursion_limit() * 20
26382638

26392639

26402640
# Windows doesn't have os.uname() but it doesn't support s390x.

Lib/test/test_compile.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -712,11 +712,11 @@ def test_yet_more_evil_still_undecodable(self):
712712
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
713713
@support.skip_emscripten_stack_overflow()
714714
def test_compiler_recursion_limit(self):
715-
# Expected limit is Py_C_RECURSION_LIMIT
716-
limit = get_c_recursion_limit()
717-
fail_depth = limit + 1
718-
crash_depth = limit * 100
719-
success_depth = int(limit * 0.8)
715+
# Compiler frames are small
716+
limit = get_c_recursion_limit() * 3 // 2
717+
fail_depth = limit * 10
718+
crash_depth = limit * 50
719+
success_depth = limit
720720

721721
def check_limit(prefix, repeated, mode="single"):
722722
expect_ok = prefix + repeated * success_depth

Lib/test/test_dict.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ def __repr__(self):
597597
@support.skip_emscripten_stack_overflow()
598598
def test_repr_deep(self):
599599
d = {}
600-
for i in range(get_c_recursion_limit() + 1):
600+
for i in range(get_c_recursion_limit() * 10):
601601
d = {1: d}
602602
self.assertRaises(RecursionError, repr, d)
603603

0 commit comments

Comments
 (0)