Crash report
_interpreters.capture_exception() builds a _PyXI_excinfo via _PyXI_NewExcInfo(). Under memory pressure that allocation can fail and return NULL. The cleanup path in _interpreters_capture_exception_impl then unconditionally calls _PyXI_FreeExcInfo(info) with info == NULL. _PyXI_FreeExcInfo has no NULL guard, so _PyXI_excinfo_clear → _excinfo_clear_type dereferences &NULL->type (offset 0) at Python/crossinterp.c:1319 → SIGSEGV.
This reproduces on all build configurations (a NULL dereference, not a debug-only assert).
Reproducer
import faulthandler, _interpreters faulthandler.enable() from _testcapi import set_nomemory, remove_mem_hooks for start in range(0, 40): try: set_nomemory(start, 0) try: _interpreters.capture_exception(Exception()) finally: remove_mem_hooks() except BaseException: pass finally: try: remove_mem_hooks() except Exception: pass
Backtrace
#0 _excinfo_clear_type Python/crossinterp.c:1319 # info == 0x0 -> offset-0 deref
#1 _PyXI_excinfo_clear Python/crossinterp.c:1374
#2 _PyXI_FreeExcInfo Python/crossinterp.c:1712 # called with info == NULL, no guard
#3 _interpreters_capture_exception_impl Modules/_interpretersmodule.c:1544
#4 cfunction_vectorcall_FASTCALL_KEYWORDS Objects/methodobject.c:465
Root cause
Modules/_interpretersmodule.c, _interpreters_capture_exception_impl (≈L1522-1544):
_PyXI_excinfo *info = _PyXI_NewExcInfo(exc); /* returns NULL under OOM */ if (info == NULL) { goto finally; } /* ... */ finally: _PyXI_FreeExcInfo(info); /* L1544: runs even when info == NULL */
_PyXI_NewExcInfo (crossinterp.c:1683) does PyMem_RawCalloc for its struct and, on any failure, PyMem_RawFrees it and returns NULL — so info here is a clean NULL, not a dangling half-built struct.
_PyXI_FreeExcInfo (crossinterp.c:1710) has no NULL guard:
void _PyXI_FreeExcInfo(_PyXI_excinfo *info) { _PyXI_excinfo_clear(info); /* dereferences info */ PyMem_RawFree(info); }
Suggested fix
Make _PyXI_FreeExcInfo NULL-safe, matching the surrounding PyMem_RawFree(NULL) idiom (and Py_XDECREF, free(), etc.):
void _PyXI_FreeExcInfo(_PyXI_excinfo *info) { if (info == NULL) { return; } _PyXI_excinfo_clear(info); PyMem_RawFree(info); }
This is more defensive than guarding only the one call site, since the function is part of the cross-interpreter exception ABI and any future caller would otherwise have the same hazard.
Notes
Part of #151763 (umbrella tracking 35 OOM-related crash findings); OOM-0031 in that table.
CPython versions tested on:
CPython main branch (3.16.0a0)
Operating systems tested on:
Linux, Windows