From b418ee6809558f6119c76103f0361f73c2317821 Mon Sep 17 00:00:00 2001
From: Vitaly Burovoy <vitaly.burovoy@gmail.com>
Date: Thu, 5 Jan 2017 15:52:38 +0000
Subject: [PATCH] Add check for overflow to 'interval' functions

Before this commit arithmeric operators could lead to a state when interval_out
raised an exception, i.e. you could not dump a table with a bad interval.
---
 src/backend/utils/adt/timestamp.c      | 54 ++++++++++++++++++++++++++++++++--
 src/test/regress/expected/interval.out | 22 ++++++++++++++
 src/test/regress/sql/interval.sql      | 20 +++++++++++++
 3 files changed, 94 insertions(+), 2 deletions(-)

diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index a87f982..53336a5 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -1034,6 +1034,29 @@ interval_out(PG_FUNCTION_ARGS)
 	PG_RETURN_CSTRING(result);
 }
 
+static void
+interval_check_overflow(Interval *i)
+{
+	/* We compare hours with INT_MIN and INT_MAX because "int" type is used
+	 * in interval_out (pg_tm->hour) and can lead to overflow.
+	 * Also make bounds be the same in modulus to be able to use the unary '-'
+	 * operator for negative intervals without possible UB.
+	 */
+#ifdef HAVE_INT64_TIMESTAMP
+	if (i->time >= ( ((int64)INT_MAX + 1) * SECS_PER_HOUR * USECS_PER_SEC) ||
+		i->time <= (-((int64)INT_MAX + 1) * SECS_PER_HOUR * USECS_PER_SEC))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+#else
+	if (i->time >= ( ((int64)INT_MAX + 1) * SECS_PER_HOUR) ||
+		i->time <= (-((int64)INT_MAX + 1) * SECS_PER_HOUR))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+#endif
+}
+
 /*
  *		interval_recv			- converts external binary format to interval
  */
@@ -1058,6 +1081,8 @@ interval_recv(PG_FUNCTION_ARGS)
 	interval->day = pq_getmsgint(buf, sizeof(interval->day));
 	interval->month = pq_getmsgint(buf, sizeof(interval->month));
 
+	interval_check_overflow(interval);
+
 	AdjustIntervalForTypmod(interval, typmod);
 
 	PG_RETURN_INTERVAL_P(interval);
@@ -1610,6 +1635,9 @@ make_interval(PG_FUNCTION_ARGS)
 	double		secs = PG_GETARG_FLOAT8(6);
 	Interval   *result;
 
+	int32		years_month;
+	int32		weeks_days;
+
 	/*
 	 * 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.
@@ -1619,9 +1647,23 @@ make_interval(PG_FUNCTION_ARGS)
 				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
 				 errmsg("interval out of range")));
 
+	years_month = years * MONTHS_PER_YEAR;
+	weeks_days = weeks * 7;
+
+	if((years_month / MONTHS_PER_YEAR != years) || (weeks_days / 7 != weeks))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
 	result = (Interval *) palloc(sizeof(Interval));
-	result->month = years * MONTHS_PER_YEAR + months;
-	result->day = weeks * 7 + days;
+	result->month = years_month + months;
+	result->day = weeks_days + days;
+
+	if ((SAMESIGN(years_month, months) && !SAMESIGN(result->month, months)) ||
+		(SAMESIGN(weeks_days, days) && !SAMESIGN(result->day, days)))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
 
 	secs += hours * (double) SECS_PER_HOUR + mins * (double) SECS_PER_MINUTE;
 
@@ -1631,6 +1673,8 @@ make_interval(PG_FUNCTION_ARGS)
 	result->time = secs;
 #endif
 
+	interval_check_overflow(result);
+
 	PG_RETURN_INTERVAL_P(result);
 }
 
@@ -3383,6 +3427,8 @@ interval_pl(PG_FUNCTION_ARGS)
 				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
 				 errmsg("interval out of range")));
 
+	interval_check_overflow(result);
+
 	PG_RETURN_INTERVAL_P(result);
 }
 
@@ -3417,6 +3463,8 @@ interval_mi(PG_FUNCTION_ARGS)
 				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
 				 errmsg("interval out of range")));
 
+	interval_check_overflow(result);
+
 	PG_RETURN_INTERVAL_P(result);
 }
 
@@ -3504,6 +3552,8 @@ interval_mul(PG_FUNCTION_ARGS)
 	result->time = span->time * factor + sec_remainder;
 #endif
 
+	interval_check_overflow(result);
+
 	PG_RETURN_INTERVAL_P(result);
 }
 
diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out
index 946c97a..9b85ad5 100644
--- a/src/test/regress/expected/interval.out
+++ b/src/test/regress/expected/interval.out
@@ -863,3 +863,25 @@ select make_interval(secs := 7e12);
  @ 1944444444 hours 26 mins 40 secs
 (1 row)
 
+-- overflow check
+-- To be sure an error is raised by an internal check (not output) function, INSERT is used instead of SELECT.
+CREATE TABLE interval_chk(i interval);
+INSERT INTO interval_chk VALUES(7730941132799 * interval '1 second'); -- ok, maximal allowed value
+INSERT INTO interval_chk VALUES(7730941132800 * interval '1 second');
+ERROR:  interval out of range
+INSERT INTO interval_chk VALUES(-7730941132799 * interval '1 second'); -- ok, minimal allowed value
+INSERT INTO interval_chk VALUES(-7730941132800 * interval '1 second');
+ERROR:  interval out of range
+INSERT INTO interval_chk VALUES('2147483647:59:59'::interval);  -- OK
+INSERT INTO interval_chk VALUES('-2147483647:59:59'::interval);  -- OK
+INSERT INTO interval_chk VALUES('2147483647:59:59'::interval + '1s'::interval);
+ERROR:  interval out of range
+INSERT INTO interval_chk VALUES('-2147483647:59:59'::interval - '1s'::interval);
+ERROR:  interval out of range
+INSERT INTO interval_chk VALUES(make_interval(hours =>  2147483647, secs =>  3599)); --OK
+INSERT INTO interval_chk VALUES(make_interval(hours =>  2147483647, secs =>  3600));
+ERROR:  interval out of range
+INSERT INTO interval_chk VALUES(make_interval(hours => -2147483647, secs => -3599)); --OK
+INSERT INTO interval_chk VALUES(make_interval(hours => -2147483647, secs => -3600));
+ERROR:  interval out of range
+DROP TABLE interval_chk;
diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql
index cff9ada..843838a 100644
--- a/src/test/regress/sql/interval.sql
+++ b/src/test/regress/sql/interval.sql
@@ -281,3 +281,23 @@ select make_interval(months := 'NaN'::float::int);
 select make_interval(secs := 'inf');
 select make_interval(secs := 'NaN');
 select make_interval(secs := 7e12);
+
+-- overflow check
+-- To be sure an error is raised by an internal check (not output) function, INSERT is used instead of SELECT.
+CREATE TABLE interval_chk(i interval);
+INSERT INTO interval_chk VALUES(7730941132799 * interval '1 second'); -- ok, maximal allowed value
+INSERT INTO interval_chk VALUES(7730941132800 * interval '1 second');
+INSERT INTO interval_chk VALUES(-7730941132799 * interval '1 second'); -- ok, minimal allowed value
+INSERT INTO interval_chk VALUES(-7730941132800 * interval '1 second');
+
+INSERT INTO interval_chk VALUES('2147483647:59:59'::interval);  -- OK
+INSERT INTO interval_chk VALUES('-2147483647:59:59'::interval);  -- OK
+
+INSERT INTO interval_chk VALUES('2147483647:59:59'::interval + '1s'::interval);
+INSERT INTO interval_chk VALUES('-2147483647:59:59'::interval - '1s'::interval);
+
+INSERT INTO interval_chk VALUES(make_interval(hours =>  2147483647, secs =>  3599)); --OK
+INSERT INTO interval_chk VALUES(make_interval(hours =>  2147483647, secs =>  3600));
+INSERT INTO interval_chk VALUES(make_interval(hours => -2147483647, secs => -3599)); --OK
+INSERT INTO interval_chk VALUES(make_interval(hours => -2147483647, secs => -3600));
+DROP TABLE interval_chk;
-- 
2.10.2

