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

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

commit cbf56d29da46f1191ef309d4d8e5a807948f3521
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Sep 23 08:35:01 2026 +0800

    branch-4.1: [fix](be) Reject oversized Unix timestamps before year 
narrowing #68115 (#68364)
    
    Cherry-picked from #68115
    
    Co-authored-by: HappenLee <[email protected]>
---
 be/src/core/value/vdatetime_value.h                |   4 +
 be/src/exprs/function/date_time_transforms.h       |   2 +-
 .../function_date_or_datetime_computation.h        |   5 +-
 be/test/exprs/function/function_time_test.cpp      |  37 ++++++++
 .../datatype_p0/date/test_unix_timestamp_range.out |  28 ++++++
 .../date/test_unix_timestamp_range.groovy          | 102 +++++++++++++++++++++
 6 files changed, 175 insertions(+), 3 deletions(-)

diff --git a/be/src/core/value/vdatetime_value.h 
b/be/src/core/value/vdatetime_value.h
index a3a13805609..5b5f768c0e2 100644
--- a/be/src/core/value/vdatetime_value.h
+++ b/be/src/core/value/vdatetime_value.h
@@ -209,6 +209,10 @@ static constexpr uint64_t MAX_DATETIME_V2 = 
((uint64_t)MAX_DATE_V2 << TIME_PART_
 static constexpr uint64_t MIN_DATETIME_V2 = (uint64_t)MIN_DATE_V2 << 
TIME_PART_LENGTH;
 
 static constexpr uint32_t MAX_YEAR = 9999;
+// UTC 9999-12-31 23:59:59 plus one day for time zone offsets. Reject larger 
inputs
+// before narrowing the civil year; the converted date still needs its exact 
range check.
+static constexpr int64_t MAX_UNIX_TIMESTAMP_WITH_TIMEZONE =
+        253402300799LL + HOUR_PER_DAY * SECOND_PER_HOUR;
 static constexpr uint32_t MAX_MONTH = 12;
 static constexpr uint32_t MAX_HOUR = 23;
 static constexpr uint32_t MAX_MINUTE = 59;
diff --git a/be/src/exprs/function/date_time_transforms.h 
b/be/src/exprs/function/date_time_transforms.h
index e1b8c22b361..c5de24aa845 100644
--- a/be/src/exprs/function/date_time_transforms.h
+++ b/be/src/exprs/function/date_time_transforms.h
@@ -410,7 +410,7 @@ struct FromUnixTimeImpl {
 
     [[nodiscard]] static bool check_valid(const ArgType& val) {
         if constexpr (NewVersion) {
-            if (val < 0) [[unlikely]] {
+            if (val < 0 || val > MAX_UNIX_TIMESTAMP_WITH_TIMEZONE) 
[[unlikely]] {
                 return false;
             }
         } else {
diff --git a/be/src/exprs/function/function_date_or_datetime_computation.h 
b/be/src/exprs/function/function_date_or_datetime_computation.h
index 2350266efbb..f20c544cb5e 100644
--- a/be/src/exprs/function/function_date_or_datetime_computation.h
+++ b/be/src/exprs/function/function_date_or_datetime_computation.h
@@ -1292,12 +1292,13 @@ struct TimestampToDateTime : IFunction {
                 continue;
             }
             Int64 value = column_data.get_element(i);
-            if (value < 0) [[unlikely]] {
+            const Int64 seconds = value / Impl::ratio;
+            if (value < 0 || seconds > MAX_UNIX_TIMESTAMP_WITH_TIMEZONE) 
[[unlikely]] {
                 throw_out_of_bound_int(name, value);
             }
 
             auto& dt = 
reinterpret_cast<DateV2Value<DateTimeV2ValueType>&>(res_data[i]);
-            dt.from_unixtime(value / Impl::ratio, time_zone);
+            dt.from_unixtime(seconds, time_zone);
 
             if (!dt.is_valid_date()) [[unlikely]] {
                 throw_out_of_bound_int(name, value);
diff --git a/be/test/exprs/function/function_time_test.cpp 
b/be/test/exprs/function/function_time_test.cpp
index 0ddc2771a59..738129640a9 100644
--- a/be/test/exprs/function/function_time_test.cpp
+++ b/be/test/exprs/function/function_time_test.cpp
@@ -17,6 +17,7 @@
 
 #include <gtest/gtest.h>
 
+#include <limits>
 #include <string>
 
 #include "core/data_type/data_type_date.h"
@@ -234,6 +235,42 @@ TEST(VTimestampFunctionsTest, from_unix_test) {
     }
 }
 
+TEST(VTimestampFunctionsTest, from_unixtime_rejects_year_wrap) {
+    TimezoneUtils::load_timezones_to_cache();
+    // The first two values produce civil years 67506 and 133042 in UTC. 
Narrowing
+    // either year to uint16_t produces 1970, which used to pass date 
validation.
+    const int64_t timestamps[] = {2068116364800LL, 4136232816000LL, 
1789000000000000000LL,
+                                  std::numeric_limits<int64_t>::max()};
+    for (const int64_t seconds : timestamps) {
+        SCOPED_TRACE(seconds);
+        const DataSet data_set = {{{seconds}, std::string("unused")}};
+        EXPECT_FALSE(
+                (check_function<DataTypeString, true>(
+                         "from_unixtime_new", {PrimitiveType::TYPE_BIGINT}, 
data_set, -1, -1, true)
+                         .ok()));
+        EXPECT_FALSE((check_function<DataTypeString>("from_unixtime_new",
+                                                     {ConstedNotnull 
{PrimitiveType::TYPE_BIGINT}},
+                                                     data_set, -1, -1, true)
+                              .ok()));
+    }
+}
+
+TEST(VTimestampFunctionsTest, timestamp_units_reject_year_wrap) {
+    TimezoneUtils::load_timezones_to_cache();
+    for (const int64_t seconds : {2068116364800LL, 4136232816000LL}) {
+        SCOPED_TRACE(seconds);
+        for (const auto& [name, ratio] : {std::pair {"from_second", int64_t 
{1}},
+                                          std::pair {"from_millisecond", 
int64_t {1000}},
+                                          std::pair {"from_microsecond", 
int64_t {1000000}}}) {
+            SCOPED_TRACE(name);
+            const DataSet data_set = {{{seconds * ratio}, 
std::string("unused")}};
+            EXPECT_FALSE((check_function<DataTypeDateTimeV2, true>(
+                                  name, {PrimitiveType::TYPE_BIGINT}, 
data_set, -1, -1, true)
+                                  .ok()));
+        }
+    }
+}
+
 TEST(VTimestampFunctionsTest, unix_timestamp_test) {
     std::string func_name = "unix_timestamp_new";
     TimezoneUtils::load_timezones_to_cache();
diff --git 
a/regression-test/data/datatype_p0/date/test_unix_timestamp_range.out 
b/regression-test/data/datatype_p0/date/test_unix_timestamp_range.out
new file mode 100644
index 00000000000..f49ebed0c75
--- /dev/null
+++ b/regression-test/data/datatype_p0/date/test_unix_timestamp_range.out
@@ -0,0 +1,28 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !upper_0_false --
+9999-12-31 23:59:59    9999-12-31 23:59:59.999999      9999-12-31T23:59:59     
9999-12-31T23:59:59.999 9999-12-31T23:59:59.999999
+
+-- !upper_0_true --
+9999-12-31 23:59:59    9999-12-31 23:59:59.999999      9999-12-31T23:59:59     
9999-12-31T23:59:59.999 9999-12-31T23:59:59.999999
+
+-- !null_0 --
+\N     \N      \N      \N
+
+-- !upper_1_false --
+9999-12-31 23:59:59    9999-12-31 23:59:59.999999      9999-12-31T23:59:59     
9999-12-31T23:59:59.999 9999-12-31T23:59:59.999999
+
+-- !upper_1_true --
+9999-12-31 23:59:59    9999-12-31 23:59:59.999999      9999-12-31T23:59:59     
9999-12-31T23:59:59.999 9999-12-31T23:59:59.999999
+
+-- !null_1 --
+\N     \N      \N      \N
+
+-- !upper_2_false --
+9999-12-31 23:59:59    9999-12-31 23:59:59.999999      9999-12-31T23:59:59     
9999-12-31T23:59:59.999 9999-12-31T23:59:59.999999
+
+-- !upper_2_true --
+9999-12-31 23:59:59    9999-12-31 23:59:59.999999      9999-12-31T23:59:59     
9999-12-31T23:59:59.999 9999-12-31T23:59:59.999999
+
+-- !null_2 --
+\N     \N      \N      \N
+
diff --git 
a/regression-test/suites/datatype_p0/date/test_unix_timestamp_range.groovy 
b/regression-test/suites/datatype_p0/date/test_unix_timestamp_range.groovy
new file mode 100644
index 00000000000..dd23a7dd94f
--- /dev/null
+++ b/regression-test/suites/datatype_p0/date/test_unix_timestamp_range.groovy
@@ -0,0 +1,102 @@
+// 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_unix_timestamp_range") {
+    sql "DROP TABLE IF EXISTS test_unix_timestamp_range_values"
+    sql """
+        CREATE TABLE test_unix_timestamp_range_values (
+            id INT,
+            seconds BIGINT,
+            milliseconds BIGINT,
+            microseconds BIGINT
+        )
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES("replication_num" = "1")
+    """
+    // The first two inputs reach civil years 67506 and 133042. Both years used
+    // to wrap to 1970 when passed to the uint16_t date setter.
+    def invalidValues = [
+        [2068116364800L, 2068116364800000L, 2068116364800000000L],
+        [4136232816000L, 4136232816000000L, 4136232816000000000L],
+        [Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE]
+    ]
+    invalidValues.eachWithIndex { values, index ->
+        sql """INSERT INTO test_unix_timestamp_range_values
+               VALUES (${index}, ${values[0]}, ${values[1]}, ${values[2]})"""
+    }
+    sql "INSERT INTO test_unix_timestamp_range_values VALUES (3, NULL, NULL, 
NULL)"
+
+    def functions = [
+        ["from_unixtime", "seconds", 0, ""],
+        ["from_unixtime", "seconds", 0, ", '%Y-%m-%d %H:%i:%s'"],
+        ["from_second", "seconds", 0, ""],
+        ["from_millisecond", "milliseconds", 1, ""],
+        ["from_microsecond", "microseconds", 2, ""]
+    ]
+    def zones = [
+        ["+00:00", 253402300799L],
+        ["+14:00", 253402250399L],
+        ["-12:00", 253402343999L]
+    ]
+    zones.eachWithIndex { zone, zoneIndex ->
+        sql "SET time_zone = '${zone[0]}'"
+        [false, true].each { skipFold ->
+            sql "SET debug_skip_fold_constant = ${skipFold}"
+            invalidValues.eachWithIndex { values, index ->
+                functions.each { function ->
+                    def name = function[0]
+                    def argument = function[1]
+                    def value = values[function[2]]
+                    def format = function[3]
+                    test {
+                        sql "SELECT ${name}(${value}${format})"
+                        exception "Operation ${name}"
+                    }
+                    test {
+                        sql """SELECT ${name}(${argument}${format})
+                               FROM test_unix_timestamp_range_values WHERE id 
= ${index}"""
+                        exception "Operation ${name}"
+                    }
+                }
+            }
+
+            // The exact upper bound depends on the session time zone. In 
-12:00,
+            // a valid local date can have a UTC timestamp beyond the UTC year 
boundary.
+            def lastSecond = zone[1]
+            "order_qt_upper_${zoneIndex}_${skipFold}" """
+                SELECT from_unixtime(${lastSecond}),
+                       from_unixtime(CAST('${lastSecond}.999999' AS 
DECIMAL(18,6))),
+                       from_second(${lastSecond}),
+                       from_millisecond(${lastSecond * 1000L + 999L}),
+                       from_microsecond(${lastSecond * 1000000L + 999999L})
+            """
+            functions.each { function ->
+                def nextSecond = lastSecond + 1L
+                def ratios = [1L, 1000L, 1000000L]
+                test {
+                    sql "SELECT ${function[0]}(${nextSecond * 
ratios[function[2]]}${function[3]})"
+                    exception "Operation ${function[0]}"
+                }
+            }
+        }
+        "qt_null_${zoneIndex}" """
+            SELECT from_unixtime(seconds), from_second(seconds),
+                   from_millisecond(milliseconds), 
from_microsecond(microseconds)
+            FROM test_unix_timestamp_range_values WHERE id = 3 ORDER BY id
+        """
+    }
+}


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

Reply via email to