Skip to content

BUG: extensions built against NumPy 2.5 segfault at runtime against NumPy 2.4 because get_(datetime|timedelta)64_value/_unit route through unguarded 2.5 C-API slots #31814

Closed

Description

@mroeschke

Describe the issue:

In pandas, we released a version built with NumPy 2.5 and users experienced a segfault when run with NumPy <= 2.4 e.g. pandas-dev/pandas#66086.

The following agent analysis identified that #31091 updated get_datetime64_value, get_timedelta64_value and get_datetime64_unit to call _PyDatetimeScalarObject_GetValue, _PyTimedeltaScalarObject_GetValue and _PyDatetimeScalarObject_GetMetadata respectively, and with NumPy <=2.4 the PyArray_API table is shorter so accessing these new implementations will segfault


Commit 15a344d changed the Cython inline helpers in numpy/__init__.cython-30.pxd from direct struct-field access to calls through brand-new C-API table slots, and registered those slots without a MinVersion("2.5") guard. As a result, any extension that cimport numpy and calls these helpers while targeting an older NPY_FEATURE_VERSION builds a binary that calls PyArray_API[378]. Run against NumPy ≤ 2.4 (where the table is shorter), this reads past the end of the API table → garbage function pointer → hard crash (SIGBUS / SIGSEGV), with no Python-level traceback.

numpy/__init__.cython-30.pxd before #31091 (≤ 2.4):

cdef inline npy_timedelta get_timedelta64_value(object obj) noexcept nogil:
    return (<PyTimedeltaScalarObject*>obj).obval        # version-agnostic struct access

After #31091 (2.5):

cdef inline npy_timedelta get_timedelta64_value(object obj) noexcept nogil:
    return _PyTimedeltaScalarObject_GetValue(obj)       # -> PyArray_API[378], new in 2.5

The new slots in numpy/_core/code_generators/numpy_api.py:

'_PyDatetimeScalarObject_GetMetadata':  (375,),   # note: no MinVersion
'_PyTimedeltaScalarObject_GetMetadata': (376,),
'_PyDatetimeScalarObject_GetValue':     (377,),
'_PyTimedeltaScalarObject_GetValue':    (378,),

Reproduce the code example:

(A minimal, AI generated example)

# td_repro.pyx
# cython: language_level=3
cimport numpy as cnp

cnp.import_array()

def td_value(obj):
    """Return the int64 underlying a numpy.timedelta64 scalar."""
    return cnp.get_timedelta64_value(obj)

def dt_value(obj):
    """Return the int64 underlying a numpy.datetime64 scalar."""
    return cnp.get_datetime64_value(obj)

# setup.py
from setuptools import Extension, setup
import numpy as np
from Cython.Build import cythonize

ext = Extension(
    "td_repro",
    ["td_repro.pyx"],
    include_dirs=[np.get_include()],
    define_macros=[
        ("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION"),
        ("NPY_TARGET_VERSION", "NPY_1_21_API_VERSION"),
    ],
)
setup(name="td_repro", ext_modules=cythonize([ext], language_level="3"))

# run.py
import numpy as np
import td_repro
td = np.timedelta64(28, "D")
print("timedelta value:", td_repro.td_value(td))
dt = np.datetime64("2020-01-01", "ns")
print("datetime value:", td_repro.dt_value(dt))

Error message:

(Run with NumPy 2.4)

Current thread's C stack trace (most recent call first):
  Binary file ".../bin/python3.14", at _Py_DumpStack+0x48 [0x10282ba54]
  Binary file ".../bin/python3.14", at faulthandler_dump_c_stack+0x58 [0x102840914]
  Binary file ".../bin/python3.14", at faulthandler_fatal_error+0x17c [0x102840854]
  Binary file "/usr/lib/system/libsystem_platform.dylib", at _sigtramp+0x38 [0x185463744]
  Binary file ".../td_repro.cpython-314-darwin.so", at __pyx_pw_8td_repro_1td_value+0x29c [0x102e5f2c8]
  Binary file ".../bin/python3.14", at _TAIL_CALL_CALL+0x13c [0x10276e390]
  Binary file ".../bin/python3.14", at _PyEval_Vector+0x2c8 [0x102765584]
  Binary file ".../bin/python3.14", at PyEval_EvalCode+0xb0 [0x1027651fc]
  Binary file ".../bin/python3.14", at run_mod+0xf0 [0x10280daac]
  Binary file ".../bin/python3.14", at pyrun_file+0x90 [0x10280d84c]
  Binary file ".../bin/python3.14", at _PyRun_SimpleFileObject+0x100 [0x10280d280]
  Binary file ".../bin/python3.14", at _PyRun_AnyFileObject+0x50 [0x10280cbc4]
  Binary file ".../bin/python3.14", at pymain_run_file_obj+0xb4 [0x10283d500]
  Binary file ".../bin/python3.14", at pymain_run_file+0x48 [0x10283cc84]
  Binary file ".../bin/python3.14", at Py_RunMain+0x634 [0x10283c420]
  Binary file ".../bin/python3.14", at pymain_main+0x1e0 [0x10283d820]
  Binary file ".../bin/python3.14", at main+0x3c [0x102572634]
  Binary file "/usr/lib/dyld", at start+0x1b50 [0x18509be00]
Extension modules: numpy._core._multiarray_umath, numpy.linalg._umath_linalg, td_repro (total: 3)

Python and NumPy Versions:

NumPy 2.5

Runtime Environment:

No response

How does this issue affect you or how did you find it:

No response

Activity

  1. seberg commented on Jul 1, 2026

    @seberg
    Member

    Thanks for checking this @mroeschke, your bot isn't wrong that it should have MinVersion there, but that would only get you so far: a compilation error rather than a segfault.
    The real issue is that return _PyTimedeltaScalarObject_GetValue(obj) is just wrong, it has to be: return PyTimedeltaScalarObject_GetValue(obj) without the underscore.

  2. added this to the 2.5.1 release milestone on Jul 1, 2026
  3. kumaraditya303 commented on Jul 1, 2026

    @kumaraditya303
    Contributor

    The real issue is that return _PyTimedeltaScalarObject_GetValue(obj) is just wrong, it has to be: return PyTimedeltaScalarObject_GetValue(obj) without the underscore.

    If you expose it without an underscore it would still only make it forward compatible because numpy 2.4 does not has PyTimedeltaScalarObject_GetValue symbol, I think we just need a fallback static inline function for this for non-opaque builds.

  4. kumaraditya303 commented on Jul 1, 2026

    @kumaraditya303
    Contributor

    I created #31821 to fix this. I was not aware that numpy headers are supposed to be backwards compatible as well as forward based on my reading of https://numpy.org/doc/stable/dev/depending_on_numpy.html#understanding-numpy-s-versioning-and-api-abi-stability earlier, it would nice to clarify it there. Sorry about the breakage though, I should've caught this earlier.

    Separately, to avoid such breakage in future I'd add some ABI tests for numpy where we actually test for this in CI, working on that next.

  5. seberg commented on Jul 1, 2026

    @seberg
    Member

    If you expose it without an underscore it would still only make it forward compatible because numpy 2.4 does not has

    Ah I am came from expecting to already have non-underscored stable API. And I am a bit shocked that this isn't the case, the way this should be designed is:

    • PyArray_GET_ITEM_DATA should be defined to what _PyArray_GET_ITEM_DATA currently is.
    • _PyArray_GET_ITEM_DATA can then always the API table entry guarded with MinVersion. We could #undef it on build that don't need it just to avoid accidental usage, I guess.

    You could put it into the table without an underscore, but I personally don't like this #undef dance being vital for correctness, plus the above pattern is prior art with e.g. PyArray_ArrFuncs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions