YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
burn-onnx / onnx-ir uncapped external_data length -> DoS (crash)
Target: tracel-ai/burn-onnx (crates.io onnx-ir + burn-onnx), commit
727753694198fe8daca60969cdfb8a3a4e8ad66a, tag v0.21.0.
Binary under test: onnx2burn (crates/burn-onnx/src/bin/onnx2burn.rs),
built with default features (mmap feature ON, matching upstream defaults
for both onnx-ir and burn-onnx).
Sink
crates/onnx-ir/src/tensor_store.rs
fn load_external_read(file_path: &PathBuf, offset: u64, length: u64) -> Vec<u8> {
let mut file = File::open(file_path).unwrap_or_else(|e| { panic!(...) });
file.seek(SeekFrom::Start(offset)).unwrap_or_else(|e| { panic!(...) });
let mut buffer = vec![0u8; length as usize]; // <-- L185: no upper bound on `length`
file.read_exact(&mut buffer).unwrap_or_else(|e| { // <-- L186-193: panics on short/EOF read
panic!("Failed to read {} bytes from '{}': {}", length, file_path.display(), e)
});
buffer
}
length originates from the ONNX file's TensorProto.external_data
"length" key (crates/onnx-ir/src/external_data.rs:76-82), parsed with a
bare value.parse::<u64>() β no cap, no comparison against the actual
external file size at parse time.
crates/onnx-ir/src/tensor_store.rs::load_external_data (L119-134) tries
load_external_mmap first (when the mmap feature is on, which is the
default); if the declared offset+length exceeds the real mmap'd file size,
load_external_mmap returns Err(UnexpectedEof) and the dispatcher
silently falls through to the unguarded load_external_read above.
Materialization is lazy (TensorDataRef::to_tensor_data) but is inevitably
triggered during normal onnx2burn codegen: any node that consumes the
initializer as a constant (e.g. type inference on a Constant/weight
argument via Argument::value() -> ValueStore::get_tensor_data() ->
TensorStore::get() -> to_tensor_data()) forces the load. A model that
uses the initializer in an actual op (e.g. MatMul) guarantees this path
is hit; no dead-code elimination shortcut avoids it.
Two observed crash variants, same root cause, same line (tensor_store.rs L185)
- Allocator abort (
length= 100 GiB,poc_variant_a/): thevec![0u8; length as usize]allocation itself fails and Rust's global allocator callshandle_alloc_error, which aborts the process (memory allocation of 107374182400 bytes failed, SIGABRT, exit 134). This is not catchable viacatch_unwindβ process death is immediate. - Unwinding panic on short read (
length= 512 MiB,poc_variant_a_moderate/): the 512 MiB allocation succeeds, butread_exactfrom the real (1-byte) file hits EOF and theunwrap_or_elseexplicitly panics:panicked at crates/onnx-ir/src/tensor_store.rs:187:13: Failed to read 536870912 bytes from '.../w.bin': failed to fill whole buffer(exit 101 β Rust's standard uncaught-panic exit code).
Either way, a 1-byte external-data file plus a 160-byte crafted .onnx
is enough to kill the onnx2burn process outright β no large payload, no
disk/transfer amplification required, purely from a length field the
parser never validates against reality.
Reproduction
cd /home/wey/fuzz/burn/repo
cargo build -p burn-onnx --bin onnx2burn # default features (mmap on)
# Variant A (lead): allocator-abort DoS from a 160-byte model + 1-byte file
RUST_BACKTRACE=full ./target/debug/onnx2burn \
/home/wey/fuzz/burn/poc/poc_variant_a/model.onnx \
/home/wey/fuzz/burn/poc/poc_variant_a/out
# => "memory allocation of 107374182400 bytes failed", SIGABRT, exit 134
# Backtrace confirms: load_external_read (tensor_store.rs:185) <-
# load_external_data <- to_tensor_data <- TensorStore::get <-
# ValueStore::get_tensor_data <- Argument::value <- ConstantProcessor::infer_types
# Variant A-moderate: catchable-panic DoS (smaller declared length)
./target/debug/onnx2burn \
/home/wey/fuzz/burn/poc/poc_variant_a_moderate/model.onnx \
/home/wey/fuzz/burn/poc/poc_variant_a_moderate/out
# => panicked at crates/onnx-ir/src/tensor_store.rs:187:13, exit 101
# Negative control: correct length (64 bytes, matches real 64-byte weight
# file) -> loads and codegens successfully, exit 0
./target/debug/onnx2burn \
/home/wey/fuzz/burn/poc/control_valid/model.onnx \
/home/wey/fuzz/burn/poc/control_valid/out
# => "Model generated", "Finished converting ONNX to Burn", exit 0
Files
craft_onnx.pyβ dependency-free protobuf encoder that builds a minimal valid ONNX graph (X[1,4] --MatMul(weight[4,4])--> Y[1,4]) with theweightinitializer'sexternal_datalengthfield fully attacker-controlled. Noonnx/protobufPython package required.poc_variant_a/{model.onnx,w.bin,run.log}β 100 GiB declared length, 1-byte real file -> allocator abort (SIGABRT).poc_variant_a_moderate/{model.onnx,w.bin,run.log}β 512 MiB declared length, 1-byte real file ->read_exactpanic (unwinding, exit 101).control_valid/{model.onnx,w.bin,out/}β negative control, correctly sized external data (64 bytes), loads and generates code successfully.
Dedup note
onnx-ir is a from-scratch Rust protobuf/ONNX parser (not a wrapper around
upstream onnx/onnxruntime). CVE-2025-51480 is a path-traversal bug in
the upstream Python onnx package's model writer β a different codebase.
The equivalent path-traversal class of bug is already hardened here (see
crates/onnx-ir/src/external_data.rs header comment + tests: null-byte,
absolute-path, ..-traversal, and canonicalize+starts_with checks are all
present and passing). This finding is the separate, still-open "length is
unchecked against reality" issue in the same external-data loader.