Fix various overflow hazards in date and timestamp functions.
authorNathan Bossart <nathan@postgresql.org>
Mon, 9 Dec 2024 19:47:23 +0000 (13:47 -0600)
committerNathan Bossart <nathan@postgresql.org>
Mon, 9 Dec 2024 19:47:23 +0000 (13:47 -0600)
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 usage, 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.

Since this is a bug fix, it could be back-patched, but given the
risk of conflicts with the new routines in int.h and the overall
risk/reward ratio of this patch, I've opted not to do so for now.

Fixes bug #18585 (except for the one case that's just commented).

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

src/backend/utils/adt/date.c
src/backend/utils/adt/formatting.c
src/backend/utils/adt/timestamp.c
src/include/common/int.h
src/test/regress/expected/date.out
src/test/regress/expected/horology.out
src/test/regress/sql/date.sql
src/test/regress/sql/horology.sql

index 8130f3e8ac0932e9ebe00a0bc71f988dc09e423f..da61ac0e867186012e7b311787d715501af8c189 100644 (file)
@@ -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);
index 9ec8769eb9dcf843c3c620b2f3d85707177bb0f2..0dcb5515119f40f1c17119dc227cbd1e38e8b2d3 100644 (file)
@@ -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_FIELD_OVERFLOW),
+                                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,31 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std,
            fmask |= DTK_DATE_M;
        }
        else
-           tmfc.ddd = (tmfc.ww - 1) * 7 + 1;
+       {
+           /* tmfc.ddd = (tmfc.ww - 1) * 7 + 1; */
+           if (pg_sub_s32_overflow(tmfc.ww, 1, &tmfc.ddd) ||
+               pg_mul_s32_overflow(tmfc.ddd, 7, &tmfc.ddd) ||
+               pg_add_s32_overflow(tmfc.ddd, 1, &tmfc.ddd))
+           {
+               DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL,
+                                  date_str, "timestamp", escontext);
+               goto fail;
+           }
+       }
    }
 
    if (tmfc.w)
-       tmfc.dd = (tmfc.w - 1) * 7 + 1;
+   {
+       /* tmfc.dd = (tmfc.w - 1) * 7 + 1; */
+       if (pg_sub_s32_overflow(tmfc.w, 1, &tmfc.dd) ||
+           pg_mul_s32_overflow(tmfc.dd, 7, &tmfc.dd) ||
+           pg_add_s32_overflow(tmfc.dd, 1, &tmfc.dd))
+       {
+           DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL,
+                              date_str, "timestamp", escontext);
+           goto fail;
+       }
+   }
    if (tmfc.dd)
    {
        tm->tm_mday = tmfc.dd;
@@ -4912,7 +4985,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)
index 57fcfefdaf2578ba17b5411d7bac523f2fff981d..18d7d8a108ac09ec41ee60ec539dc55f6eb2ce36 100644 (file)
@@ -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)
index 3b1590d676f511a06fc5ea7c8adda67071dbef6e..6b50aa67b911e7fe8ead3123c59d050b7aaff1ab 100644 (file)
@@ -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)
 {
index c9cec70c38d5a904ee798e2c5fc28e93e7f13898..dcab9e76f45dea8e8abca1ab5c7a258327634ec4 100644 (file)
@@ -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);
index 8a68379578baa4b3dc7a7195558c3d9592dace8f..cb28dfbaee71631af397450513e26a92a18a37d0 100644 (file)
@@ -3754,6 +3754,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');
@@ -3794,6 +3802,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         
index 1c58ff6966dbb0a4862ad448b8e07050b5d8ea61..805aec706cc06a6b88b608fc3c85827de93a2559 100644 (file)
@@ -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);
index 864816372236340ee317a36148882e382b685400..4aa88b4ba9a823e4eea26a5446ea772fac8910c5 100644 (file)
@@ -651,6 +651,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
@@ -661,6 +665,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');