GeezerVison is showing github.com — CPython crashes on memory-allocation failure (OOM): 35 findings (4 dups → 31 distinct) · Issue #151763 · python/cpython
CPython crashes on memory-allocation failure (OOM): 35 findings (4 dups → 31 distinct) #151763Open 10 / 1510 of 15 issues completedOpen 10 / 1510 of 15 issues completed DescriptionCrash 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-0014 —
Py_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 tp_dealloc docs should mention error indicator may be set #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
Activityzainnadeem786 commented on Jun 20, 2026 Hi @devdanzin ,
I'd like to investigate and work on a couple of the findings from this umbrella issue.
I spent some time reviewing the relevant code paths and, based on source inspection, I'm currently looking at:
- OOM-0028 (
os__path_normpath_impl in Modules/posixmodule.c)
- OOM-0034 (
_PyPegen_byte_offset_to_character_offset_line in Parser/pegen.c)
OOM-0028
My investigation suggests that there is a potential OOM error path in os__path_normpath_impl() where result may become NULL after Unicode object creation fails, but execution can still proceed into the bytes-path encoding logic. This appears to be a localized fix with conventional NULL propagation semantics.
OOM-0034
For OOM-0034, I found that PyUnicode_AsUTF8() may return NULL under allocation failure, while the returned pointer is later used without a guard. However, this one appears slightly more involved because the surrounding tokenization path may also need proper error propagation rather than only adding a local NULL check.
Next Steps
My plan is:
- Prepare a focused PR for OOM-0028 with a minimal fix and regression coverage if feasible.
- Investigate and prepare a separate PR for OOM-0034, ensuring that any
MemoryError is propagated correctly through the caller chain rather than only preventing the immediate crash.
I'll continue investigating these code paths and will post updates here as I make progress. If the analysis holds up, I'll prepare focused PRs for the relevant fixes and link them in this issue.
Thanks for collecting and documenting these findings. prakashsellathurai commented on Jun 20, 2026 amruthamodela06 commented on Jun 21, 2026 zainnadeem786 commented on Jun 21, 2026 I’d like to take OOM-0014 if no one else is already working on it.
I reproduced it locally on both debug and release builds.
Summary of my investigation:
_interpchannels._channel_id(0) calls channelsmod__channel_id().
channelsmod__channel_id() calls get_module_from_owned_type(cls).
- That path can return
NULL under OOM because _get_current_module() allocates the module name with PyUnicode_FromString(MODULE_NAME_STR).
channelsmod__channel_id() currently treats that return value as infallible:
PyObject *mod = get_module_from_owned_type(cls);
assert(mod == self);
Py_DECREF(mod);
When the allocation fails:
- debug build aborts at
assert(mod == self) because mod == NULL
- release build reaches
Py_DECREF(NULL) and crashes with an access violation
The minimal fix appears to be a local NULL check:
PyObject *mod = get_module_from_owned_type(cls);
if (mod == NULL) {
return NULL;
}
assert(mod == self);
Py_DECREF(mod);
This preserves the existing invariant check and correctly propagates the pending MemoryError.
I also verified the focused test suite still passes locally:
PCbuild/amd64/python_d.exe -m test test__interpchannels
Result: SUCCESS, 73 tests run, 5 skipped
I’ll prepare a focused PR with the NULL check and a small subprocess regression test unless there are concerns with this approach. Abhi210 commented on Jul 11, 2026 @devdanzin Thank you for the reply! I found a fix for the issue above, which is available in OOM-0018. While investigating further, I found that the new issue is related to OOM-0004. Applying that fix exposed yet another issue that appears to be independent, so I'll open a separate issue to track it.
Edit: I resolved the last bug. It's an exact match for #151818, which was already open with fixes in flight (#152114, #152117). I pulled #152114's fix and #152117's regression test onto my tree, and with all four fixes stacked (OOM-0023 - OOM-0018- OOM-0004- #151818), the original sweep now runs clean end-to-end with no crash at all. I would open a PR for OOM-0004 if no one else takes it. devdanzin commented on Jul 11, 2026 @Abhi210 Wow, impressive investigation and fixing, thank you! Please go ahead and open a PR for OOM-0004. Reacted by Abhishek Tiwari flameastro commented on Jul 11, 2026 BHUVANSH855 commented on Oct 9, 2026 Another uninit-dealloc on the OOM path, same pattern as OOM-0024:
odictiter_new, Objects/odictobject.c:1945
PyObject_GC_New succeeds but di->di_odict, di->di_current, and di->kind are uninitialized. The next allocation (di->di_result) fails → Py_DECREF(di) → odictiter_dealloc reads garbage fields → crash.
Reproducer (needs freelist drain — tuple freelist masks index 0):
import _testcapi, collections
live = [tuple([None, None]) for _ in range(4000)] # drain freelist
od = collections.OrderedDict(a=1)
for start in range(20):
_testcapi.set_nomemory(start, start + 1)
try:
iter(od.items())
except MemoryError:
pass
finally:
_testcapi.remove_mem_hooks()
Confirmed on main 1a852139408, Linux, Python 3.16.0a0.
Found with cpython-review-toolkit (maintained by @devdanzin and myself). added 3 commits that reference this issue on Nov 10, 2026gh-151763: Fix crash in PyList_New() on allocation failure under low …
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|