Skip to content

Use After Free in Stop(_lsprof.c) #121225

Description

@kcatss

What happened?

Version

Python 3.14.0a0 (remotes/origin/HEAD-dirty:bd473aa598, Jul 1 2024, 16:59:13) [GCC 9.4.0]
patch from PR #120297
if not patched, can still trigger uaf

build with asan

Details
 patch -p0 < patch.diff

Run the command after saving the following code to patch.diff

patch
diff --git Modules/_lsprof.c Modules/_lsprof.c
index 5cf9eba243..a049565002 100644
--- Modules/_lsprof.c
+++ Modules/_lsprof.c
@@ -311,10 +311,17 @@ initContext(ProfilerObject *pObj, ProfilerContext *self, ProfilerEntry *entry)
 }
 
 static void
-Stop(ProfilerObject *pObj, ProfilerContext *self, ProfilerEntry *entry)
+Stop(ProfilerObject *pObj, ProfilerContext **pself, ProfilerEntry *entry)
 {
+    ProfilerContext *self = *pself;
     PyTime_t tt = call_timer(pObj) - self->t0;
     PyTime_t it = tt - self->subt;
+    // call_timer could have flushed all contexts, in this case, set pself to NULL
+    // to indicate that there's nothing needs to be done for it
+    if (pObj->currentProfilerContext == NULL) {
+        *pself = NULL;
+        return;
+    }
     if (self->previous)
         self->previous->subt += tt;
     pObj->currentProfilerContext = self->previous;
@@ -395,14 +402,17 @@ ptrace_leave_call(PyObject *self, void *key)
         return;
     profEntry = getEntry(pObj, key);
     if (profEntry) {
-        Stop(pObj, pContext, profEntry);
+        Stop(pObj, &pContext, profEntry);
     }
     else {
         pObj->currentProfilerContext = pContext->previous;
     }
-    /* put pContext into the free list */
-    pContext->previous = pObj->freelistProfilerContext;
-    pObj->freelistProfilerContext = pContext;
+    /* put pContext into the free list if it's not NULL */
+    /* we need to check because Stop could potentially set pContext to NULL*/
+    if (pContext != NULL) {
+        pContext->previous = pObj->freelistProfilerContext;
+        pObj->freelistProfilerContext = pContext;
+    }
 }
 
 static int
@@ -759,11 +769,13 @@ flush_unmatched(ProfilerObject *pObj)
         ProfilerContext *pContext = pObj->currentProfilerContext;
         ProfilerEntry *profEntry= pContext->ctxEntry;
         if (profEntry)
-            Stop(pObj, pContext, profEntry);
+            Stop(pObj, &pContext, profEntry);
         else
             pObj->currentProfilerContext = pContext->previous;
-        if (pContext)
-            PyMem_Free(pContext);
+        if (pContext != NULL) {
+            pContext->previous = pObj->freelistProfilerContext;
+            pObj->freelistProfilerContext = pContext;
+        }
     }
 
 }

Root Cause

The call_timer function calls the __call__ method of evil() twice, as shown in the following call stack.

self->t0 = call_timer(pObj);

PyTime_t tt = call_timer(pObj) - self->t0;

On the first call, it invokes profiler_disable(). On the second call, if the condition for self->flags is met, it returns and then calls profiler_clear(). This sequence allows all ProfilerContext to be completely freed. Consequently, after calling call_timer(pObj), self->t0 triggers a use-after-free error.

Stop(ProfilerObject *pObj, ProfilerContext **pself, ProfilerEntry *entry)
{
    ProfilerContext *self = *pself;
    PyTime_t tt = call_timer(pObj) - self->t0; // call arbitrary code, then self can be free , uaf
    PyTime_t it = tt - self->subt;  // uaf
    ...
 }

Call Stack

initContext()
    call_timer()
        profiler_disable()  <- first call
            flush_unmatched()
                Stop()
                    call_timer() 
                        profiler_disable() <- second call, return by self->flags
                        profiler_clear() <-- free all ProfilerContext
                PyTime_t tt = call_timer(pObj) - self->t0; <- after return, trigger use after free

POC

import _lsprof
class evil(dict):
    def __call__(self):
        prof.disable()
        prof.clear()
        return True
prof = _lsprof.Profiler(evil())
prof.enable()
print ("dummy")

ASAN

asan
==903512==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000066e70 at pc 0x7ffff3fd19e2 bp 0x7fffffffc430 sp 0x7fffffffc420
READ of size 8 at 0x606000066e70 thread T0
    #0 0x7ffff3fd19e1 in Stop Modules/_lsprof.c:317
    #1 0x7ffff3fd1e20 in flush_unmatched Modules/_lsprof.c:772
    #2 0x7ffff3fd4052 in profiler_disable Modules/_lsprof.c:827
    #3 0x5555559a3189 in method_vectorcall_NOARGS Objects/descrobject.c:447
    #4 0x555555979751 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #5 0x555555979751 in PyObject_Vectorcall Objects/call.c:327
    #6 0x555555cfb720 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:879
    #7 0x555555d41505 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:119
    #8 0x555555d41505 in _PyEval_Vector Python/ceval.c:1901
    #9 0x5555559788a6 in _PyFunction_Vectorcall Objects/call.c:413
    #10 0x55555597d75c in _PyObject_VectorcallDictTstate Objects/call.c:135
    #11 0x55555597dc35 in _PyObject_Call_Prepend Objects/call.c:504
    #12 0x555555b26701 in slot_tp_call Objects/typeobject.c:9700
    #13 0x555555978c70 in _PyObject_MakeTpCall Objects/call.c:242
    #14 0x7ffff3fd152e in _PyObject_VectorcallTstate Include/internal/pycore_call.h:165
    #15 0x7ffff3fd152e in _PyObject_CallNoArgs Include/internal/pycore_call.h:183
    #16 0x7ffff3fd152e in CallExternalTimer Modules/_lsprof.c:90
    #17 0x7ffff3fd2166 in call_timer Modules/_lsprof.c:121
    #18 0x7ffff3fd2166 in initContext Modules/_lsprof.c:310
    #19 0x7ffff3fd3b52 in ptrace_enter_call Modules/_lsprof.c:386
    #20 0x7ffff3fd539b in ccall_callback Modules/_lsprof.c:663
    #21 0x555555a75675 in cfunction_vectorcall_FASTCALL Objects/methodobject.c:425
    #22 0x555555e0a5df in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #23 0x555555e0a5df in call_one_instrument Python/instrumentation.c:907
    #24 0x555555e0beb2 in call_instrumentation_vector Python/instrumentation.c:1095
    #25 0x555555e0ff78 in _Py_call_instrumentation_2args Python/instrumentation.c:1150
    #26 0x555555d18305 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:3543
    #27 0x555555d41505 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:119
    #28 0x555555d41505 in _PyEval_Vector Python/ceval.c:1901
    #29 0x555555d41726 in PyEval_EvalCode Python/ceval.c:624
    #30 0x555555e6016f in run_eval_code_obj Python/pythonrun.c:1292
    #31 0x555555e630da in run_mod Python/pythonrun.c:1377
    #32 0x555555e63ec0 in pyrun_file Python/pythonrun.c:1210
    #33 0x555555e6639b in _PyRun_SimpleFileObject Python/pythonrun.c:459
    #34 0x555555e6688f in _PyRun_AnyFileObject Python/pythonrun.c:77
    #35 0x555555ecc247 in pymain_run_file_obj Modules/main.c:357
    #36 0x555555eceb5d in pymain_run_file Modules/main.c:376
    #37 0x555555ecf76e in pymain_run_python Modules/main.c:640
    #38 0x555555ecf8fe in Py_RunMain Modules/main.c:719
    #39 0x555555ecfae5 in pymain_main Modules/main.c:749
    #40 0x555555ecfe4c in Py_BytesMain Modules/main.c:773
    #41 0x5555557d6a85 in main Programs/python.c:15
    #42 0x7ffff7230082 in __libc_start_main ../csu/libc-start.c:308
    #43 0x5555557d69bd in _start (/home/kk/projects/cpython/python+0x2829bd)

0x606000066e70 is located 16 bytes inside of 56-byte region [0x606000066e60,0x606000066e98)
freed by thread T0 here:
    #0 0x7ffff768840f in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:122
    #1 0x555555a8b945 in _PyMem_RawFree Objects/obmalloc.c:90
    #2 0x555555a8dbff in _PyMem_DebugRawFree Objects/obmalloc.c:2754
    #3 0x555555a8e440 in _PyMem_DebugFree Objects/obmalloc.c:2891
    #4 0x555555ac7d45 in PyMem_Free Objects/obmalloc.c:1010
    #5 0x7ffff3fd0d21 in clearEntries Modules/_lsprof.c:282
    #6 0x7ffff3fd0dba in profiler_clear Modules/_lsprof.c:845
    #7 0x5555559a3189 in method_vectorcall_NOARGS Objects/descrobject.c:447
    #8 0x555555979751 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #9 0x555555979751 in PyObject_Vectorcall Objects/call.c:327
    #10 0x555555cfb720 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:879
    #11 0x555555d41505 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:119
    #12 0x555555d41505 in _PyEval_Vector Python/ceval.c:1901
    #13 0x5555559788a6 in _PyFunction_Vectorcall Objects/call.c:413
    #14 0x55555597d75c in _PyObject_VectorcallDictTstate Objects/call.c:135
    #15 0x55555597dc35 in _PyObject_Call_Prepend Objects/call.c:504
    #16 0x555555b26701 in slot_tp_call Objects/typeobject.c:9700
    #17 0x555555978c70 in _PyObject_MakeTpCall Objects/call.c:242
    #18 0x7ffff3fd152e in _PyObject_VectorcallTstate Include/internal/pycore_call.h:165
    #19 0x7ffff3fd152e in _PyObject_CallNoArgs Include/internal/pycore_call.h:183
    #20 0x7ffff3fd152e in CallExternalTimer Modules/_lsprof.c:90
    #21 0x7ffff3fd1702 in call_timer Modules/_lsprof.c:121
    #22 0x7ffff3fd1702 in Stop Modules/_lsprof.c:317
    #23 0x7ffff3fd1e20 in flush_unmatched Modules/_lsprof.c:772
    #24 0x7ffff3fd4052 in profiler_disable Modules/_lsprof.c:827
    #25 0x5555559a3189 in method_vectorcall_NOARGS Objects/descrobject.c:447
    #26 0x555555979751 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #27 0x555555979751 in PyObject_Vectorcall Objects/call.c:327
    #28 0x555555cfb720 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:879
    #29 0x555555d41505 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:119
    #30 0x555555d41505 in _PyEval_Vector Python/ceval.c:1901
    #31 0x5555559788a6 in _PyFunction_Vectorcall Objects/call.c:413
    #32 0x55555597d75c in _PyObject_VectorcallDictTstate Objects/call.c:135
    #33 0x55555597dc35 in _PyObject_Call_Prepend Objects/call.c:504
    #34 0x555555b26701 in slot_tp_call Objects/typeobject.c:9700
    #35 0x555555978c70 in _PyObject_MakeTpCall Objects/call.c:242
    #36 0x7ffff3fd152e in _PyObject_VectorcallTstate Include/internal/pycore_call.h:165
    #37 0x7ffff3fd152e in _PyObject_CallNoArgs Include/internal/pycore_call.h:183
    #38 0x7ffff3fd152e in CallExternalTimer Modules/_lsprof.c:90

previously allocated by thread T0 here:
    #0 0x7ffff7688808 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:144
    #1 0x555555a8c4f9 in _PyMem_RawMalloc Objects/obmalloc.c:62
    #2 0x555555a8b821 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2686
    #3 0x555555a8b889 in _PyMem_DebugRawMalloc Objects/obmalloc.c:2719
    #4 0x555555a8e482 in _PyMem_DebugMalloc Objects/obmalloc.c:2876
    #5 0x555555ac7c01 in PyMem_Malloc Objects/obmalloc.c:981
    #6 0x7ffff3fd3b95 in ptrace_enter_call Modules/_lsprof.c:380
    #7 0x7ffff3fd539b in ccall_callback Modules/_lsprof.c:663
    #8 0x555555a75675 in cfunction_vectorcall_FASTCALL Objects/methodobject.c:425
    #9 0x555555e0a5df in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #10 0x555555e0a5df in call_one_instrument Python/instrumentation.c:907
    #11 0x555555e0beb2 in call_instrumentation_vector Python/instrumentation.c:1095
    #12 0x555555e0ff78 in _Py_call_instrumentation_2args Python/instrumentation.c:1150
    #13 0x555555d18305 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:3543
    #14 0x555555d41505 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:119
    #15 0x555555d41505 in _PyEval_Vector Python/ceval.c:1901
    #16 0x555555d41726 in PyEval_EvalCode Python/ceval.c:624
    #17 0x555555e6016f in run_eval_code_obj Python/pythonrun.c:1292
    #18 0x555555e630da in run_mod Python/pythonrun.c:1377
    #19 0x555555e63ec0 in pyrun_file Python/pythonrun.c:1210
    #20 0x555555e6639b in _PyRun_SimpleFileObject Python/pythonrun.c:459
    #21 0x555555e6688f in _PyRun_AnyFileObject Python/pythonrun.c:77
    #22 0x555555ecc247 in pymain_run_file_obj Modules/main.c:357
    #23 0x555555eceb5d in pymain_run_file Modules/main.c:376
    #24 0x555555ecf76e in pymain_run_python Modules/main.c:640
    #25 0x555555ecf8fe in Py_RunMain Modules/main.c:719
    #26 0x555555ecfae5 in pymain_main Modules/main.c:749
    #27 0x555555ecfe4c in Py_BytesMain Modules/main.c:773
    #28 0x5555557d6a85 in main Programs/python.c:15
    #29 0x7ffff7230082 in __libc_start_main ../csu/libc-start.c:308

SUMMARY: AddressSanitizer: heap-use-after-free Modules/_lsprof.c:317 in Stop
Shadow bytes around the buggy address:
  0x0c0c80004d70: fd fd fd fa fa fa fa fa fd fd fd fd fd fd fd fd
  0x0c0c80004d80: fa fa fa fa 00 00 00 00 00 00 00 fa fa fa fa fa
  0x0c0c80004d90: fd fd fd fd fd fd fd fa fa fa fa fa fd fd fd fd
  0x0c0c80004da0: fd fd fd fa fa fa fa fa fd fd fd fd fd fd fd fa
  0x0c0c80004db0: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa
=>0x0c0c80004dc0: fd fd fd fd fd fd fd fa fa fa fa fa fd fd[fd]fd
  0x0c0c80004dd0: fd fd fd fa fa fa fa fa fd fd fd fd fd fd fd fa
  0x0c0c80004de0: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa
  0x0c0c80004df0: fd fd fd fd fd fd fd fa fa fa fa fa fd fd fd fd
  0x0c0c80004e00: fd fd fd fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0c80004e10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==903512==ABORTING

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.14.0a0 (remotes/origin/HEAD-dirty:bd473aa598, Jul 1 2024, 16:59:13) [GCC 9.4.0]

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.12only security fixes3.13bugs and security fixes3.14bugs and security fixestype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions