diff --git a/Lib/test/test_lsprof_uaf.py b/Lib/test/test_lsprof_uaf.py new file mode 100644 index 00000000000000..d9f36d5e8ea760 --- /dev/null +++ b/Lib/test/test_lsprof_uaf.py @@ -0,0 +1,37 @@ +"""Test for gh-143545: UAF in lsprof via re-entrant external timer.""" +import unittest +import cProfile + + +class ReentrantTimerTest(unittest.TestCase): + """Test that profiler handles re-entrant timer correctly.""" + + def test_reentrant_timer_index(self): + """Clearing profiler during timer.__index__ should not crash.""" + + class Timer: + def __init__(self, profiler): + self.profiler = profiler + + def __call__(self): + return self + + def __index__(self): + self.profiler.clear() + return 0 + + prof = cProfile.Profile() + prof.timer = Timer(prof) + + def victim(): + pass + + # Should not crash with use-after-free + try: + prof._pystart_callback(victim.__code__, 0) + except (RuntimeError, ValueError): + pass # Expected if we block the operation + + +if __name__ == '__main__': + unittest.main() diff --git a/Misc/NEWS.d/next/Security/2026-01-08-08-05-23.gh-issue-143545.qruuXH.rst b/Misc/NEWS.d/next/Security/2026-01-08-08-05-23.gh-issue-143545.qruuXH.rst new file mode 100644 index 00000000000000..5f8883bb07604c --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-01-08-08-05-23.gh-issue-143545.qruuXH.rst @@ -0,0 +1 @@ +Fix use-after-free in the ``_lsprof`` module when external timer's ``__index__`` method clears the profiler during callback. diff --git a/Modules/_lsprof.c b/Modules/_lsprof.c index 025a3fac46e59b..7a08c9b6fa4bf3 100644 --- a/Modules/_lsprof.c +++ b/Modules/_lsprof.c @@ -46,6 +46,7 @@ typedef struct _ProfilerContext { ProfilerEntry *ctxEntry; } ProfilerContext; + typedef struct { PyObject_HEAD rotating_node_t *profilerEntries; @@ -56,6 +57,7 @@ typedef struct { double externalTimerUnit; int tool_id; PyObject* missing; + int inCallback; } ProfilerObject; #define ProfilerObject_CAST(op) ((ProfilerObject *)(op)) @@ -289,6 +291,9 @@ static int freeEntry(rotating_node_t *header, void *arg) static void clearEntries(ProfilerObject *pObj) { + if (pObj->inCallback) { + return; + } RotatingTree_Enum(pObj->profilerEntries, freeEntry, NULL); pObj->profilerEntries = EMPTY_ROTATING_TREE; /* release the memory hold by the ProfilerContexts */ @@ -321,13 +326,17 @@ initContext(ProfilerObject *pObj, ProfilerContext *self, ProfilerEntry *entry) if (subentry) ++subentry->recursionLevel; } - self->t0 = call_timer(pObj); + pObj->inCallback = 1; + self->t0 = call_timer(pObj); +pObj->inCallback = 0; } static void Stop(ProfilerObject *pObj, ProfilerContext *self, ProfilerEntry *entry) { + pObj->inCallback = 1; PyTime_t tt = call_timer(pObj) - self->t0; + pObj->inCallback = 0; PyTime_t it = tt - self->subt; if (self->previous) self->previous->subt += tt;