Skip to content

Bound the SIGPROF frame walk to prevent an infinite loop (4.2.2) - #1

Closed
lingfeng-guan-glean wants to merge 1 commit into
lg/fix-frame-walk-copyfrom
lg/fix-frame-walk-infinite-loop
Closed

Bound the SIGPROF frame walk to prevent an infinite loop (4.2.2)#1
lingfeng-guan-glean wants to merge 1 commit into
lg/fix-frame-walk-copyfrom
lg/fix-frame-walk-infinite-loop

Conversation

@lingfeng-guan-glean

Copy link
Copy Markdown
Owner

Problem

PopulateFrames (googlecloudprofiler/src/populate_frames.cc) walks the interpreter frame chain with the loop bounded only by num_frames < kMaxFramesToCapture. But num_frames advances only for complete frames (fr.f_code != nullptr && !FrameIsIncomplete(&fr)). Incomplete frames — a frame caught mid-push in its prologue, FRAME_OWNED_BY_CSTACK shims, null/unreadable code — are correctly skipped (so real stacks aren't truncated), but they don't advance the counter.

A SIGPROF can land while CPython is pushing/popping a frame. CPython's _PyFrame_Initialize deliberately leaves previous unwritten (linked later) and a mid-push frame is incomplete by construction; reused datastack memory supplies a stale previous. That can form a cycle of incomplete frames. Because none of them advance num_frames, the existing cap never trips and the walk spins forever inside the SIGPROF handler — on the GIL-holding thread. The process wedges (~1 core, GIL held); in k8s the liveness probe times out and the pod is restarted.

The existing num_frames cap only bounds cycles that contain at least one complete frame; an all-incomplete cycle is unbounded.

Fix

  • Add an absolute iteration cap kMaxWalkIterations = 4 * kMaxFramesToCapture (512) — guarantees termination regardless of frame completeness.
  • Add a if (fr.previous == faddr) break; self-loop fast-path for the trivial 1-cycle.
  • Applied to all three modern branches (3.11 / 3.12 / 3.13). The pre-3.11 branch increments num_frames unconditionally and is unaffected.
  • On cap-hit the walk keeps the frames gathered so far — same degradation as the existing SafeCopy-failure break.

Skipping incomplete frames is preserved (breaking on them would truncate legitimate stacks at shim/prologue frames); the only change is that the walk can never exceed 512 total steps.

Version

4.2.1 -> 4.2.2 (backward-compatible bug fix).

Notes

Stacked on lg/fix-frame-walk-copy (the deployed 4.2.1 line) so the diff is isolated to this fix. Distinct from the SafeCopy crash-fix (that prevents SIGSEGV on a bad pointer; this prevents an infinite loop on a cyclic chain).

PopulateFrames walked the interpreter frame chain bounded only by
num_frames < kMaxFramesToCapture, but num_frames advances only for complete
frames. A SIGPROF landing mid frame push/pop can leave a torn/stale `previous`
link forming a cycle of incomplete frames; the walk then spins forever inside
the signal handler holding the GIL, wedging the process (liveness probe
timeout -> restart).

Add an absolute step cap (kMaxWalkIterations = 4 * kMaxFramesToCapture) plus a
self-loop fast-path to the 3.11/3.12/3.13 branches. The pre-3.11 branch
increments unconditionally and is unaffected. Bump 4.2.1 -> 4.2.2.
@lingfeng-guan-glean

Copy link
Copy Markdown
Owner Author

Superseded: reopened against the Glean fork (rahul-roy-glean) stacked on the merged SafeCopy PR rahul-roy-glean#2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant