Guard against overflow in make_interval().
authorDean Rasheed <dean.a.rasheed@gmail.com>
Sun, 29 Oct 2023 15:46:04 +0000 (15:46 +0000)
committerDean Rasheed <dean.a.rasheed@gmail.com>
Sun, 29 Oct 2023 15:51:53 +0000 (15:51 +0000)
The original code did very little to guard against integer or floating
point overflow when computing the interval's fields.  Detect any such
overflows and error out, rather than silently returning bogus results.

Joseph Koshakow, reviewed by Ashutosh Bapat and me.

Discussion: https://postgr.es/m/CAAvxfHcm1TPwH_zaGWuFoL8pZBestbRZTU6Z%3D-RvAdSXTPbKfg%40mail.gmail.com

src/backend/utils/adt/timestamp.c
src/include/datatype/timestamp.h
src/test/regress/expected/interval.out
src/test/regress/sql/interval.sql

index e172e906142b4931457ca3de3f94ec69f64adedd..647b97aca6978c2b9f5a7a148c1ce495adb82cd3 100644 (file)
@@ -1509,24 +1509,41 @@ make_interval(PG_FUNCTION_ARGS)
    Interval   *result;
 
    /*
-    * Reject out-of-range inputs.  We really ought to check the integer
-    * inputs as well, but it's not entirely clear what limits to apply.
+    * Reject out-of-range inputs.  We reject any input values that cause
+    * integer overflow of the corresponding interval fields.
     */
    if (isinf(secs) || isnan(secs))
-       ereport(ERROR,
-               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                errmsg("interval out of range")));
+       goto out_of_range;
 
    result = (Interval *) palloc(sizeof(Interval));
-   result->month = years * MONTHS_PER_YEAR + months;
-   result->day = weeks * 7 + days;
 
-   secs = rint(secs * USECS_PER_SEC);
-   result->time = hours * ((int64) SECS_PER_HOUR * USECS_PER_SEC) +
-       mins * ((int64) SECS_PER_MINUTE * USECS_PER_SEC) +
-       (int64) secs;
+   /* years and months -> months */
+   if (pg_mul_s32_overflow(years, MONTHS_PER_YEAR, &result->month) ||
+       pg_add_s32_overflow(result->month, months, &result->month))
+       goto out_of_range;
+
+   /* weeks and days -> days */
+   if (pg_mul_s32_overflow(weeks, DAYS_PER_WEEK, &result->day) ||
+       pg_add_s32_overflow(result->day, days, &result->day))
+       goto out_of_range;
+
+   /* hours and mins -> usecs (cannot overflow 64-bit) */
+   result->time = hours * USECS_PER_HOUR + mins * USECS_PER_MINUTE;
+
+   /* secs -> usecs */
+   secs = rint(float8_mul(secs, USECS_PER_SEC));
+   if (!FLOAT8_FITS_IN_INT64(secs) ||
+       pg_add_s64_overflow(result->time, (int64) secs, &result->time))
+       goto out_of_range;
 
    PG_RETURN_INTERVAL_P(result);
+
+out_of_range:
+   ereport(ERROR,
+           errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+           errmsg("interval out of range"));
+
+   PG_RETURN_NULL();           /* keep compiler quiet */
 }
 
 /* EncodeSpecialTimestamp()
index ab8ccf89ca9eaf6c5d223dd5454d4b9b17272888..1a6390585c84e12e14fd2d8cdd655760d0241e1c 100644 (file)
@@ -114,6 +114,7 @@ struct pg_itm_in
  * 30 days.
  */
 #define DAYS_PER_MONTH 30      /* assumes exactly 30 days per month */
+#define DAYS_PER_WEEK  7
 #define HOURS_PER_DAY  24      /* assume no daylight savings time changes */
 
 /*
index c0ca8e041b1631d152ff450aa75a35fb3d1c95b5..75d19d659491296cdb5f77e5a922e9dc5cbf1291 100644 (file)
@@ -1587,6 +1587,33 @@ select interval '-2147483648 months -2147483648 days -9223372036854775808 micros
 ERROR:  interval field value out of range: "-2147483648 months -2147483648 days -9223372036854775808 microseconds ago"
 LINE 1: select interval '-2147483648 months -2147483648 days -922337...
                         ^
+-- overflowing using make_interval
+select make_interval(years := 178956971);
+ERROR:  interval out of range
+select make_interval(years := -178956971);
+ERROR:  interval out of range
+select make_interval(years := 1, months := 2147483647);
+ERROR:  interval out of range
+select make_interval(years := -1, months := -2147483648);
+ERROR:  interval out of range
+select make_interval(weeks := 306783379);
+ERROR:  interval out of range
+select make_interval(weeks := -306783379);
+ERROR:  interval out of range
+select make_interval(weeks := 1, days := 2147483647);
+ERROR:  interval out of range
+select make_interval(weeks := -1, days := -2147483648);
+ERROR:  interval out of range
+select make_interval(secs := 1e308);
+ERROR:  value out of range: overflow
+select make_interval(secs := 1e18);
+ERROR:  interval out of range
+select make_interval(secs := -1e18);
+ERROR:  interval out of range
+select make_interval(mins := 1, secs := 9223372036800.0);
+ERROR:  interval out of range
+select make_interval(mins := -1, secs := -9223372036800.0);
+ERROR:  interval out of range
 -- test that INT_MIN number is formatted properly
 SET IntervalStyle to postgres;
 select interval '-2147483648 months -2147483648 days -9223372036854775808 us';
index 038fc508d0c039a9bb9b16405d45b20e61ad52cf..a0a373f08bd7f96839df58b739ca4775ea2b1846 100644 (file)
@@ -511,6 +511,21 @@ select interval '-2147483648 days ago';
 select interval '-9223372036854775808 microseconds ago';
 select interval '-2147483648 months -2147483648 days -9223372036854775808 microseconds ago';
 
+-- overflowing using make_interval
+select make_interval(years := 178956971);
+select make_interval(years := -178956971);
+select make_interval(years := 1, months := 2147483647);
+select make_interval(years := -1, months := -2147483648);
+select make_interval(weeks := 306783379);
+select make_interval(weeks := -306783379);
+select make_interval(weeks := 1, days := 2147483647);
+select make_interval(weeks := -1, days := -2147483648);
+select make_interval(secs := 1e308);
+select make_interval(secs := 1e18);
+select make_interval(secs := -1e18);
+select make_interval(mins := 1, secs := 9223372036800.0);
+select make_interval(mins := -1, secs := -9223372036800.0);
+
 -- test that INT_MIN number is formatted properly
 SET IntervalStyle to postgres;
 select interval '-2147483648 months -2147483648 days -9223372036854775808 us';