Socair reads an OpenVINO IR model, an .xml that describes the graph and the
.bin that holds its weights, on its own or inside a model directory, such as
the openvino/ exports that sentence-transformers repositories ship beside
their weights and the exports the OpenVINO organization publishes. The Format
and structure row checks the graph's operations and every byte range the
.xml names in the .bin; the File inventory row scans the .xml's text for
payloads. This page says what each row reads, how it grades, and what it does
not cover.
An IR holds no code a loader executes. What a runtime executes are
operations: OpenVINO's own, in its opsets, and operations from an extension
library the deployment registers (openvino_tokenizers is one). That library
is outside the model, and outside this scan.
How the model is read
The reference is OpenVINO 2026.4.1 (commit e213a1472576): its IR frontend,
src/frontends/ir/src/frontend.cpp
and input_model.cpp,
and the deserializer it reads a model with,
src/core/xml_util/src/xml_deserialize_util.cpp.
The format is described in
OpenVINO IR format:
<layer> elements for operations, <edge> elements for the data flow, and a
Const layer, "the only operation ... that refers to the IR binary file", whose
<data> gives its element_type, shape, and the offset and size of its
bytes in the .bin.
internal/openvino reads the .xml with Go's encoding/xml, streaming, and
keeps what the deserializer would build:
- The IR. A file is an IR by its bytes, never its name: an XML document
whose root element is
net, in any case, as the frontend identifies one (get_ir_version). OpenVINO 2026.4.1 reads IR versions 10 and 11 (the root'sversion;FrontEnd::supported_impl); any other version is NOT_TESTED. In a model directory, every.xmlwhose root isnetis read. - The
.bin. OpenVINO reads the.xml's path with its extension replaced by.bin(FrontEnd::load_impl), soopenvino/openvino_model.xmlreadsopenvino/openvino_model.bin. Socair resolves it the same way, inside the scan's snapshot, and reads only its size. - Which layers are built. The deserializer builds every
Parameterand every layer aResultor anAssigndepends on through the edges, found from the outputs back (XmlDeserializer::parse_function); a later layer with an id replaces an earlier one. A layer no output depends on is never created: its type is not looked up and itsConstis not read. Socair follows this, and a layer OpenVINO does not build is NOT_TESTED, named. A builtTensorIteratororLoopbuilds its<body>, and a builtIfits<then_body>and<else_body>, the same way; a body under any other layer is not read. - Operations. A layer's
versionnames an opset and itstypean operation in it, looked up ignoring ASCII case (OpSet::create_insensitive;Constis read asConstant). OpenVINO 2026.4.1 knowsopset1toopset17(ov::get_available_opsets). Two redirects come from the deserializer: eight older operations (GRUCell,RNNCell,Proposal, and the fiveExperimentalDetectron*operations) with versionexperimentalorextensionare built fromopset6, andMVN,ROIPooling, andReorgYolonamedopset1fromopset2. The tables are embedded ininternal/openvino/opsets.txt, written byinternal/openvino/opsetgenfromsrc/core/include/openvino/opsets/opset<N>_tbl.hppat 2026.4.1, and were checked against OpenVINO 2026.4.1 itself: for each of the 17 opsets and each of the 207 operation names the tables list (besidesParameterandResult) and one name none lists, 3,536 pairs, an IR naming it was read, and OpenVINO said the opset lacks the operation exactly where the table does, apart from the threeopset1redirects. - Text. Every element and attribute name, attribute value, character data, comment, processing instruction, and declaration, each distinct value once, for the inventory.
What the XML parser does
OpenVINO parses the XML with pugixml. Where pugixml reads a document differently from a conforming XML parser, Socair follows pugixml and grades the difference:
| What the XML holds | What OpenVINO does | Grade |
|---|---|---|
| An attribute repeated on one element | pugixml reads the first; XML forbids it, and a conforming parser refuses the file | FAIL, duplicate-key |
| A layer id given to two layers of one graph | builds the later layer and drops the other | FAIL, duplicate-key |
An element OpenVINO reads once, repeated (a second <data> in a layer, a second <layers> in the net) |
reads the first and ignores the rest | NOT_TESTED, named |
An element where the IR has none (<statistics> in the net), a <pre-process> block (mean values read from the .bin), a DOCTYPE |
ignores it, or reads what this reader does not model | NOT_TESTED, named |
A layer typed Constant (not Const) |
builds the constant from the XML's value attribute, not the .bin |
NOT_TESTED, named |
A Const whose <data> has a value attribute |
builds the constant from that text and reads neither its offset nor its size, so the range they name in the .bin is never read (the deserializer's buffer adapter takes value first) |
NOT_TESTED, named; its range does not count as read |
A missing mandatory attribute (a layer's id, name, type, version; an edge's ports), or an edge from a layer the graph does not define |
refuses the model | NOT_TESTED, named |
| Malformed XML, an entity reference other than XML's own, a declared encoding other than UTF-8, a second root, text outside the root | refuses it, or reads it differently | NOT_TESTED (unparseable) |
Numbers are read only when they are plain decimal digits: OpenVINO reads
offset and size with std::stoll, which also takes a sign, leading white
space, and -0, and a shape's dimensions with std::from_chars, which stops at
the first non-digit (OpenVINO 2026.4.1 reads a shape of 4x as [4]). A value
Socair would read differently is NOT_TESTED, never guessed. A shape is split at
commas and each part trimmed of white space, as str_to_container does, so
the serializer's "30522, 384" reads as [30522, 384].
encoding/xml processes no DTD and resolves no external entity: a DOCTYPE is
recorded and skipped, and an entity it declares is an error where it is used.
There is no XML external-entity or entity-expansion path.
Bounds: a file larger than 256 MiB, elements nested more than 256 deep, more
than 2^23 elements, 4,096 attributes on one element, 16 MiB in one attribute
value, text, or comment, 2^20 layers, 2^22 edges, or 2^16 graphs. Past any of
these the IR is NOT_TESTED. Text past 64 MiB or 2^20 distinct values is not
kept, which leaves the File inventory row NOT_TESTED. OpenVINO sets none of
these limits. For scale: the largest of the 46 IRs measured below,
OpenVINO/phi-2-int8-ov's model, is a 3.3 MB XML, and the language model of
OpenVINO/Qwen3.8-27B-int4-ov is 9.7 MB (the hub's listing on 2026-10-14).
The reader is fuzzed by FuzzRead (internal/openvino), which CI's fuzz job
runs with the other parsers.
Format and structure
Const sizes
Every built Const must be exactly the size OpenVINO allocates for its
element type and shape: ov::util::get_memory_size
(src/core/src/memory_util.cpp),
which the IR writer writes as size:
- byte types:
bits / 8 × nbytes; u1,u2, and the 4-bit types (u4,i4,nf4,f4e2m1):8 / bitselements to a byte, rounded up;u3andu6:24 / bitselements to each 3-byte group, rounded up to whole groups.
An empty Const (an element count of 0) may also be 1 byte: OpenVINO's
AlignedBuffer allocates at least one byte
(aligned_buffer.cpp),
and the writer writes the buffer's size. One real detokenizer has one.
The element types are the names of element::Type_t, which
Constant::visit_attributes reads the attribute through, so FP32 and the
other aliases are refused (checked in OpenVINO 2026.4.1).
| Const | What OpenVINO does | Grade |
|---|---|---|
size short of ceil(n × bits / 8) |
refuses it ("Attribute and shape size are inconsistent") | FAIL, tensor-size |
a u3 or u6 Const at least ceil(n × bits / 8) but short of whole 3-byte groups |
reads it | FAIL, tensor-size |
size larger than its type and shape take |
loads it, with the extra bytes in the constant's buffer; no operation reads them as values, and an export holds none (OpenVINO allocates a Constant at exactly that size, and its writer writes the buffer as it is) | FAIL, tensor-size |
| an element type OpenVINO does not define, a missing attribute, a shape or range this reader does not read as OpenVINO does, or a shape whose size overflows | refuses it, or reads it differently | NOT_TESTED |
dynamic element type |
holds no defined data | NOT_TESTED |
string element type |
a packed string tensor, whose size depends on its strings; it is not read | NOT_TESTED |
The size rule needs no .bin, so it holds in a single-file scan too. Why a
larger size is a FAIL though OpenVINO loads it: it is the ONNX rule for
raw_data longer than its shape (onnx.md), the bytes are in the
model and read as no value, and an OpenVINO export does not hold them. Measured on 46 real
IRs (below), no Const is larger than its type and shape take except the one
empty Const above.
The .bin
| Finding | What OpenVINO does | Grade |
|---|---|---|
a Const's range runs past the end of the .bin, or starts past it |
refuses the model ("Incorrect weights in bin file!", is_valid_weights_range) |
NOT_TESTED: a truncated download or a different file is as likely as tampering, as for ONNX external data that runs past its file |
the .bin is missing (and the model has a Const) |
refuses the model ("Empty weights data in bin file or bin file cannot be found!") | NOT_TESTED |
bytes of the .bin outside every built Const's range: a gap, or a tail |
never reads them | NOT_TESTED, naming the ranges |
| two Const ranges that partly overlap | reads both | NOT_TESTED, naming them |
| two Consts with the same range | reads both | not graded: the writer gives a Const whose bytes and size match an earlier one's the same range (ConstantWriter::write), and real exports share hundreds |
a single-file scan of the .xml |
NOT_TESTED: the .bin is outside the scan; scan the model's directory |
Why bytes outside every Const are NOT_TESTED and not a FAIL: OpenVINO loads
the model, the IR format does not require the Consts to tile the file, and
those bytes are simply never read, so what they hold is not established. The
writer appends each Const's bytes in turn with no padding, and every one of
the 46 real IRs measured (written by OpenVINO 2022.3 to 2026.4) covers its
.bin exactly, so a gap means the IR is not as OpenVINO's writer writes one. The
row names the ranges, and an acceptor who accepts the gap accepts those bytes
unread. The openvino-bin-tail benchmark case pins this. A partial overlap
is NOT_TESTED for the same reason: OpenVINO reads it, and the writer never
produces one.
Operations
| A built layer names | Grade |
|---|---|
| an operation in one of OpenVINO's opsets, as the reader looks it up (above) | PASS; the notes count the operation types per opset |
any other version or type (extension, an unknown opset, an operation its opset lacks) |
NOT_TESTED, naming the version and its types: a runtime builds it only from an extension library the deployment registers, native code outside the model |
Why an extension operation is NOT_TESTED and not a LEAD, as ONNX's rules
grade a custom operator domain (onnx.md): nothing in the model makes a runtime
load code. OpenVINO refuses the model unless the deployment registers an
extension for that type and version, so the model needs native code outside
the attestation; that is a gap the acceptor can accept by naming the library
they deploy. Measured: every model IR in the corpus uses only OpenVINO's own
operations, and every tokenizer and detokenizer IR uses extension
operations from openvino_tokenizers (BPETokenizer, RegexSplit,
VocabDecoder, CharsMapNormalization, and others; its rt_info names
openvino_tokenizers_version). OpenVINO 2026.4.1 refuses each of those
detokenizers without the extension ("Cannot create VocabDecoder layer ...
from unsupported opset: extension"). A deployment registers an extension for
a standard operation too (the deserializer looks extensions up first); that
is outside the model.
PASS
The row PASSes only when the IR parsed with nothing unread, every layer is
built, every built Const is exactly its type and shape's size and lies
inside the .bin, the Consts cover the .bin with no byte outside them and
none partly overlapping, and every built operation is in OpenVINO's opsets.
Its notes give the IR version, the layer and graph counts, the opsets used,
and the Consts and their distinct ranges in the .bin, and say what it does
not cover: tensor values, and the native code a runtime runs for an
operation.
File inventory and payloads
The .xml's text goes through the same payload patterns as GGUF and
safetensors metadata (scripts, long base64 blobs, executable and archive
signatures): every attribute value (each layer's <data>, rt_info at the
model, layer, and port level, legacy <meta_data>), every element and
attribute name (an rt_info key is an element name), character data, and
comments. Unlike ONNX, names are scanned too: in an IR every value is text,
and on 46 real IRs (about 1.2 million attribute values and texts) no pattern
matched. Text past the reader's caps leaves the row NOT_TESTED. The .bin
holds tensor data and is not scanned for payloads; its bytes outside every
Const are graded by the structure row.
The Pickle opcode scan
A .bin is also PyTorch's extension, so in a model directory a .bin went
to the Pickle opcode scan by its name and was NOT_TESTED there ("not a pickle
opcode stream"), which withheld every repository with an openvino/ export.
Now a .bin that an IR beside it names as its weights (the same path as an
.xml whose root is net), and whose bytes hold nothing the pickle check
reads, is read through that .xml and does not reach the pickle row.
"Holds nothing the pickle check reads" (pickle.Holds) means: not a zip or
tar archive, not a NumPy array, and not a stream that walks as a pickle of
any protocol from its first byte (one that completes a pickle, or imports a
global, within its first MiB). That is wider than the test every file gets
by its bytes (pickle.Sniff, which claims only protocols 2 to 5 and zips
holding a pickle, since it applies to files of any name), because the name
matters here: a loader that reads pytorch_model.bin by its name as a torch
checkpoint unpickles a protocol 0 pickle there, or the pickle in a legacy
tar, whatever .xml sits beside it, and an IR whose u8 Const covers that
file is otherwise well formed. So a .bin that holds any of these goes to the pickle row by its
name, as before, and the .xml beside it is still read as an IR. The
openvino-bin-pickle benchmark case pins this. Raw tensor data that happens
to walk as a pickle (a first byte that is a push opcode, then a STOP) goes to
the pickle row too, as every .bin did before; none of the 46 measured does.
A .bin with no IR beside it, pytorch_model.bin included, is handled as
before, and so is a .bin scanned on its own: raw tensor data says nothing
about the IR it belongs to.
Not covered
- Tensor values: what the weights compute.
- Whether OpenVINO loads the model: ports, shapes, attribute values other than a Const's, and cycles in the graph are not checked. A model OpenVINO refuses runs nothing.
- Extension libraries (
openvino_tokenizersand others), and the runtime and its plugins. - A string Const's packed strings, and a
<pre-process>block. - The
chat_templatethatopenvino_tokenizerswrites into a tokenizer IR'srt_info, which OpenVINO GenAI applies: the File inventory row scans it with the payload patterns, but the Chat template row reads the templates intokenizer_config.json,chat_template.json, and.jinjafiles, not this copy, and does not compare the two. That is a follow-up. - The intake policy's formats (
SOCAIR_INTAKE_POLICY) do not include OpenVINO IR, as they do not include ONNX. - A model compiled for a device (
ov::CompiledModel::export_model, a blob cache) is not an IR and is not read.
Sources
- OpenVINO at 2026.4.1
(commit
e213a1472576):src/frontends/ir/src/frontend.cpp(get_ir_version,supported_impl,load_impl),input_model.cpp(parse_pre_process,InputModelIRImpl),src/core/xml_util/src/xml_deserialize_util.cpp(parse_function,create_node,set_constant_num_buffer,is_valid_weights_range,str_to_container),src/core/xml_util/src/weights_provider.cpp(make_region),src/common/util/src/xml_parse_utils.cpp(get_uint64_attr),src/common/util/include/openvino/util/common_util.hpp(trim,view_to_number),src/core/src/opsets/opset.cppandsrc/core/include/openvino/opsets/opset<N>_tbl.hpp,src/core/src/type/element_type.cpp,src/core/src/memory_util.cpp,src/core/dev_api/openvino/core/type/element_iterator.hpp,src/core/src/runtime/aligned_buffer.cpp,src/core/src/op/constant.cpp,src/core/src/runtime/string_aligned_buffer.cpp, and the writer,src/core/src/xml_util/xml_serialize_util.cppandconstant_writer.cpp. - The format description,
docs/articles_en/documentation/openvino-ir-format.rst, and the opsets it lists. - 2024.6.0's
src/frontends/ir/src/ir_deserializer.cppand 2026.0.0'sxml_deserialize_util.cpp, which make the same range and size checks.
What each grade says OpenVINO does was confirmed by reading defanged IRs with
the openvino 2026.4.1 Python package (Core.read_model, no compile, no
inference) on 2026-10-14: it refuses a range past the .bin, a size short of
its shape, a missing .bin, an operation its opset lacks, an unknown opset,
an extension operation with no extension registered, an alias element type,
and IR versions 7 and 12; it loads a larger size, bytes after the last Const,
a partly overlapping range, a layer no output depends on (even of an unknown
type), a repeated attribute (the first wins), a repeated layer id (the last
wins), a repeated <data> or <layers> (the first wins), a root spelled
NET, an unknown element, a DOCTYPE, a shape of 4x (as [4]), and a
Const whose <data> has a value attribute, whose data it takes from that
text even when its range runs past the .bin or there is no .bin at all.
2024.6.0's deserializer takes a value attribute first too.