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]

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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