From f8d2b13551698346bf1545ef7a0cde18c0701efe Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Fri, 6 Dec 2024 14:07:11 -0600 Subject: [PATCH v30 1/1] Fix various overflow hazards in date and timestamp functions. This commit makes use of the overflow-aware routines in int.h to fix a variety of reported overflow bugs in the date and timestamp code. It seems unlikely that this fixes all such bugs in this area, but since the problems seem limited to cases that are far beyond any realistic use-case, I'm not going to worry too much. Note that for one bug, I've chosen to simply add a comment about the overflow hazard because fixing it would require quite a bit of code restructuring that doesn't seem worth the risk. Reported-by: Alexander Lakhin Author: Matthew Kim, Nathan Bossart Reviewed-by: Joseph Koshakow, Jian He Discussion: https://postgr.es/m/31ad2cd1-db94-bdb3-f91a-65ffdb4bef95%40gmail.com Discussion: https://postgr.es/m/18585-db646741dd649abd%40postgresql.org Backpatch-through: 13 --- src/backend/utils/adt/date.c | 10 +++- src/backend/utils/adt/formatting.c | 80 +++++++++++++++++++++++--- src/backend/utils/adt/timestamp.c | 4 ++ src/include/common/int.h | 51 ++++++++++++++++ src/test/regress/expected/date.out | 2 + src/test/regress/expected/horology.out | 16 ++++++ src/test/regress/sql/date.sql | 1 + src/test/regress/sql/horology.sql | 8 +++ 8 files changed, 163 insertions(+), 9 deletions(-) diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c index eac296e318..a6655f2a2a 100644 --- a/src/backend/utils/adt/date.c +++ b/src/backend/utils/adt/date.c @@ -23,6 +23,7 @@ #include "access/xact.h" #include "common/hashfn.h" +#include "common/int.h" #include "libpq/pqformat.h" #include "miscadmin.h" #include "nodes/supportnodes.h" @@ -248,8 +249,15 @@ make_date(PG_FUNCTION_ARGS) /* Handle negative years as BC */ if (tm.tm_year < 0) { + int year = tm.tm_year; + bc = true; - tm.tm_year = -tm.tm_year; + if (pg_neg_s32_overflow(year, &year)) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_FIELD_OVERFLOW), + errmsg("date field value out of range: %d-%02d-%02d", + tm.tm_year, tm.tm_mon, tm.tm_mday))); + tm.tm_year = year; } dterr = ValidateDate(DTK_DATE_M, false, false, bc, &tm); diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index 5bd4d1967b..bf4c37814a 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -85,6 +85,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "common/int.h" #include "mb/pg_wchar.h" #include "parser/scansup.h" #include "utils/builtins.h" @@ -3682,7 +3683,14 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_INVALID_DATETIME_FORMAT), errmsg("invalid input string for \"Y,YYY\"")))); - years += (millennia * 1000); + + /* years += (millennia * 1000); */ + if (pg_mul_s32_overflow(millennia, 1000, &millennia) || + pg_add_s32_overflow(years, millennia, &years)) + RETURN_ERROR(ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("value for \"Y,YYY\" in source string is out of range")))); + from_char_set_int(&out->year, years, n, have_error); CHECK_ERROR; out->yysz = 4; @@ -4597,10 +4605,29 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, tm->tm_year = tmfc.year % 100; if (tm->tm_year) { + int tmp; + if (tmfc.cc >= 0) - tm->tm_year += (tmfc.cc - 1) * 100; + { + /* tm->tm_year += (tmfc.cc - 1) * 100; */ + tmp = tmfc.cc - 1; + if (pg_mul_s32_overflow(tmp, 100, &tmp) || + pg_add_s32_overflow(tm->tm_year, tmp, &tm->tm_year)) + RETURN_ERROR(DateTimeParseError(DTERR_FIELD_OVERFLOW, + text_to_cstring(date_txt), + "timestamp")); + } else - tm->tm_year = (tmfc.cc + 1) * 100 - tm->tm_year + 1; + { + /* tm->tm_year = (tmfc.cc + 1) * 100 - tm->tm_year + 1; */ + tmp = tmfc.cc + 1; + if (pg_mul_s32_overflow(tmp, 100, &tmp) || + pg_sub_s32_overflow(tmp, tm->tm_year, &tmp) || + pg_add_s32_overflow(tmp, 1, &tm->tm_year)) + RETURN_ERROR(DateTimeParseError(DTERR_FIELD_OVERFLOW, + text_to_cstring(date_txt), + "timestamp")); + } } else { @@ -4626,11 +4653,25 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, if (tmfc.bc) tmfc.cc = -tmfc.cc; if (tmfc.cc >= 0) + { /* +1 because 21st century started in 2001 */ - tm->tm_year = (tmfc.cc - 1) * 100 + 1; + /* tm->tm_year = (tmfc.cc - 1) * 100 + 1; */ + if (pg_mul_s32_overflow(tmfc.cc - 1, 100, &tm->tm_year) || + pg_add_s32_overflow(tm->tm_year, 1, &tm->tm_year)) + RETURN_ERROR(DateTimeParseError(DTERR_FIELD_OVERFLOW, + text_to_cstring(date_txt), + "timestamp")); + } else + { /* +1 because year == 599 is 600 BC */ - tm->tm_year = tmfc.cc * 100 + 1; + /* tm->tm_year = tmfc.cc * 100 + 1; */ + if (pg_mul_s32_overflow(tmfc.cc, 100, &tm->tm_year) || + pg_add_s32_overflow(tm->tm_year, 1, &tm->tm_year)) + RETURN_ERROR(DateTimeParseError(DTERR_FIELD_OVERFLOW, + text_to_cstring(date_txt), + "timestamp")); + } fmask |= DTK_M(YEAR); } @@ -4655,11 +4696,27 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, fmask |= DTK_DATE_M; } else - tmfc.ddd = (tmfc.ww - 1) * 7 + 1; + { + int tmp = 0; + + /* tmfc.ddd = (tmfc.ww - 1) * 7 + 1; */ + if (pg_sub_s32_overflow(tmfc.ww, 1, &tmp) || + pg_mul_s32_overflow(tmp, 7, &tmp) || + pg_add_s32_overflow(tmp, 1, &tmfc.ddd)) + RETURN_ERROR(DateTimeParseError(DTERR_FIELD_OVERFLOW, date_str, "timestamp")); + } } if (tmfc.w) - tmfc.dd = (tmfc.w - 1) * 7 + 1; + { + int tmp = 0; + + /* tmfc.dd = (tmfc.w - 1) * 7 + 1; */ + if (pg_sub_s32_overflow(tmfc.w, 1, &tmp) || + pg_mul_s32_overflow(tmp, 7, &tmp) || + pg_add_s32_overflow(tmp, 1, &tmfc.dd)) + RETURN_ERROR(DateTimeParseError(DTERR_FIELD_OVERFLOW, date_str, "timestamp")); + } if (tmfc.dd) { tm->tm_mday = tmfc.dd; @@ -4723,7 +4780,14 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, } if (tmfc.ms) - *fsec += tmfc.ms * 1000; + { + int tmp = 0; + + /* *fsec += tmfc.ms * 1000; */ + if (pg_mul_s32_overflow(tmfc.ms, 1000, &tmp) || + pg_add_s32_overflow(*fsec, tmp, fsec)) + RETURN_ERROR(DateTimeParseError(DTERR_FIELD_OVERFLOW, date_str, "timestamp")); + } if (tmfc.us) *fsec += tmfc.us; if (fprec) diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index 56bdb6b8d8..4e06d64cf6 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -4468,6 +4468,10 @@ interval_trunc(PG_FUNCTION_ARGS) * * Return the Julian day which corresponds to the first day (Monday) of the given ISO 8601 year and week. * Julian days are used to convert between ISO week dates and Gregorian dates. + * + * XXX: This function has integer overflow hazards, but restructuring it to + * work with the soft-error handling that its callers do is likely more + * trouble than it's worth. */ int isoweek2j(int year, int week) diff --git a/src/include/common/int.h b/src/include/common/int.h index 5ba9dd88e7..86896d1a4f 100644 --- a/src/include/common/int.h +++ b/src/include/common/int.h @@ -32,6 +32,9 @@ * - If a * b overflows, return true, otherwise store the result of a * b * into *result. The content of *result is implementation defined in case of * overflow. + * - If -a overflows, return true, otherwise store the result of -a + * into *result. The content of *result is implementation defined in case of + * overflow. *--------- */ @@ -97,6 +100,22 @@ pg_mul_s16_overflow(int16 a, int16 b, int16 *result) #endif } +static inline bool +pg_neg_s16_overflow(int16 a, int16 *result) +{ +#if defined(HAVE__BUILTIN_OP_OVERFLOW) + return __builtin_sub_overflow(0, a, result); +#else + if (unlikely(a == PG_INT16_MIN)) + { + *result = 0x5EED; /* to avoid spurious warnings */ + return true; + } + *result = -a; + return false; +#endif +} + /* * INT32 */ @@ -154,6 +173,22 @@ pg_mul_s32_overflow(int32 a, int32 b, int32 *result) #endif } +static inline bool +pg_neg_s32_overflow(int32 a, int32 *result) +{ +#if defined(HAVE__BUILTIN_OP_OVERFLOW) + return __builtin_sub_overflow(0, a, result); +#else + if (unlikely(a == PG_INT32_MIN)) + { + *result = 0x5EED; /* to avoid spurious warnings */ + return true; + } + *result = -a; + return false; +#endif +} + /* * INT64 */ @@ -258,6 +293,22 @@ pg_mul_s64_overflow(int64 a, int64 b, int64 *result) #endif } +static inline bool +pg_neg_s64_overflow(int64 a, int64 *result) +{ +#if defined(HAVE__BUILTIN_OP_OVERFLOW) + return __builtin_sub_overflow(0, a, result); +#else + if (unlikely(a == PG_INT64_MIN)) + { + *result = 0x5EED; /* to avoid spurious warnings */ + return true; + } + *result = -a; + return false; +#endif +} + /*------------------------------------------------------------------------ * Overflow routines for unsigned integers *------------------------------------------------------------------------ diff --git a/src/test/regress/expected/date.out b/src/test/regress/expected/date.out index 958b5f15e4..3d3ef4c3d4 100644 --- a/src/test/regress/expected/date.out +++ b/src/test/regress/expected/date.out @@ -1491,6 +1491,8 @@ select make_date(2013, 13, 1); ERROR: date field value out of range: 2013-13-01 select make_date(2013, 11, -1); ERROR: date field value out of range: 2013-11--1 +SELECT make_date(-2147483648, 1, 1); +ERROR: date field value out of range: -2147483648-01-01 select make_time(10, 55, 100.1); ERROR: time field value out of range: 10:55:100.1 select make_time(24, 0, 2.1); diff --git a/src/test/regress/expected/horology.out b/src/test/regress/expected/horology.out index e4b57f6215..d8baed2fbd 100644 --- a/src/test/regress/expected/horology.out +++ b/src/test/regress/expected/horology.out @@ -3269,6 +3269,14 @@ SELECT to_timestamp('2015-02-11 86000', 'YYYY-MM-DD SSSSS'); -- ok SELECT to_timestamp('2015-02-11 86400', 'YYYY-MM-DD SSSSS'); ERROR: date/time field value out of range: "2015-02-11 86400" +SELECT to_timestamp('1000000000,999', 'Y,YYY'); +ERROR: value for "Y,YYY" in source string is out of range +SELECT to_timestamp('0.-2147483648', 'SS.MS'); +ERROR: date/time field value out of range: "0.-2147483648" +SELECT to_timestamp('613566758', 'W'); +ERROR: date/time field value out of range: "613566758" +SELECT to_timestamp('2024 613566758 1', 'YYYY WW D'); +ERROR: date/time field value out of range: "2024 613566758 1" SELECT to_date('2016-13-10', 'YYYY-MM-DD'); ERROR: date/time field value out of range: "2016-13-10" SELECT to_date('2016-02-30', 'YYYY-MM-DD'); @@ -3309,6 +3317,14 @@ SELECT to_date('0000-02-01','YYYY-MM-DD'); -- allowed, though it shouldn't be 02-01-0001 BC (1 row) +SELECT to_date('100000000', 'CC'); +ERROR: date/time field value out of range: "100000000" +SELECT to_date('-100000000', 'CC'); +ERROR: date/time field value out of range: "-100000000" +SELECT to_date('-2147483648 01', 'CC YY'); +ERROR: date/time field value out of range: "-2147483648 01" +SELECT to_date('2147483647 01', 'CC YY'); +ERROR: date/time field value out of range: "2147483647 01" -- -- Check behavior with SQL-style fixed-GMT-offset time zone (cf bug #8572) -- diff --git a/src/test/regress/sql/date.sql b/src/test/regress/sql/date.sql index 8f7435b767..a7a9f67b6f 100644 --- a/src/test/regress/sql/date.sql +++ b/src/test/regress/sql/date.sql @@ -362,5 +362,6 @@ select make_date(0, 7, 15); select make_date(2013, 2, 30); select make_date(2013, 13, 1); select make_date(2013, 11, -1); +SELECT make_date(-2147483648, 1, 1); select make_time(10, 55, 100.1); select make_time(24, 0, 2.1); diff --git a/src/test/regress/sql/horology.sql b/src/test/regress/sql/horology.sql index 13f86ce8fc..5ff60c750c 100644 --- a/src/test/regress/sql/horology.sql +++ b/src/test/regress/sql/horology.sql @@ -542,6 +542,10 @@ SELECT to_timestamp('2015-02-11 86000', 'YYYY-MM-DD SSSS'); -- ok SELECT to_timestamp('2015-02-11 86400', 'YYYY-MM-DD SSSS'); SELECT to_timestamp('2015-02-11 86000', 'YYYY-MM-DD SSSSS'); -- ok SELECT to_timestamp('2015-02-11 86400', 'YYYY-MM-DD SSSSS'); +SELECT to_timestamp('1000000000,999', 'Y,YYY'); +SELECT to_timestamp('0.-2147483648', 'SS.MS'); +SELECT to_timestamp('613566758', 'W'); +SELECT to_timestamp('2024 613566758 1', 'YYYY WW D'); SELECT to_date('2016-13-10', 'YYYY-MM-DD'); SELECT to_date('2016-02-30', 'YYYY-MM-DD'); SELECT to_date('2016-02-29', 'YYYY-MM-DD'); -- ok @@ -552,6 +556,10 @@ SELECT to_date('2016 365', 'YYYY DDD'); -- ok SELECT to_date('2016 366', 'YYYY DDD'); -- ok SELECT to_date('2016 367', 'YYYY DDD'); SELECT to_date('0000-02-01','YYYY-MM-DD'); -- allowed, though it shouldn't be +SELECT to_date('100000000', 'CC'); +SELECT to_date('-100000000', 'CC'); +SELECT to_date('-2147483648 01', 'CC YY'); +SELECT to_date('2147483647 01', 'CC YY'); -- -- Check behavior with SQL-style fixed-GMT-offset time zone (cf bug #8572) -- 2.39.5 (Apple Git-154)