diff --git a/googlecloudprofiler/__version__.py b/googlecloudprofiler/__version__.py index 02d34d4..22285ae 100644 --- a/googlecloudprofiler/__version__.py +++ b/googlecloudprofiler/__version__.py @@ -17,6 +17,7 @@ # setup.py reads the version information from here to set package version # Glean fork: upstream 4.1.0 + 3.12/3.13 support (vendored as 4.2.0) + the -# SIGPROF frame-walk crash fix (copy-the-chain). Patch bump over the synthetic -# 4.2.0 -- the fix is a backward-compatible bug fix. Upstream has no 4.2.x. -__version__ = '4.2.1' +# SIGPROF frame-walk crash fix (copy-the-chain, 4.2.1) + a bound on the frame +# walk to prevent an infinite loop on a torn/cyclic `previous` chain (4.2.2). +# Backward-compatible bug fixes; upstream has no 4.2.x. +__version__ = '4.2.2' diff --git a/googlecloudprofiler/src/populate_frames.cc b/googlecloudprofiler/src/populate_frames.cc index 47f492d..24a40b9 100644 --- a/googlecloudprofiler/src/populate_frames.cc +++ b/googlecloudprofiler/src/populate_frames.cc @@ -32,6 +32,14 @@ bool SafeCopy(void *dst, const void *src, size_t n) { return got == static_cast(n); } +// Absolute cap on frames walked per SIGPROF sample. num_frames only advances for +// complete frames, so a cycle of incomplete/stale frames -- e.g. a torn +// `previous` link left by a SIGPROF landing mid frame push/pop -- would otherwise +// spin this walk forever inside the signal handler (GIL held), wedging the +// process. 4x the capture cap leaves headroom for legitimately skipped frames +// (C-stack shims, prologue frames). +static const int kMaxWalkIterations = 4 * kMaxFramesToCapture; + #if PY_VERSION_HEX >= PY_313 /** @@ -92,7 +100,9 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { _PyInterpreterFrame *faddr = ts->current_frame; int num_frames = 0; - while (faddr != nullptr && num_frames < kMaxFramesToCapture) { + int steps = 0; + while (faddr != nullptr && num_frames < kMaxFramesToCapture && + ++steps <= kMaxWalkIterations) { _PyInterpreterFrame fr; if (!SafeCopy(&fr, faddr, sizeof(fr))) { break; // unreadable frame: stop, keep the frames gathered so far @@ -108,6 +118,9 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { (fr.instr_ptr - _PyCode_CODE(code)) * sizeof(_Py_CODEUNIT)); num_frames++; } + if (fr.previous == faddr) { + break; // self-referential link: stop instead of spinning + } faddr = fr.previous; } return num_frames; @@ -174,7 +187,9 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { } int num_frames = 0; - while (faddr != nullptr && num_frames < kMaxFramesToCapture) { + int steps = 0; + while (faddr != nullptr && num_frames < kMaxFramesToCapture && + ++steps <= kMaxWalkIterations) { _PyInterpreterFrame fr; if (!SafeCopy(&fr, faddr, sizeof(fr))) { break; // unreadable frame: stop, keep the frames gathered so far @@ -188,6 +203,9 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { (fr.prev_instr - _PyCode_CODE(fr.f_code)) * sizeof(_Py_CODEUNIT)); num_frames++; } + if (fr.previous == faddr) { + break; // self-referential link: stop instead of spinning + } faddr = fr.previous; } return num_frames; @@ -248,7 +266,9 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { } int num_frames = 0; - while (faddr != nullptr && num_frames < kMaxFramesToCapture) { + int steps = 0; + while (faddr != nullptr && num_frames < kMaxFramesToCapture && + ++steps <= kMaxWalkIterations) { _PyInterpreterFrame fr; if (!SafeCopy(&fr, faddr, sizeof(fr))) { break; // unreadable frame: stop, keep the frames gathered so far @@ -262,6 +282,9 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { (fr.prev_instr - _PyCode_CODE(fr.f_code)) * sizeof(_Py_CODEUNIT)); num_frames++; } + if (fr.previous == faddr) { + break; // self-referential link: stop instead of spinning + } faddr = fr.previous; } return num_frames;