Ok, so I've attached a patch with my final unprompted changes. It
contains the following two changes:

1. I found some more overflows with the ISO8601 formats and have
included some fixes.
2. I reverted the overflow checks for the seconds field. It's actually a
bit more complicated than I thought. For example consider the following
query:
    postgres=# SELECT INTERVAL '0.99999999 min 2147483647 sec';
            interval
    ----------------------
    -596523:13:09.000001
    (1 row)
This query will overflow the tm_sec field of the struct pg_tm, however
it's not actually out of range for the Interval. I'm not sure the best
way to handle this right now, but I think it would be best to leave it
for a future patch. Perhaps the time related fields in struct pg_tm
need to be changed to 64 bit ints.

- Joe Koshakow
From 5750dcfbc00cb1259263e9986898f1960edf9c7f Mon Sep 17 00:00:00 2001
From: Joseph Koshakow <kosh...@gmail.com>
Date: Fri, 11 Feb 2022 14:18:32 -0500
Subject: [PATCH] Check for overflow when decoding an interval

When decoding an interval there are various date units which are
aliases for some multiple of another unit. For example a week is 7 days
and a decade is 10 years. When decoding these specific units, there is
no check for overflow, allowing the interval to overflow. This commit
adds an overflow check for all of these units.

Additionally fractional date/time units are rolled down into the next
smallest unit. For example 0.1 months is 3 days. When adding these
fraction units, there is no check for overflow, allowing the interval
to overflow. This commit adds an overflow check for all of the
fractional units.

Additionally adding the word "ago" to the interval negates every
field. However the negative of INT_MIN is still INT_MIN. This
commit adds a check to make sure that we don't try and negate
a field if it's INT_MIN.

Signed-off-by: Joseph Koshakow <kosh...@gmail.com>
---
 src/backend/utils/adt/datetime.c       | 115 +++++++++++++++++++------
 src/test/regress/expected/interval.out | 112 ++++++++++++++++++++++++
 src/test/regress/sql/interval.sql      |  29 +++++++
 3 files changed, 232 insertions(+), 24 deletions(-)

diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c
index 7926258c06..1c2ab58f82 100644
--- a/src/backend/utils/adt/datetime.c
+++ b/src/backend/utils/adt/datetime.c
@@ -21,6 +21,7 @@
 #include "access/htup_details.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
+#include "common/int.h"
 #include "common/string.h"
 #include "funcapi.h"
 #include "miscadmin.h"
@@ -46,8 +47,11 @@ static char *AppendSeconds(char *cp, int sec, fsec_t fsec,
 						   int precision, bool fillzeros);
 static void AdjustFractSeconds(double frac, struct pg_tm *tm, fsec_t *fsec,
 							   int scale);
-static void AdjustFractDays(double frac, struct pg_tm *tm, fsec_t *fsec,
+static bool AdjustFractDays(double frac, struct pg_tm *tm, fsec_t *fsec,
 							int scale);
+static bool AdjustFractMonths(double frac, struct pg_tm *tm, int scale);
+static bool AdjustDays(int val, struct pg_tm *tm, int multiplier);
+static bool AdjustYears(int val, struct pg_tm *tm, int multiplier);
 static int	DetermineTimeZoneOffsetInternal(struct pg_tm *tm, pg_tz *tzp,
 											pg_time_t *tp);
 static bool DetermineTimeZoneAbbrevOffsetInternal(pg_time_t t,
@@ -515,18 +519,56 @@ AdjustFractSeconds(double frac, struct pg_tm *tm, fsec_t *fsec, int scale)
 }
 
 /* As above, but initial scale produces days */
-static void
+static bool
 AdjustFractDays(double frac, struct pg_tm *tm, fsec_t *fsec, int scale)
 {
 	int			extra_days;
 
 	if (frac == 0)
-		return;
+		return true;
 	frac *= scale;
 	extra_days = (int) frac;
-	tm->tm_mday += extra_days;
+	if (pg_add_s32_overflow(tm->tm_mday, extra_days, &tm->tm_mday))
+		return false;
 	frac -= extra_days;
 	AdjustFractSeconds(frac, tm, fsec, SECS_PER_DAY);
+	return true;
+}
+
+/* As above, but initial scale produces months */
+static bool
+AdjustFractMonths(double frac, struct pg_tm *tm, int scale)
+{
+	int months = rint(frac * MONTHS_PER_YEAR * scale);
+	return !pg_add_s32_overflow(tm->tm_mon, months, &tm->tm_mon);
+}
+
+/*
+ * Multiply val by multiplier (to produce days) and add to *tm.
+ * Returns true if successful, false if tm overflows.
+ */
+static bool
+AdjustDays(int val, struct pg_tm *tm, int multiplier)
+{
+	int			extra_days;
+	return !pg_mul_s32_overflow(val, multiplier, &extra_days) &&
+		!pg_add_s32_overflow(tm->tm_mday, extra_days, &tm->tm_mday);
+}
+
+/* As above, but initial val produces months */
+static bool
+AdjustMonths(int val, struct pg_tm *tm)
+{
+	return !pg_add_s32_overflow(tm->tm_mon, val, &tm->tm_mon);
+}
+
+/* As above, but initial val produces years */
+static bool
+AdjustYears(int val, struct pg_tm *tm, int multiplier)
+{
+	int			years;
+	return !pg_mul_s32_overflow(val, multiplier, &years) &&
+		!pg_add_s32_overflow(tm->tm_year, years, &tm->tm_year);
 }
 
 /* Fetch a fractional-second value with suitable error checking */
@@ -3287,44 +3329,56 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
 						break;
 
 					case DTK_DAY:
-						tm->tm_mday += val;
+						if (!AdjustDays(val, tm, 1))
+							return DTERR_FIELD_OVERFLOW;
 						AdjustFractSeconds(fval, tm, fsec, SECS_PER_DAY);
 						tmask = DTK_M(DAY);
 						break;
 
 					case DTK_WEEK:
-						tm->tm_mday += val * 7;
-						AdjustFractDays(fval, tm, fsec, 7);
+						if (!AdjustDays(val, tm, 7))
+							return DTERR_FIELD_OVERFLOW;
+						if (!AdjustFractDays(fval, tm, fsec, 7))
+							return DTERR_FIELD_OVERFLOW;
 						tmask = DTK_M(WEEK);
 						break;
 
 					case DTK_MONTH:
-						tm->tm_mon += val;
-						AdjustFractDays(fval, tm, fsec, DAYS_PER_MONTH);
+						if (!AdjustMonths(val, tm))
+							return DTERR_FIELD_OVERFLOW;
+						if (!AdjustFractDays(fval, tm, fsec, DAYS_PER_MONTH))
+							return DTERR_FIELD_OVERFLOW;
 						tmask = DTK_M(MONTH);
 						break;
 
 					case DTK_YEAR:
 						tm->tm_year += val;
-						tm->tm_mon += rint(fval * MONTHS_PER_YEAR);
+						if (!AdjustFractMonths(fval, tm, 1))
+							return DTERR_FIELD_OVERFLOW;
 						tmask = DTK_M(YEAR);
 						break;
 
 					case DTK_DECADE:
-						tm->tm_year += val * 10;
-						tm->tm_mon += rint(fval * MONTHS_PER_YEAR * 10);
+						if (!AdjustYears(val, tm, 10))
+							return DTERR_FIELD_OVERFLOW;
+						if (!AdjustFractMonths(fval, tm, 10))
+							return DTERR_FIELD_OVERFLOW;
 						tmask = DTK_M(DECADE);
 						break;
 
 					case DTK_CENTURY:
-						tm->tm_year += val * 100;
-						tm->tm_mon += rint(fval * MONTHS_PER_YEAR * 100);
+						if (!AdjustYears(val, tm, 100))
+							return DTERR_FIELD_OVERFLOW;
+						if (!AdjustFractMonths(fval, tm, 100))
+							return DTERR_FIELD_OVERFLOW;
 						tmask = DTK_M(CENTURY);
 						break;
 
 					case DTK_MILLENNIUM:
-						tm->tm_year += val * 1000;
-						tm->tm_mon += rint(fval * MONTHS_PER_YEAR * 1000);
+						if (!AdjustYears(val, tm, 1000))
+							return DTERR_FIELD_OVERFLOW;
+						if (!AdjustFractMonths(fval, tm, 1000))
+							return DTERR_FIELD_OVERFLOW;
 						tmask = DTK_M(MILLENNIUM);
 						break;
 
@@ -3440,6 +3494,11 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
 	/* finally, AGO negates everything */
 	if (is_before)
 	{
+		if (tm->tm_hour == INT_MIN ||
+			tm->tm_mday == INT_MIN ||
+			tm->tm_mon == INT_MIN)
+			return DTERR_FIELD_OVERFLOW;
+
 		*fsec = -(*fsec);
 		tm->tm_sec = -tm->tm_sec;
 		tm->tm_min = -tm->tm_min;
@@ -3561,18 +3620,24 @@ DecodeISO8601Interval(char *str,
 			{
 				case 'Y':
 					tm->tm_year += val;
-					tm->tm_mon += rint(fval * MONTHS_PER_YEAR);
+					if (!AdjustFractMonths(fval, tm, 1))
+						return DTERR_FIELD_OVERFLOW;
 					break;
 				case 'M':
-					tm->tm_mon += val;
-					AdjustFractDays(fval, tm, fsec, DAYS_PER_MONTH);
+					if (!AdjustMonths(val, tm))
+						return DTERR_FIELD_OVERFLOW;
+					if (!AdjustFractDays(fval, tm, fsec, DAYS_PER_MONTH))
+						return DTERR_FIELD_OVERFLOW;
 					break;
 				case 'W':
-					tm->tm_mday += val * 7;
-					AdjustFractDays(fval, tm, fsec, 7);
+					if (!AdjustDays(val, tm, 7))
+						return DTERR_FIELD_OVERFLOW;
+					if (!AdjustFractDays(fval, tm, fsec, 7))
+						return DTERR_FIELD_OVERFLOW;
 					break;
 				case 'D':
-					tm->tm_mday += val;
+					if (!AdjustDays(val, tm, 1))
+						return DTERR_FIELD_OVERFLOW;
 					AdjustFractSeconds(fval, tm, fsec, SECS_PER_DAY);
 					break;
 				case 'T':		/* ISO 8601 4.4.3.3 Alternative Format / Basic */
@@ -3610,7 +3675,8 @@ DecodeISO8601Interval(char *str,
 					dterr = ParseISO8601Number(str, &str, &val, &fval);
 					if (dterr)
 						return dterr;
-					tm->tm_mon += val;
+					if (!AdjustMonths(val, tm))
+						return DTERR_FIELD_OVERFLOW;
 					AdjustFractDays(fval, tm, fsec, DAYS_PER_MONTH);
 					if (*str == '\0')
 						return 0;
@@ -3627,7 +3693,8 @@ DecodeISO8601Interval(char *str,
 					dterr = ParseISO8601Number(str, &str, &val, &fval);
 					if (dterr)
 						return dterr;
-					tm->tm_mday += val;
+					if (!AdjustDays(val, tm, 1))
+						return DTERR_FIELD_OVERFLOW;
 					AdjustFractSeconds(fval, tm, fsec, SECS_PER_DAY);
 					if (*str == '\0')
 						return 0;
diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out
index accd4a7d90..0ec40ce968 100644
--- a/src/test/regress/expected/interval.out
+++ b/src/test/regress/expected/interval.out
@@ -232,6 +232,118 @@ INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 years');
 ERROR:  interval out of range
 LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 years'...
                                                  ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 weeks');
+ERROR:  interval field value out of range: "2147483647 weeks"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 weeks')...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 weeks');
+ERROR:  interval field value out of range: "-2147483648 weeks"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 weeks'...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 decades');
+ERROR:  interval field value out of range: "2147483647 decades"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 decades...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 decades');
+ERROR:  interval field value out of range: "-2147483648 decades"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 decade...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 centuries');
+ERROR:  interval field value out of range: "2147483647 centuries"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 centuri...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 centuries');
+ERROR:  interval field value out of range: "-2147483648 centuries"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 centur...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 millennium');
+ERROR:  interval field value out of range: "2147483647 millennium"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 millenn...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 millennium');
+ERROR:  interval field value out of range: "-2147483648 millennium"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 millen...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 millennium 2147483647 months');
+ERROR:  interval field value out of range: "0.1 millennium 2147483647 months"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 millennium 214...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 centuries 2147483647 months');
+ERROR:  interval field value out of range: "0.1 centuries 2147483647 months"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 centuries 2147...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 decades 2147483647 months');
+ERROR:  interval field value out of range: "0.1 decades 2147483647 months"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 decades 214748...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 yrs 2147483647 months');
+ERROR:  interval field value out of range: "0.1 yrs 2147483647 months"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 yrs 2147483647...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 months 0.1 yrs');
+ERROR:  interval field value out of range: "2147483647 months 0.1 yrs"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 months ...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 months 2147483647 days');
+ERROR:  interval field value out of range: "0.1 months 2147483647 days"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 months 2147483...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('1 week 2147483647 days');
+ERROR:  interval field value out of range: "1 week 2147483647 days"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('1 week 2147483647 ...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 days 1 week');
+ERROR:  interval field value out of range: "2147483647 days 1 week"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 days 1 ...
+                                                 ^
+SELECT INTERVAL '0.1 weeks 2147483647 hrs';
+ERROR:  interval out of range
+SELECT INTERVAL '0.1 days 2147483647 hrs';
+ERROR:  interval out of range
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 months ago');
+ERROR:  interval field value out of range: "-2147483648 months ago"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 months...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 days ago');
+ERROR:  interval field value out of range: "-2147483648 days ago"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 days a...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 hours ago');
+ERROR:  interval field value out of range: "-2147483648 hours ago"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 hours ...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0.1M2147483647D');
+ERROR:  interval field value out of range: "P0.1M2147483647D"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0.1M2147483647D')...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0.1Y2147483647M');
+ERROR:  interval field value out of range: "P0.1Y2147483647M"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0.1Y2147483647M')...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P2147483647M0.1Y');
+ERROR:  interval field value out of range: "P2147483647M0.1Y"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P2147483647M0.1Y')...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P1W2147483647D');
+ERROR:  interval field value out of range: "P1W2147483647D"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P1W2147483647D');
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P2147483647D1W');
+ERROR:  interval field value out of range: "P2147483647D1W"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P2147483647D1W');
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0.5W2147483647D');
+ERROR:  interval field value out of range: "P0.5W2147483647D"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0.5W2147483647D')...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0000-00.1-2147483647T00:00:00');
+ERROR:  interval field value out of range: "P0000-00.1-2147483647T00:00:00"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0000-00.1-2147483...
+                                                 ^
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0000.1-2147483647-00T00:00:00');
+ERROR:  interval field value out of range: "P0000.1-2147483647-00T00:00:00"
+LINE 1: INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0000.1-2147483647...
+                                                 ^
 -- Test edge-case overflow detection in interval multiplication
 select extract(epoch from '256 microseconds'::interval * (2^55)::float8);
 ERROR:  interval out of range
diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql
index 6d532398bd..dda8fd3a16 100644
--- a/src/test/regress/sql/interval.sql
+++ b/src/test/regress/sql/interval.sql
@@ -72,6 +72,35 @@ INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483648 days');
 INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483649 days');
 INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 years');
 INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 years');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 weeks');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 weeks');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 decades');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 decades');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 centuries');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 centuries');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 millennium');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 millennium');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 millennium 2147483647 months');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 centuries 2147483647 months');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 decades 2147483647 months');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 yrs 2147483647 months');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 months 0.1 yrs');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('0.1 months 2147483647 days');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('1 week 2147483647 days');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('2147483647 days 1 week');
+SELECT INTERVAL '0.1 weeks 2147483647 hrs';
+SELECT INTERVAL '0.1 days 2147483647 hrs';
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 months ago');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 days ago');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('-2147483648 hours ago');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0.1M2147483647D');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0.1Y2147483647M');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P2147483647M0.1Y');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P1W2147483647D');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P2147483647D1W');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0.5W2147483647D');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0000-00.1-2147483647T00:00:00');
+INSERT INTO INTERVAL_TBL_OF (f1) VALUES ('P0000.1-2147483647-00T00:00:00');
 
 -- Test edge-case overflow detection in interval multiplication
 select extract(epoch from '256 microseconds'::interval * (2^55)::float8);
-- 
2.25.1

Reply via email to