python / python/cpython

CPython crashes on memory-allocation failure (OOM): 35 findings (4 dups → 31 distinct)

Open
#151,763 20 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

extension-modules interpreter-core type-crash
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

Crash report

What happened?

Allocation-failure fuzzing of CPython main turned up 35 distinct ways the interpreter
crashes
— segfault, failed assert(...), or Py_FatalError — when a memory allocation (4 have since been confirmed as duplicates via rr → 31 distinct; see the dedup comment)
fails part-way through an operation. Each is a separate underlying bug with a minimal,
stdlib-only reproducer, a backtrace, a root-cause analysis, and a suggested fix, published
as a self-contained gist (linked below).

I'm filing them under one umbrella so they can be picked off individually without flooding
the tracker with 35 issues at once. To take one: open a normal CPython issue (or PR) for
it and drop a comment here with the link — I'll mark it in the table. If any turn out to be
duplicates or non-bugs, say so and I'll annotate them.

These were found with fusil's OOM-injection mode
(fusil originally by @vstinner). The reports and reduced reproducers were drafted with
AI assistance (Claude Code) and then reviewed and re-verified by hand — see Disclosure.

Reproducing

  • Found on main (3.16.0a0) at commit 15d7406. These are allocation-path bugs, so they
    are not tied to that exact revision.
  • Every gist ships a minimal OOM-ID-repro.py (standard library only) that drives the failing path
    while _testcapi.set_nomemory(...) forces allocations to fail. Just run python OOM-ID-repro.py — where the exact failing-allocation index is sensitive to the build's
    allocation count, the repro self-sweeps (a fresh subprocess per index) and stops at the
    first crash, so no manual tuning is needed.
  • _testcapi.set_nomemory requires a test/debug interpreter (--with-pydebug exposes it).
  • Build matrix (in each report): many of these are debug-only assertion / Py_FatalError
    failures — the assert(...) is compiled out under -DNDEBUG, so a release build doesn't
    abort. They are still real bugs: on release the same defect is latent undefined behaviour
    (a use-after-free, a Py_DECREF(NULL), or a silently-lost exception). A subset segfault on
    release builds directly; those are called out below.

Highest-confidence starting points

These crash a release build (not just a debug assertion) and have a clean minimal
reproducer — the lowest-effort to confirm and fix:

  • OOM-0034, OOM-0028 — unchecked PyUnicode_AsUTF8 / PyUnicode_EncodeFSDefault
    returning NULL is dereferenced (≈ one-line NULL checks).
  • OOM-0001, OOM-0002, OOM-0014Py_DECREF(NULL) on an unchecked-allocation
    error path.
  • OOM-0031_interpreters.capture_exception calls _PyXI_FreeExcInfo(NULL) with no
    NULL guard.
  • OOM-0012, OOM-0020 — instrumentation / thread-state corruption under OOM.

Findings

Status: blank = not yet filed · #N = open issue · #N (PR) = open pull request · #N FIXED = filed and fixed (issue closed-completed or PR merged) · 🔁 = confirmed duplicate, folded into another finding — please don't pursue.

Segfaults (7)
Report Title Status
OOM-0001 Segfault: Py_DECREF of a NULL filename in do_warn (_warnings.c:1139) #151673
OOM-0002 Segfault: Py_DECREF(NULL) in PyContextVar_Set (context.c:367) #151773 FIXED
OOM-0024 Segfault: dealloc of uninitialized iterator in template_iter (templateobject.c:232) #151815
OOM-0028 Segfault: NULL deref in os__path_normpath_impl (posixmodule.c:6149) #152058 FIXED
OOM-0031 Segfault: NULL info deref in _excinfo_clear_type (crossinterp.c:1319) #151842 FIXED
OOM-0033 [SUPERSEDED — duplicate of OOM-0036 / #151818] Segfault / negative-refcount: over-decreffed sys.path entry in PyType_IsSubtype (typeobject.c:2931) 🔁 dup of OOM-0036 → #151818
OOM-0034 Segfault: unchecked PyUnicode_AsUTF8 NULL deref in pegen.c:33 #151798 (PR)
Assertion failures / aborts (23)
Report Title Status
OOM-0003 Abort: uninitialized _co_unique_id assert in code_dealloc (codeobject.c:2440) #152034 FIXED (PR)
OOM-0004 Abort/Segfault: corrupted object freelist in clear_freelist (object.c:909) #152125
OOM-0005 [SUPERSEDED — duplicate of OOM-0036 / #151818] Abort: negative-refcount over-decref in _PyFrame_ClearLocals (frame.c:101) 🔁 dup of OOM-0036 → #151818 (reported as #152147, closed as dup)
OOM-0006 Abort/Segfault: _PyObject_GC_UNTRACK assert on untracked iterator in dictiter_dealloc (dictobject.c:5532) #152107
OOM-0008 Abort: assert(!PyErr_Occurred()) in _PyType_LookupStackRefAndVersion (typeobject.c:6343) #151905 FIXED
OOM-0009 Abort: stale release1 flag trips an ownership assert in replace (unicodeobject.c:10783) #152228
OOM-0010 Abort: assert(_PyErr_Occurred(tstate)) in _PyEval_EvalFrameDefault (generated_cases.c.h:13817)
OOM-0011 [SUPERSEDED — duplicate of OOM-0008] Abort: assert(!PyErr_Occurred()) in specialize (specialize.c:364) 🔁 dup of OOM-0008 (above)
OOM-0012 Abort/Segfault: stale instrumentation in get_tools_for_instruction (instrumentation.c:1106)
OOM-0013 Abort: builtin breaks result/error contract in _Py_BuiltinCallFastWithKeywords_StackRef (ceval.c:843) #151968 FIXED (PR)
OOM-0014 Abort/Segfault: unchecked NULL in channelsmod__channel_id (_interpchannelsmodule.c:3487) #151902 (PR)
OOM-0015 Abort: stale exception in cfunction_check_kwargs (methodobject.c:409)
OOM-0016 Abort: assert(!queue->alive) in _queue_clear (_interpqueuesmodule.c:559) #152130
OOM-0017 Abort: negative gc_refs ("refcount too small") in validate_gc_objects (gc_free_threading.c:1116)
OOM-0018 Abort: ownership assert in set_keys (dictobject.c:205) #152254
OOM-0019 Abort: double-free in _PyPegen_raise_error_known_location (pegen_errors.c:363) #151931 (PR)
OOM-0025 Abort: assert(!PyErr_Occurred()) in unspecialize (specialize.c:378)
OOM-0026 Abort: err-code vs PyErr desync in handle_channel_error (_interpchannelsmodule.c:398 / :443) #152156 FIXED
OOM-0027 Abort: assert(PyStackRef_BoolCheck(cond)) in POP_JUMP_IF_FALSE (generated_cases.c.h:11120)
OOM-0029 [SUPERSEDED — duplicate of OOM-0036 / #151818] Abort: negative refcount on a MemoryError (tuple_dealloc, tupleobject.c:277) 🔁 dup of OOM-0036 → #151818
OOM-0030 Abort: Py_DECREF of NULL-data unicode in unicode_subtype_new (unicodeobject.c:13986)
OOM-0032 Abort: pending-exception assert from warn_explicit normalization (_warnings.c:799/806)
OOM-0035 Abort / malformed str: invalid maxchar in _PyUnicode_FromUCS4 (unicodeobject.c:2228)
Fatal Python error (5)
Report Title Status
OOM-0007 Fatal: context_tp_dealloc clears the pending exception (context.c:535) #152083
OOM-0020 Fatal: _PyMem_DebugRawFree: bad ID in free_threadstate (pystate.c:1527)
OOM-0021 Fatal: NULL returned without an exception set in _Py_CheckFunctionResult (call.c:43)
OOM-0022 Fatal: stale MemoryError trips _Py_CheckSlotResult in reload_singlephase_extension (import.c:2011)
OOM-0023 Fatal: dealloc clears the in-flight exception in subtype_dealloc (typeobject.c:2719)

Related groups (one fix may cover several)

  • Dealloc clears the in-flight exception (the gh-89373 _Py_Dealloc invariant): OOM-0007
    (context_tp_dealloc) and OOM-0023 (the generic subtype_dealloc, covering a family of
    pure-Python types) free an object while a MemoryError is pending and don't save/restore it.
  • A pending exception survives into code that asserts there is none (specializer / eval /
    call layers): OOM-0008, 0010, 0015, 0025, 0032 (OOM-0011 was a second detector face of
    OOM-0008 — same frame.f_back swallow — and is folded). Several share the theme that the
    adaptive specializer / call machinery is entered with a MemoryError already pending.
  • Py_DECREF/Py_CLEAR of a NULL or partially-initialized object on the OOM error path:
    OOM-0001, 0002, 0006, 0014, 0024, 0030, 0031 (an allocation fails after a slot is taken
    but before the object is valid, and the error path frees it anyway).
  • Over-decref → negative refcount under OOM (a real memory-safety bug; the assert is the
    debug-build detector): OOM-0019. (rr later showed OOM-0005, OOM-0029 and OOM-0033 are
    all the same _CALL_LIST_APPEND list.append()-under-MemoryError double-free —
    OOM-0036, filed #151818 — and are
    folded; OOM-0019 is a distinct pegen double-free.)
  • Free-threading-specific: OOM-0003 (_co_unique_id), OOM-0017 (cyclic GC),
    OOM-0018 (managed dict), OOM-0020 (thread-state reservation).

Disclosure & caveats

  • The reports and reduced reproducers were drafted with AI assistance (Claude Code); each
    gist carries an explicit disclaimer. The reproducers were re-run on the build matrix and the
    root causes audited against the CPython source before publishing.
  • A few root causes are explicitly marked partial — the trigger is minimal and verified,
    but the exact offending line wasn't pinned (noted in those reports): OOM-0010, 0027, 0035.
  • OOM-0001 is already filed as
    #151673; the other 34 had no matching
    python/cpython issue when checked and appear novel.

Found with fusil (OOM-injection mode; fusil originally
by Victor Stinner). Drafted with Claude Code; reproducers machine-generated and human-verified.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

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

Python 3.16.0a0 free-threading build (heads/main:15d74068f3a, Jun 18 2026, 16:44:30) [Clang 22.1.2 (1ubuntu1)]

Linked PRs
  • gh-151779
  • gh-151798
  • gh-151902
  • gh-151931
  • gh-151968
  • gh-152013
  • gh-152034
  • gh-152069
  • gh-152070
  • gh-152094
  • gh-152095
  • gh-152096
  • gh-152131
  • gh-152155
  • gh-152158
  • gh-152159
  • gh-152160
  • gh-152164
  • gh-152165
  • gh-152836
  • gh-152837
  • gh-152840
  • gh-153548
  • gh-154019
  • gh-154717

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Choose one unfiled OOM finding from the table and open its linked gist first. Build a CPython test/debug interpreter with --with-pydebug, run the gist's OOM-ID-repro.py, and inspect the named source location and backtrace. Done means a focused issue or PR for that finding, followed by a link in this umbrella issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.