Skip to content

Commit 721a7dd

Browse files
[3.13] gh-120289: Disallow disable() and clear() in external timer to prevent use-after-free (GH-120297) (#121984)
gh-120289: Disallow disable() and clear() in external timer to prevent use-after-free (GH-120297) (cherry picked from commit 1ab1778) Co-authored-by: Tian Gao <[email protected]>
1 parent 53774e9 commit 721a7dd

File tree

3 files changed

+58
-1
lines changed

3 files changed

+58
-1
lines changed

Lib/test/test_cprofile.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,43 @@ def test_bad_counter_during_dealloc(self):
3030

3131
self.assertEqual(cm.unraisable.exc_type, TypeError)
3232

33+
def test_evil_external_timer(self):
34+
# gh-120289
35+
# Disabling profiler in external timer should not crash
36+
import _lsprof
37+
class EvilTimer():
38+
def __init__(self, disable_count):
39+
self.count = 0
40+
self.disable_count = disable_count
41+
42+
def __call__(self):
43+
self.count += 1
44+
if self.count == self.disable_count:
45+
profiler_with_evil_timer.disable()
46+
return self.count
47+
48+
# this will trigger external timer to disable profiler at
49+
# call event - in initContext in _lsprof.c
50+
with support.catch_unraisable_exception() as cm:
51+
profiler_with_evil_timer = _lsprof.Profiler(EvilTimer(1))
52+
profiler_with_evil_timer.enable()
53+
# Make a call to trigger timer
54+
(lambda: None)()
55+
profiler_with_evil_timer.disable()
56+
profiler_with_evil_timer.clear()
57+
self.assertEqual(cm.unraisable.exc_type, RuntimeError)
58+
59+
# this will trigger external timer to disable profiler at
60+
# return event - in Stop in _lsprof.c
61+
with support.catch_unraisable_exception() as cm:
62+
profiler_with_evil_timer = _lsprof.Profiler(EvilTimer(2))
63+
profiler_with_evil_timer.enable()
64+
# Make a call to trigger timer
65+
(lambda: None)()
66+
profiler_with_evil_timer.disable()
67+
profiler_with_evil_timer.clear()
68+
self.assertEqual(cm.unraisable.exc_type, RuntimeError)
69+
3370
def test_profile_enable_disable(self):
3471
prof = self.profilerclass()
3572
# Make sure we clean ourselves up if the test fails for some reason.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed the use-after-free issue in :mod:`cProfile` by disallowing
2+
``disable()`` and ``clear()`` in external timers.

Modules/_lsprof.c

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ typedef struct {
5959
#define POF_ENABLED 0x001
6060
#define POF_SUBCALLS 0x002
6161
#define POF_BUILTINS 0x004
62+
#define POF_EXT_TIMER 0x008
6263
#define POF_NOMEMORY 0x100
6364

6465
/*[clinic input]
@@ -87,7 +88,14 @@ _lsprof_get_state(PyObject *module)
8788

8889
static PyTime_t CallExternalTimer(ProfilerObject *pObj)
8990
{
90-
PyObject *o = _PyObject_CallNoArgs(pObj->externalTimer);
91+
PyObject *o = NULL;
92+
93+
// External timer can do arbitrary things so we need a flag to prevent
94+
// horrible things to happen
95+
pObj->flags |= POF_EXT_TIMER;
96+
o = _PyObject_CallNoArgs(pObj->externalTimer);
97+
pObj->flags &= ~POF_EXT_TIMER;
98+
9199
if (o == NULL) {
92100
PyErr_WriteUnraisable(pObj->externalTimer);
93101
return 0;
@@ -777,6 +785,11 @@ Stop collecting profiling information.\n\
777785
static PyObject*
778786
profiler_disable(ProfilerObject *self, PyObject* noarg)
779787
{
788+
if (self->flags & POF_EXT_TIMER) {
789+
PyErr_SetString(PyExc_RuntimeError,
790+
"cannot disable profiler in external timer");
791+
return NULL;
792+
}
780793
if (self->flags & POF_ENABLED) {
781794
PyObject* result = NULL;
782795
PyObject* monitoring = _PyImport_GetModuleAttrString("sys", "monitoring");
@@ -830,6 +843,11 @@ Clear all profiling information collected so far.\n\
830843
static PyObject*
831844
profiler_clear(ProfilerObject *pObj, PyObject* noarg)
832845
{
846+
if (pObj->flags & POF_EXT_TIMER) {
847+
PyErr_SetString(PyExc_RuntimeError,
848+
"cannot clear profiler in external timer");
849+
return NULL;
850+
}
833851
clearEntries(pObj);
834852
Py_RETURN_NONE;
835853
}

0 commit comments

Comments
 (0)