Description
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 accessAfter #31091 (2.5):
cdef inline npy_timedelta get_timedelta64_value(object obj) noexcept nogil:
return _PyTimedeltaScalarObject_GetValue(obj) # -> PyArray_API[378], new in 2.5The 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
seberg commentedon Jul 1, 2026
Thanks for checking this @mroeschke, your bot isn't wrong that it should have
MinVersionthere, 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: returnPyTimedeltaScalarObject_GetValue(obj)without the underscore.kumaraditya303 commentedon Jul 1, 2026
If you expose it without an underscore it would still only make it forward compatible because numpy 2.4 does not has
PyTimedeltaScalarObject_GetValuesymbol, I think we just need a fallback static inline function for this for non-opaque builds.kumaraditya303 commentedon Jul 1, 2026
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.
seberg commentedon Jul 1, 2026
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_DATAshould be defined to what_PyArray_GET_ITEM_DATAcurrently is._PyArray_GET_ITEM_DATAcan then always the API table entry guarded withMinVersion. We could#undefit 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
#undefdance being vital for correctness, plus the above pattern is prior art with e.g.PyArray_ArrFuncs.