Skip to content

Use-after-free in atexit.unregister via re-entrant __eq__ #142828

@jackfromeast

Description

@jackfromeast

What happened?

atexit.unregister iterates its callback list while comparing each entry. A user-defined __eq__ that calls atexit._clear() frees the list during iteration, so the next comparison dereferences a stale pointer and reads freed memory.

Proof of Concept:

import atexit

class Callback:
    def __call__(self):
        pass

atexit.register(Callback())

class Evil:
    def __eq__(self, other):
        atexit._clear()
        return NotImplemented

atexit.unregister(Evil())

Affected Versions:

Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 27 2025, 21:34:13) OK 0
Python 3.10.19+ (heads/3.10:014261980b1, Oct 27 2025, 21:19:00) [Clang 18.1.3 (1ubuntu1)] OK 0
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 27 2025, 21:20:35) [Clang 18.1.3 (1ubuntu1)] OK 0
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 27 2025, 21:27:07) [Clang 18.1.3 (1ubuntu1)] OK 0
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 27 2025, 21:28:49) [Clang 18.1.3 (1ubuntu1)] OK 0
Python 3.14.0+ (heads/3.14:2e216728038, Oct 27 2025, 21:30:55) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 27 2025, 21:32:37) [Clang 18.1.3 (1ubuntu1)] ASAN 1

Vulnerable Code:

static int
atexit_unregister_locked(PyObject *callbacks, PyObject *func)
{
    for (Py_ssize_t i = 0; i < PyList_GET_SIZE(callbacks); ++i) {
        PyObject *tuple = PyList_GET_ITEM(callbacks, i);
        assert(PyTuple_CheckExact(tuple));
        PyObject *to_compare = PyTuple_GET_ITEM(tuple, 0);
        int cmp = PyObject_RichCompareBool(func, to_compare, Py_EQ);
        if (cmp < 0)
        {
            return -1;
        }
        if (cmp == 1) {
            // We found a callback!
            if (PyList_SetSlice(callbacks, i, i + 1, NULL) < 0) {
                return -1;
            }
            --i;
        }
    }

    return 0;
}

Sanitizer Output:

=================================================================
==1088062==ERROR: AddressSanitizer: heap-use-after-free on address 0x513000025df8 at pc 0x6546cb291883 bp 0x7ffdb2707160 sp 0x7ffdb2707150
READ of size 8 at 0x513000025df8 thread T0
    #0 0x6546cb291882 in _Py_TYPE Include/object.h:277
    #1 0x6546cb291882 in do_richcompare Objects/object.c:1064
    #2 0x6546cb29193d in PyObject_RichCompare Objects/object.c:1108
    #3 0x6546cb2919ad in PyObject_RichCompareBool Objects/object.c:1130
    #4 0x6546cb5f50ad in atexit_unregister_locked Modules/atexitmodule.c:262
    #5 0x6546cb5f5130 in atexit_unregister Modules/atexitmodule.c:294
    #6 0x6546cb2843fe in cfunction_vectorcall_O Objects/methodobject.c:536
    #7 0x6546cb1d1e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #8 0x6546cb1d1f72 in PyObject_Vectorcall Objects/call.c:327
    #9 0x6546cb450056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #10 0x6546cb493e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #11 0x6546cb494148 in _PyEval_Vector Python/ceval.c:2001
    #12 0x6546cb4943f8 in PyEval_EvalCode Python/ceval.c:884
    #13 0x6546cb58b507 in run_eval_code_obj Python/pythonrun.c:1365
    #14 0x6546cb58b723 in run_mod Python/pythonrun.c:1459
    #15 0x6546cb58c57a in pyrun_file Python/pythonrun.c:1293
    #16 0x6546cb58f220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #17 0x6546cb58f4f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #18 0x6546cb5e074d in pymain_run_file_obj Modules/main.c:410
    #19 0x6546cb5e09b4 in pymain_run_file Modules/main.c:429
    #20 0x6546cb5e21b2 in pymain_run_python Modules/main.c:691
    #21 0x6546cb5e2842 in Py_RunMain Modules/main.c:772
    #22 0x6546cb5e2a2e in pymain_main Modules/main.c:802
    #23 0x6546cb5e2db3 in Py_BytesMain Modules/main.c:826
    #24 0x6546cb066645 in main Programs/python.c:15
    #25 0x7fdd5822a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #26 0x7fdd5822a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #27 0x6546cb066574 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: 202d5dbb945f6d5f5a66ad50e2688d56affd6ecb)

0x513000025df8 is located 56 bytes inside of 352-byte region [0x513000025dc0,0x513000025f20)
freed by thread T0 here:
    #0 0x7fdd586fc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x6546cb29896d in _PyMem_RawFree Objects/obmalloc.c:91
    #2 0x6546cb29acd9 in _PyMem_DebugRawFree Objects/obmalloc.c:2955
    #3 0x6546cb29ad1a in _PyMem_DebugFree Objects/obmalloc.c:3100
    #4 0x6546cb2c306c in PyObject_Free Objects/obmalloc.c:1522
    #5 0x6546cb501cf7 in PyObject_GC_Del Python/gc.c:2435
    #6 0x6546cb2dd1cb in object_dealloc Objects/typeobject.c:7177
    #7 0x6546cb2fb663 in subtype_dealloc Objects/typeobject.c:2852
    #8 0x6546cb28f481 in _Py_Dealloc Objects/object.c:3200
    #9 0x6546cb4f792f in Py_DECREF_MORTAL Include/internal/pycore_object.h:450
    #10 0x6546cb4f79e5 in PyStackRef_XCLOSE Include/internal/pycore_stackref.h:736
    #11 0x6546cb4f8a86 in _PyFrame_ClearLocals Python/frame.c:101
    #12 0x6546cb4f8c7e in _PyFrame_ClearExceptCode Python/frame.c:126
    #13 0x6546cb43cbcd in clear_thread_frame Python/ceval.c:1826
    #14 0x6546cb440c31 in _PyEval_FrameClearAndPop Python/ceval.c:1850
    #15 0x6546cb4874c9 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:10403
    #16 0x6546cb493e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #17 0x6546cb494148 in _PyEval_Vector Python/ceval.c:2001
    #18 0x6546cb1d19b8 in _PyFunction_Vectorcall Objects/call.c:413
    #19 0x6546cb2e456b in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #20 0x6546cb3009a6 in vectorcall_unbound Objects/typeobject.c:3033
    #21 0x6546cb3009a6 in maybe_call_special_one_arg Objects/typeobject.c:3175
    #22 0x6546cb300ad3 in _PyObject_MaybeCallSpecialOneArg Objects/typeobject.c:3190
    #23 0x6546cb300b19 in slot_tp_richcompare Objects/typeobject.c:10729
    #24 0x6546cb291687 in do_richcompare Objects/object.c:1059
    #25 0x6546cb29193d in PyObject_RichCompare Objects/object.c:1108
    #26 0x6546cb2919ad in PyObject_RichCompareBool Objects/object.c:1130
    #27 0x6546cb5f50ad in atexit_unregister_locked Modules/atexitmodule.c:262
    #28 0x6546cb5f5130 in atexit_unregister Modules/atexitmodule.c:294
    #29 0x6546cb2843fe in cfunction_vectorcall_O Objects/methodobject.c:536
    #30 0x6546cb1d1e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169

previously allocated by thread T0 here:
    #0 0x7fdd586fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x6546cb299284 in _PyMem_RawMalloc Objects/obmalloc.c:63
    #2 0x6546cb298655 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2887
    #3 0x6546cb2986bd in _PyMem_DebugRawMalloc Objects/obmalloc.c:2920
    #4 0x6546cb299f3b in _PyMem_DebugMalloc Objects/obmalloc.c:3085
    #5 0x6546cb2c2f28 in PyObject_Malloc Objects/obmalloc.c:1493
    #6 0x6546cb2f503b in _PyObject_MallocWithType Include/internal/pycore_object_alloc.h:46
    #7 0x6546cb2f503b in _PyType_AllocNoTrack Objects/typeobject.c:2504
    #8 0x6546cb2f51c7 in PyType_GenericAlloc Objects/typeobject.c:2535
    #9 0x6546cb2ed10e in object_new Objects/typeobject.c:7167
    #10 0x6546cb2f8346 in type_call Objects/typeobject.c:2448
    #11 0x6546cb1d1c71 in _PyObject_MakeTpCall Objects/call.c:242
    #12 0x6546cb1d1f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #13 0x6546cb1d1f72 in PyObject_Vectorcall Objects/call.c:327
    #14 0x6546cb450056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #15 0x6546cb493e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #16 0x6546cb494148 in _PyEval_Vector Python/ceval.c:2001
    #17 0x6546cb4943f8 in PyEval_EvalCode Python/ceval.c:884
    #18 0x6546cb58b507 in run_eval_code_obj Python/pythonrun.c:1365
    #19 0x6546cb58b723 in run_mod Python/pythonrun.c:1459
    #20 0x6546cb58c57a in pyrun_file Python/pythonrun.c:1293
    #21 0x6546cb58f220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #22 0x6546cb58f4f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #23 0x6546cb5e074d in pymain_run_file_obj Modules/main.c:410
    #24 0x6546cb5e09b4 in pymain_run_file Modules/main.c:429
    #25 0x6546cb5e21b2 in pymain_run_python Modules/main.c:691
    #26 0x6546cb5e2842 in Py_RunMain Modules/main.c:772
    #27 0x6546cb5e2a2e in pymain_main Modules/main.c:802
    #28 0x6546cb5e2db3 in Py_BytesMain Modules/main.c:826
    #29 0x6546cb066645 in main Programs/python.c:15
    #30 0x7fdd5822a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-use-after-free Include/object.h:277 in _Py_TYPE
Shadow bytes around the buggy address:
  0x513000025b00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x513000025b80: fd fd fd fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x513000025c00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513000025c80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513000025d00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x513000025d80: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd[fd]
  0x513000025e00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x513000025e80: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x513000025f00: fd fd fd fd fa fa fa fa fa fa fa fa fa fa fa fa
  0x513000025f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513000026000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
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
==1088062==ABORTING

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtype-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