From 406f3f93a313b83b0c7216ea85f52562a23157ae Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 22 Oct 2026 16:48:19 -0700 Subject: [PATCH 1/2] gh-159739: Guard cProfile against reentry while processing an external timer 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. --- .../test_profiling/test_tracing_profiler.py | 33 +++++++++++++++++++ ...-10-22-16-30-00.gh-issue-159739.mFWntK.rst | 2 ++ Modules/_lsprof.c | 33 +++++++++++-------- Modules/clinic/_lsprof.c.h | 4 ++- 4 files changed, 57 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_profiling/test_tracing_profiler.py b/Lib/test/test_profiling/test_tracing_profiler.py index 50de9af86e86d02..7e2ed24f4918e82 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 000000000000000..5933b6f7fccdde2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-16-30-00.gh-issue-159739.mFWntK.rst @@ -0,0 +1,2 @@ +Fix crashes in :mod:`cProfile` 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 65857c495f2339f..42fd1eaa0f088a5 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 acb4aaf27e377f2..0b1913a729ae895 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]*/ From 6c11b4b188eb2a221add77289faf12df4f0247c6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 23 Oct 2026 13:49:45 -0700 Subject: [PATCH 2/2] gh-159739: Fix profiler NEWS documentation reference --- .../Library/2026-10-22-16-30-00.gh-issue-159739.mFWntK.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 5933b6f7fccdde2..73e7f5e0c1c15f1 100644 --- 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 @@ -1,2 +1,3 @@ -Fix crashes in :mod:`cProfile` when a custom timer reinitializes the profiler -or its result clears or disables the profiler during conversion or cleanup. +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.