YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
np.load(allow_pickle=False) leaks an uncaught IndexError on a .npy/.npz header whose dtype descr is a <2-element tuple β out-of-contract exception / parsing DoS
Summary
numpy.load(), used in its safe default configuration (allow_pickle=False, no
pickle involved), raises an uncaught IndexError when parsing a crafted .npy
(or .npz) file whose header descr field is a 1-element tuple ('|u1',) or an
empty tuple (). IndexError is not in numpy's documented Raises contract for
load (OSError / UnpicklingError / ValueError / EOFError). Any consumer that follows
that contract β e.g. an ingest / deserialization path wrapping np.load in
except (OSError, ValueError, EOFError): β is not protected and crashes.
This is a header-parsing bug that fires before any array data is read and does
not require allow_pickle=True, so it is reachable on completely untrusted input
that an application deliberately loads with the safe default.
- Target: numpy β
numpy/lib/_format_impl.py - Reproduced on: numpy 2.5.1 (pip venv, Python 3.13.12) and numpy 2.3.5 (system, Python 3.13.12) β identical behavior.
- Class: out-of-contract exception / parsing denial-of-service (CWE-248 uncaught exception / CWE-20 improper input validation).
- Impact: DoS of any service/library that ingests untrusted
.npy/.npzand relies on numpy's documented exception set to sandbox parse failures.
Root cause
descr_to_dtype() in numpy/lib/_format_impl.py (lines 335β338):
elif isinstance(descr, tuple):
# subtype, will always have a shape descr[1]
dt = descr_to_dtype(descr[0])
return numpy.dtype((dt, descr[1]))
The inline comment literally asserts "subtype, will always have a shape descr[1]" β
but descr originates from the attacker-controlled .npy header, decoded via
ast.literal_eval, and is never validated to have >= 2 elements. A header whose
descr is a 1-element tuple ('|u1',) or an empty tuple () makes descr[1] raise
IndexError: tuple index out of range.
The caller, _read_array_header() (lines 693β697), guards this call with
try/except TypeError only β it deliberately converts TypeError into an
in-contract ValueError:
try:
dtype = descr_to_dtype(d['descr'])
except TypeError as e:
msg = "descr is not a valid dtype descriptor: {!r}"
raise ValueError(msg.format(d['descr'])) from e
IndexError is not a TypeError (nor a subclass of any contract exception), so it
escapes _read_array_header -> read_array -> np.load unmodified.
PoC
A 130-byte NPY v1.0 file with header:
{'descr': ('|u1',), 'fortran_order': False, 'shape': (0,), }
import numpy
numpy.load('poc_descr_tuple_indexerror.npy', allow_pickle=False) # safe default
# -> IndexError: tuple index out of range (OUT OF CONTRACT)
A smaller 66-byte variant uses descr = ().
.npz variant (the malformed member stored as arr.npy inside a zip) is a deferred
crash: NpzFile opens cleanly and reports files=['arr']; the IndexError fires
only at member access z['arr'], defeating any open-time validation.
Generator / reproducer: poc_descr_indexerror.py. PoC file:
poc_descr_tuple_indexerror.npy.
Captured evidence (verbatim)
numpy 2.5.1, Python 3.13.12
[1-tuple descr ('|u1',)] *** OUT-OF-CONTRACT *** builtins.IndexError: tuple index out of range (file 130B)
[empty-tuple descr ()] *** OUT-OF-CONTRACT *** builtins.IndexError: tuple index out of range (file 66B)
[NEG normal str '|u1'] LOADED ok -> array([], dtype=uint8)
[NEG normal subarray ('|u1',(2,))] loads / in-contract (no crash)
[NEG structured [('a','|u1')]] IN-CONTRACT ValueError: Failed to read all data for array...
Only the malformed <2-element tuple descr yields the out-of-contract IndexError.
Full traceback through np.load
File ".../numpy/lib/_npyio_impl.py", line 483, in load
return format.read_array(fid, allow_pickle=allow_pickle, ...)
File ".../numpy/lib/_format_impl.py", line 822, in read_array
shape, fortran_order, dtype = _read_array_header(fp, version, max_header_size=max_header_size)
File ".../numpy/lib/_format_impl.py", line 694, in _read_array_header
dtype = descr_to_dtype(d['descr'])
File ".../numpy/lib/_format_impl.py", line 338, in descr_to_dtype
return numpy.dtype((dt, descr[1]))
IndexError: tuple index out of range
.npz deferred behavior
NpzFile opened; files=['arr']
member access: IndexError tuple index out of range
System numpy 2.3.5, Python 3.13.12
[1-tuple descr ('|u1',)] *** OUT-OF-CONTRACT *** builtins.IndexError: tuple index out of range (same)
Suggested fix
In descr_to_dtype, validate the tuple length before indexing descr[1] and raise a
ValueError (in-contract) on a malformed descriptor; or broaden the caller's
except TypeError in _read_array_header to also catch IndexError and re-raise it
as the same ValueError("descr is not a valid dtype descriptor: ...").
Dedup note
This is distinct from other numpy .npy/.npz header findings:
- Not the
.npyshape integer-overflow DoS (craftedshape, allocation). - Not the
.npybool/shapeTypeErrorcontract bug (different field, different exception origin). - Not the
.npzBadZipFilecontract leak (zip container level). - Not the "big header" / header-size RCE-adjacent issue.
The defect here is specifically the unvalidated descr[1] index in the tuple
"subtype" branch of descr_to_dtype, producing an IndexError that numpy's own
caller does not normalize. No public CVE was found for this specific IndexError
path at time of writing.