This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 82e3954c39a [fix](be) Reject malformed JSON tokens during casts 
(#68448)
82e3954c39a is described below

commit 82e3954c39a4d4ee85f75e464f0e6f32cba12c9a
Author: Jerry Hu <[email protected]>
AuthorDate: Mon Sep 28 09:40:54 2026 +0800

    [fix](be) Reject malformed JSON tokens during casts (#68448)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Problem Summary:
    
    CAST from strings to JSON accepted incomplete `null` literals and
    malformed number tokens because the parser trusted simdjson token
    classification and the large-number fallback without validating the
    complete raw token. As a result, inputs such as `nul`, `[nul]`, `01`,
    and `1.` could be normalized and persisted as valid JSON values.
    
    This change validates `null` tokens, applies the JSON number grammar
    before accepting large-number fallback errors, and verifies that the
    parser consumed the complete document. It preserves the existing
    fallback behavior for syntactically valid numbers outside the binary64
    or int128 range, such as `9.6E400`, while continuing to support valid
    integers larger than 64 bits and surrounding JSON whitespace.
    
    ### Release note
    
    Fix CAST from strings to JSON to reject malformed JSON tokens instead of
    normalizing them.
    
    ### Check List (For Author)
    
    - Test
        - [x] Regression test
            - `test_json_cast_validation`
            - `test_json_type_cast`
        - [x] Unit Test
    - `./run-be-ut.sh --run --filter='JsonbParserTest.*'` (60 tests passed,
    ASAN_UT)
        - [x] Manual test
            - `./build.sh --be --fe` (ASAN)
    - Verified `1e9999` and a 400-digit integer remain accepted as JSON
            - `build-support/clang-format.sh`
            - `build-support/check-format.sh`
            - `build-support/check-build-hygiene.sh`
    
    - Behavior changed:
    - [x] Yes. Malformed JSON text now returns SQL NULL in non-strict casts
    and raises an error in strict casts instead of being normalized and
    persisted; syntactically valid oversized numbers retain their existing
    fallback behavior.
    
    - Does this need documentation?
        - [x] No.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 be/src/util/jsonb_parser_simd.h                    | 230 ++++++++++++++-------
 be/test/util/jsonb_parser_simd_test.cpp            |  35 +++-
 .../datatype_p0/json/test_json_cast_validation.out |  23 +++
 .../json/test_json_cast_validation.groovy          |  80 +++++++
 4 files changed, 297 insertions(+), 71 deletions(-)

diff --git a/be/src/util/jsonb_parser_simd.h b/be/src/util/jsonb_parser_simd.h
index 43f12f024ed..48e2a0cd1af 100644
--- a/be/src/util/jsonb_parser_simd.h
+++ b/be/src/util/jsonb_parser_simd.h
@@ -59,8 +59,8 @@
 #pragma once
 #include <simdjson.h>
 
-#include <cmath>
 #include <limits>
+#include <string_view>
 
 #include "common/status.h"
 #include "util/jsonb_document.h"
@@ -70,17 +70,85 @@
 namespace doris {
 using int128_t = __int128;
 struct JsonbParser {
+private:
+    static bool is_json_whitespace(char c) {
+        return c == ' ' || c == '\t' || c == '\n' || c == '\r';
+    }
+
+    static std::string_view trim_json_whitespace(std::string_view token) {
+        while (!token.empty() && is_json_whitespace(token.front())) {
+            token.remove_prefix(1);
+        }
+        while (!token.empty() && is_json_whitespace(token.back())) {
+            token.remove_suffix(1);
+        }
+        return token;
+    }
+
+    static bool is_valid_json_number(std::string_view token) {
+        size_t pos = 0;
+        if (pos < token.size() && token[pos] == '-') {
+            ++pos;
+        }
+        if (pos == token.size()) {
+            return false;
+        }
+
+        if (token[pos] == '0') {
+            ++pos;
+        } else if (token[pos] >= '1' && token[pos] <= '9') {
+            do {
+                ++pos;
+            } while (pos < token.size() && token[pos] >= '0' && token[pos] <= 
'9');
+        } else {
+            return false;
+        }
+
+        if (pos < token.size() && token[pos] == '.') {
+            ++pos;
+            const size_t fraction_start = pos;
+            while (pos < token.size() && token[pos] >= '0' && token[pos] <= 
'9') {
+                ++pos;
+            }
+            if (pos == fraction_start) {
+                return false;
+            }
+        }
+
+        if (pos < token.size() && (token[pos] == 'e' || token[pos] == 'E')) {
+            ++pos;
+            if (pos < token.size() && (token[pos] == '+' || token[pos] == 
'-')) {
+                ++pos;
+            }
+            const size_t exponent_start = pos;
+            while (pos < token.size() && token[pos] >= '0' && token[pos] <= 
'9') {
+                ++pos;
+            }
+            if (pos == exponent_start) {
+                return false;
+            }
+        }
+
+        return pos == token.size();
+    }
+
     // According to https://github.com/simdjson/simdjson/pull/2139
     // For numbers larger than 64 bits, we can obtain the raw_json_token and 
parse it ourselves.
     // This allows handling numbers larger than 64 bits, such as int128.
     // For example, try to parse a 18446744073709551616, this number is just 1 
greater than the maximum value of uint64_t, and simdjson will return a 
NUMBER_ERROR
     // If try to parse a 18446744073709551616231231, it is obviously a large 
integer, at this time simdjson will return a BIGINT_ERROR
-    static bool parse_number_success(simdjson::error_code error_code) {
-        return error_code == simdjson::error_code::SUCCESS ||
-               error_code == simdjson::error_code::NUMBER_ERROR ||
-               error_code == simdjson::error_code::BIGINT_ERROR;
+    static bool parse_number_success(simdjson::error_code error_code, 
std::string_view raw_token) {
+        if (error_code == simdjson::error_code::SUCCESS) {
+            return true;
+        }
+        if (error_code != simdjson::error_code::NUMBER_ERROR &&
+            error_code != simdjson::error_code::BIGINT_ERROR) {
+            return false;
+        }
+        return is_valid_json_number(raw_token);
     }
 
+public:
     // parse a UTF-8 JSON string with length
     // will reset writer before parse
     static Status parse(const char* pch, size_t len, JsonbWriter& writer) {
@@ -92,6 +160,7 @@ struct JsonbParser {
             simdjson::ondemand::parser simdjson_parser;
             simdjson::padded_string json_str {pch, len};
             simdjson::ondemand::document doc = 
simdjson_parser.iterate(json_str);
+            bool root_number_validated_from_raw_token = false;
 
             // simdjson process top level primitive types specially
             // so some repeated code here
@@ -102,9 +171,7 @@ struct JsonbParser {
                 break;
             }
             case simdjson::ondemand::json_type::null: {
-                if (writer.writeNull() == 0) {
-                    return Status::InvalidArgument("writeNull failed");
-                }
+                RETURN_IF_ERROR(write_null(doc, writer));
                 break;
             }
             case simdjson::ondemand::json_type::boolean: {
@@ -120,16 +187,26 @@ struct JsonbParser {
             case simdjson::ondemand::json_type::number: {
                 simdjson::ondemand::number num;
                 simdjson::error_code res = doc.get_number().get(num);
-                if (!parse_number_success(res)) {
+                const auto raw_token = 
trim_json_whitespace(doc.raw_json_token());
+                if (!parse_number_success(res, raw_token)) {
                     return Status::InvalidArgument(fmt::format("simdjson 
get_number failed: {}",
                                                                
simdjson::error_message(res)));
                 }
                 // simdjson get_number() returns a number object, which can be
-                RETURN_IF_ERROR(
-                        write_number(num, doc.get_number_type(), 
doc.raw_json_token(), writer));
+                RETURN_IF_ERROR(write_number(num, doc.get_number_type(), 
raw_token, writer));
+                if (res != simdjson::error_code::SUCCESS) {
+                    const auto document = 
trim_json_whitespace(std::string_view {pch, len});
+                    if (raw_token != document) {
+                        return Status::InvalidArgument("JSON document was not 
fully consumed");
+                    }
+                    root_number_validated_from_raw_token = true;
+                }
                 break;
             }
             }
+            if (!root_number_validated_from_raw_token && !doc.at_end()) {
+                return Status::InvalidArgument("JSON document was not fully 
consumed");
+            }
         } catch (simdjson::simdjson_error& e) {
             return Status::InvalidArgument(fmt::format("simdjson parse 
exception: {}", e.what()));
         }
@@ -137,15 +214,34 @@ struct JsonbParser {
     }
 
 private:
+    template <typename JsonValue>
+    static Status write_null(JsonValue& value, JsonbWriter& writer) {
+        if (!value.is_null()) {
+            return Status::InvalidArgument("Invalid null literal");
+        }
+        if (writer.writeNull() == 0) {
+            return Status::InvalidArgument("writeNull failed");
+        }
+        return Status::OK();
+    }
+
+    static Status parse_number(simdjson::ondemand::value& value, JsonbWriter& 
writer) {
+        simdjson::ondemand::number num;
+        const auto result = value.get_number().get(num);
+        const auto raw_token = trim_json_whitespace(value.raw_json_token());
+        if (!parse_number_success(result, raw_token)) {
+            return Status::InvalidArgument(
+                    fmt::format("simdjson get_number failed: {}", 
simdjson::error_message(result)));
+        }
+        return write_number(num, value.get_number_type(), raw_token, writer);
+    }
+
     // parse json, recursively if necessary, by simdjson
     //  and serialize to binary format by writer
     static Status parse(simdjson::ondemand::value value, JsonbWriter& writer) {
         switch (value.type()) {
         case simdjson::ondemand::json_type::null: {
-            if (writer.writeNull() == 0) {
-                return Status::InvalidArgument("writeNull failed");
-            }
-            break;
+            return write_null(value, writer);
         }
         case simdjson::ondemand::json_type::boolean: {
             if (writer.writeBool(value.get_bool()) == 0) {
@@ -158,16 +254,7 @@ private:
             break;
         }
         case simdjson::ondemand::json_type::number: {
-            simdjson::ondemand::number num;
-            auto res = value.get_number().get(num);
-            if (!parse_number_success(res)) {
-                return Status::InvalidArgument(fmt::format("simdjson 
get_number failed: {}",
-                                                           
simdjson::error_message(res)));
-            }
-
-            RETURN_IF_ERROR(
-                    write_number(num, value.get_number_type(), 
value.raw_json_token(), writer));
-            break;
+            return parse_number(value, writer);
         }
         case simdjson::ondemand::json_type::object: {
             if (!writer.writeStartObject()) {
@@ -196,7 +283,6 @@ private:
 
             if (!writer.writeEndObject()) {
                 return Status::InvalidArgument("writeEndObject failed");
-                break;
             }
 
             break;
@@ -224,6 +310,50 @@ private:
         return Status::OK();
     }
 
+    static Status write_floating_number(double number, std::string_view 
raw_string,
+                                        JsonbWriter& writer) {
+        // When a double exceeds the precision that can be represented by a 
double type in
+        // simdjson, it gets converted to 0. The correct approach is to 
truncate the value instead.
+        if (number == 0) {
+            StringParser::ParseResult result;
+            number = StringParser::string_to_float<double>(raw_string.data(), 
raw_string.size(),
+                                                           &result);
+            if (result != StringParser::PARSE_SUCCESS) {
+                return Status::InvalidArgument("invalid number, raw string is: 
" +
+                                               std::string(raw_string));
+            }
+        }
+        if (writer.writeDouble(number) == 0) {
+            return Status::InvalidArgument("writeDouble failed");
+        }
+        return Status::OK();
+    }
+
+    static Status write_big_integer(std::string_view raw_string, JsonbWriter& 
writer) {
+        StringParser::ParseResult result;
+        auto value = StringParser::string_to_int<int128_t>(raw_string.data(), 
raw_string.size(),
+                                                           &result);
+        if (result == StringParser::PARSE_SUCCESS) {
+            if (!writer.writeInt128(value)) {
+                return Status::InvalidArgument("writeInt128 failed");
+            }
+            return Status::OK();
+        }
+
+        // JSON text can represent integers beyond int128. Preserve the 
existing fallback to
+        // double, even though the conversion may lose precision.
+        double double_value = 
StringParser::string_to_float<double>(raw_string.data(),
+                                                                    
raw_string.size(), &result);
+        if (result != StringParser::PARSE_SUCCESS) {
+            return Status::InvalidArgument("invalid number, raw string is: " +
+                                           std::string(raw_string));
+        }
+        if (!writer.writeDouble(double_value)) {
+            return Status::InvalidArgument("writeDouble failed");
+        }
+        return Status::OK();
+    }
+
     static Status write_string(std::string_view str, JsonbWriter& writer) {
         // start writing string
         if (!writer.writeStartString()) {
@@ -259,24 +389,7 @@ private:
 
         switch (num_type) {
         case simdjson::ondemand::number_type::floating_point_number: {
-            double number = num.get_double();
-            // When a double exceeds the precision that can be represented by 
a double type in simdjson, it gets converted to 0.
-            // The correct approach, should be to truncate the double value 
instead.
-            if (number == 0) {
-                StringParser::ParseResult result;
-                number = 
StringParser::string_to_float<double>(raw_string.data(), raw_string.size(),
-                                                               &result);
-                if (result != StringParser::PARSE_SUCCESS) {
-                    return Status::InvalidArgument("invalid number, raw string 
is: " +
-                                                   std::string(raw_string));
-                }
-            }
-
-            if (writer.writeDouble(number) == 0) {
-                return Status::InvalidArgument("writeDouble failed");
-            }
-
-            break;
+            return write_floating_number(num.get_double(), raw_string, writer);
         }
         case simdjson::ondemand::number_type::signed_integer:
         case simdjson::ondemand::number_type::unsigned_integer: {
@@ -301,36 +414,13 @@ private:
             if (!success) {
                 return Status::InvalidArgument("writeInt failed");
             }
-            break;
+            return Status::OK();
         }
         case simdjson::ondemand::number_type::big_integer: {
-            StringParser::ParseResult result;
-            auto val = 
StringParser::string_to_int<int128_t>(raw_string.data(), raw_string.size(),
-                                                             &result);
-            if (result != StringParser::PARSE_SUCCESS) {
-                // If the string exceeds the range of int128_t, it will 
attempt to convert it to double.
-                // This may result in loss of precision, but for JSON, 
exchanging data as plain text between different systems may inherently cause 
precision loss.
-                // try parse as double
-                double double_val = StringParser::string_to_float<double>(
-                        raw_string.data(), raw_string.size(), &result);
-                if (result != StringParser::PARSE_SUCCESS) {
-                    // if both parse failed, return error
-                    return Status::InvalidArgument("invalid number, raw string 
is: " +
-                                                   std::string(raw_string));
-                }
-                if (!writer.writeDouble(double_val)) {
-                    return Status::InvalidArgument("writeDouble failed");
-                }
-            } else {
-                // as int128_t
-                if (!writer.writeInt128(val)) {
-                    return Status::InvalidArgument("writeInt128 failed");
-                }
-            }
-            break;
+            return write_big_integer(raw_string, writer);
         }
         }
-        return Status::OK();
+        return Status::InvalidArgument("unknown number type");
     }
 };
 } // namespace doris
diff --git a/be/test/util/jsonb_parser_simd_test.cpp 
b/be/test/util/jsonb_parser_simd_test.cpp
index 8f5b418a193..8ccbaf46519 100644
--- a/be/test/util/jsonb_parser_simd_test.cpp
+++ b/be/test/util/jsonb_parser_simd_test.cpp
@@ -15,6 +15,8 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#include <string_view>
+
 #include "common/status.h"
 #include "core/value/jsonb_value.h"
 #include "gtest/gtest.h"
@@ -221,6 +223,10 @@ TEST_F(JsonbParserTest, ParseJsonWithLongInt2) {
     std::string_view json_with_long_int = R"(19389892839283982938923)";
     std::string_view expected_json_with_long_int = 
R"(19389892839283982938923)";
     EXPECT_EQ(parse_json_and_check(json_with_long_int, 
expected_json_with_long_int), Status::OK());
+
+    std::string_view json_with_whitespace = " \n19389892839283982938923\t";
+    EXPECT_EQ(parse_json_and_check(json_with_whitespace, 
expected_json_with_long_int),
+              Status::OK());
 }
 
 TEST_F(JsonbParserTest, ParseJsonWithLongInt3) {
@@ -255,6 +261,33 @@ TEST_F(JsonbParserTest, ParseJsonWithInvalidNumberFormat) {
     EXPECT_FALSE(parse_json_and_check(json_with_invalid_number, 
json_with_invalid_number));
 }
 
+TEST_F(JsonbParserTest, RejectInvalidTokens) {
+    constexpr std::string_view invalid_json[] = {"nul", "[nul]", 
R"({"x":nul})", "01",    "-01",
+                                                 "1.",  "[01]",  "[-01]",      
  "[1.]",  "1e+",
+                                                 "{}x", "[]x",   "{} {}",      
  "[] []", "nullx"};
+
+    for (const auto json : invalid_json) {
+        JsonBinaryValue jsonb_val;
+        EXPECT_FALSE(jsonb_val.from_json_string(json.data(), 
json.size()).ok()) << json;
+    }
+}
+
+TEST_F(JsonbParserTest, PreserveValidNumbersBeyondDoubleRange) {
+    constexpr std::string_view oversized_floating_point_json[] = {"9.6E400", 
"1e9999", "[9.6E400]",
+                                                                  
R"({"value":1e9999})"};
+
+    for (const auto json : oversized_floating_point_json) {
+        JsonBinaryValue jsonb_val;
+        const auto status = jsonb_val.from_json_string(json.data(), 
json.size());
+        EXPECT_TRUE(status.ok()) << status;
+    }
+
+    const std::string oversized_integer(400, '9');
+    JsonBinaryValue jsonb_val;
+    const auto status = jsonb_val.from_json_string(oversized_integer);
+    EXPECT_TRUE(status.ok()) << status;
+}
+
 TEST_F(JsonbParserTest, ParseJsonWithInvalidBoolean) {
     std::string_view json_with_invalid_boolean = R"({"invalid_bool": True})";
     EXPECT_FALSE(parse_json_and_check(json_with_invalid_boolean, 
json_with_invalid_boolean));
@@ -437,4 +470,4 @@ TEST_F(JsonbParserTest, ParseJsonWithEscapedNulInKey) {
     EXPECT_EQ(parse_json_and_check(json_with_nul, expected_json_with_nul), 
Status::OK());
 }
 
-} // namespace doris
\ No newline at end of file
+} // namespace doris
diff --git 
a/regression-test/data/datatype_p0/json/test_json_cast_validation.out 
b/regression-test/data/datatype_p0/json/test_json_cast_validation.out
new file mode 100644
index 00000000000..b5d2a614a38
--- /dev/null
+++ b/regression-test/data/datatype_p0/json/test_json_cast_validation.out
@@ -0,0 +1,23 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !non_strict --
+1      null    false
+2      \N      true
+3      \N      true
+4      \N      true
+5      \N      true
+6      \N      true
+7      \N      true
+8      18446744073709551616    false
+
+-- !persisted --
+1      null    false
+2      \N      true
+3      \N      true
+4      \N      true
+5      \N      true
+6      \N      true
+7      \N      true
+8      18446744073709551616    false
+
+-- !strict_insert_atomic --
+0
diff --git 
a/regression-test/suites/datatype_p0/json/test_json_cast_validation.groovy 
b/regression-test/suites/datatype_p0/json/test_json_cast_validation.groovy
new file mode 100644
index 00000000000..a2031804315
--- /dev/null
+++ b/regression-test/suites/datatype_p0/json/test_json_cast_validation.groovy
@@ -0,0 +1,80 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_json_cast_validation", "p0") {
+    sql "drop table if exists test_json_cast_validation_src"
+    sql """
+        create table test_json_cast_validation_src (
+            id int,
+            s varchar(30)
+        ) duplicate key(id)
+        distributed by hash(id) buckets 1
+        properties("replication_num" = "1")
+    """
+    sql """
+        insert into test_json_cast_validation_src values
+            (1, 'null'),
+            (2, 'nul'),
+            (3, '[nul]'),
+            (4, '{\"x\":nul}'),
+            (5, '01'),
+            (6, '-01'),
+            (7, '1.'),
+            (8, '18446744073709551616')
+    """
+
+    sql "set enable_strict_cast = false"
+    order_qt_non_strict """
+        select id, cast(s as json), cast(s as json) is null
+        from test_json_cast_validation_src
+        order by id
+    """
+
+    sql "drop table if exists test_json_cast_validation_sink"
+    sql """
+        create table test_json_cast_validation_sink (
+            id int,
+            j json
+        ) duplicate key(id)
+        distributed by hash(id) buckets 1
+        properties("replication_num" = "1")
+    """
+    sql """
+        insert into test_json_cast_validation_sink
+        select id, cast(s as json) from test_json_cast_validation_src
+    """
+    order_qt_persisted """
+        select id, j, j is null
+        from test_json_cast_validation_sink
+        order by id
+    """
+
+    sql "truncate table test_json_cast_validation_sink"
+    sql "set enable_strict_cast = true"
+    test {
+        sql "select cast(s as json) from test_json_cast_validation_src where 
id = 2"
+        exception "Failed to parse json string"
+    }
+    test {
+        sql """
+            insert into test_json_cast_validation_sink
+            select id, cast(s as json) from test_json_cast_validation_src
+        """
+        exception "Failed to parse json string"
+    }
+    qt_strict_insert_atomic "select count(*) from 
test_json_cast_validation_sink"
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to