YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
onnx-tensorrt weight-converter TensorProto dims-vs-data-length OOB read (ASan PoC)
Authorized security research for the huntr.com Model File Vulnerability program. Local-only reproduction. No third-party systems are touched.
Summary
onnx/onnx-tensorrt's ONNX weight converter reads volume(shape) elements from a
TensorProto's data field without checking that the field actually contains that
many elements/bytes. The declared dims/shape is attacker-controlled and stored
separately from the actual data, so a crafted .onnx with large declared dims
but a short data field causes an out-of-bounds heap READ.
Two distinct instances, one root cause (dims-declared-count vs actual-data-length mismatch in the typed weight-conversion helpers):
INSTANCE 1 -
convertInt32Data<DataType>(WeightsContext.hpplines 134-145). Readsvolume(shape)int32 values fromTensorProto.int32_data().data()with no check thatint32_data_size() >= volume(shape). Reached fromconvertOnnxWeightsfor FLOAT16/BFLOAT16/INT8/BOOL initializers (WeightsContext.cpplines 401/404/407) whenraw_datais empty.INSTANCE 2 -
convertDouble(WeightsContext.cpplines 48-78). Readsvolume(shape)doubles with no check that the source buffer is that long. On the DOUBLEraw_datapath (WeightsContext.cppline 342) theraw_databyte length is never checked to be>= volume(shape) * sizeof(double).
Verified against onnx/onnx-tensorrt commit
7c51a63a719180eb5160c874c111746f3fb46a6b (main).
Why there is no upstream guard
convertOnnxWeights calls validateOnnxInitializer (WeightsContext.cpp
136-159) before conversion. That function validates only: data type is
supported, rank <= MAX_DIMS, and that volume * typeSize does not integer
overflow. It does NOT check that the actual data field length is >= volume(shape).
The post-conversion sanity check trt_weights.size_bytes() != nbytes
(WeightsContext.cpp 489) fires only AFTER the read loop has already run off the
end of the buffer, so it cannot prevent the OOB read.
Note: the sibling helper convertPackedInt32Data (WeightsContext.cpp 80-97)
DOES guard (nbytes > newWeightsObj.size_bytes() -> reject). The int32/double
helpers simply omit the same check.
Files
| file | what |
|---|---|
harness.cpp |
Faithful standalone ASan harness. Parses the real crafted TensorProto wire bytes and runs the convertInt32Data / convertDouble read loops VERBATIM as in upstream. |
build.sh |
protoc + clang++ -fsanitize=address build. |
onnx.proto |
ONNX proto schema (from onnx/onnx main) used to parse the crafted TensorProto. |
make_onnx.py |
Builds the crafted malicious .onnx models and raw TensorProto bytes (dims=[1024], data=4). Builds the proto directly to bypass onnx.helper client-side count validation, exactly as a real attacker crafting wire bytes would. |
make_control.py |
Builds control TensorProto bytes (dims=[4], data=4) that match. |
poc_int8_int32data_oob.onnx |
Malicious model, INT8 initializer, dims=[1024], int32_data=4. |
poc_double_rawdata_oob.onnx |
Malicious model, DOUBLE initializer, dims=[1024], raw_data=4 doubles. |
tp_int8.pb / tp_double.pb |
The raw crafted TensorProto bytes the harness consumes. |
tp_int8_control.pb / tp_double_control.pb |
Matching-length controls. |
asan_int8.log |
INSTANCE 1 ASan heap-buffer-overflow READ trace. |
asan_double.log |
INSTANCE 2 ASan heap-buffer-overflow READ trace. |
asan_control.log |
Both controls run clean (no crash). |
Harness fidelity
The harness is a faithful copy of the upstream read paths, not a re-implementation:
convertInt32Data<DataType>andconvertDoubleare copied VERBATIM (only logging removed). The loop boundvolume(shape)and the source pointer (int32_data().data()/raw_data().data()) are unchanged.volume(shape)is the product of dims, matching upstream.createTempWeightsis replaced by a heap allocation of the SAME size the real allocator produces (volume(shape) * sizeof(T)), so both the source (protobuf buffer) and destination are ASan-tracked.- The source buffer is the ACTUAL protobuf backing store of the crafted
TensorProto(ASan reports the overflow as being in the protobufRepeatedField<int>::Reserve/InlineGreedyStringParserallocation), i.e. real attacker-controlled data, parsed from the crafted wire bytes.
Full onnx-tensorrt cannot be built standalone (requires TensorRT), so the read
loops are extracted verbatim and driven by the real TensorProto structure.
Build and run
Requires clang++ with AddressSanitizer, protoc, and libprotobuf.
bash build.sh
./harness_asan int8 tp_int8.pb # INSTANCE 1 -> ASan heap-buffer-overflow READ
./harness_asan double tp_double.pb # INSTANCE 2 -> ASan heap-buffer-overflow READ
./harness_asan int8 tp_int8_control.pb # control -> clean
./harness_asan double tp_double_control.pb # control -> clean
Result
- INSTANCE 1:
AddressSanitizer: heap-buffer-overflow ... READ of size 4inconvertInt32Data<signed char>, 0 bytes after the 32-byte protobufint32_dataregion. - INSTANCE 2:
AddressSanitizer: heap-buffer-overflow ... READ of size 8inconvertDouble, 0 bytes after the 33-byte protobufraw_dataregion. - Controls (matching lengths): no crash.
Impact
An attacker who supplies a crafted .onnx (a common untrusted input - models are
routinely downloaded from public hubs and loaded by parsing pipelines) triggers
an out-of-bounds heap read during weight conversion. Consequences: crash / denial
of service, and potential disclosure of adjacent heap memory into the converted
weights buffer.
Remediation
In convertOnnxWeights / validateOnnxInitializer, before invoking
convertInt32Data / convertDouble (and the analogous typed helpers), validate
that the actual data field length is >= volume(shape):
- int32_data path: require
onnxTensor.int32_data_size() >= volume(shape). - raw_data DOUBLE path: require
onnxTensor.raw_data().size() >= volume(shape) * sizeof(double).
Reject the initializer if the check fails, mirroring the guard already present in
convertPackedInt32Data.