From 23752290f2595f707164bb542e262b930c5aa07f Mon Sep 17 00:00:00 2001 From: exzile Date: Sat, 27 Jun 2026 23:21:30 -0400 Subject: [PATCH 1/2] Return clear error for non-finite floats in KServe REST output The KServe v2 REST output serializer wrote floats via rapidjson Writer::Double() and ignored its return value. rapidjson writes nothing for NaN/Inf (invalid in JSON), so a model output containing non-finite values silently produced malformed JSON. Add an isJsonRepresentable finite-check in both PARSE_OUTPUT_DATA write paths and return JSON_SERIALIZATION_ERROR with a clear message instead. Integer outputs are unaffected. Adds unit tests. Co-Authored-By: Claude Opus 4.8 --- src/rest_utils.cpp | 28 ++++++++++++++++++++++++++-- src/test/rest_utils_test.cpp | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/rest_utils.cpp b/src/rest_utils.cpp index 8d65fe8ee5..76e6a35e79 100644 --- a/src/rest_utils.cpp +++ b/src/rest_utils.cpp @@ -15,6 +15,7 @@ //***************************************************************************** #include "rest_utils.hpp" +#include #include #include #include @@ -59,6 +60,21 @@ enum : unsigned int { namespace ovms { +namespace { +// Integral types are always JSON-representable; only floating values can be NaN/Inf, +// which rapidjson cannot serialize (and which are invalid per the JSON spec). +template +inline bool isJsonRepresentable(T) { + return true; +} +inline bool isJsonRepresentable(float v) { + return std::isfinite(v); +} +inline bool isJsonRepresentable(double v) { + return std::isfinite(v); +} +} // namespace + static Status checkValField(const size_t& fieldSize, const size_t& expectedElementsNumber) { if (fieldSize != expectedElementsNumber) { std::stringstream ss; @@ -291,6 +307,9 @@ static void appendBinaryOutput(std::string& bytesOutputsBuffer, char* output, si appendBinaryOutput(bytesOutputsBuffer, (char*)tensor.contents().CONTENTS_FIELD().data(), expectedContentSize); \ } else { \ for (auto& number : tensor.contents().CONTENTS_FIELD()) { \ + if (!isJsonRepresentable(number)) \ + return Status(StatusCode::JSON_SERIALIZATION_ERROR, \ + "Output \"" + tensor.name() + "\" contains a non-finite (NaN/Inf) value that cannot be serialized to JSON"); \ writer.WRITER_TYPE(number); \ } \ } \ @@ -298,8 +317,13 @@ static void appendBinaryOutput(std::string& bytesOutputsBuffer, char* output, si if (binaryOutput) { \ appendBinaryOutput(bytesOutputsBuffer, (char*)response_proto.raw_output_contents(tensor_it).data(), expectedContentSize); \ } else { \ - for (size_t i = 0; i < response_proto.raw_output_contents(tensor_it).size(); i += sizeof(DATATYPE)) \ - writer.WRITER_TYPE(*(reinterpret_cast(response_proto.raw_output_contents(tensor_it).data() + i))); \ + for (size_t i = 0; i < response_proto.raw_output_contents(tensor_it).size(); i += sizeof(DATATYPE)) { \ + DATATYPE valToWrite = *(reinterpret_cast(response_proto.raw_output_contents(tensor_it).data() + i)); \ + if (!isJsonRepresentable(valToWrite)) \ + return Status(StatusCode::JSON_SERIALIZATION_ERROR, \ + "Output \"" + tensor.name() + "\" contains a non-finite (NaN/Inf) value that cannot be serialized to JSON"); \ + writer.WRITER_TYPE(valToWrite); \ + } \ } \ } diff --git a/src/test/rest_utils_test.cpp b/src/test/rest_utils_test.cpp index 6d6df46be5..6cd5523b48 100644 --- a/src/test/rest_utils_test.cpp +++ b/src/test/rest_utils_test.cpp @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. //***************************************************************************** +#include #include #include @@ -791,6 +792,23 @@ TEST_F(KFSMakeJsonFromPredictResponseRawTest, Positive) { })"); } +TEST_F(KFSMakeJsonFromPredictResponseRawTest, NonFiniteInfFloatError) { + float bad[8] = {5.0f, std::numeric_limits::infinity(), -3.0f, 2.5f, 9.0f, 55.5f, -0.5f, -1.5f}; + proto.mutable_raw_output_contents(0)->assign(reinterpret_cast(bad), 8 * sizeof(float)); + EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, inferenceHeaderContentLength), StatusCode::JSON_SERIALIZATION_ERROR); +} + +TEST_F(KFSMakeJsonFromPredictResponseRawTest, NonFiniteNaNFloatError) { + float bad[8] = {std::numeric_limits::quiet_NaN(), 10.0f, -3.0f, 2.5f, 9.0f, 55.5f, -0.5f, -1.5f}; + proto.mutable_raw_output_contents(0)->assign(reinterpret_cast(bad), 8 * sizeof(float)); + EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, inferenceHeaderContentLength), StatusCode::JSON_SERIALIZATION_ERROR); +} + +TEST_F(KFSMakeJsonFromPredictResponseRawTest, IntOutputsUnaffectedByFiniteCheck) { + // sanity: integer outputs must still serialize fine (helper returns true for them) + ASSERT_EQ(makeJsonFromPredictResponse(proto, &json, inferenceHeaderContentLength), StatusCode::OK); +} + TEST_F(KFSMakeJsonFromPredictResponseRawTest, EmptyRawOutputContentsError) { proto.mutable_raw_output_contents()->Clear(); EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, inferenceHeaderContentLength), StatusCode::REST_SERIALIZE_VAL_FIELD_INVALID_SIZE); From 0328fd1d119784aaadcae474d043ff2e762bc1fd Mon Sep 17 00:00:00 2001 From: exzile Date: Sun, 28 Jun 2026 00:05:35 -0400 Subject: [PATCH 2/2] Extend non-finite (NaN/Inf) JSON guard to TFS REST output path Apply the same finiteness check used on the KServe path to the TF Serving makeJsonFromPredictResponse: float/double outputs are validated before serialization and a clear JSON_SERIALIZATION_ERROR is returned for NaN/Inf instead of producing malformed JSON. Co-Authored-By: Claude Opus 4.8 --- src/rest_utils.cpp | 10 ++++++++++ src/test/rest_utils_test.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/rest_utils.cpp b/src/rest_utils.cpp index 76e6a35e79..912ac1a9dc 100644 --- a/src/rest_utils.cpp +++ b/src/rest_utils.cpp @@ -123,6 +123,11 @@ Status makeJsonFromPredictResponse( for (size_t i = 0; i < tensor.tensor_content().size(); i += sizeof(float)) tensor.add_float_val(*reinterpret_cast(tensor.mutable_tensor_content()->data() + i)); } + for (float v : tensor.float_val()) { + if (!isJsonRepresentable(v)) + return Status(StatusCode::JSON_SERIALIZATION_ERROR, + "Output \"" + kv.first + "\" contains a non-finite (NaN/Inf) value that cannot be serialized to JSON"); + } break; case DataType::DT_INT32: if (seekDataInValField) { @@ -163,6 +168,11 @@ Status makeJsonFromPredictResponse( for (size_t i = 0; i < tensor.tensor_content().size(); i += sizeof(double)) tensor.add_double_val(*reinterpret_cast(tensor.mutable_tensor_content()->data() + i)); } + for (double v : tensor.double_val()) { + if (!isJsonRepresentable(v)) + return Status(StatusCode::JSON_SERIALIZATION_ERROR, + "Output \"" + kv.first + "\" contains a non-finite (NaN/Inf) value that cannot be serialized to JSON"); + } break; case DataType::DT_INT16: if (seekDataInValField) { diff --git a/src/test/rest_utils_test.cpp b/src/test/rest_utils_test.cpp index 6cd5523b48..b2676891fa 100644 --- a/src/test/rest_utils_test.cpp +++ b/src/test/rest_utils_test.cpp @@ -134,6 +134,33 @@ TEST_F(TFSMakeJsonFromPredictResponseRawTest, CannotConvertInvalidPrecision) { EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, Order::COLUMN), StatusCode::REST_UNSUPPORTED_PRECISION); } +TEST_F(TFSMakeJsonFromPredictResponseRawTest, NonFiniteInfFloatError) { + float data[8] = {5.0f, std::numeric_limits::infinity(), -3.0f, 2.5f, + 9.0f, 55.5f, -0.5f, -1.5f}; + output1->mutable_tensor_content()->assign(reinterpret_cast(data), 8 * sizeof(float)); + EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, Order::COLUMN), StatusCode::JSON_SERIALIZATION_ERROR); +} + +TEST_F(TFSMakeJsonFromPredictResponseRawTest, NonFiniteNaNFloatError) { + float data[8] = {5.0f, 10.0f, std::numeric_limits::quiet_NaN(), 2.5f, + 9.0f, 55.5f, -0.5f, -1.5f}; + output1->mutable_tensor_content()->assign(reinterpret_cast(data), 8 * sizeof(float)); + EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, Order::COLUMN), StatusCode::JSON_SERIALIZATION_ERROR); +} + +TEST_F(TFSMakeJsonFromPredictResponseRawTest, NonFiniteDoubleError) { + output1->set_dtype(tensorflow::DataType::DT_DOUBLE); + double data[2] = {1.0, std::numeric_limits::infinity()}; + output1->mutable_tensor_content()->assign(reinterpret_cast(data), 2 * sizeof(double)); + output1->clear_tensor_shape(); + output1->mutable_tensor_shape()->add_dim()->set_size(2); + EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, Order::COLUMN), StatusCode::JSON_SERIALIZATION_ERROR); +} + +TEST_F(TFSMakeJsonFromPredictResponseRawTest, IntOutputsUnaffectedByFiniteCheck) { + EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, Order::COLUMN), StatusCode::OK); +} + const char* rawPositiveFirstOrderResponseRow = R"({ "predictions": [ {