OpenVINO Version
2026.5.0-23099-d015add94c1
Operating System
Other (Please specify in description)
Device used for inference
GPU
Framework
None
Model used
OpenVINO/gemma-4-26b-a4b-it-int4-ov
Issue description
System information
|
|
| OpenVINO version |
2026.5.0-23099-d015add94c1 (stock binary; also reproduced with a self-build of the same commit) |
| Still present on |
releases/2026/4 (9bcdbd5, 2026-09-17) and master (2d4c480, 2026-09-18) — make_tensor.cpp and itensor.cpp are byte-identical to the build we crashed on, and the patch below applies cleanly to both (git apply --check verified on each) |
| Consumer |
OpenVINO Model Server 2026.5.0 (openvino/model_server:weekly, digest sha256:fcc16047f413…), GenAI 2026.5.0.0-3455-a521ae75728 |
| OS / compiler |
Ubuntu 24.04, gcc 13 |
| Hardware |
Intel Core Ultra 7 356H (Panther Lake) + Arc Pro B70 32 GB, driver xe, kernel 6.6.129 |
| Model |
OpenVINO/gemma-4-26b-a4b-it-int4-ov served through the OVMS VLM pipeline |
Summary
RoiTensor::get_strides() returns the strides of the tensor it is a view of, but BaseRoiTensor::set_shape()
is allowed to expand the ROI's rank with leading ones (added in #22257). After such a set_shape(), the
tensor reports a shape of rank N and strides of rank M < N.
ITensor::copy_to() performs that set_shape() itself and then walks shape and strides in lockstep, so a
rank-1 ROI used as the destination of a rank-2 copy makes it read dst_strides[1] out of bounds. When the
stale value there happens to compare equal, the "find a step" loop never fires, both stride vectors are left
empty, and the copy loop evaluates src_strides[src_strides.size() - 1] — that is, src_strides[SIZE_MAX] —
which dereferences data() - 8 on an empty vector and faults at 0xFFFFFFFFFFFFFFF8.
This kills the whole model server process. We hit it 9 times across independent runs on a stock image.
Minimal reproducer (no GenAI, no model server, no model)
#include <openvino/openvino.hpp>
#include <iostream>
static void dump(const char* when, const ov::Tensor& t) {
std::cout << when << ": shape " << t.get_shape() << " (rank " << t.get_shape().size()
<< "), strides " << t.get_strides() << " (rank " << t.get_strides().size() << ")\n";
}
int main() {
ov::Tensor owner(ov::element::i64, ov::Shape{4}); // rank 1
ov::Tensor dst_roi(owner, ov::Coordinate{0}, ov::Coordinate{1}); // rank-1 ROI, 1 element
ov::Tensor src(ov::element::i64, ov::Shape{1, 1}); // rank 2, 1 element
src.data<int64_t>()[0] = 42;
dump("before", dst_roi);
src.copy_to(dst_roi); // copy_to() calls dst->set_shape({1,1}) first
dump("after ", dst_roi);
std::cout << (dst_roi.get_shape().size() == dst_roi.get_strides().size()
? "OK: shape and strides have the same rank\n"
: "BUG: shape and strides have different ranks\n");
}
before: shape [1] (rank 1), strides Strides{8} (rank 1)
after : shape [1,1] (rank 2), strides Strides{8} (rank 1) <-- inconsistent
BUG: shape and strides have different ranks
The invariant violation reproduces 100% of the time, on the 2026.5 build above and on the current
releases/2026/4 and master sources (those two files are unchanged there). The out-of-bounds read it causes is reported
deterministically by valgrind, so you do not need to reproduce the crash itself to see the defect:
==336== Invalid read of size 8
==336== at 0x507C73F: ov::ITensor::copy_to(std::shared_ptr<ov::ITensor> const&) const (in /ovms/lib/libopenvino.so.2026.5.0)
==336== by 0x50782DF: ov::Tensor::copy_to(ov::Tensor) const (in /ovms/lib/libopenvino.so.2026.5.0)
==336== by 0x10A781: main (/roi_repro.cpp:18)
==336== Address 0x618c7f8 is 0 bytes after a block of size 8 alloc'd
==336== at 0x4846FA3: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==336== by 0x4C94226: ov::Strides::Strides(std::initializer_list<unsigned long> const&) (in /ovms/lib/libopenvino.so.2026.5.0)
==336== by 0x507C0F8: ov::ITensor::copy_to(std::shared_ptr<ov::ITensor> const&) const (in /ovms/lib/libopenvino.so.2026.5.0)
==336== by 0x50782DF: ov::Tensor::copy_to(ov::Tensor) const (in /ovms/lib/libopenvino.so.2026.5.0)
==336== by 0x10A781: main (/roi_repro.cpp:18)
==336== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
Whether the process actually dies depends on what that out-of-bounds read returns, which is why the
production failure looks random while always landing on the same instruction.
Root cause
src/inference/src/dev/make_tensor.cpp:
void BaseRoiTensor::set_shape(ov::Shape new_shape) { // :427
OPENVINO_ASSERT(new_shape.size() >= m_shape.size()); // :428 rank may grow
... // the added dimensions must be 1
m_shape = std::move(new_shape);
}
const Strides& RoiTensor::get_strides() const { // :477 (RoiRemoteTensor :527 is identical)
return m_owner->get_strides(); // still the owner's rank
}
src/core/src/runtime/itensor.cpp:
const auto& shape = get_shape();
if (shape != dst->get_shape())
dst->set_shape(shape); // :84 dst's shape rank grows here
...
src_strides = get_strides(); // :110
dst_strides = dst->get_strides(); // :111 rank does NOT grow
ov::Strides src_str, dst_str; // :113 left empty unless a step is found
for (size_t inverted_idx = shape_rank - 1; inverted_idx < shape_rank; --inverted_idx) {
if (default_strides[inverted_idx] == src_strides[inverted_idx] && // :119
src_strides[inverted_idx] == dst_strides[inverted_idx]) { // :120 OOB read when ranks differ
continue;
}
... // only here are src_str/dst_str resized
}
src_strides = std::move(src_str); // :150 stays empty if nothing above fired
...
copy_function(src_data + src_idx, dst_data + dst_idx,
src_strides[src_strides.size() - 1]); // :174 SIZE_MAX index on an empty vector
How it shows up in a real service
OVMS serving a VLM model reaches this on every decode step: ModelRunner::forward()
(openvino.genai, continuous_batching/model_runner.hpp:804) copies a per-token position_ids element of
shape {1,1} into a ROI of the rank-1 position_ids model input.
The run below used only synthetic requests (a fictional transit-operations assistant): one streaming
tool-calling chat request that enters a repetition loop and is cut off by the client after 20 s, followed by
one unary /v3/responses request. The server died on the 16th such cycle. The servable was configured
with no explicit parser, so OVMS auto-detected gemma4 for both the tool and reasoning parsers — the
recommended configuration for this model. The core is the stock binary; only GenAI was swapped for a
self-build of the same commit so that frames carry file and line numbers. The first crash of this
investigation happened on a completely unmodified image, with no self-built component at all — the
GenAI swap only made the frames readable, it did not create the fault.
[2026-09-18 04:16:41][serving][info][server.cpp:116] OpenVINO backend 2026.5.0-23099-d015add94c1
[2026-09-18 04:16:41][llm_calculator][info][servable_initializer.cpp:231] Auto-detected tool_parser: gemma4
[2026-09-18 04:16:41][llm_calculator][info][servable_initializer.cpp:241] Auto-detected reasoning_parser: gemma4
[2026-09-18 04:18:51.001][157][llm_executor][info][legacy_executor.cpp:76] All requests: 1;
Thread 140 "ovms" received signal SIGSEGV, Segmentation fault.
0x00007ffff2779275 in ov::ITensor::copy_to(std::shared_ptr<ov::ITensor> const&) const
#0 ov::ITensor::copy_to(std::shared_ptr<ov::ITensor> const&) const libopenvino.so.2650
#1 ov::Tensor::copy_to(ov::Tensor) const libopenvino.so.2650
#2 ov::genai::ModelRunner::forward(...) continuous_batching/model_runner.hpp:804
#3 ov::genai::ContinuousBatchingPipeline::ContinuousBatchingImpl::step(...)
#4 ov::genai::ContinuousBatchingPipeline::ContinuousBatchingImpl::generate(...) pipeline_impl.cpp:766
#5 ov::genai::ContinuousBatchingPipeline::IContinuousBatchingPipeline::generate(...) pipeline_base.cpp:482
#6 ov::genai::ContinuousBatchingPipeline::IContinuousBatchingPipeline::generate(...) pipeline_base.cpp:581
#7 ov::genai::ContinuousBatchingPipeline::generate(...) pipeline.cpp:668
#8 ov::genai::ContinuousBatchingPipeline::generate<...>(...) continuous_batching_pipeline.hpp:337
#9 ov::genai::VLMPipeline::VLMContinuousBatchingAdapter::generate(...) continuous_batching_adapter.hpp:93
#12 ov::genai::VLMPipeline::generate(...) visual_language/pipeline.cpp:1051
#13 ovms::VisualLanguageModelLegacyExecutor::processRequest()
#14 ovms::VisualLanguageModelLegacyExecutorWrapper::run(...)
The tensors at the faulting call, read out of the inferior at the stop:
|
shape |
strides |
src (position_ids_elem) |
{1, 1} |
{8, 8} |
dst (the ROI, after copy_to's set_shape) |
{1, 1} |
{8} |
dst's owner (position_ids model input) |
{1} |
{8} |
| ROI coordinates |
begin {0}, end {1} |
element type i64 |
The faulting instruction is mov rax,[rax-0x8] with rax = 0 — an empty std::vector indexed with
size() - 1. The kernel logs segfault at fffffffffffffff8 ... in libopenvino.so.2026.5.0. Seven crashes
across independent runs all landed on the same instruction (ip - vma_base identical every time) but at
unpredictable request counts (after 1, 1, 7, 8, 10, 16, 16, 27, 27 cycles), which is what you would expect
when an out-of-bounds read decides the outcome. The full gdb output is attached.
Suggested fix
Keep a ROI's strides at the same rank as its shape. The expanded dimensions are ones, so their stride is the
row-major continuation of the dimension below. RoiTensor and RoiRemoteTensor have the same mismatch, so
the helper lives in their shared base:
diff --git a/src/inference/src/dev/make_tensor.cpp b/src/inference/src/dev/make_tensor.cpp
index 9ef2032db1..ba9bca2e24 100644
--- a/src/inference/src/dev/make_tensor.cpp
+++ b/src/inference/src/dev/make_tensor.cpp
@@ -448,6 +448,7 @@ public:
std::distance(new_shape.cbegin(), new_dim.base()) - 1);
m_shape = std::move(new_shape);
+ update_padded_strides();
}
size_t get_offset() const {
@@ -455,10 +456,36 @@ public:
}
protected:
+ // set_shape() above may expand the rank of the ROI with leading ones, while the strides of a
+ // ROI are the owner's and keep the owner's rank. A tensor whose shape and strides have
+ // different ranks breaks every consumer that walks the two in lockstep -- ITensor::copy_to()
+ // reads dst_strides[i] past the end of the vector -- so keep a padded copy to hand out
+ // instead. The expanding dimensions are ones (set_shape() enforces that), so their stride is
+ // whatever keeps the row-major relation with the dimension below.
+ void update_padded_strides() {
+ const auto& owner_strides = m_owner->get_strides();
+ if (m_shape.size() <= owner_strides.size()) {
+ m_padded_strides.clear();
+ return;
+ }
+ const auto pad = m_shape.size() - owner_strides.size();
+ m_padded_strides.assign(m_shape.size(), 0);
+ std::copy(owner_strides.begin(), owner_strides.end(), m_padded_strides.begin() + pad);
+ for (auto i = pad; i-- > 0;) {
+ m_padded_strides[i] = m_shape[i + 1] * m_padded_strides[i + 1];
+ }
+ }
+
+ const Strides& roi_strides() const {
+ const auto& owner_strides = m_owner->get_strides();
+ return m_padded_strides.size() == m_shape.size() ? m_padded_strides : owner_strides;
+ }
+
std::shared_ptr<ITensor> m_owner;
Shape m_shape;
const Shape m_capacity;
const size_t m_offset;
+ Strides m_padded_strides;
};
/**
@@ -475,7 +502,7 @@ public:
}
const Strides& get_strides() const override {
- return m_owner->get_strides();
+ return roi_strides();
}
const Shape& get_shape() const override {
@@ -525,7 +552,7 @@ public:
}
const Strides& get_strides() const override {
- return m_owner->get_strides();
+ return roi_strides();
}
const Shape& get_shape() const override {
That is the whole change: 29 added / 2 removed lines in one file, against d015add94c1. It applies cleanly
to master and to releases/2026/4 as well.
With it applied the reproducer prints strides Strides{8, 8} (rank 2), valgrind reports 0 errors, and
copy_to() takes the fast memcpy path — the strides now equal the destination's and the source is
contiguous — instead of the strided walk.
Worth hardening either way: itensor.cpp:174 indexes src_strides[src_strides.size() - 1] without
checking that the vector is non-empty, and the loop at :118 indexes dst_strides using the shape's rank.
An assert that both stride vectors have the shape's rank, or a fallback to the contiguous path when no step
is found, would turn any future instance of this class of bug into a diagnosable error rather than a wild
read.
What we verified
- Patched core built from
d015add94c1 and dropped into the stock model server image — only
libopenvino.so replaced; every plugin and GenAI is the stock binary.
- Every symbol the 17 in-image consumers resolve against the core is still defined (0 missing).
- Greedy-decoding output byte-identical to the stock build across 15 requests.
- The workload that used to kill the server within ~13 request cycles on average survived 137 and
120 cycles in two independent runs, with no crash and no new segfault in dmesg.
(Soak runs were driven under both parser configurations; the crash reproduces under both, so neither the
failure nor the fix is configuration-specific.)
Note on the caller
The GenAI side has an inconsistency worth a look, though it is not the defect: for a rank-2 position_ids
element ({1,1}), ModelRunner::forward() allocates the position_ids model input as rank-1
{total_num_tokens} (model_runner.hpp:573), while the 3-D (M-RoPE) branch keeps the original rank. That is
what produces the rank-mismatched pair here. With the core fixed the usage becomes legal, so we have not
filed it separately — happy to do so if you prefer.
ovms-vlm-segv-gdb.txt
Step-by-step reproduction
No response
Relevant log output
Issue submission checklist
OpenVINO Version
2026.5.0-23099-d015add94c1
Operating System
Other (Please specify in description)
Device used for inference
GPU
Framework
None
Model used
OpenVINO/gemma-4-26b-a4b-it-int4-ov
Issue description
System information
2026.5.0-23099-d015add94c1(stock binary; also reproduced with a self-build of the same commit)releases/2026/4(9bcdbd5, 2026-09-17) andmaster(2d4c480, 2026-09-18) —make_tensor.cppanditensor.cppare byte-identical to the build we crashed on, and the patch below applies cleanly to both (git apply --checkverified on each)openvino/model_server:weekly, digestsha256:fcc16047f413…), GenAI2026.5.0.0-3455-a521ae75728xe, kernel 6.6.129OpenVINO/gemma-4-26b-a4b-it-int4-ovserved through the OVMS VLM pipelineSummary
RoiTensor::get_strides()returns the strides of the tensor it is a view of, butBaseRoiTensor::set_shape()is allowed to expand the ROI's rank with leading ones (added in #22257). After such a
set_shape(), thetensor reports a shape of rank N and strides of rank M < N.
ITensor::copy_to()performs thatset_shape()itself and then walks shape and strides in lockstep, so arank-1 ROI used as the destination of a rank-2 copy makes it read
dst_strides[1]out of bounds. When thestale value there happens to compare equal, the "find a step" loop never fires, both stride vectors are left
empty, and the copy loop evaluates
src_strides[src_strides.size() - 1]— that is,src_strides[SIZE_MAX]—which dereferences
data() - 8on an empty vector and faults at0xFFFFFFFFFFFFFFF8.This kills the whole model server process. We hit it 9 times across independent runs on a stock image.
Minimal reproducer (no GenAI, no model server, no model)
The invariant violation reproduces 100% of the time, on the 2026.5 build above and on the current
releases/2026/4andmastersources (those two files are unchanged there). The out-of-bounds read it causes is reporteddeterministically by valgrind, so you do not need to reproduce the crash itself to see the defect:
Whether the process actually dies depends on what that out-of-bounds read returns, which is why the
production failure looks random while always landing on the same instruction.
Root cause
src/inference/src/dev/make_tensor.cpp:src/core/src/runtime/itensor.cpp:How it shows up in a real service
OVMS serving a VLM model reaches this on every decode step:
ModelRunner::forward()(openvino.genai,
continuous_batching/model_runner.hpp:804) copies a per-tokenposition_idselement ofshape
{1,1}into a ROI of the rank-1position_idsmodel input.The run below used only synthetic requests (a fictional transit-operations assistant): one streaming
tool-calling chat request that enters a repetition loop and is cut off by the client after 20 s, followed by
one unary
/v3/responsesrequest. The server died on the 16th such cycle. The servable was configuredwith no explicit parser, so OVMS auto-detected
gemma4for both the tool and reasoning parsers — therecommended configuration for this model. The core is the stock binary; only GenAI was swapped for a
self-build of the same commit so that frames carry file and line numbers. The first crash of this
investigation happened on a completely unmodified image, with no self-built component at all — the
GenAI swap only made the frames readable, it did not create the fault.
The tensors at the faulting call, read out of the inferior at the stop:
position_ids_elem){1, 1}{8, 8}copy_to'sset_shape){1, 1}{8}position_idsmodel input){1}{8}{0}, end{1}i64The faulting instruction is
mov rax,[rax-0x8]withrax = 0— an emptystd::vectorindexed withsize() - 1. The kernel logssegfault at fffffffffffffff8 ... in libopenvino.so.2026.5.0. Seven crashesacross independent runs all landed on the same instruction (
ip - vma_baseidentical every time) but atunpredictable request counts (after 1, 1, 7, 8, 10, 16, 16, 27, 27 cycles), which is what you would expect
when an out-of-bounds read decides the outcome. The full gdb output is attached.
Suggested fix
Keep a ROI's strides at the same rank as its shape. The expanded dimensions are ones, so their stride is the
row-major continuation of the dimension below.
RoiTensorandRoiRemoteTensorhave the same mismatch, sothe helper lives in their shared base:
That is the whole change: 29 added / 2 removed lines in one file, against
d015add94c1. It applies cleanlyto
masterand toreleases/2026/4as well.With it applied the reproducer prints
strides Strides{8, 8} (rank 2), valgrind reports 0 errors, andcopy_to()takes the fast memcpy path — the strides now equal the destination's and the source iscontiguous — instead of the strided walk.
Worth hardening either way:
itensor.cpp:174indexessrc_strides[src_strides.size() - 1]withoutchecking that the vector is non-empty, and the loop at
:118indexesdst_stridesusing the shape's rank.An assert that both stride vectors have the shape's rank, or a fallback to the contiguous path when no step
is found, would turn any future instance of this class of bug into a diagnosable error rather than a wild
read.
What we verified
d015add94c1and dropped into the stock model server image — onlylibopenvino.soreplaced; every plugin and GenAI is the stock binary.120 cycles in two independent runs, with no crash and no new
segfaultindmesg.(Soak runs were driven under both parser configurations; the crash reproduces under both, so neither the
failure nor the fix is configuration-specific.)
Note on the caller
The GenAI side has an inconsistency worth a look, though it is not the defect: for a rank-2
position_idselement (
{1,1}),ModelRunner::forward()allocates theposition_idsmodel input as rank-1{total_num_tokens}(model_runner.hpp:573), while the 3-D (M-RoPE) branch keeps the original rank. That iswhat produces the rank-mismatched pair here. With the core fixed the usage becomes legal, so we have not
filed it separately — happy to do so if you prefer.
ovms-vlm-segv-gdb.txt
Step-by-step reproduction
No response
Relevant log output
Issue submission checklist