diff --git a/googlecloudprofiler/src/populate_frames.cc b/googlecloudprofiler/src/populate_frames.cc index 2d042e2..75832f7 100644 --- a/googlecloudprofiler/src/populate_frames.cc +++ b/googlecloudprofiler/src/populate_frames.cc @@ -2,6 +2,12 @@ #include +#include +#include +#include +#include +#include + #include "stacktraces.h" // Python version definitions @@ -9,6 +15,147 @@ #define PY_312 0x030C0000 // 3.12 #define PY_313 0x030D0000 // 3.13 +namespace { +// Pipe used by IsReadable() to probe whether memory is mapped without faulting. +// This MUST be a pipe (or socket), NOT /dev/null: the kernel's /dev/null write +// path returns the count without ever doing copy_from_user, so it never detects +// a bad pointer and the probe is useless. A pipe forces copy_from_user. +// Initialized once via InitFramePointerProbe() before profiling starts. +int g_probe_pipe[2] = {-1, -1}; +} // namespace + +// Sets up the pipe used for memory-readability probing. Must be called once +// (with the GIL held, before the SIGPROF handler can run) -- see +// Profiler::Reset(). +void InitFramePointerProbe() { + if (g_probe_pipe[1] >= 0) { + return; + } + if (pipe(g_probe_pipe) != 0) { + g_probe_pipe[0] = g_probe_pipe[1] = -1; + return; + } + // Non-blocking so a transiently full pipe can never block the SIGPROF + // handler; close-on-exec so the fds don't leak into child processes. + for (int i = 0; i < 2; i++) { + int fl = fcntl(g_probe_pipe[i], F_GETFL); + if (fl != -1) { + fcntl(g_probe_pipe[i], F_SETFL, fl | O_NONBLOCK); + } + fcntl(g_probe_pipe[i], F_SETFD, FD_CLOEXEC); + } +} + +// Returns true iff every byte of [addr, addr+len) is currently readable, +// WITHOUT faulting if it is not. +// +// The frame walk in this file runs inside the SIGPROF handler, which can +// interrupt the interpreter while it is pushing or popping frames (e.g. while a +// data-stack chunk is being munmap'd). In that window the interpreter-internal +// pointers we follow (tstate->cframe->current_frame, frame->previous, +// frame->f_code, ...) can hold non-NULL but invalid values -- empirically a +// small integer such as 0x10, or a stale pointer into freed/unmapped memory. A +// plain `== NULL` check does not catch these, so dereferencing them takes down +// the whole process with SIGSEGV. +// +// write(2) to a pipe forces the kernel to copy_from_user, which returns +// -1/EFAULT (rather than raising SIGSEGV) if any byte is unmapped; write()/read() +// are on the POSIX async-signal-safe list. We immediately drain whatever we +// wrote so the pipe never fills up. +static inline bool IsReadable(const void *addr, size_t len) { + if (addr == NULL || g_probe_pipe[1] < 0) { + return false; + } + ssize_t w; + do { + w = write(g_probe_pipe[1], addr, len); + } while (w < 0 && errno == EINTR); + if (w > 0) { + char buf[256]; + ssize_t drained = 0; + while (drained < w) { + ssize_t r; + do { + r = read(g_probe_pipe[0], buf, sizeof(buf)); + } while (r < 0 && errno == EINTR); + if (r <= 0) { + break; + } + drained += r; + } + } + return w == static_cast(len); +} + +// --------------------------------------------------------------------------- +// Fault-protected frame walk. +// +// The readability probe above guards the frame *struct*, but the walk also +// dereferences pointers nested inside CPython inlines that we call as black +// boxes -- e.g. _PyFrame_IsIncomplete() follows frame->f_code to the code +// object, and PyCode_Addr2Line() follows code->co_linetable. During the +// SIGPROF-vs-frame-teardown race any of those can be garbage at arbitrary +// depth, so per-pointer probing cannot cover them all. +// +// We therefore run the whole walk inside a sigsetjmp region with a temporary +// SIGSEGV/SIGBUS handler: any faulting dereference, at any depth, siglongjmps +// back out and the walk is abandoned (0 frames) instead of crashing the +// process. A fault that is NOT inside our walk restores the previous handler +// and returns, so genuine crashes still terminate (and core-dump) normally. +// --------------------------------------------------------------------------- +namespace { +thread_local sigjmp_buf g_walk_jmp; +thread_local volatile sig_atomic_t g_walk_active = 0; +struct sigaction g_old_segv; +struct sigaction g_old_bus; +bool g_guard_installed = false; + +void FrameWalkFaultHandler(int sig) { + if (g_walk_active) { + g_walk_active = 0; + siglongjmp(g_walk_jmp, 1); + } + // Not a fault inside our frame walk: restore the previous disposition so the + // re-executed instruction crashes (and core-dumps) as it normally would. + sigaction(sig, (sig == SIGBUS) ? &g_old_bus : &g_old_segv, NULL); +} +} // namespace + +// Installs the SIGSEGV/SIGBUS guard used by PopulateFramesGuarded(). Call once, +// with the GIL held, before the SIGPROF handler can run (see Profiler::Reset()). +void InstallFrameWalkFaultGuard() { + if (g_guard_installed) { + return; + } + struct sigaction sa = {}; + sa.sa_handler = FrameWalkFaultHandler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_NODEFER; + if (sigaction(SIGSEGV, &sa, &g_old_segv) == 0 && + sigaction(SIGBUS, &sa, &g_old_bus) == 0) { + g_guard_installed = true; + } +} + +// Runs PopulateFrames() under the fault-protected region. If any dereference in +// the walk faults, the walk is abandoned and 0 frames are returned instead of +// taking down the process. +int PopulateFramesGuarded(CallFrame *frames, PyThreadState *ts) { + if (!g_guard_installed) { + return PopulateFrames(frames, ts); + } + int num_frames; + g_walk_active = 1; + if (sigsetjmp(g_walk_jmp, 1) == 0) { + num_frames = PopulateFrames(frames, ts); + } else { + // A dereference inside the walk faulted; recover with no frames. + num_frames = 0; + } + g_walk_active = 0; + return num_frames; +} + #if PY_VERSION_HEX >= PY_313 /** @@ -16,7 +163,7 @@ * - f_code renamed to f_executable (now PyObject* instead of PyCodeObject*) * - prev_instr renamed to instr_ptr * - Must use _PyFrame_GetCode() helper to access code object - * + * * The PyFrameObject structure members have been removed from the public C API * in 3.11: * https://docs.python.org/3/whatsnew/3.11.html#pyframeobject-3-11-hiding. @@ -32,28 +179,28 @@ // Modified from CPython 3.13 source for async-signal-safe access // Python 3.13 flattened cframe->current_frame to just current_frame -// -// IMPORTANT: This can be called from a signal handler (SIGPROF), so we must -// be defensive about race conditions where the interpreter is in the middle -// of setting up frames. The current_frame pointer might be NULL or partially -// initialized if we interrupt during _PyEval_EvalFrameDefault setup. +// +// IMPORTANT: This can be called from a signal handler (SIGPROF), so we must be +// defensive about race conditions where the interpreter is in the middle of +// setting up or tearing down frames. current_frame / previous can be NULL, or +// non-NULL but pointing at invalid/unmapped memory; every dereference is guarded +// with IsReadable() to avoid segfaulting the process. static inline _PyInterpreterFrame *unsafe_PyThreadState_GetInterpreterFrame( PyThreadState *tstate) { if (tstate == NULL) { return NULL; } - + _PyInterpreterFrame *f = tstate->current_frame; - - // Handle race condition: current_frame might be NULL or uninitialized - // if we interrupted during frame setup - if (f == NULL) { - return NULL; - } - - while (f && _PyFrame_IsIncomplete(f)) { + + // Walk past incomplete frames, validating each pointer is readable before + // dereferencing it (current_frame / previous may be non-NULL garbage). + while (IsReadable(f, sizeof(_PyInterpreterFrame)) && _PyFrame_IsIncomplete(f)) { f = f->previous; } + if (!IsReadable(f, sizeof(_PyInterpreterFrame))) { + return NULL; + } return f; } @@ -61,48 +208,53 @@ static inline _PyInterpreterFrame *unsafe_PyThreadState_GetInterpreterFrame( // This helper safely extracts the code object static inline PyCodeObject *unsafe_PyInterpreterFrame_GetCode( _PyInterpreterFrame *frame) { - if (frame == NULL || _PyFrame_IsIncomplete(frame)) { + if (!IsReadable(frame, sizeof(_PyInterpreterFrame)) || + _PyFrame_IsIncomplete(frame)) { return NULL; } - + PyObject *executable = frame->f_executable; - if (executable == NULL) { + if (!IsReadable(executable, sizeof(PyObject))) { return NULL; } - + // f_executable can be a code object or other types; verify it's a code object // PyCode_Check uses type pointer, which should be safe to check in signal handler if (!PyCode_Check(executable)) { return NULL; } - + + if (!IsReadable(executable, sizeof(PyCodeObject))) { + return NULL; + } return (PyCodeObject *)executable; } static inline _PyInterpreterFrame *unsafe_PyInterpreterFrame_GetBack( _PyInterpreterFrame *frame) { - if (frame == NULL || _PyFrame_IsIncomplete(frame)) { + if (!IsReadable(frame, sizeof(_PyInterpreterFrame)) || + _PyFrame_IsIncomplete(frame)) { return NULL; } - + _PyInterpreterFrame *prev = frame->previous; - while (prev && _PyFrame_IsIncomplete(prev)) { + while (IsReadable(prev, sizeof(_PyInterpreterFrame)) && + _PyFrame_IsIncomplete(prev)) { prev = prev->previous; } + if (!IsReadable(prev, sizeof(_PyInterpreterFrame))) { + return NULL; + } return prev; } // Python 3.13 uses instr_ptr instead of prev_instr int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame) { - if (frame == NULL) { - return -1; - } - PyCodeObject *code = unsafe_PyInterpreterFrame_GetCode(frame); if (code == NULL) { return -1; } - + int addr = (int)(frame->instr_ptr - _PyCode_CODE(code)) * sizeof(_Py_CODEUNIT); return PyCode_Addr2Line(code, addr); } @@ -120,7 +272,7 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { // Get code object and line number - might be NULL/-1 if we hit a race condition PyCodeObject *code = unsafe_PyInterpreterFrame_GetCode(frame); int lineno = _PyInterpreterFrame_GetLine(frame); - + // Only record frames where we successfully got valid data // This handles race conditions where frame is partially initialized if (code != NULL && lineno >= 0) { @@ -128,7 +280,7 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { frames[num_frames].py_code = code; num_frames++; } - + frame = unsafe_PyInterpreterFrame_GetBack(frame); } return num_frames; @@ -160,77 +312,80 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { // // IMPORTANT: This can be called from a signal handler (SIGPROF), which can // interrupt the Python interpreter at ANY point, including during frame setup -// in _PyEval_EvalFrameDefault. Specifically, the signal can fire after: -// tstate->cframe = &cframe; -// but before: -// cframe.current_frame = frame; -// This creates a race condition where cframe is set but current_frame is -// uninitialized, causing segfaults when we dereference it. We must check -// for NULL at every step. +// in _PyEval_EvalFrameDefault. The signal can fire after `tstate->cframe = +// &cframe;` but before `cframe.current_frame = frame;`, so current_frame may be +// NULL or, worse, hold a stale/garbage value. NULL checks alone are not enough +// (we have observed values such as 0x10 and stale pointers into freed memory), +// so every dereference is guarded with IsReadable(). static inline _PyInterpreterFrame *unsafe_PyThreadState_GetInterpreterFrame( PyThreadState *tstate) { if (tstate == NULL) { return NULL; } - - // Check if cframe is set - might be NULL during initialization + + // cframe may be NULL during initialization, or non-NULL but invalid. _PyCFrame *cframe = tstate->cframe; - if (cframe == NULL) { + if (!IsReadable(cframe, sizeof(_PyCFrame))) { return NULL; } - - // CRITICAL: Check if current_frame is set - might be uninitialized - // if we interrupted during _PyEval_EvalFrameDefault setup + + // current_frame may be NULL or a non-NULL invalid pointer if we interrupted + // _PyEval_EvalFrameDefault setup. Validate before every dereference. _PyInterpreterFrame *f = cframe->current_frame; - if (f == NULL) { - return NULL; - } - - while (f && _PyFrame_IsIncomplete(f)) { + while (IsReadable(f, sizeof(_PyInterpreterFrame)) && _PyFrame_IsIncomplete(f)) { f = f->previous; } + if (!IsReadable(f, sizeof(_PyInterpreterFrame))) { + return NULL; + } return f; } // In Python 3.12, f_code is still PyCodeObject* but moved to first position static inline PyCodeObject *unsafe_PyInterpreterFrame_GetCode( _PyInterpreterFrame *frame) { - if (frame == NULL || _PyFrame_IsIncomplete(frame)) { + if (!IsReadable(frame, sizeof(_PyInterpreterFrame)) || + _PyFrame_IsIncomplete(frame)) { return NULL; } - + PyCodeObject *code = frame->f_code; - if (code == NULL) { + if (!IsReadable(code, sizeof(PyCodeObject))) { return NULL; } - + return code; } static inline _PyInterpreterFrame *unsafe_PyInterpreterFrame_GetBack( _PyInterpreterFrame *frame) { - if (frame == NULL || _PyFrame_IsIncomplete(frame)) { + if (!IsReadable(frame, sizeof(_PyInterpreterFrame)) || + _PyFrame_IsIncomplete(frame)) { return NULL; } - + _PyInterpreterFrame *prev = frame->previous; - while (prev && _PyFrame_IsIncomplete(prev)) { + while (IsReadable(prev, sizeof(_PyInterpreterFrame)) && + _PyFrame_IsIncomplete(prev)) { prev = prev->previous; } + if (!IsReadable(prev, sizeof(_PyInterpreterFrame))) { + return NULL; + } return prev; } // Python 3.12 still uses prev_instr (not renamed yet) int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame) { - if (frame == NULL) { + if (!IsReadable(frame, sizeof(_PyInterpreterFrame))) { return -1; } - + PyCodeObject *code = frame->f_code; - if (code == NULL) { + if (!IsReadable(code, sizeof(PyCodeObject))) { return -1; } - + int addr = _PyInterpreterFrame_LASTI(frame) * sizeof(_Py_CODEUNIT); return PyCode_Addr2Line(code, addr); } @@ -248,7 +403,7 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { // Get code object and line number - might be NULL/-1 if we hit a race condition PyCodeObject *code = unsafe_PyInterpreterFrame_GetCode(frame); int lineno = _PyInterpreterFrame_GetLine(frame); - + // Only record frames where we successfully got valid data // This handles race conditions where frame is partially initialized if (code != NULL && lineno >= 0) { @@ -256,7 +411,7 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { frames[num_frames].py_code = code; num_frames++; } - + frame = unsafe_PyInterpreterFrame_GetBack(frame); } return num_frames; @@ -266,7 +421,7 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { /** * Python 3.11 frame structure baseline. - * + * * The PyFrameObject structure members have been removed from the public C API * in 3.11: * https://docs.python.org/3/whatsnew/3.11.html#pyframeobject-3-11-hiding. @@ -286,78 +441,83 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { // IMPORTANT: This can be called from a signal handler (SIGPROF), which can // interrupt the Python interpreter during frame setup, creating race conditions. // See Python 3.12 comments above for details on the race condition in -// _PyEval_EvalFrameDefault where cframe is set before current_frame. +// _PyEval_EvalFrameDefault where cframe is set before current_frame. Every +// dereference is guarded with IsReadable() because the pointers can be non-NULL +// but invalid in that window. static inline _PyInterpreterFrame *unsafe_PyThreadState_GetInterpreterFrame( PyThreadState *tstate) { if (tstate == NULL) { return NULL; } - - // Check if cframe is set - might be NULL during initialization + _PyCFrame *cframe = tstate->cframe; - if (cframe == NULL) { + if (!IsReadable(cframe, sizeof(_PyCFrame))) { return NULL; } - - // Check if current_frame is set - might be uninitialized + _PyInterpreterFrame *f = cframe->current_frame; - if (f == NULL) { - return NULL; - } - - while (f && _PyFrame_IsIncomplete(f)) { + while (IsReadable(f, sizeof(_PyInterpreterFrame)) && _PyFrame_IsIncomplete(f)) { f = f->previous; } + if (!IsReadable(f, sizeof(_PyInterpreterFrame))) { + return NULL; + } return f; } // Modified from // https://github.com/python/cpython/blob/v3.11.4/Objects/frameobject.c#L1310-L1315 -// with refcounting removed and additional NULL checks for signal safety +// with refcounting removed and additional readability checks for signal safety static inline PyCodeObject *unsafe_PyInterpreterFrame_GetCode( _PyInterpreterFrame *frame) { - if (frame == NULL || _PyFrame_IsIncomplete(frame)) { + if (!IsReadable(frame, sizeof(_PyInterpreterFrame)) || + _PyFrame_IsIncomplete(frame)) { return NULL; } - + PyCodeObject *code = frame->f_code; - if (code == NULL) { + if (!IsReadable(code, sizeof(PyCodeObject))) { return NULL; } - + return code; } // Modified from // https://github.com/python/cpython/blob/v3.11.4/Objects/frameobject.c#L1326-L1329 -// with refcounting removed and additional NULL checks for signal safety +// with refcounting removed and additional readability checks for signal safety static inline _PyInterpreterFrame *unsafe_PyInterpreterFrame_GetBack( _PyInterpreterFrame *frame) { - if (frame == NULL || _PyFrame_IsIncomplete(frame)) { + if (!IsReadable(frame, sizeof(_PyInterpreterFrame)) || + _PyFrame_IsIncomplete(frame)) { return NULL; } - + _PyInterpreterFrame *prev = frame->previous; - while (prev && _PyFrame_IsIncomplete(prev)) { + while (IsReadable(prev, sizeof(_PyInterpreterFrame)) && + _PyFrame_IsIncomplete(prev)) { prev = prev->previous; } + if (!IsReadable(prev, sizeof(_PyInterpreterFrame))) { + return NULL; + } return prev; } // Copied from // https://github.com/python/cpython/blob/v3.11.4/Python/frame.c#L165-L170 as // this function is not available in libpython -// Added NULL checks for signal safety +// Added readability checks for signal safety int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame) { - if (frame == NULL) { + if (!IsReadable(frame, sizeof(_PyInterpreterFrame))) { return -1; } - + PyCodeObject *code = frame->f_code; - if (code == NULL) { + if (!IsReadable(code, sizeof(PyCodeObject))) { return -1; } - + int addr = _PyInterpreterFrame_LASTI(frame) * sizeof(_Py_CODEUNIT); return PyCode_Addr2Line(code, addr); } @@ -379,7 +539,7 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { // Get code object and line number - might be NULL/-1 if we hit a race condition PyCodeObject *code = unsafe_PyInterpreterFrame_GetCode(frame); int lineno = _PyInterpreterFrame_GetLine(frame); - + // Only record frames where we successfully got valid data // This handles race conditions where frame is partially initialized if (code != NULL && lineno >= 0) { @@ -387,7 +547,7 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { frames[num_frames].py_code = code; num_frames++; } - + frame = unsafe_PyInterpreterFrame_GetBack(frame); } return num_frames; @@ -416,4 +576,3 @@ int PopulateFrames(CallFrame *frames, PyThreadState *ts) { } #endif // PY_VERSION_HEX >= PY_311 - diff --git a/googlecloudprofiler/src/populate_frames.h b/googlecloudprofiler/src/populate_frames.h index 63bbe25..47594c4 100644 --- a/googlecloudprofiler/src/populate_frames.h +++ b/googlecloudprofiler/src/populate_frames.h @@ -5,10 +5,31 @@ #include "stacktraces.h" +/** + * Initializes the resources used to safely walk frames from the SIGPROF + * handler (currently an fd used for non-faulting memory-readability probes). + * Must be called once, with the GIL held, before profiling starts. + */ +void InitFramePointerProbe(); + +/** + * Installs the SIGSEGV/SIGBUS guard used by PopulateFramesGuarded(). Must be + * called once, with the GIL held, before the SIGPROF handler can run. + */ +void InstallFrameWalkFaultGuard(); + /** * Populates the CallFrame array with at-most kMaxFramesToCapture python frames * from the provided PyThreadState. Returns the number of frames populated. */ int PopulateFrames(CallFrame* frames, PyThreadState* ts); +/** + * Same as PopulateFrames(), but runs the walk inside a fault-protected region: + * if any dereference faults (SIGSEGV/SIGBUS) -- e.g. a torn-down frame or code + * object during the SIGPROF race -- the walk is abandoned and 0 is returned + * instead of crashing the process. Requires InstallFrameWalkFaultGuard(). + */ +int PopulateFramesGuarded(CallFrame* frames, PyThreadState* ts); + #endif // THIRD_PARTY_PY_GOOGLECLOUDPROFILER_SRC_POPULATE_FRAMES_H_ diff --git a/googlecloudprofiler/src/profiler.cc b/googlecloudprofiler/src/profiler.cc index 8d95870..9ede77f 100644 --- a/googlecloudprofiler/src/profiler.cc +++ b/googlecloudprofiler/src/profiler.cc @@ -151,7 +151,7 @@ void Profiler::Handle(int signum, siginfo_t *info, void *context) { // there are ways to avoid the problems. PyThreadState *ts = get_thread_state_func(); - trace.num_frames = PopulateFrames(frames, ts); + trace.num_frames = PopulateFramesGuarded(frames, ts); if (!fixed_traces_->Add(&trace)) { unknown_stack_count_++; return; @@ -159,6 +159,11 @@ void Profiler::Handle(int signum, siginfo_t *info, void *context) { } void GetFuncLoc(PyCodeObject *code_object, FuncLoc *func_loc) { + if (code_object == nullptr) { + func_loc->name = "unknown"; + func_loc->filename = "unknown"; + return; + } // Note that PyUnicode_AsUTF8 caches the char array in the unicodeobject // and the memory is released when the unicodeobject is deallocated. const char *name = PyUnicode_AsUTF8(code_object->co_name); @@ -171,6 +176,11 @@ void GetFuncLoc(PyCodeObject *code_object, FuncLoc *func_loc) { // otherwise PyCode_Type.tp_dealloc may be updating // CodeDeallocHook.deallocated_code_ in another thread. void Profiler::Reset() { + // Set up the frame-walk safety mechanisms before the SIGPROF handler can + // run (GIL held here): the readability-probe pipe and the SIGSEGV/SIGBUS + // fault guard. + InitFramePointerProbe(); + InstallFrameWalkFaultGuard(); if (fixed_traces_ == nullptr) { fixed_traces_ = new AsyncSafeTraceMultiset(); } else {