SurrealML (.surml) header-parser panic -> process abort (DoS)
Gated PoC repository for a huntr submission against
github.com/surrealdb/surrealml (crate surrealml-core v0.1.9, HEAD
152ac2d).
Summary
SurMlFile::from_file / SurMlFile::from_bytes (the public .surml
model-file loader in surrealml-core) calls Header::from_bytes, which in
turn calls several header-field parsers that panic (instead of returning
the Result::Err the API contracts promise) on malformed-but-well-formed-
looking header content:
storage/header/input_dims.rs::InputDims::from_string-x.parse::<i32>().unwrap()panics on non-numeric input, anddims[1]indexes out of bounds (panics) when the field contains a single number with no comma.storage/header/origin.rs::Origin::from_string-split.next().unwrap()panics when the origin field is non-empty but lacks the inner"=>"separator.storage/header/output.rs::Output::from_string-NormaliserType::from_string(data).unwrap()panics when the output field contains"=>"but the trailing value is neither"none"nor a well-formedlabel(a,b)normaliser string.
These parsers are reached directly from the public loading API with zero
sanitisation of the header string extracted from the file. Because the
real-world entry point is the C FFI function
c-wrapper/src/api/storage/load_model.rs::load_model, which is an
extern "C" fn with no catch_unwind, an unwinding panic that crosses
this FFI boundary is a "panic in a function that cannot unwind" in Rust,
which forces abort() - i.e. this is not a recoverable per-request
panic, it aborts the entire host process (confirmed below).
This is exactly the call path used by the official surrealml Python
client: SurMlFile.load(path) -> RustAdapter.load(path) ->
loader.lib.load_model(path) where loader.lib is the ctypes CDLL of
the compiled libc_wrapper.so/.dylib/.dll.
Attacker input
A 55-62 byte .surml file is enough. Format: [4-byte BE header length][header string][arbitrary model bytes]. Any application, service,
or pipeline that loads a .surml file supplied/uploaded by another party
(a shared/downloaded model, a file passed through a multi-tenant ML
pipeline, etc.) is affected.
Example header string that triggers the InputDims out-of-bounds panic:
//=>//=>//=>//=>//=>//=>//=>//=>//=>5//=>
Real evidence (unmodified upstream code)
poc_panic.rs- acargo run --exampleharness linked directly against the realsurrealml-corecrate (no source modified), usingstd::panic::catch_unwindto demonstrate 4 independent, reachable panics fromHeader::from_byteson 4 different malformed inputs. All 4 produce a real Rust panic with a real message (index-out-of-bounds /ParseIntError/unwrap on None/unwrap on Err) instead of a gracefulResult::Err.ctypes_harness.py- loads the real, release-compiledlibc_wrapper.soproduced from the unmodifiedc-wrappercrate (the exact artifact the official Python client loads) and calls its publicload_model()FFI export on each malicious.surmlfile via Pythonctypes, exactly asclients/python/surrealml/rust_adapter.pydoes in production.
Observed for all 4 crafted files (malicious_inputdims_oob.surml,
malicious_inputdims_parse.surml, malicious_origin.surml,
malicious_output.surml):
thread '<unnamed>' (...) panicked at modules/core/src/storage/header/input_dims.rs:36:33:
index out of bounds: the len is 1 but the index is 1
thread '<unnamed>' (...) panicked at .../panicking.rs:225:5:
panic in a function that cannot unwind
thread caused non-unwinding panic. aborting.
Process exit code: 134 (SIGABRT) for every case - the entire Python process embedding the library is killed, not just the current call.
Reproduce
# 1. build surrealml-core + c-wrapper from the real (unmodified) repo
cd surrealml/modules/core && cargo run --example poc_panic # see poc_panic.rs
cd surrealml && cargo build -p c-wrapper --release
# 2. craft the malicious files and drive the real compiled library
python3 build_variants.py
python3 ctypes_harness.py malicious_inputdims_oob.surml # -> SIGABRT (exit 134)
python3 ctypes_harness.py malicious_inputdims_parse.surml # -> SIGABRT (exit 134)
python3 ctypes_harness.py malicious_origin.surml # -> SIGABRT (exit 134)
python3 ctypes_harness.py malicious_output.surml # -> SIGABRT (exit 134)
Dedup note
GitHub PR #40 ("Index overflow", merged 2024-03-25) previously hardened
only the outer 4-byte length-prefix check in
SurMlFile::from_bytes/from_file (storage/surml_file.rs). It does
not touch any of the four field parsers exploited here
(input_dims.rs, origin.rs, output.rs), which remain unpatched on
current HEAD 152ac2d. No GitHub Security Advisory exists for this
repository at time of testing.
Impact
Denial of service: any process that loads an untrusted/shared/downloaded
.surml file through the official Python client (or any consumer of
libc_wrapper.so / surrealml-core::storage::surml_file::SurMlFile)
crashes outright on a handful of crafted bytes, with no way for calling
code to catch or recover from it (FFI-boundary panic forces abort()).