Skip to content

Commit 406f3f9

Browse files
committed
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.
1 parent 2c24bc1 commit 406f3f9

4 files changed

Lines changed: 57 additions & 15 deletions

File tree

‎Lib/test/test_profiling/test_tracing_profiler.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,39 @@ def __call__(self):
8585
profiler_with_evil_timer.clear()
8686
self.assertEqual(cm.unraisable.exc_type, RuntimeError)
8787

88+
def test_external_timer_result_reentry(self):
89+
import _lsprof
90+
91+
class Ticks:
92+
def __index__(self):
93+
profiler.clear()
94+
return 0
95+
96+
profiler = _lsprof.Profiler(Ticks, timeunit=1)
97+
with support.catch_unraisable_exception() as cm:
98+
profiler.enable()
99+
try:
100+
(lambda: None)()
101+
finally:
102+
profiler.disable()
103+
self.assertIs(cm.unraisable.exc_type, RuntimeError)
104+
105+
def test_external_timer_reinitializes_profiler(self):
106+
import _lsprof
107+
108+
def timer():
109+
profiler.__init__()
110+
return 0
111+
112+
profiler = _lsprof.Profiler(timer)
113+
with support.catch_unraisable_exception() as cm:
114+
profiler.enable()
115+
try:
116+
(lambda: None)()
117+
finally:
118+
profiler.disable()
119+
self.assertIs(cm.unraisable.exc_type, RuntimeError)
120+
88121
def test_enable_in_external_timer(self):
89122
# gh-157639: Enabling the profiler from an external timer should not crash
90123
import _lsprof
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix crashes in :mod:`cProfile` when a custom timer reinitializes the profiler
2+
or its result clears or disables the profiler during conversion or cleanup.

‎Modules/_lsprof.c‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ typedef struct {
5252
ProfilerContext *currentProfilerContext;
5353
ProfilerContext *freelistProfilerContext;
5454
int flags;
55+
unsigned int activeTimers; /* may overlap if the profiler lock is released */
5556
PyObject *externalTimer;
5657
double externalTimerUnit;
5758
int tool_id;
@@ -63,7 +64,6 @@ typedef struct {
6364
#define POF_ENABLED 0x001
6465
#define POF_SUBCALLS 0x002
6566
#define POF_BUILTINS 0x004
66-
#define POF_EXT_TIMER 0x008
6767
#define POF_NOMEMORY 0x100
6868

6969
/*[clinic input]
@@ -92,13 +92,7 @@ _lsprof_get_state(PyObject *module)
9292

9393
static PyTime_t CallExternalTimer(ProfilerObject *pObj)
9494
{
95-
PyObject *o = NULL;
96-
97-
// External timer can do arbitrary things so we need a flag to prevent
98-
// horrible things to happen
99-
pObj->flags |= POF_EXT_TIMER;
100-
o = _PyObject_CallNoArgs(pObj->externalTimer);
101-
pObj->flags &= ~POF_EXT_TIMER;
95+
PyObject *o = _PyObject_CallNoArgs(pObj->externalTimer);
10296

10397
if (o == NULL) {
10498
PyErr_FormatUnraisable("Exception ignored while calling "
@@ -132,7 +126,12 @@ static inline PyTime_t
132126
call_timer(ProfilerObject *pObj)
133127
{
134128
if (pObj->externalTimer != NULL) {
135-
return CallExternalTimer(pObj);
129+
/* Converting or releasing the result and reporting an unraisable
130+
exception can all run Python code. */
131+
pObj->activeTimers++;
132+
PyTime_t result = CallExternalTimer(pObj);
133+
pObj->activeTimers--;
134+
return result;
136135
}
137136
else {
138137
PyTime_t t;
@@ -365,7 +364,7 @@ ptrace_enter_call(PyObject *self, void *key, PyObject *userObj)
365364

366365
/* Events raised by the external timer must be ignored: it can run
367366
arbitrary code while a context is still being unwound. */
368-
if (pObj->flags & POF_EXT_TIMER) {
367+
if (pObj->activeTimers) {
369368
return;
370369
}
371370

@@ -411,7 +410,7 @@ ptrace_leave_call(PyObject *self, void *key)
411410
ProfilerEntry *profEntry;
412411
ProfilerContext *pContext;
413412

414-
if (pObj->flags & POF_EXT_TIMER) {
413+
if (pObj->activeTimers) {
415414
return;
416415
}
417416

@@ -897,7 +896,7 @@ static PyObject *
897896
_lsprof_Profiler_disable_impl(ProfilerObject *self)
898897
/*[clinic end generated code: output=838cffef7f651870 input=f7e4787cae20f7f6]*/
899898
{
900-
if (self->flags & POF_EXT_TIMER) {
899+
if (self->activeTimers) {
901900
PyErr_SetString(PyExc_RuntimeError,
902901
"cannot disable profiler in external timer");
903902
return NULL;
@@ -957,7 +956,7 @@ static PyObject *
957956
_lsprof_Profiler_clear_impl(ProfilerObject *self)
958957
/*[clinic end generated code: output=dd1c668fb84b1335 input=4aab219d5d7a9bec]*/
959958
{
960-
if (self->flags & POF_EXT_TIMER) {
959+
if (self->activeTimers) {
961960
PyErr_SetString(PyExc_RuntimeError,
962961
"cannot clear profiler in external timer");
963962
return NULL;
@@ -1003,6 +1002,7 @@ profiler_dealloc(PyObject *op)
10031002
}
10041003

10051004
/*[clinic input]
1005+
@critical_section
10061006
_lsprof.Profiler.__init__ as profiler_init
10071007
10081008
timer: object(c_default='NULL') = None
@@ -1021,8 +1021,13 @@ is, in seconds).
10211021
static int
10221022
profiler_init_impl(ProfilerObject *self, PyObject *timer, double timeunit,
10231023
int subcalls, int builtins)
1024-
/*[clinic end generated code: output=ac523803ec9f9df2 input=8285ca746f96a414]*/
1024+
/*[clinic end generated code: output=ac523803ec9f9df2 input=5add5fc72beb63ba]*/
10251025
{
1026+
if (self->activeTimers) {
1027+
PyErr_SetString(PyExc_RuntimeError,
1028+
"cannot initialize profiler in external timer");
1029+
return -1;
1030+
}
10261031
if (setSubcalls(self, subcalls) < 0 || setBuiltins(self, builtins) < 0) {
10271032
return -1;
10281033
}

‎Modules/clinic/_lsprof.c.h‎

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)