Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
kcatss opened this issue Nov 15, 2023 · 4 comments
Open

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

kcatss opened this issue Nov 15, 2023 · 4 comments
Labels
extension-modules C modules in the Modules dir type-crash A hard crash of the interpreter, possibly with a core dump

Comments

@kcatss
Copy link
Contributor

kcatss commented Nov 15, 2023

Bug report

Bug description:

Version

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

bisect from commit 670e692,

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

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

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

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

atexit.unregister(t)

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

@kcatss kcatss added the type-bug An unexpected behavior, bug, or error label Nov 15, 2023
@sobolevn
Copy link
Member

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
Copy link
Contributor Author

kcatss commented Nov 16, 2023

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
Copy link
Member

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

:)

@arhadthedev arhadthedev added type-crash A hard crash of the interpreter, possibly with a core dump extension-modules C modules in the Modules dir and removed type-bug An unexpected behavior, bug, or error labels Nov 16, 2023
benjaminJohnson2204 added a commit to benjaminJohnson2204/cpython that referenced this issue Jan 15, 2024
@kcatss
Copy link
Contributor Author

kcatss commented Feb 12, 2024

@sobolevn Hi there! How should we proceed on this issue.
The PR with this issue is #114092

can I do more anything? please tell me

benjaminJohnson2204 added a commit to benjaminJohnson2204/cpython that referenced this issue Feb 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extension-modules C modules in the Modules dir type-crash A hard crash of the interpreter, possibly with a core dump
Projects
None yet
Development

No branches or pull requests

3 participants