Skip to content

Commit 4448ac8

Browse files
committed
gh-148925: Add signal-safe PyUnstable_* APIs for call-stack iteration
Add four new public APIs for walking the Python call stack without allocating memory, changing reference counts, or acquiring the GIL, making them safe to call from signal handlers and custom memory allocator hooks: - PyUnstable_ThreadState_GetInterpreterFrame(tstate): returns the innermost complete interpreter frame, skipping entry trampolines and pre-RESUME frames automatically. - PyUnstable_InterpreterFrame_GetNextComplete(frame): returns the next complete calling frame, skipping incomplete frames. Mirrors PyFrame_GetBack semantics without allocating. - PyUnstable_InterpreterFrame_GetCodeSafe(frame): returns a borrowed reference to the code object, or NULL if freed memory is detected. - PyUnstable_InterpreterFrame_GetLineSafe(frame): returns the current line number, validating the instruction offset rather than asserting, safe when the frame may be partially torn down. All four use _Py_NO_SANITIZE_THREAD to suppress intentional racy reads and heuristics (_PyMem_IsPtrFreed) to detect freed memory. Switch tracemalloc and the fatal error traceback printer to use the new APIs instead of internal helpers. Add tests in Lib/test/test_capi/test_misc.py and test helpers in Modules/_testinternalcapi.c.
1 parent c56f335 commit 4448ac8

8 files changed

Lines changed: 430 additions & 26 deletions

File tree

Doc/c-api/frame.rst

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,31 @@ Unless using :pep:`523`, you will not need this.
226226
227227
.. c:function:: PyObject* PyUnstable_InterpreterFrame_GetCode(struct _PyInterpreterFrame *frame);
228228
229-
Return a :term:`strong reference` to the code object for the frame.
229+
Return a :term:`strong reference` to the code object for the frame.
230+
Does not raise an exception.
231+
232+
If allocation and reference count changes are not permitted (for example,
233+
from a signal handler or a custom memory allocator), use
234+
:c:func:`PyUnstable_InterpreterFrame_GetCodeSafe` instead.
230235
231236
.. versionadded:: 3.12
232237
233238
239+
.. c:function:: PyObject* PyUnstable_InterpreterFrame_GetCodeSafe(struct _PyInterpreterFrame *frame);
240+
241+
Return a :term:`borrowed reference` to the code object for the frame.
242+
The reference is valid as long as the frame is alive.
243+
244+
Use this instead of :c:func:`PyUnstable_InterpreterFrame_GetCode` when
245+
allocation and reference count changes are not permitted (for example,
246+
from a signal handler or a custom memory allocator). Does not allocate
247+
memory, does not change any reference counts, does not acquire or release
248+
the GIL, and does not raise an exception. Uses heuristics to detect freed
249+
memory — not 100% reliable in the presence of concurrent deallocation.
250+
251+
.. versionadded:: 3.15
252+
253+
234254
.. c:function:: int PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame *frame);
235255
236256
Return the byte offset into the last executed instruction.
@@ -243,3 +263,53 @@ Unless using :pep:`523`, you will not need this.
243263
Return the currently executing line number, or -1 if there is no line number.
244264
245265
.. versionadded:: 3.12
266+
267+
268+
.. c:function:: int PyUnstable_InterpreterFrame_GetLineSafe(struct _PyInterpreterFrame *frame)
269+
270+
Return the currently executing line number, or ``-1`` if there is no line
271+
number or the frame is invalid. Does not raise an exception.
272+
273+
Unlike :c:func:`PyUnstable_InterpreterFrame_GetLine`, validates the code
274+
object and instruction offset before accessing the line table rather than
275+
asserting them, making it safe to call when the frame state may be
276+
partially torn down.
277+
278+
.. versionadded:: 3.15
279+
280+
281+
.. c:function:: struct _PyInterpreterFrame* PyUnstable_ThreadState_GetInterpreterFrame(PyThreadState *tstate)
282+
283+
Return the innermost complete interpreter frame of *tstate*, or ``NULL`` if
284+
the thread has no complete frame or freed memory is detected. Incomplete
285+
frames (interpreter entry trampolines and frames that have not yet begun
286+
executing) are skipped automatically.
287+
288+
Does not allocate memory, does not raise an exception, and does not acquire
289+
or release the GIL. Safe to call from a signal handler; racy reads from
290+
other threads are intentional. Uses heuristics to detect freed memory —
291+
not 100% reliable in the presence of concurrent deallocation.
292+
293+
To iterate over the full call stack, call
294+
:c:func:`PyUnstable_InterpreterFrame_GetNextComplete` repeatedly on the
295+
returned frame until it returns ``NULL``.
296+
297+
.. versionadded:: 3.15
298+
299+
300+
.. c:function:: struct _PyInterpreterFrame* PyUnstable_InterpreterFrame_GetNextComplete(struct _PyInterpreterFrame *frame)
301+
302+
Return the next (calling) complete frame, or ``NULL`` if *frame* is the
303+
outermost complete frame or freed memory is detected. Incomplete frames are
304+
skipped automatically.
305+
306+
Does not allocate memory, does not raise an exception, and does not acquire
307+
or release the GIL. Safe to call from a signal handler; racy reads from
308+
other threads are intentional. Uses heuristics to detect freed memory —
309+
not 100% reliable in the presence of concurrent deallocation.
310+
311+
Unlike :c:func:`PyFrame_GetBack`, this function never allocates memory,
312+
making it safe to call from a custom memory allocator hook without risking
313+
re-entrant allocation.
314+
315+
.. versionadded:: 3.15

Include/cpython/pyframe.h

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,20 @@ PyAPI_FUNC(PyObject*) PyFrame_GetVarString(PyFrameObject *frame, const char *nam
2525
struct _PyInterpreterFrame;
2626

2727
/* Returns the code object of the frame (strong reference).
28-
* Does not raise an exception. */
28+
* Does not raise an exception.
29+
* If allocation and reference count changes are not permitted, use
30+
* PyUnstable_InterpreterFrame_GetCodeSafe instead. */
2931
PyAPI_FUNC(PyObject *) PyUnstable_InterpreterFrame_GetCode(struct _PyInterpreterFrame *frame);
3032

33+
/* Returns the code object of the frame as a borrowed reference.
34+
* The reference is valid as long as the frame is alive.
35+
* Use instead of PyUnstable_InterpreterFrame_GetCode when allocation and
36+
* reference count changes are not permitted (e.g. from a signal handler or
37+
* a custom memory allocator). Does not allocate, does not change any
38+
* reference counts, does not acquire or release the GIL, does not raise an
39+
* exception. Uses heuristics to detect freed memory; not 100% reliable. */
40+
PyAPI_FUNC(PyObject *) PyUnstable_InterpreterFrame_GetCodeSafe(struct _PyInterpreterFrame *frame);
41+
3142
/* Returns a byte offset into the last executed instruction.
3243
* Does not raise an exception. */
3344
PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame *frame);
@@ -36,6 +47,36 @@ PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame
3647
* Does not raise an exception. */
3748
PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLine(struct _PyInterpreterFrame *frame);
3849

50+
/* Returns the currently executing line number, or -1 if there is no line
51+
* number or the frame is invalid.
52+
* Unlike PyUnstable_InterpreterFrame_GetLine, validates the code object and
53+
* instruction offset before accessing the line table rather than asserting
54+
* them, making it safe to call when the frame state may be partially torn
55+
* down. Does not raise an exception. */
56+
PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLineSafe(struct _PyInterpreterFrame *frame);
57+
58+
59+
/* Returns the innermost complete interpreter frame of the thread state, or
60+
* NULL if the thread has no complete frame or freed memory is detected.
61+
* Skips over incomplete frames (interpreter entry trampolines and frames that
62+
* have not yet begun executing) automatically.
63+
* Does not allocate memory, does not acquire or release the GIL, does not
64+
* raise an exception. Safe to call from signal handlers; racy reads from
65+
* other threads are intentional and suppressed (_Py_NO_SANITIZE_THREAD).
66+
* Uses heuristics to detect freed memory; not 100% reliable. */
67+
PyAPI_FUNC(struct _PyInterpreterFrame *)
68+
PyUnstable_ThreadState_GetInterpreterFrame(PyThreadState *tstate);
69+
70+
/* Returns the next (calling) complete frame, or NULL if frame is the
71+
* outermost complete frame or freed memory is detected.
72+
* Skips over incomplete frames automatically.
73+
* Does not allocate memory, does not acquire or release the GIL, does not
74+
* raise an exception. Safe to call from signal handlers; racy reads from
75+
* other threads are intentional and suppressed (_Py_NO_SANITIZE_THREAD).
76+
* Uses heuristics to detect freed memory; not 100% reliable. */
77+
PyAPI_FUNC(struct _PyInterpreterFrame *)
78+
PyUnstable_InterpreterFrame_GetNextComplete(struct _PyInterpreterFrame *frame);
79+
3980
#define PyUnstable_EXECUTABLE_KIND_SKIP 0
4081
#define PyUnstable_EXECUTABLE_KIND_PY_FUNCTION 1
4182
#define PyUnstable_EXECUTABLE_KIND_BUILTIN_FUNCTION 3

Lib/test/test_capi/test_misc.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2789,6 +2789,86 @@ def test_line(self):
27892789
firstline = self.func.__code__.co_firstlineno
27902790
self.assertEqual(line, firstline + 2)
27912791

2792+
def test_tstate_getframe_is_current(self):
2793+
# PyUnstable_ThreadState_GetInterpreterFrame must return the same
2794+
# PyFrameObject as sys._getframe(0) when called at the same level.
2795+
frame_from_c = _testinternalcapi.tstate_getframe()
2796+
self.assertIs(frame_from_c, sys._getframe(0))
2797+
2798+
def test_getnextcomplete_matches_f_back(self):
2799+
# GetNextComplete must match f_back at every step, all the way to None.
2800+
c_frame = _testinternalcapi.tstate_getframe()
2801+
py_frame = sys._getframe(0)
2802+
while c_frame is not None:
2803+
self.assertIs(c_frame, py_frame)
2804+
c_frame = _testinternalcapi.iframe_getnextcomplete(c_frame)
2805+
py_frame = py_frame.f_back
2806+
self.assertIsNone(py_frame)
2807+
2808+
def test_stack_to_yaml(self):
2809+
# stack_to_yaml uses only signal-safe operations for the walk and
2810+
# emission (no allocation, no refcount changes, no GIL release).
2811+
# Verify the output is well-formed and contains the expected frames.
2812+
def inner():
2813+
return _testinternalcapi.stack_to_yaml()
2814+
2815+
yaml_str = inner()
2816+
self.assertIsInstance(yaml_str, str)
2817+
2818+
# Parse the YAML manually to avoid a PyYAML dependency.
2819+
# Format is blocks of:
2820+
# - filename: <path>
2821+
# name: <name>
2822+
# lineno: <n>
2823+
frames = []
2824+
current = {}
2825+
for line in yaml_str.splitlines():
2826+
if line.startswith('- filename: '):
2827+
if current:
2828+
frames.append(current)
2829+
current = {'filename': line[len('- filename: '):]}
2830+
elif line.startswith(' name: '):
2831+
current['name'] = line[len(' name: '):]
2832+
elif line.startswith(' lineno: '):
2833+
current['lineno'] = int(line[len(' lineno: '):])
2834+
if current:
2835+
frames.append(current)
2836+
2837+
self.assertGreater(len(frames), 1)
2838+
# Innermost frame is the Python function that called stack_to_yaml.
2839+
self.assertEqual(frames[0]['name'], 'inner')
2840+
# Next frame is this test method.
2841+
self.assertEqual(frames[1]['name'], 'test_stack_to_yaml')
2842+
# Every frame must have a non-empty filename and a valid lineno.
2843+
for f in frames:
2844+
self.assertTrue(f.get('filename'), f)
2845+
self.assertIsInstance(f.get('lineno'), int, f)
2846+
2847+
def test_getcodesafe_matches_fcode(self):
2848+
# GetCodeSafe must return the same code object as frame.f_code.
2849+
frame = _testinternalcapi.tstate_getframe()
2850+
self.assertIs(_testinternalcapi.iframe_getcodesafe(frame), frame.f_code)
2851+
2852+
2853+
def test_iframe_getlinesafe(self):
2854+
# Use a generator frame frozen at a yield point so that iframe_getlasti
2855+
# and iframe_getlinesafe both read the same (stable) instruction pointer.
2856+
def gen():
2857+
yield
2858+
g = gen()
2859+
next(g)
2860+
frame = g.gi_frame
2861+
lasti = _testinternalcapi.iframe_getlasti(frame)
2862+
lineno = _testinternalcapi.iframe_getlinesafe(frame)
2863+
if lasti >= 0:
2864+
expected = None
2865+
for start, end, ln in frame.f_code.co_lines():
2866+
if ln is not None and start <= lasti < end:
2867+
expected = ln
2868+
break
2869+
if expected is not None:
2870+
self.assertEqual(lineno, expected)
2871+
27922872

27932873
SUFFICIENT_TO_DEOPT_AND_SPECIALIZE = 100
27942874

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Add four new ``PyUnstable_*`` C API functions for signal-safe, allocation-free
2+
call-stack iteration: :c:func:`PyUnstable_ThreadState_GetInterpreterFrame`,
3+
:c:func:`PyUnstable_InterpreterFrame_GetNextComplete`,
4+
:c:func:`PyUnstable_InterpreterFrame_GetCodeSafe`, and
5+
:c:func:`PyUnstable_InterpreterFrame_GetLineSafe`. These functions do not
6+
allocate memory, do not change reference counts, and do not acquire the GIL,
7+
making them safe to call from signal handlers and custom memory allocator hooks.

0 commit comments

Comments
 (0)