python / cpython

The Python programming language
https://www.python.org
Other
62.41k stars 29.97k forks source link

Use-after-free in unregister() of atexit module #112127

Open kcatss opened 10 months ago

kcatss commented 10 months ago

Bug report

Bug description:

Version

3.10.13 (tags/v3.10.13:49965601d6, Nov 12 2023, 04:15:18)

bisect from commit 670e6921349dd408b6958a0c5d3b1486725f9beb,

Root cause

atexit_unregister dont’ consider counting reference and reentering other functions including atexit_unregister , atexit_clear .

It causes a free cb-func in the second loop while the first loop holds that reference.

This vulnerability caused by calling PyObject_RichCompareBool in c code, which call arbitrary user defined code like eq

static PyObject *
atexit_unregister(PyObject *module, PyObject *func)
{
    struct atexit_state *state = get_atexit_state();
    for (int i = 0; i < state->ncallbacks; i++)
    {
        atexit_py_callback *cb = state->callbacks[i];
        if (cb == NULL) {
            continue;
        }

        int eq = PyObject_RichCompareBool(cb->func, func, Py_EQ); //<--here
        if (eq < 0) {
            return NULL;
        }
        if (eq) {
            atexit_delete_cb(state, i);
        }
    }
    Py_RETURN_NONE;
}

Poc

import atexit

class test(object):
    def __eq__(self,o):
        atexit._clear()
        return NotImplemented
    def __call__(self): 
        return

atexit.register(test())
atexit.unregister(test())

Asan

asan =75703==ERROR: AddressSanitizer: heap-use-after-free on address 0x607000644cb0 at pc 0x563a1a579158 bp 0x7f feb4ffa460 sp 0x7ffeb4ffa450 READ of size 8 at 0x607000644cb0 thread T0 #0 0x563a1a579157 in _Py_INCREF Include/object.h:472 #1 0x563a1a579157 in _PyEval_MakeFrameVector Python/ceval.c:4826 #2 0x563a1a57a8b7 in _PyEval_Vector Python/ceval.c:5059 #3 0x563a1a3b8c9a in _PyFunction_Vectorcall Objects/call.c:342 #4 0x563a1a494550 in _PyObject_VectorcallTstate Include/cpython/abstract.h:114 #5 0x563a1a494550 in vectorcall_unbound Objects/typeobject.c:1629 #6 0x563a1a494550 in slot_tp_richcompare Objects/typeobject.c:7628 #7 0x563a1a440c51 in do_richcompare Objects/object.c:699 #8 0x563a1a440f93 in PyObject_RichCompare Objects/object.c:743 #9 0x563a1a4410a9 in PyObject_RichCompareBool Objects/object.c:765 #10 0x563a1a71618c in atexit_unregister Modules/atexitmodule.c:241 #11 0x563a1a7ef56d in cfunction_vectorcall_O Objects/methodobject.c:516 #12 0x563a1a548745 in _PyObject_VectorcallTstate Include/cpython/abstract.h:114 #13 0x563a1a551b8f in PyObject_Vectorcall Include/cpython/abstract.h:123 #14 0x563a1a551b8f in call_function Python/ceval.c:5893 #15 0x563a1a574978 in _PyEval_EvalFrameDefault Python/ceval.c:4181 #16 0x563a1a57a95d in _PyEval_EvalFrame Include/internal/pycore_ceval.h:46 #17 0x563a1a57a95d in _PyEval_Vector Python/ceval.c:5067 #18 0x563a1a57af25 in PyEval_EvalCode Python/ceval.c:1134 #19 0x563a1a60c907 in run_eval_code_obj Python/pythonrun.c:1291 #20 0x563a1a60d329 in run_mod Python/pythonrun.c:1312 #21 0x563a1a60d4c3 in pyrun_file Python/pythonrun.c:1208 #22 0x563a1a612b87 in _PyRun_SimpleFileObject Python/pythonrun.c:456 #23 0x563a1a612e80 in _PyRun_AnyFileObject Python/pythonrun.c:90 #24 0x563a1a3939b4 in pymain_run_file_obj Modules/main.c:353 #25 0x563a1a394184 in pymain_run_file Modules/main.c:372 #26 0x563a1a39668b in pymain_run_python Modules/main.c:587 #27 0x563a1a396809 in Py_RunMain Modules/main.c:666 #28 0x563a1a3969f9 in pymain_main Modules/main.c:696 #29 0x563a1a396d71 in Py_BytesMain Modules/main.c:720 #30 0x563a1a393245 in main Programs/python.c:15 #31 0x7f6c90d20d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #32 0x7f6c90d20e3f in __libc_start_main_impl ../csu/libc-start.c:392 #33 0x563a1a393174 in _start (/home/ubuntu/cpython/python+0x1fa174) 0x607000644cb0 is located 32 bytes inside of 72-byte region [0x607000644c90,0x607000644cd8) freed by thread T0 here: #0 0x7f6c910ba537 in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127 #1 0x563a1a4460e8 in _PyMem_RawFree Objects/obmalloc.c:127 #2 0x563a1a446e01 in _PyMem_DebugRawFree Objects/obmalloc.c:2538 #3 0x563a1a4476e2 in _PyMem_DebugFree Objects/obmalloc.c:2671 #4 0x563a1a449273 in PyObject_Free Objects/obmalloc.c:709 #5 0x563a1a663cf0 in PyObject_GC_Del Modules/gcmodule.c:2375 #6 0x563a1a46a386 in object_dealloc Objects/typeobject.c:4510 #7 0x563a1a47da06 in subtype_dealloc Objects/typeobject.c:1460 #8 0x563a1a43ed59 in _Py_Dealloc Objects/object.c:2301 #9 0x563a1a7e0a89 in _Py_DECREF Include/object.h:500 #10 0x563a1a7e0a89 in frame_dealloc Objects/frameobject.c:591 #11 0x563a1a43ed59 in _Py_Dealloc Objects/object.c:2301 #12 0x563a1a57acc7 in _Py_DECREF Include/object.h:500 #13 0x563a1a57acc7 in _PyEval_Vector Python/ceval.c:5080 #14 0x563a1a3b8c9a in _PyFunction_Vectorcall Objects/call.c:342 #15 0x563a1a494550 in _PyObject_VectorcallTstate Include/cpython/abstract.h:114 #16 0x563a1a494550 in vectorcall_unbound Objects/typeobject.c:1629 #17 0x563a1a494550 in slot_tp_richcompare Objects/typeobject.c:7628 #18 0x563a1a440b52 in do_richcompare Objects/object.c:693 #19 0x563a1a440f93 in PyObject_RichCompare Objects/object.c:743 #20 0x563a1a4410a9 in PyObject_RichCompareBool Objects/object.c:765 #21 0x563a1a71618c in atexit_unregister Modules/atexitmodule.c:241 #22 0x563a1a7ef56d in cfunction_vectorcall_O Objects/methodobject.c:516 #23 0x563a1a548745 in _PyObject_VectorcallTstate Include/cpython/abstract.h:114 #24 0x563a1a551b8f in PyObject_Vectorcall Include/cpython/abstract.h:123 #25 0x563a1a551b8f in call_function Python/ceval.c:5893 #26 0x563a1a574978 in _PyEval_EvalFrameDefault Python/ceval.c:4181 #27 0x563a1a57a95d in _PyEval_EvalFrame Include/internal/pycore_ceval.h:46 #28 0x563a1a57a95d in _PyEval_Vector Python/ceval.c:5067 #29 0x563a1a57af25 in PyEval_EvalCode Python/ceval.c:1134 #30 0x563a1a60c907 in run_eval_code_obj Python/pythonrun.c:1291 #31 0x563a1a60d329 in run_mod Python/pythonrun.c:1312 #32 0x563a1a60d4c3 in pyrun_file Python/pythonrun.c:1208 #33 0x563a1a612b87 in _PyRun_SimpleFileObject Python/pythonrun.c:456 #34 0x563a1a612e80 in _PyRun_AnyFileObject Python/pythonrun.c:90 #35 0x563a1a3939b4 in pymain_run_file_obj Modules/main.c:353 previously allocated by thread T0 here: #0 0x7f6c910ba887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145 #1 0x563a1a446164 in _PyMem_RawMalloc Objects/obmalloc.c:99 #2 0x563a1a446c5c in _PyMem_DebugRawAlloc Objects/obmalloc.c:2471 #3 0x563a1a446cbb in _PyMem_DebugRawMalloc Objects/obmalloc.c:2504 #4 0x563a1a447724 in _PyMem_DebugMalloc Objects/obmalloc.c:2656 #5 0x563a1a4491ec in PyObject_Malloc Objects/obmalloc.c:685 #6 0x563a1a661abe in _PyObject_GC_Alloc Modules/gcmodule.c:2280 #7 0x563a1a663816 in _PyObject_GC_Malloc Modules/gcmodule.c:2307 #8 0x563a1a477585 in PyType_GenericAlloc Objects/typeobject.c:1156 #9 0x563a1a47b189 in object_new Objects/typeobject.c:4504 #10 0x563a1a48200c in type_call Objects/typeobject.c:1123 #11 0x563a1a3b9378 in _PyObject_MakeTpCall Objects/call.c:215 #12 0x563a1a548874 in _PyObject_VectorcallTstate Include/cpython/abstract.h:112 #13 0x563a1a551b8f in PyObject_Vectorcall Include/cpython/abstract.h:123 #14 0x563a1a551b8f in call_function Python/ceval.c:5893 #15 0x563a1a574b5d in _PyEval_EvalFrameDefault Python/ceval.c:4213 #16 0x563a1a57a95d in _PyEval_EvalFrame Include/internal/pycore_ceval.h:46 #17 0x563a1a57a95d in _PyEval_Vector Python/ceval.c:5067 #18 0x563a1a57af25 in PyEval_EvalCode Python/ceval.c:1134 #19 0x563a1a60c907 in run_eval_code_obj Python/pythonrun.c:1291 #20 0x563a1a60d329 in run_mod Python/pythonrun.c:1312 #21 0x563a1a60d4c3 in pyrun_file Python/pythonrun.c:1208 #22 0x563a1a612b87 in _PyRun_SimpleFileObject Python/pythonrun.c:456 #23 0x563a1a612e80 in _PyRun_AnyFileObject Python/pythonrun.c:90 #24 0x563a1a3939b4 in pymain_run_file_obj Modules/main.c:353 #25 0x563a1a394184 in pymain_run_file Modules/main.c:372 #26 0x563a1a39668b in pymain_run_python Modules/main.c:587 #27 0x563a1a396809 in Py_RunMain Modules/main.c:666 #28 0x563a1a3969f9 in pymain_main Modules/main.c:696 #29 0x563a1a396d71 in Py_BytesMain Modules/main.c:720 #30 0x563a1a393245 in main Programs/python.c:15 #31 0x7f6c90d20d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:5 SUMMARY: AddressSanitizer: heap-use-after-free Include/object.h:472 in _Py_INCREF Shadow bytes around the buggy address: 0x0c0e800c0940: fd fd fd fd fd fd fd fd fa fa fa fa fd fd fd fd 0x0c0e800c0950: fd fd fd fd fd fd fa fa fa fa fd fd fd fd fd fd 0x0c0e800c0960: fd fd fd fd fa fa fa fa fd fd fd fd fd fd fd fd 0x0c0e800c0970: fd fd fa fa fa fa fd fd fd fd fd fd fd fd fd fd 0x0c0e800c0980: fa fa fa fa 00 00 00 00 00 00 00 00 00 fa fa fa =>0x0c0e800c0990: fa fa fd fd fd fd[fd]fd fd fd fd fa fa fa fa fa 0x0c0e800c09a0: 00 00 00 00 00 00 00 00 00 02 fa fa fa fa fd fd 0x0c0e800c09b0: fd fd fd fd fd fd fd fd fa fa fa fa fd fd fd fd 0x0c0e800c09c0: fd fd fd fd fd fd fa fa fa fa fd fd fd fd fd fd 0x0c0e800c09d0: fd fd fd fd fa fa fa fa fd fd fd fd fd fd fd fd 0x0c0e800c09e0: fd fd fa fa fa fa fd fd fd fd fd fd fd fd fd fd 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 ==75703==ABORTING

CPython versions tested on:

3.10

Operating systems tested on:

Linux

Linked PRs

sobolevn commented 10 months ago

Here's the traceback that I get for 3.13a1:

>>> atexit.unregister(t)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    atexit.unregister(t)
  File "<stdin>", line 5, in __eq__
    atexit.unregister(self)
  File "<stdin>", line 5, in __eq__
    atexit.unregister(self)
  File "<stdin>", line 5, in __eq__
    atexit.unregister(self)
  [Previous line repeated 371 more times]
RecursionError: maximum recursion depth exceeded
kcatss commented 10 months ago

is it same with follwing poc?

import atexit

cnt = 0
class test(object):
    def __eq__(self,o):
        pass
    def __call__(self):
        pass

class test2(object):
    def __eq__(self,o):
        global cnt
        cnt += 1
        if cnt == 1:
                atexit.unregister(self)
        return NotImplemented
    def __call__(self):
        return

t = test()
atexit.register(test2())
atexit.register(t)

atexit.unregister(t)

the poc code has some problem that could not correctly consider recursion

sobolevn commented 10 months ago

This one failed successfully:

>>> atexit.unregister(t)
./Include/object.h:979: _Py_NegativeRefcount: Assertion failed: object has negative ref count
<object at 0x100e68bf0 is freed>
Fatal Python error: _PyObject_AssertFailed: _PyObject_AssertFailed
Python runtime state: initialized

Current thread 0x00000001db465300 (most recent call first):
Assertion failed: (PyCode_Check(f->f_executable)), function _PyFrame_GetCode, file pycore_frame.h, line 77.
[1]    68841 abort      ./python.exe

:)

kcatss commented 7 months ago

@sobolevn Hi there! How should we proceed on this issue. The PR with this issue is https://github.com/python/cpython/pull/114092

can I do more anything? please tell me

erlend-aasland commented 2 months ago

Related: