From d829661d2fdee93fa353ffcc94eda10cddb96286 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Fri, 1 May 2026 12:35:59 -0700 Subject: [PATCH 1/8] Fix stack buffer write overrun in `chrono::_Time_parse_fields::_Get_int()`. Testing against `cend(_Ac) - 1` is the fix. Add a comment explaining why. Moving `*_Ptr = _Ch;` inside the bounds test isn't necessary for the fix, but avoids potentially wacky behavior. If we continue to consume digits, we should exhibit the commented behavior of "drop trailing digits if already too large". Previously, we would keep updating the last digit, which makes no sense. --- stl/inc/chrono | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stl/inc/chrono b/stl/inc/chrono index 6c82f5177d..1dfd8d6a3b 100644 --- a/stl/inc/chrono +++ b/stl/inc/chrono @@ -4212,8 +4212,8 @@ namespace chrono { char _Ch; while (_First != _Last && _Width > 0 && '0' <= (_Ch = _Ctype_fac.narrow(*_First)) && _Ch <= '9') { // copy digits - *_Ptr = _Ch; - if (_Ptr < _STD cend(_Ac)) { + if (_Ptr < _STD cend(_Ac) - 1) { // preserve room for the null terminator written below + *_Ptr = _Ch; ++_Ptr; // drop trailing digits if already too large } ++_First; From 1e4e7500993df6d893bb2d6d5bf2591585ee3762 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Fri, 1 May 2026 12:41:26 -0700 Subject: [PATCH 2/8] ``: Improve `_Getint_v2()`. Add the same comment to the `_Ptr < _Pe` test. Similarly, move `*_Ptr = _Ch;` inside the bounds test, in accordance with the "drop trailing digits if already too large" comment. AFAICT, this has no behavioral impact since we're additionally limited by `_Digits_read < _Hi_digits` and `_Hi_digits` is at most 4. --- stl/inc/xloctime | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stl/inc/xloctime b/stl/inc/xloctime index 01e9697fd5..f86e4aa0af 100644 --- a/stl/inc/xloctime +++ b/stl/inc/xloctime @@ -57,8 +57,8 @@ ios_base::iostate _Getint_v2(_InIt& _First, _InIt& _Last, int _Lo, int _Hi, int& for (char* const _Pe = &_Ac[_Max_int_dig - 1]; _First != _Last && '0' <= (_Ch = _Ctype_fac.narrow(*_First)) && _Ch <= '9' && _Digits_read < _Hi_digits; ++_Digits_read, (void) ++_First) { // copy digits - *_Ptr = _Ch; - if (_Ptr < _Pe) { + if (_Ptr < _Pe) { // preserve room for the null terminator written below + *_Ptr = _Ch; ++_Ptr; // drop trailing digits if already too large } } From ff668214cd65931e483787b0af1429dd1c85f67e Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Fri, 1 May 2026 13:01:37 -0700 Subject: [PATCH 3/8] Avoid unnecessarily confusing overloading: `test_parse()` => `test_all_parse()` --- tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp index 8ee6020e17..73e9178f11 100644 --- a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp +++ b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp @@ -1297,7 +1297,7 @@ void test_lwg_3956() { } } -void test_parse() { +void test_all_parse() { test_lwg_3536(); test_lwg_3956(); parse_seconds(); @@ -1319,7 +1319,7 @@ void test_parse() { void test() { test_duration_output(); - test_parse(); + test_all_parse(); } int main() { From 8f5dafd289126121ebb40034de6a3d9d965e7935 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Fri, 1 May 2026 13:06:30 -0700 Subject: [PATCH 4/8] Reorder `test_lwg_3536()`, `test_lwg_3956()`, no other changes. We try to call test functions in definition order, and we usually test LWG resolutions last. --- .../test.cpp | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp index 73e9178f11..c76819792a 100644 --- a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp +++ b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp @@ -208,29 +208,6 @@ void test_limits(const char* flag, const IntType min, const IntType max) { assert(value == TimeType{max}); } -void test_lwg_3536() { - // LWG-3536, "Should chrono::from_stream() assign zero to duration for failure?" - minutes mm{20}; - - { - istringstream iss{"2:2:30"}; - iss >> parse("%H:%M:%S", mm); - assert(iss.fail() && mm == 20min); - } - - { - istringstream iss{"June"}; - iss >> parse("%B", mm); - assert(iss.fail() && mm == 20min); - } - - { - istringstream iss{""}; - iss >> parse("%B", mm); - assert(iss.fail() && mm == 20min); - } -} - void parse_seconds() { seconds time; test_parse("1", "%S", time); @@ -1254,6 +1231,29 @@ void test_io_manipulator() { fail_parse(WIDEN(CharT, "a b"), CStringOrStdString{WIDEN(CharT, "a%nb")}, time); } +void test_lwg_3536() { + // LWG-3536, "Should chrono::from_stream() assign zero to duration for failure?" + minutes mm{20}; + + { + istringstream iss{"2:2:30"}; + iss >> parse("%H:%M:%S", mm); + assert(iss.fail() && mm == 20min); + } + + { + istringstream iss{"June"}; + iss >> parse("%B", mm); + assert(iss.fail() && mm == 20min); + } + + { + istringstream iss{""}; + iss >> parse("%B", mm); + assert(iss.fail() && mm == 20min); + } +} + namespace lwg_3956 { struct has_adl_from_stream { int value = 0; @@ -1298,8 +1298,6 @@ void test_lwg_3956() { } void test_all_parse() { - test_lwg_3536(); - test_lwg_3956(); parse_seconds(); parse_minutes(); parse_hours(); @@ -1315,6 +1313,8 @@ void test_all_parse() { test_io_manipulator(); test_io_manipulator(); test_io_manipulator(); + test_lwg_3536(); + test_lwg_3956(); } void test() { From 4ea08c047d374191b53d385d45b445645e3f8939 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sun, 3 May 2026 03:31:00 -0700 Subject: [PATCH 5/8] Enhance test_parse(), fail_parse() to take basic_string and use source_location. --- .../test.cpp | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp index c76819792a..a048ad396a 100644 --- a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp +++ b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp @@ -6,11 +6,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -173,14 +175,40 @@ ios_base::iostate parse_state(const CharT* str, const CStringOrStdString& fmt, P template void test_parse(const CharT* str, const CStringOrStdString& fmt, Parsable& p, - type_identity_t*> abbrev = nullptr, minutes* offset = nullptr) { - assert((parse_state(str, fmt, p, abbrev, offset) & ~ios_base::eofbit) == ios_base::goodbit); + type_identity_t*> abbrev = nullptr, minutes* offset = nullptr, + const source_location sl = source_location::current()) { + const auto masked_state = parse_state(str, fmt, p, abbrev, offset) & ~ios_base::eofbit; + const bool desirable = masked_state == ios_base::goodbit; + if (!desirable) { + printf("test_parse() encountered a problem on line %u.\n", static_cast(sl.line())); + } + assert(desirable); +} + +template +void test_parse(const basic_string& str, const CStringOrStdString& fmt, Parsable& p, + type_identity_t*> abbrev = nullptr, minutes* offset = nullptr, + const source_location sl = source_location::current()) { + test_parse(str.c_str(), fmt, p, abbrev, offset, sl); } template void fail_parse(const CharT* str, const CStringOrStdString& fmt, Parsable& p, - type_identity_t*> abbrev = nullptr, minutes* offset = nullptr) { - assert((parse_state(str, fmt, p, abbrev, offset) & ~ios_base::eofbit) != ios_base::goodbit); + type_identity_t*> abbrev = nullptr, minutes* offset = nullptr, + const source_location sl = source_location::current()) { + const auto masked_state = parse_state(str, fmt, p, abbrev, offset) & ~ios_base::eofbit; + const bool desirable = masked_state != ios_base::goodbit; + if (!desirable) { + printf("fail_parse() encountered a problem on line %u.\n", static_cast(sl.line())); + } + assert(desirable); +} + +template +void fail_parse(const basic_string& str, const CStringOrStdString& fmt, Parsable& p, + type_identity_t*> abbrev = nullptr, minutes* offset = nullptr, + const source_location sl = source_location::current()) { + fail_parse(str.c_str(), fmt, p, abbrev, offset, sl); } template From f3a848418f99f2fc63a715b66ca572d5a7b6a575 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sun, 3 May 2026 05:55:27 -0700 Subject: [PATCH 6/8] Fix pre-existing inconsistency: Use `29d` when testing `year_month_day`. --- tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp index a048ad396a..4e11b35147 100644 --- a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp +++ b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp @@ -636,7 +636,7 @@ void parse_calendar_types_basic() { // not ambiguous with year year_month_day ymd; test_parse("60 2004-02-29", "%j %F", ymd); - assert(ymd == 2004y / February / 29); + assert(ymd == 2004y / February / 29d); // basic year_month_day tests // different ways of specifying year From 84e3f5364bc3eeef36f379b9917a40db1918e154 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Mon, 4 May 2026 10:54:43 -0700 Subject: [PATCH 7/8] Pre-existing: Use `basic_istringstream`, `basic_ostringstream` instead of `basic_stringstream`. --- tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp index 4e11b35147..b96affdccb 100644 --- a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp +++ b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp @@ -38,7 +38,7 @@ bool test_duration_basic_out(const duration& d, const CharT* expect template bool test_duration_locale_out() { - basic_stringstream ss; + basic_ostringstream ss; const duration d{0.140625}; ss.precision(3); ss << d; @@ -155,7 +155,7 @@ ios_base::iostate parse_state(const CharT* str, const CStringOrStdString& fmt, P *offset = minutes::min(); } - basic_stringstream sstr{str}; + basic_istringstream sstr{str}; if (abbrev) { if (offset) { sstr >> parse(fmt, p, *abbrev, *offset); From 2986a56732de36e4659c1a71599efde454e50fc8 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sun, 3 May 2026 03:31:57 -0700 Subject: [PATCH 8/8] Add tests: parse_modified_maximum_characters() --- .../test.cpp | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp index b96affdccb..9d26f33b46 100644 --- a/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp +++ b/tests/std/tests/P0355R7_calendars_and_time_zones_io/test.cpp @@ -1226,6 +1226,187 @@ void parse_timepoints() { assert(ut == clock_cast(sys_days{1d / January / 2017y}) - 1s); } +void parse_modified_maximum_characters() { + const string hundred_digits(100, '1'); + const string seventy_zeroes(70, '0'); + + { + day d{7}; + test_parse(hundred_digits, "%d", d); + assert(d == day{11}); + fail_parse(seventy_zeroes + "22", "%d", d); // 0 is not a valid day of the month + fail_parse(hundred_digits, "%100d", d); // overflow + test_parse(seventy_zeroes + "22", "%100d", d); + assert(d == day{22}); + + test_parse(hundred_digits, "%e", d); + assert(d == day{11}); + fail_parse(seventy_zeroes + "22", "%e", d); // 0 is not a valid day of the month + fail_parse(hundred_digits, "%100e", d); // overflow + test_parse(seventy_zeroes + "22", "%100e", d); + assert(d == day{22}); + } + + { + hours h{33h}; + test_parse(hundred_digits, "%H", h); + assert(h == 11h); + test_parse(seventy_zeroes + "22", "%H", h); + assert(h == 0h); + fail_parse(hundred_digits, "%100H", h); // overflow + test_parse(seventy_zeroes + "22", "%100H", h); + assert(h == 22h); + + test_parse(hundred_digits, "%I", h); + assert(h == 11h); + fail_parse(seventy_zeroes + "7", "%I", h); // 0 is not a valid 12-hour clock + fail_parse(hundred_digits, "%100I", h); // overflow + test_parse(seventy_zeroes + "7", "%100I", h); + assert(h == 7h); + } + + { + days ds{22}; + test_parse(hundred_digits, "%j", ds); + assert(ds == days{111}); + test_parse(seventy_zeroes + "1729", "%j", ds); + assert(ds == days{0}); + fail_parse(hundred_digits, "%100j", ds); // overflow + test_parse(seventy_zeroes + "1729", "%100j", ds); + assert(ds == days{1729}); + } + + { + year_month_day ymd{1970y / January / 1d}; + test_parse("2030 " + hundred_digits, "%Y %j", ymd); + assert(ymd == 2030y / April / 21d); + fail_parse("2030 " + seventy_zeroes + "124", "%Y %j", ymd); // 0 is not a valid day of the year + fail_parse("2030 " + hundred_digits, "%Y %100j", ymd); // overflow + test_parse("2030 " + seventy_zeroes + "124", "%Y %100j", ymd); + assert(ymd == 2030y / May / 4d); + + fail_parse(hundred_digits + "-02-03", "%100F", ymd); // overflow + test_parse(seventy_zeroes + "1969-07-20", "%100F", ymd); + assert(ymd == 1969y / July / 20d); + + test_parse("2015 4 " + hundred_digits, "%Y %u %U", ymd); + assert(ymd == 2015y / March / 19d); + test_parse("2015 4 " + seventy_zeroes + "6", "%Y %u %U", ymd); + assert(ymd == 2015y / January / 1d); + fail_parse("2015 4 " + hundred_digits, "%Y %u %100U", ymd); // overflow + test_parse("2015 4 " + seventy_zeroes + "6", "%Y %u %100U", ymd); + assert(ymd == 2015y / February / 12d); + + test_parse("2015 4 " + hundred_digits, "%Y %u %W", ymd); + assert(ymd == 2015y / March / 19d); + test_parse("2015 4 " + seventy_zeroes + "6", "%Y %u %W", ymd); + assert(ymd == 2015y / January / 1d); + fail_parse("2015 4 " + hundred_digits, "%Y %u %100W", ymd); // overflow + test_parse("2015 4 " + seventy_zeroes + "6", "%Y %u %100W", ymd); + assert(ymd == 2015y / February / 12d); + + test_parse("4 03 19 " + hundred_digits, "%u %V %C %g", ymd); + assert(ymd == 1911y / January / 19d); + test_parse("4 03 19 " + seventy_zeroes + "33", "%u %V %C %g", ymd); + assert(ymd == 1900y / January / 18d); + fail_parse("4 03 19 " + hundred_digits, "%u %V %C %100g", ymd); // overflow + test_parse("4 03 19 " + seventy_zeroes + "55", "%u %V %C %100g", ymd); + assert(ymd == 1955y / January / 20d); + + test_parse("4 03 " + hundred_digits, "%u %V %G", ymd); + assert(ymd == 1111y / January / 19d); + test_parse("4 03 " + seventy_zeroes + "2030", "%u %V %G", ymd); + assert(ymd == 0y / January / 20d); + fail_parse("4 03 " + hundred_digits, "%u %V %100G", ymd); // overflow + test_parse("4 03 " + seventy_zeroes + "2030", "%u %V %100G", ymd); + assert(ymd == 2030y / January / 17d); + + test_parse("4 2015 " + hundred_digits, "%u %G %V", ymd); + assert(ymd == 2015y / March / 12d); + fail_parse("4 2015 " + seventy_zeroes + "33", "%u %G %V", ymd); // 0 is not a valid ISO week + fail_parse("4 2015 " + hundred_digits, "%u %G %100V", ymd); // overflow + test_parse("4 2015 " + seventy_zeroes + "33", "%u %G %100V", ymd); + assert(ymd == 2015y / August / 13d); + } + + { + month mo{January}; + test_parse(hundred_digits, "%m", mo); + assert(mo == November); + fail_parse(seventy_zeroes + "12", "%m", mo); // 0 is not a valid month + fail_parse(hundred_digits, "%100m", mo); // overflow + test_parse(seventy_zeroes + "12", "%100m", mo); + assert(mo == December); + } + + { + minutes mi{22min}; + test_parse(hundred_digits, "%M", mi); + assert(mi == 11min); + test_parse(seventy_zeroes + "55", "%M", mi); + assert(mi == 0min); + fail_parse(hundred_digits, "%100M", mi); // overflow + test_parse(seventy_zeroes + "55", "%100M", mi); + assert(mi == 55min); + } + + { + seconds s{22s}; + test_parse(hundred_digits, "%S", s); + assert(s == 11s); + test_parse(seventy_zeroes + "55", "%S", s); + assert(s == 0s); + fail_parse(hundred_digits, "%100S", s); // overflow + test_parse(seventy_zeroes + "55", "%100S", s); + assert(s == 55s); + } + + { + weekday wd{Thursday}; + test_parse(hundred_digits, "%u", wd); + assert(wd == Monday); + fail_parse(seventy_zeroes + "2", "%u", wd); // 0 is not a valid weekday for %u + fail_parse(hundred_digits, "%100u", wd); // overflow + test_parse(seventy_zeroes + "2", "%100u", wd); + assert(wd == Tuesday); + + test_parse(hundred_digits, "%w", wd); + assert(wd == Monday); + test_parse(seventy_zeroes + "2", "%w", wd); + assert(wd == Sunday); + fail_parse(hundred_digits, "%100w", wd); // overflow + test_parse(seventy_zeroes + "2", "%100w", wd); + assert(wd == Tuesday); + } + + { + year y{1970y}; + test_parse(hundred_digits, "%y", y); + assert(y == 2011y); + test_parse(seventy_zeroes + "55", "%y", y); + assert(y == 2000y); + fail_parse(hundred_digits, "%100y", y); // overflow + test_parse(seventy_zeroes + "55", "%100y", y); + assert(y == 2055y); + + test_parse(hundred_digits, "%Y", y); + assert(y == 1111y); + test_parse(seventy_zeroes + "2026", "%Y", y); + assert(y == 0y); + fail_parse(hundred_digits, "%100Y", y); // overflow + test_parse(seventy_zeroes + "2026", "%100Y", y); + assert(y == 2026y); + + test_parse("33 " + hundred_digits, "%y %C", y); + assert(y == 1133y); + test_parse("33 " + seventy_zeroes + "22", "%y %C", y); + assert(y == 33y); + fail_parse("33 " + hundred_digits, "%y %100C", y); // overflow + test_parse("33 " + seventy_zeroes + "22", "%y %100C", y); + assert(y == 2233y); + } +} + template void test_io_manipulator() { seconds time; @@ -1337,6 +1518,7 @@ void test_all_parse() { parse_incomplete(); parse_whitespace(); parse_timepoints(); + parse_modified_maximum_characters(); test_io_manipulator(); test_io_manipulator(); test_io_manipulator();