YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
onnx-tensorrt staticSliceImporter out-of-bounds heap READ (CWE-125)
ASan-confirmed proof of concept for an out-of-bounds heap read in the
staticSliceImporter function of onnx-tensorrt when parsing a crafted .onnx
model whose Slice node has a STARTS initializer longer than its ENDS / AXES /
STEPS initializers.
- Target:
github.com/onnx/onnx-tensorrt - Pinned commit:
7c51a63a719180eb5160c874c111746f3fb46a6b(ONNX-TRT 11.1 GA, 2026-06-22) - File:
importerUtils.cpp, functionstaticSliceImporter - Class: CWE-125 Out-of-bounds Read (heap)
- Impact: heap information disclosure / crash (DoS) at model-parse time
Root cause
In staticSliceImporter (importerUtils.cpp), the loop trip count nbValues is
derived from the STARTS tensor length ONLY:
// Get int32 pointer representation of ONNX provided values
int32_t* startVals = static_cast<int32_t*>(inputs.at(1).weights().values);
int32_t* endVals = static_cast<int32_t*>(inputs.at(2).weights().values);
int32_t* axesVals = nbInputs > 3 ? static_cast<int32_t*>(inputs.at(3).weights().values) : defaultAxes.data();
int32_t* stepVals = nbInputs > 4 ? static_cast<int32_t*>(inputs.at(4).weights().values) : defaultSteps.data();
...
auto const nbValues = inputs.at(1).shape().d[0]; // STARTS length only
for (int32_t i = 0; i < nbValues; i++)
{
auto axesIndex = convertAxes(axesVals[i]); // reads axesVals[i]
...
int32_t stepSign = stepVals[i] < 0 ? -1 : 0; // reads stepVals[i]
starts.d[axesIndex] = convertStarts(startVals[i], ...);
int32_t modifiedEnds = convertEnds(endVals[i], ...); // reads endVals[i]
...
}
The loop indexes the SEPARATE endVals, axesVals, stepVals heap buffers by
the same i up to nbValues - 1. Nothing checks that ENDS / AXES / STEPS each
have at least nbValues elements. The only in-loop ONNXTRT_CHECK validates the
axis VALUE (axesIndex within nbDims), not buffer LENGTH.
Why the upstream equal-length check does NOT protect this path
DEFINE_BUILTIN_OP_IMPORTER(Slice) (onnxOpImporters.cpp) does contain
starts.size() == axes.size() and ends.size() == axes.size() checks, but they
sit AFTER the static fast-path early return:
if (isInt32 && isWeightsOrEmpty(1) && isWeightsOrEmpty(2) && isWeightsOrEmpty(3)
&& isWeightsOrEmpty(4) && !isDynamic(data.getDimensions()))
{
return staticSliceImporter(ctx, node, nodeIdx, inputs, data); // returns BEFORE the size checks
}
... ONNXTRT_CHECK_NODE(starts.size() == axes.size(), ...) // never reached on the static path
So when all of starts/ends/axes/steps are INT32 constant initializers and the
data tensor has a static shape, execution enters staticSliceImporter with NO
equal-length guard. The vulnerability is reachable via a plain crafted model.
Files
harness.cpp- faithful standalone ASan harness copying the vulnerable loop verbatimrun.sh- builds with clang ASan, runs control (clean) then vulnerablecraft_onnx.py- generatesslice_oob_read.onnxslice_oob_read.onnx- crafted model: STARTS len 64, ENDS/AXES/STEPS len 1asan.log- captured AddressSanitizer heap-buffer-overflow READcontrol.out- control run (equal-length buffers) exits cleanly
Reproduce
# clang + AddressSanitizer required
bash run.sh
Control run (starts=ends=axes=steps len 4) completes cleanly, exit 0. Vulnerable run (starts len 64, ends/axes/steps len 1) aborts with:
==ERROR: AddressSanitizer: heap-buffer-overflow ... READ of size 4 ...
#0 ... in main harness.cpp:117 (convertAxes(axesVals[i]))
0x... is located 0 bytes after 4-byte region [...] (the 1-element axes buffer)
SUMMARY: AddressSanitizer: heap-buffer-overflow ... in main
The read fires the moment i exceeds the length of the short ends/axes/steps
buffer while nbValues (from STARTS) keeps the loop going.
The crafted model
slice_oob_read.onnx is a single-node graph:
- input
data: FLOAT [8,8,8,8] (static shape ->!isDynamic) - Slice node, opset 13, inputs data/starts/ends/axes/steps
- initializers (all INT32 ->
isInt32fast path):startsshape [64]endsshape [1]axesshape [1]stepsshape [1]
Loading this model with onnx-tensorrt takes the static fast path into
staticSliceImporter, which loops 64 times over 1-element ends/axes/steps
buffers -> OOB heap read.
Remediation
In staticSliceImporter, before the loop, require that STARTS, ENDS, AXES and
STEPS all have at least nbValues elements (equivalently, validate equal element
counts), e.g.:
auto const nbValues = inputs.at(1).shape().d[0];
ONNXTRT_CHECK_NODE(inputs.at(2).shape().d[0] == nbValues
&& (nbInputs <= 3 || inputs.at(3).shape().d[0] == nbValues)
&& (nbInputs <= 4 || inputs.at(4).shape().d[0] == nbValues),
"Slice starts/ends/axes/steps must have equal length.",
node, nodeIdx, ErrorCode::kINVALID_NODE);
Alternatively, move the existing starts.size() == axes.size() /
ends.size() == axes.size() checks so they run BEFORE the static fast-path
return.
Note on the harness
Building the full onnx-tensorrt importer requires TensorRT, which is not
redistributable. This harness copies the staticSliceImporter loop and its
helper lambdas VERBATIM and drives them with heap buffers sized exactly to the
ONNX initializer lengths (as TensorRT sizes weights().values), so the OOB read
is the same one the real importer performs on the crafted model.