OpenVINO IR shape_size() integer-overflow sanity-check bypass PoC
Proof-of-concept files for a huntr Model Format Vulnerability report against openvinotoolkit/openvino (IR model loader).
evil.xml/evil.bin - a minimal, valid OpenVINO IR pair with a single
Const layer declaring shape="4194304,4194304,4194304" (three dimensions
of 2^22 each; true element product = 2^66, an exact multiple of 2^64) and
size="16" matched by a real 16-byte .bin file.
shape_size() (src/core/include/openvino/core/shape.hpp:87) computes the
element count via std::accumulate/std::multiplies with no overflow
check, in an int64_t accumulator later cast to size_t. Because the true
product is an exact multiple of 2^64, the overflow wraps to exactly 0 -
which matches the tiny 16-byte buffer's byte count and lets the sanity check
in set_constant_num_buffer (xml_deserialize_util.cpp:880-918) pass
silently, even though the declared shape and the real backing buffer differ
by dozens of orders of magnitude.
Verified live (openvino 2026.2.1-21919, pip wheel, no C++ rebuild):
import openvino as ov
core = ov.Core()
model = core.read_model("evil.xml") # loads WITHOUT any exception
const = next(n for n in model.get_ops() if n.get_type_name() == "Constant")
const.get_output_shape(0) # -> [4194304, 4194304, 4194304] (full attacker control)
const.get_byte_size() # -> 0 (same overflow, wrapped independently)
Control experiment (proving the guard is real and specifically bypassed by
overflow, not just generally absent): a non-overflowing mismatched shape
("100,100", still with size="16", true product 10,000 elements =
40,000 bytes vs 16 real bytes) correctly raises
"Empty weights data in bin file or bin file cannot be found!" at the exact
same check (xml_deserialize_util.cpp:881) - confirming the sanity check is
active and effective for ordinary mismatches, and only silently bypassed
when the true product wraps around 2^64.
Reachable via the public ov.Core().read_model() API - the standard,
documented way to load any OpenVINO IR model, hitting essentially every
model with a numeric Const layer.
Honest limit: a crash/OOB-read via Python bindings specifically
(Constant.get_data()/get_vector()) was not achieved - both of those
particular accessors happen to avoid the mine via their own internal
implementation quirks (numpy's own pre-allocation size check; get_vector()
self-consistently reusing the same wrapped-to-0 byte count). This does not
mean the underlying state mismatch is safe - it means the real danger lies
in code paths that trust get_shape()/get_output_shape() to size a raw
read directly (constant folding, evaluate(), internal transformations),
which were not exercised in this PoC.
Impact: an attacker who can supply or influence an OpenVINO IR (.xml/.bin
pair) can make the loader silently accept a Constant node with a
declared shape/byte-size state that is internally inconsistent by many
orders of magnitude - a sanity-check bypass that downstream code relying on
get_shape() to size reads could turn into an out-of-bounds read, not just
a denial of service.