Handle integer overflow in interval justification functions.
authorTom Lane <tgl@sss.pgh.pa.us>
Mon, 28 Feb 2022 20:36:54 +0000 (15:36 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Mon, 28 Feb 2022 20:36:54 +0000 (15:36 -0500)
justify_interval, justify_hours, and justify_days didn't check for
overflow when promoting hours to days or days to months; but that's
possible when the upper field's value is already large.  Detect and
report any such overflow.

Also, we can avoid unnecessary overflow in some cases in justify_interval
by pre-justifying the days field.  (Thanks to Nathan Bossart for this
idea.)

Joe Koshakow

Discussion: https://postgr.es/m/CAAvxfHeNqsJ2xYFbPUf_8nNQUiJqkag04NW6aBQQ0dbZsxfWHA@mail.gmail.com

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

index 36f8a84bcc5f8e4048ca424a9cfbef6d0704b183..ae36ff33285af2d8617043a88772626df81c8441 100644 (file)
@@ -2717,12 +2717,33 @@ interval_justify_interval(PG_FUNCTION_ARGS)
    result->day = span->day;
    result->time = span->time;
 
+   /* pre-justify days if it might prevent overflow */
+   if ((result->day > 0 && result->time > 0) ||
+       (result->day < 0 && result->time < 0))
+   {
+       wholemonth = result->day / DAYS_PER_MONTH;
+       result->day -= wholemonth * DAYS_PER_MONTH;
+       if (pg_add_s32_overflow(result->month, wholemonth, &result->month))
+           ereport(ERROR,
+                   (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                    errmsg("interval out of range")));
+   }
+
+   /*
+    * Since TimeOffset is int64, abs(wholeday) can't exceed about 1.07e8.  If
+    * we pre-justified then abs(result->day) is less than DAYS_PER_MONTH, so
+    * this addition can't overflow.  If we didn't pre-justify, then day and
+    * time are of different signs, so it still can't overflow.
+    */
    TMODULO(result->time, wholeday, USECS_PER_DAY);
-   result->day += wholeday;    /* could overflow... */
+   result->day += wholeday;
 
    wholemonth = result->day / DAYS_PER_MONTH;
    result->day -= wholemonth * DAYS_PER_MONTH;
-   result->month += wholemonth;
+   if (pg_add_s32_overflow(result->month, wholemonth, &result->month))
+       ereport(ERROR,
+               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                errmsg("interval out of range")));
 
    if (result->month > 0 &&
        (result->day < 0 || (result->day == 0 && result->time < 0)))
@@ -2772,7 +2793,10 @@ interval_justify_hours(PG_FUNCTION_ARGS)
    result->time = span->time;
 
    TMODULO(result->time, wholeday, USECS_PER_DAY);
-   result->day += wholeday;    /* could overflow... */
+   if (pg_add_s32_overflow(result->day, wholeday, &result->day))
+       ereport(ERROR,
+               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                errmsg("interval out of range")));
 
    if (result->day > 0 && result->time < 0)
    {
@@ -2808,7 +2832,10 @@ interval_justify_days(PG_FUNCTION_ARGS)
 
    wholemonth = result->day / DAYS_PER_MONTH;
    result->day -= wholemonth * DAYS_PER_MONTH;
-   result->month += wholemonth;
+   if (pg_add_s32_overflow(result->month, wholemonth, &result->month))
+       ereport(ERROR,
+               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                errmsg("interval out of range")));
 
    if (result->month > 0 && result->day < 0)
    {
index accd4a7d9089a79082dabb909349e3769543bf8a..146f7c55d0b5f1e82af536651b070106010da10b 100644 (file)
@@ -396,6 +396,10 @@ SELECT justify_days(interval '6 months 36 days 5 hours 4 minutes 3 seconds') as
  @ 7 mons 6 days 5 hours 4 mins 3 secs
 (1 row)
 
+SELECT justify_hours(interval '2147483647 days 24 hrs');
+ERROR:  interval out of range
+SELECT justify_days(interval '2147483647 months 30 days');
+ERROR:  interval out of range
 -- test justify_interval()
 SELECT justify_interval(interval '1 month -1 hour') as "1 month -1 hour";
   1 month -1 hour   
@@ -403,6 +407,38 @@ SELECT justify_interval(interval '1 month -1 hour') as "1 month -1 hour";
  @ 29 days 23 hours
 (1 row)
 
+SELECT justify_interval(interval '2147483647 days 24 hrs');
+       justify_interval        
+-------------------------------
+ @ 5965232 years 4 mons 8 days
+(1 row)
+
+SELECT justify_interval(interval '-2147483648 days -24 hrs');
+         justify_interval          
+-----------------------------------
+ @ 5965232 years 4 mons 9 days ago
+(1 row)
+
+SELECT justify_interval(interval '2147483647 months 30 days');
+ERROR:  interval out of range
+SELECT justify_interval(interval '-2147483648 months -30 days');
+ERROR:  interval out of range
+SELECT justify_interval(interval '2147483647 months 30 days -24 hrs');
+         justify_interval         
+----------------------------------
+ @ 178956970 years 7 mons 29 days
+(1 row)
+
+SELECT justify_interval(interval '-2147483648 months -30 days 24 hrs');
+           justify_interval           
+--------------------------------------
+ @ 178956970 years 8 mons 29 days ago
+(1 row)
+
+SELECT justify_interval(interval '2147483647 months -30 days 1440 hrs');
+ERROR:  interval out of range
+SELECT justify_interval(interval '-2147483648 months 30 days -1440 hrs');
+ERROR:  interval out of range
 -- test fractional second input, and detection of duplicate units
 SET DATESTYLE = 'ISO';
 SET IntervalStyle TO postgres;
index 6d532398bd694406ad9d8d8baa9dff547ab46681..c31f0eec0546e3f6ef2014c770c09c54a0c44843 100644 (file)
@@ -149,10 +149,22 @@ select '100000000y 10mon -1000000000d -100000h -10min -10.000001s ago'::interval
 SELECT justify_hours(interval '6 months 3 days 52 hours 3 minutes 2 seconds') as "6 mons 5 days 4 hours 3 mins 2 seconds";
 SELECT justify_days(interval '6 months 36 days 5 hours 4 minutes 3 seconds') as "7 mons 6 days 5 hours 4 mins 3 seconds";
 
+SELECT justify_hours(interval '2147483647 days 24 hrs');
+SELECT justify_days(interval '2147483647 months 30 days');
+
 -- test justify_interval()
 
 SELECT justify_interval(interval '1 month -1 hour') as "1 month -1 hour";
 
+SELECT justify_interval(interval '2147483647 days 24 hrs');
+SELECT justify_interval(interval '-2147483648 days -24 hrs');
+SELECT justify_interval(interval '2147483647 months 30 days');
+SELECT justify_interval(interval '-2147483648 months -30 days');
+SELECT justify_interval(interval '2147483647 months 30 days -24 hrs');
+SELECT justify_interval(interval '-2147483648 months -30 days 24 hrs');
+SELECT justify_interval(interval '2147483647 months -30 days 1440 hrs');
+SELECT justify_interval(interval '-2147483648 months 30 days -1440 hrs');
+
 -- test fractional second input, and detection of duplicate units
 SET DATESTYLE = 'ISO';
 SET IntervalStyle TO postgres;