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](https://github.com/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>>&)`:
```cpp
// 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`)