From 99032579840c75c15acb88e6d9fb9a8a76a39dcd Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Fri, 6 Dec 2024 10:16:51 -0600 Subject: [PATCH v29 1/1] Fix various overflow hazards in date and timestamp functions. 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 | 9 ++- src/backend/utils/adt/formatting.c | 104 +++++++++++++++++++++++-- src/backend/utils/adt/timestamp.c | 4 + src/include/common/int.h | 48 ++++++++++++ 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, 183 insertions(+), 9 deletions(-) diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c index 8130f3e8ac..da61ac0e86 100644 --- a/src/backend/utils/adt/date.c +++ b/src/backend/utils/adt/date.c @@ -256,8 +256,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 2bcc185708..a9daa5c591 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -77,6 +77,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "common/int.h" #include "common/unicode_case.h" #include "common/unicode_category.h" #include "mb/pg_wchar.h" @@ -3826,7 +3827,14 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, ereturn(escontext,, (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)) + ereturn(escontext,, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("value for \"Y,YYY\" in source string is out of range"))); + if (!from_char_set_int(&out->year, years, n, escontext)) return; out->yysz = 4; @@ -4785,10 +4793,35 @@ 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)) + { + DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL, + text_to_cstring(date_txt), "timestamp", + escontext); + goto fail; + } + } 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)) + { + DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL, + text_to_cstring(date_txt), "timestamp", + escontext); + goto fail; + } + } } else { @@ -4814,11 +4847,31 @@ 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)) + { + DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL, + text_to_cstring(date_txt), "timestamp", + escontext); + goto fail; + } + } 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)) + { + DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL, + text_to_cstring(date_txt), "timestamp", + escontext); + goto fail; + } + } fmask |= DTK_M(YEAR); } @@ -4843,11 +4896,35 @@ 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)) + { + DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL, + date_str, "timestamp", escontext); + goto fail; + } + } } 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)) + { + DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL, + date_str, "timestamp", escontext); + goto fail; + } + } if (tmfc.dd) { tm->tm_mday = tmfc.dd; @@ -4912,7 +4989,18 @@ 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)) + { + DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL, + date_str, "timestamp", escontext); + goto fail; + } + } 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 57fcfefdaf..18d7d8a108 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -5104,6 +5104,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 3b1590d676..6b50aa67b9 100644 --- a/src/include/common/int.h +++ b/src/include/common/int.h @@ -117,6 +117,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 +} + static inline uint16 pg_abs_s16(int16 a) { @@ -185,6 +201,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 +} + static inline uint32 pg_abs_s32(int32 a) { @@ -300,6 +332,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 +} + static inline uint64 pg_abs_s64(int64 a) { diff --git a/src/test/regress/expected/date.out b/src/test/regress/expected/date.out index c9cec70c38..dcab9e76f4 100644 --- a/src/test/regress/expected/date.out +++ b/src/test/regress/expected/date.out @@ -1528,6 +1528,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 6d7dd5c988..b87b88f038 100644 --- a/src/test/regress/expected/horology.out +++ b/src/test/regress/expected/horology.out @@ -3743,6 +3743,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'); @@ -3783,6 +3791,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" -- to_char's TZ format code produces zone abbrev if known SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ'); to_char diff --git a/src/test/regress/sql/date.sql b/src/test/regress/sql/date.sql index 1c58ff6966..805aec706c 100644 --- a/src/test/regress/sql/date.sql +++ b/src/test/regress/sql/date.sql @@ -371,5 +371,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 0fe3c783e6..808083a6d8 100644 --- a/src/test/regress/sql/horology.sql +++ b/src/test/regress/sql/horology.sql @@ -650,6 +650,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 @@ -660,6 +664,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'); -- to_char's TZ format code produces zone abbrev if known SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ'); -- 2.39.5 (Apple Git-154)