From d3ce6c74cd3bc8b3afcc4eb517b01288638da79a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 23 Oct 2026 21:59:01 -0700 Subject: [PATCH] gh-159739: Guard cProfile against reentry while processing an external timer (GH-160405) An external timer can run Python again when the profiler converts or releases its result or reports an unraisable exception. Calling clear() or disable() at that point could free a profiling context still in use. Calling Profiler.__init__() from the timer could also destroy the active callable. Keep the guard active through result handling and reject reinitialization while an external timer is running. (cherry picked from commit c2b7e0a866b95f55de98bf7a45009cb8a0652fe0) Co-authored-by: Jelle Zijlstra --- Lib/test/test_cprofile.py | 33 +++++++++++++++++++ ...-10-22-16-30-00.gh-issue-159739.mFWntK.rst | 3 ++ Modules/_lsprof.c | 33 +++++++++++-------- Modules/clinic/_lsprof.c.h | 4 ++- 4 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-22-16-30-00.gh-issue-159739.mFWntK.rst diff --git a/Lib/test/test_cprofile.py b/Lib/test/test_cprofile.py index 5230348c0eb68f..26b99835a34ad9 100644 --- a/Lib/test/test_cprofile.py +++ b/Lib/test/test_cprofile.py @@ -84,6 +84,39 @@ def __call__(self): profiler_with_evil_timer.clear() self.assertEqual(cm.unraisable.exc_type, RuntimeError) + def test_external_timer_result_reentry(self): + import _lsprof + + class Ticks: + def __index__(self): + profiler.clear() + return 0 + + profiler = _lsprof.Profiler(Ticks, timeunit=1) + with support.catch_unraisable_exception() as cm: + profiler.enable() + try: + (lambda: None)() + finally: + profiler.disable() + self.assertIs(cm.unraisable.exc_type, RuntimeError) + + def test_external_timer_reinitializes_profiler(self): + import _lsprof + + def timer(): + profiler.__init__() + return 0 + + profiler = _lsprof.Profiler(timer) + with support.catch_unraisable_exception() as cm: + profiler.enable() + try: + (lambda: None)() + finally: + profiler.disable() + self.assertIs(cm.unraisable.exc_type, RuntimeError) + def test_enable_in_external_timer(self): # gh-157639: Enabling the profiler from an external timer should not crash import _lsprof diff --git a/Misc/NEWS.d/next/Library/2026-10-22-16-30-00.gh-issue-159739.mFWntK.rst b/Misc/NEWS.d/next/Library/2026-10-22-16-30-00.gh-issue-159739.mFWntK.rst new file mode 100644 index 00000000000000..73e7f5e0c1c15f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-16-30-00.gh-issue-159739.mFWntK.rst @@ -0,0 +1,3 @@ +Fix crashes in :mod:`profiling.tracing` when a custom timer reinitializes the +profiler or its result clears or disables the profiler during conversion or +cleanup. diff --git a/Modules/_lsprof.c b/Modules/_lsprof.c index 237919e889ca0c..79296ebb79f81a 100644 --- a/Modules/_lsprof.c +++ b/Modules/_lsprof.c @@ -52,6 +52,7 @@ typedef struct { ProfilerContext *currentProfilerContext; ProfilerContext *freelistProfilerContext; int flags; + unsigned int activeTimers; /* may overlap if the profiler lock is released */ PyObject *externalTimer; double externalTimerUnit; int tool_id; @@ -63,7 +64,6 @@ typedef struct { #define POF_ENABLED 0x001 #define POF_SUBCALLS 0x002 #define POF_BUILTINS 0x004 -#define POF_EXT_TIMER 0x008 #define POF_NOMEMORY 0x100 /*[clinic input] @@ -92,13 +92,7 @@ _lsprof_get_state(PyObject *module) static PyTime_t CallExternalTimer(ProfilerObject *pObj) { - PyObject *o = NULL; - - // External timer can do arbitrary things so we need a flag to prevent - // horrible things to happen - pObj->flags |= POF_EXT_TIMER; - o = _PyObject_CallNoArgs(pObj->externalTimer); - pObj->flags &= ~POF_EXT_TIMER; + PyObject *o = _PyObject_CallNoArgs(pObj->externalTimer); if (o == NULL) { PyErr_FormatUnraisable("Exception ignored while calling " @@ -132,7 +126,12 @@ static inline PyTime_t call_timer(ProfilerObject *pObj) { if (pObj->externalTimer != NULL) { - return CallExternalTimer(pObj); + /* Converting or releasing the result and reporting an unraisable + exception can all run Python code. */ + pObj->activeTimers++; + PyTime_t result = CallExternalTimer(pObj); + pObj->activeTimers--; + return result; } else { PyTime_t t; @@ -364,7 +363,7 @@ ptrace_enter_call(PyObject *self, void *key, PyObject *userObj) /* Events raised by the external timer must be ignored: it can run arbitrary code while a context is still being unwound. */ - if (pObj->flags & POF_EXT_TIMER) { + if (pObj->activeTimers) { return; } @@ -410,7 +409,7 @@ ptrace_leave_call(PyObject *self, void *key) ProfilerEntry *profEntry; ProfilerContext *pContext; - if (pObj->flags & POF_EXT_TIMER) { + if (pObj->activeTimers) { return; } @@ -895,7 +894,7 @@ static PyObject * _lsprof_Profiler_disable_impl(ProfilerObject *self) /*[clinic end generated code: output=838cffef7f651870 input=f7e4787cae20f7f6]*/ { - if (self->flags & POF_EXT_TIMER) { + if (self->activeTimers) { PyErr_SetString(PyExc_RuntimeError, "cannot disable profiler in external timer"); return NULL; @@ -955,7 +954,7 @@ static PyObject * _lsprof_Profiler_clear_impl(ProfilerObject *self) /*[clinic end generated code: output=dd1c668fb84b1335 input=4aab219d5d7a9bec]*/ { - if (self->flags & POF_EXT_TIMER) { + if (self->activeTimers) { PyErr_SetString(PyExc_RuntimeError, "cannot clear profiler in external timer"); return NULL; @@ -998,6 +997,7 @@ profiler_dealloc(PyObject *op) } /*[clinic input] +@critical_section _lsprof.Profiler.__init__ as profiler_init timer: object(c_default='NULL') = None @@ -1016,8 +1016,13 @@ is, in seconds). static int profiler_init_impl(ProfilerObject *self, PyObject *timer, double timeunit, int subcalls, int builtins) -/*[clinic end generated code: output=ac523803ec9f9df2 input=8285ca746f96a414]*/ +/*[clinic end generated code: output=ac523803ec9f9df2 input=5add5fc72beb63ba]*/ { + if (self->activeTimers) { + PyErr_SetString(PyExc_RuntimeError, + "cannot initialize profiler in external timer"); + return -1; + } if (setSubcalls(self, subcalls) < 0 || setBuiltins(self, builtins) < 0) { return -1; } diff --git a/Modules/clinic/_lsprof.c.h b/Modules/clinic/_lsprof.c.h index acb4aaf27e377f..0b1913a729ae89 100644 --- a/Modules/clinic/_lsprof.c.h +++ b/Modules/clinic/_lsprof.c.h @@ -471,9 +471,11 @@ profiler_init(PyObject *self, PyObject *args, PyObject *kwargs) goto exit; } skip_optional_pos: + Py_BEGIN_CRITICAL_SECTION(self); return_value = profiler_init_impl((ProfilerObject *)self, timer, timeunit, subcalls, builtins); + Py_END_CRITICAL_SECTION(); exit: return return_value; } -/*[clinic end generated code: output=af26a0b0ddcc3351 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=82541e3b3fe9819c input=a9049054013a1b77]*/