From 04354d78d21c53366cb75b69b4fc019ed9414fe5 Mon Sep 17 00:00:00 2001 From: Pearu Peterson Date: Mon, 19 Oct 2026 20:52:51 +0300 Subject: [PATCH] GH-54646: [C++] Reject years out of range in the strptime timestamp parser The year was computed as tm_year + 1900, which can overflow int (e.g. with %s), and passed to date::year, which holds 16 bits, so out-of-range years gave wrong timestamps. Compute the year in 64 bits and reject it if date::year cannot hold it. Drafted by Claude Code (an AI agent) and reviewed & approved by pearu. --- cpp/src/arrow/util/value_parsing.h | 9 ++++++++- cpp/src/arrow/util/value_parsing_test.cc | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cpp/src/arrow/util/value_parsing.h b/cpp/src/arrow/util/value_parsing.h index 1b300074851..1c2102fe370 100644 --- a/cpp/src/arrow/util/value_parsing.h +++ b/cpp/src/arrow/util/value_parsing.h @@ -829,11 +829,18 @@ static inline bool ParseTimestampStrptime(const char* buf, size_t length, if (!allow_trailing_chars && static_cast(ret - clean_copy.c_str()) != length) { return false; } + // tm_year + 1900 can overflow int (e.g. with %s), and date::year only + // holds 16 bits + const int64_t year = static_cast(result.tm_year) + 1900; + if (year < static_cast(arrow_vendored::date::year::min()) || + year > static_cast(arrow_vendored::date::year::max())) { + return false; + } // Reject impossible dates such as February 30 or a zero day, which strptime // may accept (the day of month is checked against 31 at most), instead of // rolling them over into the next month; consistent with the ISO-8601 parser. const arrow_vendored::date::year_month_day ymd = - arrow_vendored::date::year(result.tm_year + 1900) / (result.tm_mon + 1) / + arrow_vendored::date::year(static_cast(year)) / (result.tm_mon + 1) / result.tm_mday; if (!ymd.ok()) { return false; diff --git a/cpp/src/arrow/util/value_parsing_test.cc b/cpp/src/arrow/util/value_parsing_test.cc index 69baba169ff..875418f3195 100644 --- a/cpp/src/arrow/util/value_parsing_test.cc +++ b/cpp/src/arrow/util/value_parsing_test.cc @@ -928,6 +928,25 @@ TEST(TimestampParser, StrptimeWithoutDay) { ASSERT_EQ(expected, converted); } +TEST(TimestampParser, StrptimeYearOutOfRange) { + auto parser = TimestampParser::MakeStrptime("%s"); + int64_t converted = 0; + // A value past 2038 (2100-01-01), as a strptime that parses %s into a 32-bit + // time_t wraps larger values into ordinary dates + if (!(*parser)("4102444800", 10, TimeUnit::SECOND, &converted) || + converted != 4102444800) { + GTEST_SKIP() << "strptime does not support 64-bit %s values on this platform"; + } + + // Years that do not fit in a date::year are rejected rather than wrapped, + // also where tm_year + 1900 would overflow int + for (const std::string value : + {"1000000000000000", "67768036000000000", "67768036191676799"}) { + SCOPED_TRACE(value); + ASSERT_FALSE((*parser)(value.c_str(), value.size(), TimeUnit::SECOND, &converted)); + } +} + TEST(TimestampParser, StrptimeZoneOffset) { if (!kStrptimeSupportsZone) { GTEST_SKIP() << "strptime does not support %z on this platform";