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`:
```cpp
// 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).
```bash
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.