From 3752e3d210287787a881ae3c68da019aa86f414e Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 28 Apr 2024 13:42:13 -0400 Subject: [PATCH] Detect more overflows in timestamp[tz]_pl_interval. In commit 25cd2d640 I (tgl) opined that "The additions of the months and microseconds fields could also overflow, of course. However, I believe we need no additional checks there; the existing range checks should catch such cases". This is demonstrably wrong however for the microseconds field, and given that discovery it seems prudent to be paranoid about the months addition as well. Report and patch by Joseph Koshakow. As before, back-patch to all supported branches. (However, the test case doesn't work before v15 because we didn't allow wider-than-int32 numbers in interval literals. A variant test could probably be built that fits within that restriction, but it didn't seem worth the trouble.) Discussion: https://postgr.es/m/CAAvxfHf77sRHKoEzUw9_cMYSpbpNS2C+J_+8Dq4+0oi8iKopeA@mail.gmail.com --- src/backend/utils/adt/timestamp.c | 20 ++++++++++++++++---- src/test/regress/expected/horology.out | 4 ++++ src/test/regress/sql/horology.sql | 2 ++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index fe100d5615..fd92287948 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -2932,7 +2932,10 @@ timestamp_pl_interval(PG_FUNCTION_ARGS) (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); - tm->tm_mon += span->month; + if (pg_add_s32_overflow(tm->tm_mon, span->month, &tm->tm_mon)) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("timestamp out of range"))); if (tm->tm_mon > MONTHS_PER_YEAR) { tm->tm_year += (tm->tm_mon - 1) / MONTHS_PER_YEAR; @@ -2984,7 +2987,10 @@ timestamp_pl_interval(PG_FUNCTION_ARGS) errmsg("timestamp out of range"))); } - timestamp += span->time; + if (pg_add_s64_overflow(timestamp, span->time, ×tamp)) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("timestamp out of range"))); if (!IS_VALID_TIMESTAMP(timestamp)) ereport(ERROR, @@ -3052,7 +3058,10 @@ timestamptz_pl_interval_internal(TimestampTz timestamp, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); - tm->tm_mon += span->month; + if (pg_add_s32_overflow(tm->tm_mon, span->month, &tm->tm_mon)) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("timestamp out of range"))); if (tm->tm_mon > MONTHS_PER_YEAR) { tm->tm_year += (tm->tm_mon - 1) / MONTHS_PER_YEAR; @@ -3111,7 +3120,10 @@ timestamptz_pl_interval_internal(TimestampTz timestamp, errmsg("timestamp out of range"))); } - timestamp += span->time; + if (pg_add_s64_overflow(timestamp, span->time, ×tamp)) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("timestamp out of range"))); if (!IS_VALID_TIMESTAMP(timestamp)) ereport(ERROR, diff --git a/src/test/regress/expected/horology.out b/src/test/regress/expected/horology.out index f3cda4afe1..e48534ba00 100644 --- a/src/test/regress/expected/horology.out +++ b/src/test/regress/expected/horology.out @@ -484,6 +484,8 @@ SELECT timestamp without time zone 'Jan 1, 4713 BC' + interval '109203489 days' SELECT timestamp without time zone '2000-01-01' - interval '2483590 days' AS "out of range"; ERROR: timestamp out of range +SELECT timestamp without time zone '294276-12-31 23:59:59' + interval '9223372036854775807 microseconds' AS "out of range"; +ERROR: timestamp out of range SELECT timestamp without time zone '12/31/294276' - timestamp without time zone '12/23/1999' AS "106751991 Days"; 106751991 Days ------------------ @@ -746,6 +748,8 @@ SELECT timestamp with time zone '1999-12-01' + interval '1 month - 1 second' AS SELECT timestamp with time zone '2000-01-01' - interval '2483590 days' AS "out of range"; ERROR: timestamp out of range +SELECT timestamp with time zone '294276-12-31 23:59:59 UTC' + interval '9223372036854775807 microseconds' AS "out of range"; +ERROR: timestamp out of range SELECT (timestamp with time zone 'today' = (timestamp with time zone 'yesterday' + interval '1 day')) as "True"; True ------ diff --git a/src/test/regress/sql/horology.sql b/src/test/regress/sql/horology.sql index 8621446afd..af70f28e3d 100644 --- a/src/test/regress/sql/horology.sql +++ b/src/test/regress/sql/horology.sql @@ -121,6 +121,7 @@ SELECT timestamp without time zone 'Jan 1, 4713 BC' + interval '106000000 days' SELECT timestamp without time zone 'Jan 1, 4713 BC' + interval '107000000 days' AS "Jan 20, 288244"; SELECT timestamp without time zone 'Jan 1, 4713 BC' + interval '109203489 days' AS "Dec 31, 294276"; SELECT timestamp without time zone '2000-01-01' - interval '2483590 days' AS "out of range"; +SELECT timestamp without time zone '294276-12-31 23:59:59' + interval '9223372036854775807 microseconds' AS "out of range"; SELECT timestamp without time zone '12/31/294276' - timestamp without time zone '12/23/1999' AS "106751991 Days"; -- Shorthand values @@ -153,6 +154,7 @@ SELECT timestamp with time zone '1999-03-01' - interval '1 second' AS "Feb 28"; SELECT timestamp with time zone '2000-03-01' - interval '1 second' AS "Feb 29"; SELECT timestamp with time zone '1999-12-01' + interval '1 month - 1 second' AS "Dec 31"; SELECT timestamp with time zone '2000-01-01' - interval '2483590 days' AS "out of range"; +SELECT timestamp with time zone '294276-12-31 23:59:59 UTC' + interval '9223372036854775807 microseconds' AS "out of range"; SELECT (timestamp with time zone 'today' = (timestamp with time zone 'yesterday' + interval '1 day')) as "True"; SELECT (timestamp with time zone 'today' = (timestamp with time zone 'tomorrow' - interval '1 day')) as "True";