EnigmaConsultant's picture
Upload folder using huggingface_hub
5fa9e73 verified
|
Raw
History Blame Contribute Delete
7.39 kB

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)