Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Next Next commit
gh-138122: Add incomplete sample detection and fix generator frame un…
…winding

Add PyThreadState.entry_frame to track the bottommost frame in each thread.
The profiler validates unwinding reached this frame, rejecting incomplete
samples caused by race conditions or errors. Also fix unwinding to skip
suspended generator frames which have NULL previous pointers.
  • Loading branch information
pablogsal committed Nov 25, 2025
commit 7209306f5787adcdfa0b14136be8cd1aca06d472
5 changes: 5 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ struct _ts {
/* Pointer to currently executing frame. */
struct _PyInterpreterFrame *current_frame;

/* Pointer to the entry/bottommost frame of the current call stack.
* This is the frame that was entered when starting execution.
* Used by profiling/sampling to detect incomplete stack traces. */
struct _PyInterpreterFrame *entry_frame;

Py_tracefunc c_profilefunc;
Py_tracefunc c_tracefunc;
PyObject *c_profileobj;
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_debug_offsets.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ typedef struct _Py_DebugOffsets {
uint64_t next;
uint64_t interp;
uint64_t current_frame;
uint64_t entry_frame;
uint64_t thread_id;
uint64_t native_thread_id;
uint64_t datastack_chunk;
Expand Down Expand Up @@ -272,6 +273,7 @@ typedef struct _Py_DebugOffsets {
.next = offsetof(PyThreadState, next), \
.interp = offsetof(PyThreadState, interp), \
.current_frame = offsetof(PyThreadState, current_frame), \
.entry_frame = offsetof(PyThreadState, entry_frame), \
.thread_id = offsetof(PyThreadState, thread_id), \
.native_thread_id = offsetof(PyThreadState, native_thread_id), \
.datastack_chunk = offsetof(PyThreadState, datastack_chunk), \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Add incomplete sample detection to prevent corrupted profiling data. The
interpreter now tracks the base frame (bottommost frame) in each thread's
``PyThreadState.base_frame``, which the profiler uses to validate that
stack unwinding reached the expected bottom. Samples that fail to unwind
completely (due to race conditions, memory corruption, or other errors)
are now rejected rather than being included as spurious single-frame stacks.
3 changes: 2 additions & 1 deletion Modules/_remote_debugging/_remote_debugging.h
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,8 @@ extern int process_frame_chain(
uintptr_t initial_frame_addr,
StackChunkList *chunks,
PyObject *frame_info,
uintptr_t gc_frame
uintptr_t gc_frame,
uintptr_t base_frame_addr
);

/* ============================================================================
Expand Down
16 changes: 15 additions & 1 deletion Modules/_remote_debugging/frames.c
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,17 @@ process_frame_chain(
uintptr_t initial_frame_addr,
StackChunkList *chunks,
PyObject *frame_info,
uintptr_t gc_frame)
uintptr_t gc_frame,
uintptr_t base_frame_addr)
{
uintptr_t frame_addr = initial_frame_addr;
uintptr_t prev_frame_addr = 0;
uintptr_t last_frame_addr = 0; // Track the last frame we processed
const size_t MAX_FRAMES = 1024;
size_t frame_count = 0;

while ((void*)frame_addr != NULL) {
last_frame_addr = frame_addr; // Remember this frame before moving to next
PyObject *frame = NULL;
uintptr_t next_frame_addr = 0;
uintptr_t stackpointer = 0;
Expand Down Expand Up @@ -347,5 +350,16 @@ process_frame_chain(
frame_addr = next_frame_addr;
}

// Validate we reached the base frame if it's set
if (base_frame_addr != 0 && last_frame_addr != 0) {
if (last_frame_addr != base_frame_addr) {
// We didn't reach the expected bottom frame - incomplete sample
PyErr_Format(PyExc_RuntimeError,
"Incomplete sample: reached frame 0x%lx but expected base frame 0x%lx",
last_frame_addr, base_frame_addr);
return -1;
}
}

return 0;
}
8 changes: 7 additions & 1 deletion Modules/_remote_debugging/threads.c
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,13 @@
goto error;
}

if (process_frame_chain(unwinder, frame_addr, &chunks, frame_info, gc_frame) < 0) {
// Read base_frame for validation
uintptr_t base_frame_addr = 0;
if (unwinder->debug_offsets.thread_state.base_frame != 0) {

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Hypothesis tests on Ubuntu

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Cross build Linux

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

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

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Windows / Build and test (x64)

'base_frame': is not a member of '_thread_state' [D:\a\cpython\cpython\PCbuild\_remote_debugging.vcxproj]

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

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

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Windows (free-threading) / Build and test (x64)

'base_frame': is not a member of '_thread_state' [D:\a\cpython\cpython\PCbuild\_remote_debugging.vcxproj]

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

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

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

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

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

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

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Address sanitizer (ubuntu-24.04)

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Windows (free-threading) / Build and test (arm64)

'base_frame': is not a member of '_thread_state' [C:\a\cpython\cpython\PCbuild\_remote_debugging.vcxproj]

Check failure on line 393 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Windows / Build and test (arm64)

'base_frame': is not a member of '_thread_state' [C:\a\cpython\cpython\PCbuild\_remote_debugging.vcxproj]
base_frame_addr = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.base_frame);

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Hypothesis tests on Ubuntu

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Cross build Linux

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

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

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Windows / Build and test (x64)

'base_frame': is not a member of '_thread_state' [D:\a\cpython\cpython\PCbuild\_remote_debugging.vcxproj]

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

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

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Windows (free-threading) / Build and test (x64)

'base_frame': is not a member of '_thread_state' [D:\a\cpython\cpython\PCbuild\_remote_debugging.vcxproj]

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

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

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

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

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

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

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Address sanitizer (ubuntu-24.04)

‘struct _thread_state’ has no member named ‘base_frame’

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Windows (free-threading) / Build and test (arm64)

'base_frame': is not a member of '_thread_state' [C:\a\cpython\cpython\PCbuild\_remote_debugging.vcxproj]

Check failure on line 394 in Modules/_remote_debugging/threads.c

View workflow job for this annotation

GitHub Actions / Windows / Build and test (arm64)

'base_frame': is not a member of '_thread_state' [C:\a\cpython\cpython\PCbuild\_remote_debugging.vcxproj]
}

if (process_frame_chain(unwinder, frame_addr, &chunks, frame_info, gc_frame, base_frame_addr) < 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to process frame chain");
goto error;
}
Expand Down
8 changes: 8 additions & 0 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,10 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
entry.frame.previous = tstate->current_frame;
frame->previous = &entry.frame;
tstate->current_frame = frame;
/* Track entry frame for profiling/sampling */
if (entry.frame.previous == NULL) {
tstate->entry_frame = &entry.frame;
}
entry.frame.localsplus[0] = PyStackRef_NULL;
#ifdef _Py_TIER2
if (tstate->current_executor != NULL) {
Expand Down Expand Up @@ -1300,6 +1304,10 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
assert(frame->owner == FRAME_OWNED_BY_INTERPRETER);
/* Restore previous frame and exit */
tstate->current_frame = frame->previous;
/* Clear entry frame if we're returning to no frame */
if (tstate->current_frame == NULL) {
tstate->entry_frame = NULL;
}
return NULL;
}
#ifdef _Py_TIER2
Expand Down
1 change: 1 addition & 0 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,7 @@ init_threadstate(_PyThreadStateImpl *_tstate,
tstate->gilstate_counter = 1;

tstate->current_frame = NULL;
tstate->entry_frame = NULL;
tstate->datastack_chunk = NULL;
tstate->datastack_top = NULL;
tstate->datastack_limit = NULL;
Expand Down
Loading