Skip to content

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

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

Activity

  1. yihong0618 commented on Dec 16, 2025

    @yihong0618
    Contributor

    seems kind of similar issue as #112127

  2. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    on Dec 16, 2025
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

    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