Skip to content

Segfault: NULL info deref in _excinfo_clear_type via _PyXI_FreeExcInfo(NULL) (Python/crossinterp.c:1319) #151842

Description

@amruthamodela06

Crash report

Segfault: NULL info deref in _excinfo_clear_type via _PyXI_FreeExcInfo(NULL)

_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

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No fields configured for issues without a type.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions