diff --git a/Lib/test/test_profiling/test_tracing_profiler.py b/Lib/test/test_profiling/test_tracing_profiler.py index 50de9af86e86d0..7e2ed24f4918e8 100644 --- a/Lib/test/test_profiling/test_tracing_profiler.py +++ b/Lib/test/test_profiling/test_tracing_profiler.py @@ -85,6 +85,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 65857c495f2339..42fd1eaa0f088a 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; @@ -365,7 +364,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; } @@ -411,7 +410,7 @@ ptrace_leave_call(PyObject *self, void *key) ProfilerEntry *profEntry; ProfilerContext *pContext; - if (pObj->flags & POF_EXT_TIMER) { + if (pObj->activeTimers) { return; } @@ -897,7 +896,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; @@ -957,7 +956,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; @@ -1003,6 +1002,7 @@ profiler_dealloc(PyObject *op) } /*[clinic input] +@critical_section _lsprof.Profiler.__init__ as profiler_init timer: object(c_default='NULL') = None @@ -1021,8 +1021,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]*/