From 9de0bd7405325d7a1a7e910046ade479125b6f3a Mon Sep 17 00:00:00 2001 From: Adrian Tobiszewski Date: Tue, 7 Jul 2026 15:15:11 +0200 Subject: [PATCH 1/7] Fix KFS multi-dimensional string tensor deserialization Both convertStringRequestToOVTensor (contents path) and convertBinaryExtensionStringFromBufferToNativeOVTensor (raw_input_contents path) hardcoded a 1D ov::Shape{N}, ignoring the shape field in the request. For models with string inputs of rank > 1 (e.g. [batch, seq_len]) this caused inference to fail with: 'model input (shape=[?,?]) and the tensor (shape=(N)) are incompatible' Fix: read shape from request, use it when its product matches element count; fall back to 1D otherwise to preserve backward compatibility. Add two new unit tests (StringInputsConversionKFSTest): - native_ov_string_2d_shape (contents path) - rawInputContents_native_ov_string_2d_shape (raw_input_contents path) Resolves CVS-187884 --- src/kfs_frontend/kfs_utils.cpp | 18 +++++++- src/kfs_frontend/tensor_conversion.cpp | 30 ++++++++++++- src/test/tensor_conversion_test.cpp | 62 ++++++++++++++++++++++++-- 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/kfs_frontend/kfs_utils.cpp b/src/kfs_frontend/kfs_utils.cpp index 246239d081..401c774097 100644 --- a/src/kfs_frontend/kfs_utils.cpp +++ b/src/kfs_frontend/kfs_utils.cpp @@ -305,7 +305,23 @@ Status convertBinaryExtensionStringFromBufferToNativeOVTensor(const ::KFSRequest SPDLOG_DEBUG("Input string format conversion failed"); return StatusCode::INVALID_STRING_INPUT; } - tensor = ov::Tensor(ov::element::Type_t::string, ov::Shape{batchSize}); + // Use shape from request if provided and its element count matches the parsed string count. + // This preserves multi-dimensional shapes (e.g. [1, 5] for batch=1, seq_len=5). + ov::Shape shape; + if (src.shape_size() > 0) { + size_t shapeElements = 1; + for (int i = 0; i < src.shape_size(); i++) { + shape.push_back(static_cast(src.shape().at(i))); + shapeElements *= static_cast(src.shape().at(i)); + } + if (shapeElements != batchSize) { + SPDLOG_DEBUG("Input string shape mismatch: shape product {} != parsed string count {}", shapeElements, batchSize); + shape = ov::Shape{batchSize}; + } + } else { + shape = ov::Shape{batchSize}; + } + tensor = ov::Tensor(ov::element::Type_t::string, shape); std::string* data = tensor.data(); size_t tensorStringsOffset = 0; for (size_t i = 0; i < stringSizes.size(); i++) { diff --git a/src/kfs_frontend/tensor_conversion.cpp b/src/kfs_frontend/tensor_conversion.cpp index 138204e410..e30b15867a 100644 --- a/src/kfs_frontend/tensor_conversion.cpp +++ b/src/kfs_frontend/tensor_conversion.cpp @@ -61,7 +61,35 @@ Status convertStringRequestFromBufferToOVTensor2D(const ::KFSRequest::InferInput return StatusCode::OK; } -template Status convertStringRequestToOVTensor<::KFSRequest::InferInputTensor>(const ::KFSRequest::InferInputTensor& src, ov::Tensor& tensor, const std::string* buffer); +template <> +Status convertStringRequestToOVTensor<::KFSRequest::InferInputTensor>(const ::KFSRequest::InferInputTensor& src, ov::Tensor& tensor, const std::string* buffer) { + OVMS_PROFILE_FUNCTION(); + if (buffer != nullptr) { + return convertBinaryExtensionStringFromBufferToNativeOVTensor(src, tensor, buffer); + } + int numElements = getBinaryInputsSize(src); + // Build ov::Shape from request shape, preserving multi-dimensional shapes (e.g. [1,5]). + ov::Shape shape; + if (src.shape_size() > 0) { + size_t shapeElements = 1; + for (int i = 0; i < src.shape_size(); i++) { + shape.push_back(static_cast(src.shape().at(i))); + shapeElements *= static_cast(src.shape().at(i)); + } + if (static_cast(shapeElements) != numElements) { + SPDLOG_DEBUG("String input shape product {} != contents size {}; falling back to 1D", shapeElements, numElements); + shape = ov::Shape{static_cast(numElements)}; + } + } else { + shape = ov::Shape{static_cast(numElements)}; + } + tensor = ov::Tensor(ov::element::Type_t::string, shape); + std::string* data = tensor.data(); + for (int i = 0; i < numElements; i++) { + data[i].assign(getBinaryInput(src, i)); + } + return StatusCode::OK; +} template Status convertNativeFileFormatRequestTensorToOVTensor<::KFSRequest::InferInputTensor>(const ::KFSRequest::InferInputTensor& src, ov::Tensor& tensor, const TensorInfo& tensorInfo, const std::string* buffer); } // namespace ovms diff --git a/src/test/tensor_conversion_test.cpp b/src/test/tensor_conversion_test.cpp index 950fd6aed1..f23dc5fd7a 100644 --- a/src/test/tensor_conversion_test.cpp +++ b/src/test/tensor_conversion_test.cpp @@ -637,7 +637,7 @@ TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Positive) { TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Positive_batchSizeBiggerThan1) { uint8_t rgb_expected_tensor[] = {0x24, 0x1b, 0xed, 0x24, 0x1b, 0xed}; - this->requestTensor.mutable_shape()->Clear(); + requestTensor.mutable_shape()->Clear(); this->requestTensor.mutable_shape()->Add(2); size_t filesize; @@ -658,7 +658,7 @@ TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Positive_batchSizeBigg } TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Negative_batchSizeBiggerThan1WithEmptyString) { - this->requestTensor.mutable_shape()->Clear(); + requestTensor.mutable_shape()->Clear(); this->requestTensor.mutable_shape()->Add(2); uint8_t imageSize[] = {0x00, 0x00, 0x00, 0x00}; @@ -681,7 +681,7 @@ TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Negative_emptyString) } TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Negative_invalidFormat) { - this->requestTensor.mutable_shape()->Clear(); + requestTensor.mutable_shape()->Clear(); this->requestTensor.mutable_shape()->Add(2); uint8_t imageSize[] = {0x01, 0x00, 0x00, 0x00}; @@ -826,6 +826,62 @@ TYPED_TEST(StringInputsConversionTest, rawInputContents_native_ov_string) { } } +TEST(StringInputsConversionKFSTest, native_ov_string_2d_shape) { + // Request shape [1, 5] with 5 strings - simulates 2D string input (batch=1, seq_len=5) + ::KFSRequest::InferInputTensor requestTensor; + requestTensor.set_datatype("BYTES"); + requestTensor.mutable_shape()->Clear(); + requestTensor.add_shape(1); + requestTensor.add_shape(5); + std::vector expectedStrings = {"a", "b", "c", "d", "e"}; + for (const auto& s : expectedStrings) { + auto bytes_val = requestTensor.mutable_contents()->mutable_bytes_contents()->Add(); + bytes_val->append(s.data(), s.size()); + } + ov::Tensor tensor; + ASSERT_EQ(convertStringRequestToOVTensor(requestTensor, tensor, nullptr), ovms::StatusCode::OK); + ASSERT_EQ(tensor.get_element_type(), ov::element::string); + ASSERT_EQ(tensor.get_shape().size(), 2); + ASSERT_THAT(tensor.get_shape(), ::testing::ElementsAre(1, 5)); + std::string* data = tensor.data(); + for (size_t i = 0; i < expectedStrings.size(); i++) { + ASSERT_EQ(data[i], expectedStrings[i]) << " at index " << i; + } +} + +TEST(StringInputsConversionKFSTest, rawInputContents_native_ov_string_2d_shape) { + // raw_input_contents path: shape [1, 5] with 5 strings in buffer + ::KFSRequest::InferInputTensor requestTensor; + requestTensor.set_datatype("BYTES"); + requestTensor.mutable_shape()->Clear(); + requestTensor.add_shape(1); + requestTensor.add_shape(5); + std::vector expectedStrings = {"a", "b", "c", "d", "e"}; + std::string rawInputContents; + size_t dataSize = 0; + for (const auto& s : expectedStrings) { + dataSize += s.size() + sizeof(uint32_t); + } + rawInputContents.resize(dataSize); + size_t offset = 0; + for (const auto& s : expectedStrings) { + uint32_t inputSize = s.size(); + std::memcpy(rawInputContents.data() + offset, &inputSize, sizeof(uint32_t)); + offset += sizeof(uint32_t); + std::memcpy(rawInputContents.data() + offset, s.data(), s.size()); + offset += s.size(); + } + ov::Tensor tensor; + ASSERT_EQ(convertStringRequestToOVTensor(requestTensor, tensor, &rawInputContents), ovms::StatusCode::OK); + ASSERT_EQ(tensor.get_element_type(), ov::element::string); + ASSERT_EQ(tensor.get_shape().size(), 2); + ASSERT_THAT(tensor.get_shape(), ::testing::ElementsAre(1, 5)); + std::string* data = tensor.data(); + for (size_t i = 0; i < expectedStrings.size(); i++) { + ASSERT_EQ(data[i], expectedStrings[i]) << " at index " << i; + } +} + template class StringOutputsConversionTest : public ::testing::Test { public: From a5b1c09df0c19bfb7396a9e8bc889797d4dbbfa1 Mon Sep 17 00:00:00 2001 From: Adrian Tobiszewski Date: Wed, 8 Jul 2026 10:33:42 +0200 Subject: [PATCH 2/7] Self-review --- src/test/tensor_conversion_test.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/tensor_conversion_test.cpp b/src/test/tensor_conversion_test.cpp index f23dc5fd7a..975045fd3c 100644 --- a/src/test/tensor_conversion_test.cpp +++ b/src/test/tensor_conversion_test.cpp @@ -637,7 +637,7 @@ TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Positive) { TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Positive_batchSizeBiggerThan1) { uint8_t rgb_expected_tensor[] = {0x24, 0x1b, 0xed, 0x24, 0x1b, 0xed}; - requestTensor.mutable_shape()->Clear(); + this->requestTensor.mutable_shape()->Clear(); this->requestTensor.mutable_shape()->Add(2); size_t filesize; @@ -658,7 +658,7 @@ TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Positive_batchSizeBigg } TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Negative_batchSizeBiggerThan1WithEmptyString) { - requestTensor.mutable_shape()->Clear(); + this->requestTensor.mutable_shape()->Clear(); this->requestTensor.mutable_shape()->Add(2); uint8_t imageSize[] = {0x00, 0x00, 0x00, 0x00}; @@ -681,7 +681,7 @@ TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Negative_emptyString) } TEST_F(NativeFileInputConversionTestKFSRawInputsContents, Negative_invalidFormat) { - requestTensor.mutable_shape()->Clear(); + this->requestTensor.mutable_shape()->Clear(); this->requestTensor.mutable_shape()->Add(2); uint8_t imageSize[] = {0x01, 0x00, 0x00, 0x00}; @@ -833,7 +833,7 @@ TEST(StringInputsConversionKFSTest, native_ov_string_2d_shape) { requestTensor.mutable_shape()->Clear(); requestTensor.add_shape(1); requestTensor.add_shape(5); - std::vector expectedStrings = {"a", "b", "c", "d", "e"}; + std::vector expectedStrings = {"aa", "bbb", "c", "dddd", "e"}; for (const auto& s : expectedStrings) { auto bytes_val = requestTensor.mutable_contents()->mutable_bytes_contents()->Add(); bytes_val->append(s.data(), s.size()); @@ -856,7 +856,7 @@ TEST(StringInputsConversionKFSTest, rawInputContents_native_ov_string_2d_shape) requestTensor.mutable_shape()->Clear(); requestTensor.add_shape(1); requestTensor.add_shape(5); - std::vector expectedStrings = {"a", "b", "c", "d", "e"}; + std::vector expectedStrings = {"a", "bbbbb", "c", "dd", "eee"}; std::string rawInputContents; size_t dataSize = 0; for (const auto& s : expectedStrings) { From 4ec128052f143e3a496984bd7355277ef83cf89c Mon Sep 17 00:00:00 2001 From: Adrian Tobiszewski Date: Wed, 8 Jul 2026 14:35:23 +0200 Subject: [PATCH 3/7] Review fixes --- src/kfs_frontend/kfs_utils.cpp | 24 ++++++++++++---- src/kfs_frontend/tensor_conversion.cpp | 24 ++++++++++++---- src/test/tensor_conversion_test.cpp | 40 ++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/kfs_frontend/kfs_utils.cpp b/src/kfs_frontend/kfs_utils.cpp index 401c774097..233ab1df1a 100644 --- a/src/kfs_frontend/kfs_utils.cpp +++ b/src/kfs_frontend/kfs_utils.cpp @@ -16,6 +16,7 @@ #include "kfs_utils.hpp" #include +#include #include #include #include @@ -310,13 +311,24 @@ Status convertBinaryExtensionStringFromBufferToNativeOVTensor(const ::KFSRequest ov::Shape shape; if (src.shape_size() > 0) { size_t shapeElements = 1; - for (int i = 0; i < src.shape_size(); i++) { - shape.push_back(static_cast(src.shape().at(i))); - shapeElements *= static_cast(src.shape().at(i)); + bool validShape = true; + for (int i = 0; i < src.shape_size() && validShape; i++) { + int64_t dim = src.shape().at(i); + if (dim < 0) { + validShape = false; + break; + } + size_t dimSize = static_cast(dim); + if (dimSize != 0 && shapeElements > std::numeric_limits::max() / dimSize) { + validShape = false; // multiplication would overflow + break; + } + shape.push_back(dimSize); + shapeElements *= dimSize; } - if (shapeElements != batchSize) { - SPDLOG_DEBUG("Input string shape mismatch: shape product {} != parsed string count {}", shapeElements, batchSize); - shape = ov::Shape{batchSize}; + if (!validShape || shapeElements != batchSize) { + SPDLOG_DEBUG("Input string shape mismatch: shape product {} != parsed string count {}; rejecting request", shapeElements, batchSize); + return StatusCode::INVALID_STRING_INPUT; } } else { shape = ov::Shape{batchSize}; diff --git a/src/kfs_frontend/tensor_conversion.cpp b/src/kfs_frontend/tensor_conversion.cpp index e30b15867a..bbaa94c0b9 100644 --- a/src/kfs_frontend/tensor_conversion.cpp +++ b/src/kfs_frontend/tensor_conversion.cpp @@ -18,6 +18,7 @@ #include "../tensor_conversion_common.hpp" #include +#include #include #include #include @@ -72,13 +73,24 @@ Status convertStringRequestToOVTensor<::KFSRequest::InferInputTensor>(const ::KF ov::Shape shape; if (src.shape_size() > 0) { size_t shapeElements = 1; - for (int i = 0; i < src.shape_size(); i++) { - shape.push_back(static_cast(src.shape().at(i))); - shapeElements *= static_cast(src.shape().at(i)); + bool validShape = (numElements >= 0); + for (int i = 0; i < src.shape_size() && validShape; i++) { + int64_t dim = src.shape().at(i); + if (dim < 0) { + validShape = false; + break; + } + size_t dimSize = static_cast(dim); + if (dimSize != 0 && shapeElements > std::numeric_limits::max() / dimSize) { + validShape = false; // multiplication would overflow + break; + } + shape.push_back(dimSize); + shapeElements *= dimSize; } - if (static_cast(shapeElements) != numElements) { - SPDLOG_DEBUG("String input shape product {} != contents size {}; falling back to 1D", shapeElements, numElements); - shape = ov::Shape{static_cast(numElements)}; + if (!validShape || shapeElements != static_cast(numElements)) { + SPDLOG_DEBUG("String input shape product {} != contents size {}; rejecting request", shapeElements, numElements); + return StatusCode::INVALID_STRING_INPUT; } } else { shape = ov::Shape{static_cast(numElements)}; diff --git a/src/test/tensor_conversion_test.cpp b/src/test/tensor_conversion_test.cpp index 975045fd3c..d9d189bf0b 100644 --- a/src/test/tensor_conversion_test.cpp +++ b/src/test/tensor_conversion_test.cpp @@ -882,6 +882,46 @@ TEST(StringInputsConversionKFSTest, rawInputContents_native_ov_string_2d_shape) } } +TEST(StringInputsConversionKFSTest, native_ov_string_shape_mismatch_invalid) { + // Shape product [2,5]=10 does not match 3 provided strings - request must be rejected + ::KFSRequest::InferInputTensor requestTensor; + requestTensor.set_datatype("BYTES"); + requestTensor.add_shape(2); + requestTensor.add_shape(5); + std::vector strings = {"aa", "bbb", "c"}; + for (const auto& s : strings) { + auto bytes_val = requestTensor.mutable_contents()->mutable_bytes_contents()->Add(); + bytes_val->append(s.data(), s.size()); + } + ov::Tensor tensor; + ASSERT_EQ(convertStringRequestToOVTensor(requestTensor, tensor, nullptr), ovms::StatusCode::INVALID_STRING_INPUT); +} + +TEST(StringInputsConversionKFSTest, rawInputContents_native_ov_string_shape_mismatch_invalid) { + // Shape product [2,5]=10 does not match 3 strings in buffer - request must be rejected + ::KFSRequest::InferInputTensor requestTensor; + requestTensor.set_datatype("BYTES"); + requestTensor.add_shape(2); + requestTensor.add_shape(5); + std::vector strings = {"a", "bb", "ccc"}; + std::string rawInputContents; + size_t dataSize = 0; + for (const auto& s : strings) { + dataSize += s.size() + sizeof(uint32_t); + } + rawInputContents.resize(dataSize); + size_t offset = 0; + for (const auto& s : strings) { + uint32_t inputSize = s.size(); + std::memcpy(rawInputContents.data() + offset, &inputSize, sizeof(uint32_t)); + offset += sizeof(uint32_t); + std::memcpy(rawInputContents.data() + offset, s.data(), s.size()); + offset += s.size(); + } + ov::Tensor tensor; + ASSERT_EQ(convertStringRequestToOVTensor(requestTensor, tensor, &rawInputContents), ovms::StatusCode::INVALID_STRING_INPUT); +} + template class StringOutputsConversionTest : public ::testing::Test { public: From 16e467d3a27e0640407855cde4c6cf9341d764ad Mon Sep 17 00:00:00 2001 From: Adrian Tobiszewski Date: Wed, 8 Jul 2026 16:39:29 +0200 Subject: [PATCH 4/7] Fix ODR: declare KFS template specialization in header Add forward declaration of inference::ModelInferRequest_InferInputTensor and explicit specialization declaration for convertStringRequestToOVTensor in tensor_conversion.hpp so TUs that include the header suppress primary template instantiation for KFSRequest::InferInputTensor instead of using the old 1D-only body. --- src/tensor_conversion.hpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tensor_conversion.hpp b/src/tensor_conversion.hpp index c5eb537062..bda7a1b413 100644 --- a/src/tensor_conversion.hpp +++ b/src/tensor_conversion.hpp @@ -31,6 +31,9 @@ namespace tensorflow { class TensorProto; } +namespace inference { +class ModelInferRequest_InferInputTensor; +} namespace ovms { class Status; template @@ -174,6 +177,9 @@ Status convertStringRequestToOVTensor(const TensorType& src, ov::Tensor& tensor, template <> Status convertStringRequestToOVTensor(const InferenceTensor& src, ov::Tensor& tensor, const std::string* buffer); +template <> +Status convertStringRequestToOVTensor(const ::inference::ModelInferRequest_InferInputTensor& src, ov::Tensor& tensor, const std::string* buffer); + template Status convertOVTensor2DToStringResponse(const ov::Tensor& tensor, TensorType& dst) { if (tensor.get_shape().size() != 2) { From 0628365b3d3c611542d5d741e4a5dff6ee07d279 Mon Sep 17 00:00:00 2001 From: Adrian Tobiszewski Date: Fri, 10 Jul 2026 09:52:12 +0200 Subject: [PATCH 5/7] Validation improvements for string handling --- src/predict_request_validation_utils.hpp | 3 ++ src/predict_request_validation_utils_impl.cpp | 13 +++++ src/predict_request_validation_utils_impl.hpp | 6 ++- src/test/predict_validation_test.cpp | 47 +++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/predict_request_validation_utils.hpp b/src/predict_request_validation_utils.hpp index 2e9bfcb4ed..8e5c8e8f39 100644 --- a/src/predict_request_validation_utils.hpp +++ b/src/predict_request_validation_utils.hpp @@ -326,6 +326,9 @@ Status RequestValidator(elementCount) > MAX_NATIVE_STRING_ELEMENTS) { + std::stringstream ss; + ss << "; element count " << elementCount << " exceeds max " << MAX_NATIVE_STRING_ELEMENTS << " (1GB string-object limit)"; + const std::string details = ss.str(); + SPDLOG_DEBUG(details); + return Status(StatusCode::INVALID_STRING_MAX_SIZE_EXCEEDED, details); + } + return StatusCode::OK; +} Mode getShapeMode(const shapes_info_map_t& shapeInfo, const std::string& name) { if (shapeInfo.size() == 0) { return Mode::FIXED; diff --git a/src/predict_request_validation_utils_impl.hpp b/src/predict_request_validation_utils_impl.hpp index 14eff89771..2208650400 100644 --- a/src/predict_request_validation_utils_impl.hpp +++ b/src/predict_request_validation_utils_impl.hpp @@ -28,9 +28,13 @@ namespace ovms { namespace request_validation_utils { -const size_t MAX_2D_STRING_ARRAY_SIZE = 1024 * 1024 * 1024 * 1; // 1GB +const size_t MAX_2D_STRING_ARRAY_SIZE = 1024 * 1024 * 1024 * 1; // 1GB +// Each ov::Tensor(string, {N}) allocates N std::string objects (~32 B each). +// Cap element count so string-object heap growth stays within 1 GB. +const size_t MAX_NATIVE_STRING_ELEMENTS = (1ULL << 30) / sizeof(std::string); // ~33.5M on 64-bit Status getRawInputContentsBatchSizeAndWidth(const std::string& buffer, int32_t& batchSize, size_t& width); // this comes from KFS - may need to move there Status validateAgainstMax2DStringArraySize(int32_t inputBatchSize, size_t inputWidth); +Status validateAgainstMaxNativeStringElementCount(int32_t elementCount); Mode getShapeMode(const shapes_info_map_t& shapeInfo, const std::string& name); } // namespace request_validation_utils } // namespace ovms diff --git a/src/test/predict_validation_test.cpp b/src/test/predict_validation_test.cpp index c8fcb1574d..3427d23560 100644 --- a/src/test/predict_validation_test.cpp +++ b/src/test/predict_validation_test.cpp @@ -2188,6 +2188,53 @@ TYPED_TEST(PredictValidationStringNativeTest, string_not_allowed_with_demultiple EXPECT_EQ(status, ovms::StatusCode::INVALID_NO_OF_SHAPE_DIMENSIONS); } +TEST(PredictValidationStringNativeKFSTest, negative_over_element_count_limit) { + const char* tensorName = DUMMY_MODEL_INPUT_NAME; + ovms::tensor_map_t mockedInputsInfo, mockedOutputsInfo; + mockedInputsInfo[tensorName] = std::make_shared( + tensorName, ovms::Precision::STRING, ovms::Shape{-1}, ovms::Layout{"N..."}); + + const size_t N = ovms::request_validation_utils::MAX_NATIVE_STRING_ELEMENTS + 1; + // Build raw buffer: N entries, each with 4-byte zero length (empty string). + // Buffer size = N * 4 bytes (~128 MB), much less than N * sizeof(std::string) (~1 GB). + std::string rawBuffer(N * sizeof(uint32_t), '\0'); + + ::KFSRequest request; + auto* input = request.add_inputs(); + input->set_name(tensorName); + input->set_datatype("BYTES"); + input->add_shape(static_cast(N)); + *request.add_raw_input_contents() = std::move(rawBuffer); + + auto status = ovms::request_validation_utils::validate( + request, mockedInputsInfo, mockedOutputsInfo, "dummy", ovms::model_version_t{1}); + EXPECT_EQ(status, ovms::StatusCode::INVALID_STRING_MAX_SIZE_EXCEEDED) << status.string(); +} + +TEST(PredictValidationStringNativeKFSTest, negative_over_element_count_limit_contents) { + const char* tensorName = DUMMY_MODEL_INPUT_NAME; + ovms::tensor_map_t mockedInputsInfo, mockedOutputsInfo; + mockedInputsInfo[tensorName] = std::make_shared( + tensorName, ovms::Precision::STRING, ovms::Shape{-1}, ovms::Layout{"N..."}); + + const int32_t N = static_cast(ovms::request_validation_utils::MAX_NATIVE_STRING_ELEMENTS) + 1; + + ::KFSRequest request; + auto* input = request.add_inputs(); + input->set_name(tensorName); + input->set_datatype("BYTES"); + input->add_shape(static_cast(N)); + auto* contents = input->mutable_contents()->mutable_bytes_contents(); + contents->Reserve(N); + for (int32_t i = 0; i < N; i++) { + contents->Add(); + } + + auto status = ovms::request_validation_utils::validate( + request, mockedInputsInfo, mockedOutputsInfo, "dummy", ovms::model_version_t{1}); + EXPECT_EQ(status, ovms::StatusCode::INVALID_STRING_MAX_SIZE_EXCEEDED) << status.string(); +} + #define VERIFY_COMPUTE_BUFFER_SIZE(SHAPE, ELEMENT_SIZE, WILL_NOT_OVERFLOW, EXPECTED_BYTES) \ { \ size_t elementSize = ELEMENT_SIZE; \ From 2b6ed1245fdecbf956d98db26048e7c776f163cf Mon Sep 17 00:00:00 2001 From: Adrian Tobiszewski Date: Mon, 13 Jul 2026 14:16:01 +0200 Subject: [PATCH 6/7] Refactor --- src/kfs_frontend/kfs_utils.cpp | 46 +++++++++++++++----------- src/kfs_frontend/kfs_utils.hpp | 1 + src/kfs_frontend/tensor_conversion.cpp | 30 ++++------------- 3 files changed, 33 insertions(+), 44 deletions(-) diff --git a/src/kfs_frontend/kfs_utils.cpp b/src/kfs_frontend/kfs_utils.cpp index 233ab1df1a..15bc97d1ab 100644 --- a/src/kfs_frontend/kfs_utils.cpp +++ b/src/kfs_frontend/kfs_utils.cpp @@ -293,26 +293,11 @@ int getBinaryInputsSize(const ::KFSRequest::InferInputTensor& tensor) { return tensor.contents().bytes_contents_size(); } -Status convertBinaryExtensionStringFromBufferToNativeOVTensor(const ::KFSRequest::InferInputTensor& src, ov::Tensor& tensor, const std::string* buffer) { - std::vector stringSizes; - uint32_t totalStringsLength = 0; - while (totalStringsLength + stringSizes.size() * sizeof(uint32_t) + sizeof(uint32_t) <= buffer->size()) { - uint32_t inputSize = *(reinterpret_cast(buffer->data() + totalStringsLength + stringSizes.size() * sizeof(uint32_t))); - stringSizes.push_back(inputSize); - totalStringsLength += inputSize; - } - size_t batchSize = stringSizes.size(); - if ((totalStringsLength + batchSize * sizeof(uint32_t)) != buffer->size()) { - SPDLOG_DEBUG("Input string format conversion failed"); - return StatusCode::INVALID_STRING_INPUT; - } - // Use shape from request if provided and its element count matches the parsed string count. - // This preserves multi-dimensional shapes (e.g. [1, 5] for batch=1, seq_len=5). - ov::Shape shape; +Status buildShapeFromStringTensorRequest(const ::KFSRequest::InferInputTensor& src, size_t numElements, ov::Shape& shape) { if (src.shape_size() > 0) { size_t shapeElements = 1; bool validShape = true; - for (int i = 0; i < src.shape_size() && validShape; i++) { + for (int i = 0; i < src.shape_size(); i++) { int64_t dim = src.shape().at(i); if (dim < 0) { validShape = false; @@ -326,12 +311,33 @@ Status convertBinaryExtensionStringFromBufferToNativeOVTensor(const ::KFSRequest shape.push_back(dimSize); shapeElements *= dimSize; } - if (!validShape || shapeElements != batchSize) { - SPDLOG_DEBUG("Input string shape mismatch: shape product {} != parsed string count {}; rejecting request", shapeElements, batchSize); + if (!validShape || shapeElements != numElements) { + SPDLOG_DEBUG("Input string shape mismatch: shape product {} != parsed string count {}; rejecting request", shapeElements, numElements); return StatusCode::INVALID_STRING_INPUT; } } else { - shape = ov::Shape{batchSize}; + shape = ov::Shape{numElements}; + } + return StatusCode::OK; +} + +Status convertBinaryExtensionStringFromBufferToNativeOVTensor(const ::KFSRequest::InferInputTensor& src, ov::Tensor& tensor, const std::string* buffer) { + std::vector stringSizes; + uint32_t totalStringsLength = 0; + while (totalStringsLength + stringSizes.size() * sizeof(uint32_t) + sizeof(uint32_t) <= buffer->size()) { + uint32_t inputSize = *(reinterpret_cast(buffer->data() + totalStringsLength + stringSizes.size() * sizeof(uint32_t))); + stringSizes.push_back(inputSize); + totalStringsLength += inputSize; + } + size_t batchSize = stringSizes.size(); + if ((totalStringsLength + batchSize * sizeof(uint32_t)) != buffer->size()) { + SPDLOG_DEBUG("Input string format conversion failed"); + return StatusCode::INVALID_STRING_INPUT; + } + ov::Shape shape; + auto status = buildShapeFromStringTensorRequest(src, batchSize, shape); + if (!status.ok()) { + return status; } tensor = ov::Tensor(ov::element::Type_t::string, shape); std::string* data = tensor.data(); diff --git a/src/kfs_frontend/kfs_utils.hpp b/src/kfs_frontend/kfs_utils.hpp index 8faa5ce2ff..23fd16e8ca 100644 --- a/src/kfs_frontend/kfs_utils.hpp +++ b/src/kfs_frontend/kfs_utils.hpp @@ -73,4 +73,5 @@ Status validateTensor(const TensorInfo& tensorInfo, Status convertBinaryExtensionStringFromBufferToNativeOVTensor(const ::KFSRequest::InferInputTensor& src, ov::Tensor& tensor, const std::string* buffer); const std::string& getBinaryInput(const ::KFSRequest::InferInputTensor& tensor, size_t i); int getBinaryInputsSize(const ::KFSRequest::InferInputTensor& tensor); +Status buildShapeFromStringTensorRequest(const ::KFSRequest::InferInputTensor& src, size_t numElements, ov::Shape& shape); } // namespace ovms diff --git a/src/kfs_frontend/tensor_conversion.cpp b/src/kfs_frontend/tensor_conversion.cpp index bbaa94c0b9..ae1726c76d 100644 --- a/src/kfs_frontend/tensor_conversion.cpp +++ b/src/kfs_frontend/tensor_conversion.cpp @@ -69,31 +69,13 @@ Status convertStringRequestToOVTensor<::KFSRequest::InferInputTensor>(const ::KF return convertBinaryExtensionStringFromBufferToNativeOVTensor(src, tensor, buffer); } int numElements = getBinaryInputsSize(src); - // Build ov::Shape from request shape, preserving multi-dimensional shapes (e.g. [1,5]). + if (numElements < 0) { + return StatusCode::INVALID_STRING_INPUT; + } ov::Shape shape; - if (src.shape_size() > 0) { - size_t shapeElements = 1; - bool validShape = (numElements >= 0); - for (int i = 0; i < src.shape_size() && validShape; i++) { - int64_t dim = src.shape().at(i); - if (dim < 0) { - validShape = false; - break; - } - size_t dimSize = static_cast(dim); - if (dimSize != 0 && shapeElements > std::numeric_limits::max() / dimSize) { - validShape = false; // multiplication would overflow - break; - } - shape.push_back(dimSize); - shapeElements *= dimSize; - } - if (!validShape || shapeElements != static_cast(numElements)) { - SPDLOG_DEBUG("String input shape product {} != contents size {}; rejecting request", shapeElements, numElements); - return StatusCode::INVALID_STRING_INPUT; - } - } else { - shape = ov::Shape{static_cast(numElements)}; + auto status = buildShapeFromStringTensorRequest(src, static_cast(numElements), shape); + if (!status.ok()) { + return status; } tensor = ov::Tensor(ov::element::Type_t::string, shape); std::string* data = tensor.data(); From bb2bd249149e4f775a8a0191080526a8be336ec5 Mon Sep 17 00:00:00 2001 From: Adrian Tobiszewski Date: Mon, 13 Jul 2026 15:20:44 +0200 Subject: [PATCH 7/7] Self-review --- src/predict_request_validation_utils_impl.cpp | 5 +++-- src/tensor_conversion.hpp | 18 ------------------ 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/predict_request_validation_utils_impl.cpp b/src/predict_request_validation_utils_impl.cpp index 4bf579d039..918ad2357b 100644 --- a/src/predict_request_validation_utils_impl.cpp +++ b/src/predict_request_validation_utils_impl.cpp @@ -47,11 +47,12 @@ Status validateAgainstMax2DStringArraySize(int32_t inputBatchSize, size_t inputW } Status validateAgainstMaxNativeStringElementCount(int32_t elementCount) { if (elementCount < 0) { - return StatusCode::INVALID_BATCH_SIZE; + SPDLOG_DEBUG("Invalid(negative) element count: {}", elementCount); + return Status(StatusCode::INVALID_BATCH_SIZE, "Batch size is negative"); } if (static_cast(elementCount) > MAX_NATIVE_STRING_ELEMENTS) { std::stringstream ss; - ss << "; element count " << elementCount << " exceeds max " << MAX_NATIVE_STRING_ELEMENTS << " (1GB string-object limit)"; + ss << "; element count " << elementCount << " exceeds max " << MAX_NATIVE_STRING_ELEMENTS; const std::string details = ss.str(); SPDLOG_DEBUG(details); return Status(StatusCode::INVALID_STRING_MAX_SIZE_EXCEEDED, details); diff --git a/src/tensor_conversion.hpp b/src/tensor_conversion.hpp index bda7a1b413..2b4dc86175 100644 --- a/src/tensor_conversion.hpp +++ b/src/tensor_conversion.hpp @@ -28,9 +28,6 @@ #include "tensorinfo.hpp" #include "status.hpp" -namespace tensorflow { -class TensorProto; -} namespace inference { class ModelInferRequest_InferInputTensor; } @@ -159,21 +156,6 @@ class InferenceTensor; template <> Status convertNativeFileFormatRequestTensorToOVTensor(const InferenceTensor& src, ov::Tensor& tensor, const TensorInfo& tensorInfo, const std::string* buffer); -template -Status convertStringRequestToOVTensor(const TensorType& src, ov::Tensor& tensor, const std::string* buffer) { - OVMS_PROFILE_FUNCTION(); - if (buffer != nullptr) { - return convertBinaryExtensionStringFromBufferToNativeOVTensor(src, tensor, buffer); - } - int batchSize = getBinaryInputsSize(src); - tensor = ov::Tensor(ov::element::Type_t::string, ov::Shape{static_cast(batchSize)}); - std::string* data = tensor.data(); - for (int i = 0; i < batchSize; i++) { - data[i].assign(getBinaryInput(src, i)); - } - return StatusCode::OK; -} - template <> Status convertStringRequestToOVTensor(const InferenceTensor& src, ov::Tensor& tensor, const std::string* buffer);