Skip to content

Crash: use-after-free in OrderedDict_copy_impl #148660

Description

@kdsjZh

Crash report

What happened?

POC

from collections import OrderedDict

eq_count = 0

class CollidingKey:
    """Key with fixed hash to cause collisions in od_copy."""
    def __init__(self, name):
        self.name = name

    def __hash__(self):
        return 42  # All keys collide

    def __eq__(self, other):
        global eq_count
        eq_count += 1
        # Fire on 4th call (during od_copy's probe into od_copy, not during od lookup)
        if eq_count == 4:
            # Trigger from inside SetItem's probe — this frees all nodes in od
            od.clear()
        return self.name == getattr(other, 'name', None)

od = OrderedDict()
od[CollidingKey('k1')] = 'v1'
od[CollidingKey('k2')] = 'v2'

# ASan should report heap-use-after-free at Objects/odictobject.c:1254
od.copy()

Analysis

        _odict_FOREACH(od, node) {
            PyObject *key = _odictnode_KEY(node);
            PyObject *value = _odictnode_VALUE(node, od);
            if (value == NULL) {
                if (!PyErr_Occurred())
                    PyErr_SetObject(PyExc_KeyError, key);
                goto fail;
            }
            if (_PyODict_SetItem_KnownHash_LockHeld((PyObject *)od_copy, key, value,
                                                    _odictnode_HASH(node)) != 0)
                goto fail;
        }

The _PyODict_SetItem_KnownHash_LockHeld fire user-defined python code, in which the __eq__ trigger od.clear() and clear all container being iterated. Thus when returning to the _odict_FOREACH(od, node) loop, next iteration use an freed object.

ASan

=================================================================
==2330421==ERROR: AddressSanitizer: heap-use-after-free on address 0x50600006bec0 at pc 0x5a4039bee5e1 bp 0x7ffd2ee588c0 sp 0x7ffd2ee588b0
READ of size 8 at 0x50600006bec0 thread T0
    #0 0x5a4039bee5e0 in OrderedDict_copy_impl Objects/odictobject.c:1256
    #1 0x5a4039bee691 in OrderedDict_copy Objects/clinic/odictobject.c.h:377
    #2 0x5a4039b6b88a in method_vectorcall_NOARGS Objects/descrobject.c:448
    #3 0x5a4039b4be65 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:177
...

0x50600006bec0 is located 32 bytes inside of 56-byte region [0x50600006bea0,0x50600006bed8)
freed by thread T0 here:
    #0 0x7faa614fc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x5a4039c13399 in _PyMem_RawFree Objects/obmalloc.c:91
    #2 0x5a4039c15705 in _PyMem_DebugRawFree Objects/obmalloc.c:2959
    #3 0x5a4039c15746 in _PyMem_DebugFree Objects/obmalloc.c:3104
    #4 0x5a4039c3cd7f in PyMem_Free Objects/obmalloc.c:1089
    #5 0x5a4039bea7f2 in _odict_clear_nodes Objects/odictobject.c:810
    #6 0x5a4039bec36d in OrderedDict_clear_impl Objects/odictobject.c:1227
    #7 0x5a4039bec387 in OrderedDict_clear Objects/clinic/odictobject.c.h:353
    #8 0x5a4039b6b88a in method_vectorcall_NOARGS Objects/descrobject.c:448
    #9 0x5a4039b4be65 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:177
    #10 0x5a4039b4bf58 in PyObject_Vectorcall Objects/call.c:327
    #11 0x5a4039dc0b15 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1621
...

previously allocated by thread T0 here:
    #0 0x7faa614fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x5a4039c13cb0 in _PyMem_RawMalloc Objects/obmalloc.c:63
    #2 0x5a4039c13081 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2891
    #3 0x5a4039c130e9 in _PyMem_DebugRawMalloc Objects/obmalloc.c:2924
    #4 0x5a4039c14967 in _PyMem_DebugMalloc Objects/obmalloc.c:3089
    #5 0x5a4039c3cc3b in PyMem_Malloc Objects/obmalloc.c:1060
    #6 0x5a4039beb685 in _odict_add_new_node Objects/odictobject.c:705
    #7 0x5a4039bec2e7 in _PyODict_SetItem_KnownHash_LockHeld Objects/odictobject.c:1628
    #8 0x5a4039bec342 in PyODict_SetItem_LockHeld Objects/odictobject.c:1648
    #9 0x5a4039bee6a0 in PyODict_SetItem Objects/odictobject.c:1656
    #10 0x5a4039bee729 in odict_mp_ass_sub Objects/odictobject.c:879
    #11 0x5a4039b1a55a in PyObject_SetItem Objects/abstract.c:235
    #12 0x5a4039e00aaf in _PyEval_EvalFrameDefault Python/generated_cases.c.h:11487
...
SUMMARY: AddressSanitizer: heap-use-after-free Objects/odictobject.c:1256 in OrderedDict_copy_impl
Shadow bytes around the buggy address:
  0x50600006bc00: 00 00 00 00 00 00 00 fa fa fa fa fa fd fd fd fd
  0x50600006bc80: fd fd fd fd fa fa fa fa fd fd fd fd fd fd fd fd
  0x50600006bd00: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa
  0x50600006bd80: fd fd fd fd fd fd fd fd fa fa fa fa 00 00 00 00
  0x50600006be00: 00 00 00 fa fa fa fa fa fd fd fd fd fd fd fd fa
=>0x50600006be80: fa fa fa fa fd fd fd fd[fd]fd fd fa fa fa fa fa
  0x50600006bf00: 00 00 00 00 00 00 00 fa fa fa fa fa 00 00 00 00
  0x50600006bf80: 00 00 00 fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50600006c000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50600006c080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50600006c100: 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
==2330421==ABORTING

CPython versions tested on:

3.15

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.15.0a8 (tags/v3.15.0a8:55ea59e7dc3, Apr 16 2026, 21:14:44) [GCC 13.3.0]

Linked PRs

Metadata

Metadata

Assignees

Labels

extension-modulesC modules in the Modules dirtype-crashA hard crash of the interpreter, possibly with a core dump
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions