Attached is a new version switching from ints to bools, as requested. - Joe Koshakow
From af8f030ad146602b4386f77b5664c6013743569b 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 | 100 +++++++++++++++++++------ src/test/regress/expected/interval.out | 68 +++++++++++++++++ src/test/regress/sql/interval.sql | 18 +++++ 3 files changed, 164 insertions(+), 22 deletions(-) diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index 7926258c06..15a1ebbe67 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" @@ -44,10 +45,13 @@ static int DecodeDate(char *str, int fmask, int *tmask, bool *is2digits, struct pg_tm *tm); 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, +static bool 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, @@ -499,34 +503,68 @@ AppendTimestampSeconds(char *cp, struct pg_tm *tm, fsec_t fsec) /* * Multiply frac by scale (to produce seconds) and add to *tm & *fsec. * We assume the input frac is less than 1 so overflow is not an issue. + * Returns true if successful, false if tm overflows. */ -static void +static bool AdjustFractSeconds(double frac, struct pg_tm *tm, fsec_t *fsec, int scale) { int sec; if (frac == 0) - return; + return true; frac *= scale; sec = (int) frac; - tm->tm_sec += sec; + if (pg_add_s32_overflow(tm->tm_sec, sec, &tm->tm_sec)) + return false; frac -= sec; *fsec += rint(frac * 1000000); + return true; } /* 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 AdjustFractSeconds(frac, tm, fsec, SECS_PER_DAY); +} + +/* 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 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 */ @@ -3275,56 +3313,69 @@ DecodeInterval(char **field, int *ftype, int nf, int range, case DTK_MINUTE: tm->tm_min += val; - AdjustFractSeconds(fval, tm, fsec, SECS_PER_MINUTE); + if (!AdjustFractSeconds(fval, tm, fsec, SECS_PER_MINUTE)) + return DTERR_FIELD_OVERFLOW; tmask = DTK_M(MINUTE); break; case DTK_HOUR: tm->tm_hour += val; - AdjustFractSeconds(fval, tm, fsec, SECS_PER_HOUR); + if (!AdjustFractSeconds(fval, tm, fsec, SECS_PER_HOUR)) + return DTERR_FIELD_OVERFLOW; tmask = DTK_M(HOUR); type = DTK_DAY; /* set for next field */ break; case DTK_DAY: tm->tm_mday += val; - AdjustFractSeconds(fval, tm, fsec, SECS_PER_DAY); + if (!AdjustFractSeconds(fval, tm, fsec, SECS_PER_DAY)) + return DTERR_FIELD_OVERFLOW; 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 (!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 +3491,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; diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out index accd4a7d90..794f82b373 100644 --- a/src/test/regress/expected/interval.out +++ b/src/test/regress/expected/interval.out @@ -232,6 +232,74 @@ 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 ('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... + ^ +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 ... + ^ -- 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..d524e2dfcf 100644 --- a/src/test/regress/sql/interval.sql +++ b/src/test/regress/sql/interval.sql @@ -72,6 +72,24 @@ 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 ('0.1 months 2147483647 days'); +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'); -- Test edge-case overflow detection in interval multiplication select extract(epoch from '256 microseconds'::interval * (2^55)::float8); -- 2.25.1