Skip to content

gh-148660: Fix use-after-free in OrderedDict.copy() on re-entrant mutation#151531

Open
harjothkhara wants to merge 1 commit into
python:mainfrom
harjothkhara:gh-148660-ordereddict-copy-uaf
Open

gh-148660: Fix use-after-free in OrderedDict.copy() on re-entrant mutation#151531
harjothkhara wants to merge 1 commit into
python:mainfrom
harjothkhara:gh-148660-ordereddict-copy-uaf

Conversation

@harjothkhara

@harjothkhara harjothkhara commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

OrderedDict.copy() walked the source dict's internal linked list while building the new dict. Each step runs arbitrary Python — a key's __eq__/__hash__, a subclass's __getitem__/__setitem__, or a value's __del__ — which can call od.clear() (or otherwise mutate od) and free the very nodes being iterated, so the next node->next is a use-after-free (segfault; ASan report in the issue).

Following the existing guard for OrderedDict.__eq__ (gh-119004), the copy now snapshots od_state and raises RuntimeError("OrderedDict mutated during iteration") if the dict is mutated mid-copy, before any freed node is dereferenced. Keys are pinned with Py_NewRef across each re-entrant call.

Behavior change: od.copy() now raises RuntimeError (instead of crashing/corrupting) when re-entrant code mutates od during the copy — the same trade-off OrderedDict.__eq__ already makes. A genuine exception raised by the re-entrant call is propagated unchanged.

Verification

  • ./configure --with-pydebug && make -j8 (clang, macOS arm64); odictobject.c compiles clean under -Wall -Wextra.
  • ./python -m test test_ordered_dict → SUCCESS (309 tests; the new tests run against both the exact OrderedDict and a subclass).
  • ./python -m test test_ordered_dict -R 3:3 -m '*issue148660*' → SUCCESS (no reference leaks).
  • ./python -m test test_collections → SUCCESS.
  • Confirmed the destination-insert regression test exercises the destination-insert guard (the line in the ASan backtrace): temporarily reverting only that check makes the test segfault.

Real behavior proof

Behavior addressed: segfault / use-after-free in OrderedDict.copy() when a key's __eq__/__hash__, a subclass __getitem__/__setitem__, or a value's __del__ mutates the dict during the copy.

Real environment tested: CPython main, built --with-pydebug on macOS arm64 (clang), default (GIL) build.

Exact steps or command run after this patch (poc.py is the issue reproducer):

$ ./python -X faulthandler poc.py

Evidence after fix:

# BEFORE (main, unfixed):
Fatal Python error: Segmentation fault
Current thread's C stack trace (most recent call first):
  ... at OrderedDict_copy+0x15c
  ... at method_vectorcall_NOARGS+0x12c

# AFTER (this patch):
Traceback (most recent call last):
  File "poc.py", line 13, in <module>
    od.copy()
RuntimeError: OrderedDict mutated during iteration

All four reproducers (__eq__, subclass __getitem__, subclass __setitem__, value __del__) raise the same RuntimeError instead of crashing.

Observed result after fix: no crash; copy() raises a catchable RuntimeError; normal/subclass/empty copies are unchanged.

What was not tested: the free-threaded (--disable-gil) build was not exercised at runtime — reasoned about via copy()'s @critical_section(od) (cross-thread mutation is excluded; same-thread reentrancy is what's guarded). ASan was not run locally; the issue reporter's ASan trace plus a --with-pydebug + faulthandler segfault establish the crash.


This is a crash fix and may be a backport candidate for the active maintenance branches (as gh-119004 was); I'll leave that decision to a maintainer.

@harjothkhara harjothkhara force-pushed the gh-148660-ordereddict-copy-uaf branch from 454e21a to 5576393 Compare June 16, 2026 16:21
…nt mutation

Building the copy iterated od's node list while running arbitrary Python (a key's __eq__/__hash__, a subclass' __getitem__/__setitem__, or a value's __del__) that could clear od and free the nodes being walked, causing a use-after-free.

Snapshot od_state and raise RuntimeError if it changes during the copy, as is already done for OrderedDict equality (pythongh-119004).
@harjothkhara harjothkhara force-pushed the gh-148660-ordereddict-copy-uaf branch from 5576393 to ea89579 Compare June 16, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant