On Sat, Aug 24, 2024 at 08:44:40AM -0400, Joseph Koshakow wrote:
> FWIW, Matthew's patch actually does resolve a bug with `to_timestamp`
> and `to_date`. It converts the following incorrect queries
>
> test=# SELECT to_timestamp('2147483647,999', 'Y,YYY');
> to_timestamp
> ---------------------------------
> 0001-01-01 00:00:00-04:56:02 BC
> (1 row)
>
> test=# SELECT to_date('-2147483648', 'CC');
> to_date
> ------------
> 0001-01-01
> (1 row)
>
> into errors
>
> test=# SELECT to_timestamp('2147483647,999', 'Y,YYY');
> ERROR: invalid input string for "Y,YYY"
> test=# SELECT to_date('-2147483648', 'CC');
> ERROR: date out of range: "-2147483648"
>
> So, it might be worth committing only his changes before moving on.
Good point. Here is a v27 patch that extracts the bug fix portions of the
v26 patch. If/when this is committed, I think we should close the
commitfest entry.
--
nathan
>From 13e74f283a0994274e656a7d3e430d360884ae13 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <[email protected]>
Date: Thu, 5 Dec 2024 16:39:00 -0600
Subject: [PATCH v27 1/1] fix date/time overflows
---
src/backend/utils/adt/date.c | 6 +++-
src/backend/utils/adt/formatting.c | 32 +++++++++++++++--
src/include/common/int.h | 48 ++++++++++++++++++++++++++
src/test/regress/expected/date.out | 2 ++
src/test/regress/expected/horology.out | 6 ++++
src/test/regress/sql/date.sql | 1 +
src/test/regress/sql/horology.sql | 4 +++
7 files changed, 95 insertions(+), 4 deletions(-)
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8ac..d5ee96aa6c 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -257,7 +257,11 @@ make_date(PG_FUNCTION_ARGS)
if (tm.tm_year < 0)
{
bc = true;
- tm.tm_year = -tm.tm_year;
+ if (pg_neg_s32_overflow(tm.tm_year, &tm.tm_year))
+ ereport(ERROR,
+
(errcode(ERRCODE_DATETIME_FIELD_OVERFLOW),
+ errmsg("date field value out of range:
%d-%02d-%02d",
+ tm.tm_year, tm.tm_mon,
tm.tm_mday)));
}
dterr = ValidateDate(DTK_DATE_M, false, false, bc, &tm);
diff --git a/src/backend/utils/adt/formatting.c
b/src/backend/utils/adt/formatting.c
index 2bcc185708..9eb7d08169 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -77,6 +77,7 @@
#include "catalog/pg_collation.h"
#include "catalog/pg_type.h"
+#include "common/int.h"
#include "common/unicode_case.h"
#include "common/unicode_category.h"
#include "mb/pg_wchar.h"
@@ -3826,7 +3827,12 @@ DCH_from_char(FormatNode *node, const char *in,
TmFromChar *out,
ereturn(escontext,,
(errcode(ERRCODE_INVALID_DATETIME_FORMAT),
errmsg("invalid input string for \"Y,YYY\"")));
- years += (millennia * 1000);
+ if (pg_mul_s32_overflow(millennia,
1000, &millennia) ||
+ pg_add_s32_overflow(years,
millennia, &years))
+ ereturn(escontext,,
+
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("value
for \"Y,YYY\" in source string is out of range")));
+
if (!from_char_set_int(&out->year,
years, n, escontext))
return;
out->yysz = 4;
@@ -4814,11 +4820,31 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid,
bool std,
if (tmfc.bc)
tmfc.cc = -tmfc.cc;
if (tmfc.cc >= 0)
+ {
/* +1 because 21st century started in 2001 */
- tm->tm_year = (tmfc.cc - 1) * 100 + 1;
+ /* tm->tm_year = (tmfc.cc - 1) * 100 + 1; */
+ if (pg_mul_s32_overflow(tmfc.cc - 1, 100, &tm->tm_year)
||
+ pg_add_s32_overflow(tm->tm_year, 1,
&tm->tm_year))
+ {
+ DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL,
+
text_to_cstring(date_txt), "timestamp",
+ escontext);
+ goto fail;
+ }
+ }
else
+ {
/* +1 because year == 599 is 600 BC */
- tm->tm_year = tmfc.cc * 100 + 1;
+ /* tm->tm_year = tmfc.cc * 100 + 1; */
+ if (pg_mul_s32_overflow(tmfc.cc, 100, &tm->tm_year) ||
+ pg_add_s32_overflow(tm->tm_year, 1,
&tm->tm_year))
+ {
+ DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL,
+
text_to_cstring(date_txt), "timestamp",
+ escontext);
+ goto fail;
+ }
+ }
fmask |= DTK_M(YEAR);
}
diff --git a/src/include/common/int.h b/src/include/common/int.h
index 3b1590d676..6b50aa67b9 100644
--- a/src/include/common/int.h
+++ b/src/include/common/int.h
@@ -117,6 +117,22 @@ pg_mul_s16_overflow(int16 a, int16 b, int16 *result)
#endif
}
+static inline bool
+pg_neg_s16_overflow(int16 a, int16 *result)
+{
+#if defined(HAVE__BUILTIN_OP_OVERFLOW)
+ return __builtin_sub_overflow(0, a, result);
+#else
+ if (unlikely(a == PG_INT16_MIN))
+ {
+ *result = 0x5EED; /* to avoid spurious warnings */
+ return true;
+ }
+ *result = -a;
+ return false;
+#endif
+}
+
static inline uint16
pg_abs_s16(int16 a)
{
@@ -185,6 +201,22 @@ pg_mul_s32_overflow(int32 a, int32 b, int32 *result)
#endif
}
+static inline bool
+pg_neg_s32_overflow(int32 a, int32 *result)
+{
+#if defined(HAVE__BUILTIN_OP_OVERFLOW)
+ return __builtin_sub_overflow(0, a, result);
+#else
+ if (unlikely(a == PG_INT32_MIN))
+ {
+ *result = 0x5EED; /* to avoid spurious warnings */
+ return true;
+ }
+ *result = -a;
+ return false;
+#endif
+}
+
static inline uint32
pg_abs_s32(int32 a)
{
@@ -300,6 +332,22 @@ pg_mul_s64_overflow(int64 a, int64 b, int64 *result)
#endif
}
+static inline bool
+pg_neg_s64_overflow(int64 a, int64 *result)
+{
+#if defined(HAVE__BUILTIN_OP_OVERFLOW)
+ return __builtin_sub_overflow(0, a, result);
+#else
+ if (unlikely(a == PG_INT64_MIN))
+ {
+ *result = 0x5EED; /* to avoid spurious warnings */
+ return true;
+ }
+ *result = -a;
+ return false;
+#endif
+}
+
static inline uint64
pg_abs_s64(int64 a)
{
diff --git a/src/test/regress/expected/date.out
b/src/test/regress/expected/date.out
index c9cec70c38..674fcc2456 100644
--- a/src/test/regress/expected/date.out
+++ b/src/test/regress/expected/date.out
@@ -1532,3 +1532,5 @@ select make_time(10, 55, 100.1);
ERROR: time field value out of range: 10:55:100.1
select make_time(24, 0, 2.1);
ERROR: time field value out of range: 24:00:2.1
+SELECT make_date(-2147483648, 1, 1);
+ERROR: date field value out of range: -2147483648-01-01
diff --git a/src/test/regress/expected/horology.out
b/src/test/regress/expected/horology.out
index 6d7dd5c988..5f1b9ab075 100644
--- a/src/test/regress/expected/horology.out
+++ b/src/test/regress/expected/horology.out
@@ -3453,6 +3453,8 @@ SELECT i, to_timestamp('2018-11-02 12:34:56.123456',
'YYYY-MM-DD HH24:MI:SS.FF'
SELECT i, to_timestamp('2018-11-02 12:34:56.123456789', 'YYYY-MM-DD
HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
ERROR: date/time field value out of range: "2018-11-02 12:34:56.123456789"
+SELECT to_timestamp('1000000000,999', 'Y,YYY');
+ERROR: value for "Y,YYY" in source string is out of range
SELECT to_date('1 4 1902', 'Q MM YYYY'); -- Q is ignored
to_date
------------
@@ -3783,6 +3785,10 @@ SELECT to_date('0000-02-01','YYYY-MM-DD'); -- allowed,
though it shouldn't be
02-01-0001 BC
(1 row)
+SELECT to_date('100000000', 'CC');
+ERROR: date/time field value out of range: "100000000"
+SELECT to_date('-100000000', 'CC');
+ERROR: date/time field value out of range: "-100000000"
-- to_char's TZ format code produces zone abbrev if known
SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
to_char
diff --git a/src/test/regress/sql/date.sql b/src/test/regress/sql/date.sql
index 1c58ff6966..9a4e5832b9 100644
--- a/src/test/regress/sql/date.sql
+++ b/src/test/regress/sql/date.sql
@@ -373,3 +373,4 @@ select make_date(2013, 13, 1);
select make_date(2013, 11, -1);
select make_time(10, 55, 100.1);
select make_time(24, 0, 2.1);
+SELECT make_date(-2147483648, 1, 1);
diff --git a/src/test/regress/sql/horology.sql
b/src/test/regress/sql/horology.sql
index 0fe3c783e6..553cf83b15 100644
--- a/src/test/regress/sql/horology.sql
+++ b/src/test/regress/sql/horology.sql
@@ -559,6 +559,8 @@ SELECT i, to_timestamp('2018-11-02 12:34:56.12345',
'YYYY-MM-DD HH24:MI:SS.FF' |
SELECT i, to_timestamp('2018-11-02 12:34:56.123456', 'YYYY-MM-DD
HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
SELECT i, to_timestamp('2018-11-02 12:34:56.123456789', 'YYYY-MM-DD
HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
+SELECT to_timestamp('1000000000,999', 'Y,YYY');
+
SELECT to_date('1 4 1902', 'Q MM YYYY'); -- Q is ignored
SELECT to_date('3 4 21 01', 'W MM CC YY');
SELECT to_date('2458872', 'J');
@@ -660,6 +662,8 @@ SELECT to_date('2016 365', 'YYYY DDD'); -- ok
SELECT to_date('2016 366', 'YYYY DDD'); -- ok
SELECT to_date('2016 367', 'YYYY DDD');
SELECT to_date('0000-02-01','YYYY-MM-DD'); -- allowed, though it shouldn't be
+SELECT to_date('100000000', 'CC');
+SELECT to_date('-100000000', 'CC');
-- to_char's TZ format code produces zone abbrev if known
SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
--
2.39.5 (Apple Git-154)