Skip to content

Commit 923d686

Browse files
authored
Add internal doc describing the stack protection mechanism (GH137663)
1 parent 15ab457 commit 923d686

File tree

2 files changed

+63
-0
lines changed

2 files changed

+63
-0
lines changed

InternalDocs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Program Execution
4444

4545
- [Quiescent-State Based Reclamation (QSBR)](qsbr.md)
4646

47+
- [Stack protection](stack_protection.md)
48+
4749
Modules
4850
---
4951

InternalDocs/stack_protection.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Stack Protection
2+
3+
CPython protects against stack overflow in the form of runaway, or just very deep, recursion by raising a `RecursionError` instead of just crashing.
4+
Protection against pure Python stack recursion has existed since very early, but in 3.12 we added protection against stack overflow
5+
in C code. This was initially implemented using a counter and later improved in 3.14 to use the actual stack depth.
6+
For those platforms that support it (Windows, Mac, and most Linuxes) we query the operating system to find the stack bounds.
7+
For other platforms we use conserative estimates.
8+
9+
10+
The C stack looks like this:
11+
12+
```
13+
+-------+ <--- Top of machine stack
14+
| |
15+
| |
16+
17+
~~
18+
19+
| |
20+
| |
21+
+-------+ <--- Soft limit
22+
| |
23+
| | _PyOS_STACK_MARGIN_BYTES
24+
| |
25+
+-------+ <--- Hard limit
26+
| |
27+
| | _PyOS_STACK_MARGIN_BYTES
28+
| |
29+
+-------+ <--- Bottom of machine stack
30+
```
31+
32+
33+
We get the current stack pointer using compiler intrinsics where available, or by taking the address of a C local variable. See `_Py_get_machine_stack_pointer()`.
34+
35+
The soft and hard limits pointers are set by calling `_Py_InitializeRecursionLimits()` during thread initialization.
36+
37+
Recursion checks are performed by `_Py_EnterRecursiveCall()` or `_Py_EnterRecursiveCallTstate()` which compare the stack pointer to the soft limit. If the stack pointer is lower than the soft limit, then `_Py_CheckRecursiveCall()` is called which checks against both the hard and soft limits:
38+
39+
```python
40+
kb_used = (stack_top - stack_pointer)>>10
41+
if stack_pointer < hard_limit:
42+
FatalError(f"Unrecoverable stack overflow (used {kb_used} kB)")
43+
elif stack_pointer < soft_limit:
44+
raise RecursionError(f"Stack overflow (used {kb_used} kB)")
45+
```
46+
47+
### Diagnosing and fixing stack overflows
48+
49+
For stack protection to work correctly the amount of stack consumed between calls to `_Py_EnterRecursiveCall()` must be less than `_PyOS_STACK_MARGIN_BYTES`.
50+
51+
If you see a traceback ending in: `RecursionError: Stack overflow (used ... kB)` then the stack protection is working as intended. If you don't expect to see the error, then check the amount of stack used. If it seems low then CPython may not be configured properly.
52+
53+
However, if you see a fatal error or crash, then something is not right.
54+
Either a recursive call is not checking `_Py_EnterRecursiveCall()`, or the amount of C stack consumed by a single call exceeds `_PyOS_STACK_MARGIN_BYTES`. If a hard crash occurs, it probably means that the amount of C stack consumed is more than double `_PyOS_STACK_MARGIN_BYTES`.
55+
56+
Likely causes:
57+
* Recursive code is not calling `_Py_EnterRecursiveCall()`
58+
* `-O0` compilation flags, especially for Clang. With no optimization, C calls can consume a lot of stack space
59+
* Giant, complex functions in third-party C extensions. This is unlikely as the function in question would need to be more complicated than the bytecode interpreter.
60+
* `_PyOS_STACK_MARGIN_BYTES` is just too low.
61+
* `_Py_InitializeRecursionLimits()` is not setting the soft and hard limits correctly for that platform.

0 commit comments

Comments
 (0)