1
0
mirror of https://github.com/postgres/postgres.git synced 2025-07-28 23:42:10 +03:00

Fix various overflow hazards in date and timestamp functions.

This commit makes use of the overflow-aware routines in int.h to
fix a variety of reported overflow bugs in the date and timestamp
code.  It seems unlikely that this fixes all such bugs in this
area, but since the problems seem limited to cases that are far
beyond any realistic usage, I'm not going to worry too much.  Note
that for one bug, I've chosen to simply add a comment about the
overflow hazard because fixing it would require quite a bit of code
restructuring that doesn't seem worth the risk.

Since this is a bug fix, it could be back-patched, but given the
risk of conflicts with the new routines in int.h and the overall
risk/reward ratio of this patch, I've opted not to do so for now.

Fixes bug #18585 (except for the one case that's just commented).

Reported-by: Alexander Lakhin
Author: Matthew Kim, Nathan Bossart
Reviewed-by: Joseph Koshakow, Jian He
Discussion: https://postgr.es/m/31ad2cd1-db94-bdb3-f91a-65ffdb4bef95%40gmail.com
Discussion: https://postgr.es/m/18585-db646741dd649abd%40postgresql.org
This commit is contained in:
Nathan Bossart
2024-12-09 13:47:23 -06:00
parent 3eea7a0c97
commit 0a27c3d0f7
8 changed files with 179 additions and 9 deletions

View File

@ -256,8 +256,15 @@ make_date(PG_FUNCTION_ARGS)
/* Handle negative years as BC */ /* Handle negative years as BC */
if (tm.tm_year < 0) if (tm.tm_year < 0)
{ {
int year = tm.tm_year;
bc = true; bc = true;
tm.tm_year = -tm.tm_year; if (pg_neg_s32_overflow(year, &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)));
tm.tm_year = year;
} }
dterr = ValidateDate(DTK_DATE_M, false, false, bc, &tm); dterr = ValidateDate(DTK_DATE_M, false, false, bc, &tm);

View File

@ -77,6 +77,7 @@
#include "catalog/pg_collation.h" #include "catalog/pg_collation.h"
#include "catalog/pg_type.h" #include "catalog/pg_type.h"
#include "common/int.h"
#include "common/unicode_case.h" #include "common/unicode_case.h"
#include "common/unicode_category.h" #include "common/unicode_category.h"
#include "mb/pg_wchar.h" #include "mb/pg_wchar.h"
@ -3826,7 +3827,14 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out,
ereturn(escontext,, ereturn(escontext,,
(errcode(ERRCODE_INVALID_DATETIME_FORMAT), (errcode(ERRCODE_INVALID_DATETIME_FORMAT),
errmsg("invalid input string for \"Y,YYY\""))); errmsg("invalid input string for \"Y,YYY\"")));
years += (millennia * 1000);
/* years += (millennia * 1000); */
if (pg_mul_s32_overflow(millennia, 1000, &millennia) ||
pg_add_s32_overflow(years, millennia, &years))
ereturn(escontext,,
(errcode(ERRCODE_DATETIME_FIELD_OVERFLOW),
errmsg("value for \"Y,YYY\" in source string is out of range")));
if (!from_char_set_int(&out->year, years, n, escontext)) if (!from_char_set_int(&out->year, years, n, escontext))
return; return;
out->yysz = 4; out->yysz = 4;
@ -4785,10 +4793,35 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std,
tm->tm_year = tmfc.year % 100; tm->tm_year = tmfc.year % 100;
if (tm->tm_year) if (tm->tm_year)
{ {
int tmp;
if (tmfc.cc >= 0) if (tmfc.cc >= 0)
tm->tm_year += (tmfc.cc - 1) * 100; {
/* tm->tm_year += (tmfc.cc - 1) * 100; */
tmp = tmfc.cc - 1;
if (pg_mul_s32_overflow(tmp, 100, &tmp) ||
pg_add_s32_overflow(tm->tm_year, tmp, &tm->tm_year))
{
DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL,
text_to_cstring(date_txt), "timestamp",
escontext);
goto fail;
}
}
else else
tm->tm_year = (tmfc.cc + 1) * 100 - tm->tm_year + 1; {
/* tm->tm_year = (tmfc.cc + 1) * 100 - tm->tm_year + 1; */
tmp = tmfc.cc + 1;
if (pg_mul_s32_overflow(tmp, 100, &tmp) ||
pg_sub_s32_overflow(tmp, tm->tm_year, &tmp) ||
pg_add_s32_overflow(tmp, 1, &tm->tm_year))
{
DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL,
text_to_cstring(date_txt), "timestamp",
escontext);
goto fail;
}
}
} }
else else
{ {
@ -4814,11 +4847,31 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std,
if (tmfc.bc) if (tmfc.bc)
tmfc.cc = -tmfc.cc; tmfc.cc = -tmfc.cc;
if (tmfc.cc >= 0) if (tmfc.cc >= 0)
{
/* +1 because 21st century started in 2001 */ /* +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 else
{
/* +1 because year == 599 is 600 BC */ /* +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); fmask |= DTK_M(YEAR);
} }
@ -4843,11 +4896,31 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std,
fmask |= DTK_DATE_M; fmask |= DTK_DATE_M;
} }
else else
tmfc.ddd = (tmfc.ww - 1) * 7 + 1; {
/* tmfc.ddd = (tmfc.ww - 1) * 7 + 1; */
if (pg_sub_s32_overflow(tmfc.ww, 1, &tmfc.ddd) ||
pg_mul_s32_overflow(tmfc.ddd, 7, &tmfc.ddd) ||
pg_add_s32_overflow(tmfc.ddd, 1, &tmfc.ddd))
{
DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL,
date_str, "timestamp", escontext);
goto fail;
}
}
} }
if (tmfc.w) if (tmfc.w)
tmfc.dd = (tmfc.w - 1) * 7 + 1; {
/* tmfc.dd = (tmfc.w - 1) * 7 + 1; */
if (pg_sub_s32_overflow(tmfc.w, 1, &tmfc.dd) ||
pg_mul_s32_overflow(tmfc.dd, 7, &tmfc.dd) ||
pg_add_s32_overflow(tmfc.dd, 1, &tmfc.dd))
{
DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL,
date_str, "timestamp", escontext);
goto fail;
}
}
if (tmfc.dd) if (tmfc.dd)
{ {
tm->tm_mday = tmfc.dd; tm->tm_mday = tmfc.dd;
@ -4912,7 +4985,18 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std,
} }
if (tmfc.ms) if (tmfc.ms)
*fsec += tmfc.ms * 1000; {
int tmp = 0;
/* *fsec += tmfc.ms * 1000; */
if (pg_mul_s32_overflow(tmfc.ms, 1000, &tmp) ||
pg_add_s32_overflow(*fsec, tmp, fsec))
{
DateTimeParseError(DTERR_FIELD_OVERFLOW, NULL,
date_str, "timestamp", escontext);
goto fail;
}
}
if (tmfc.us) if (tmfc.us)
*fsec += tmfc.us; *fsec += tmfc.us;
if (fprec) if (fprec)

View File

@ -5104,6 +5104,10 @@ interval_trunc(PG_FUNCTION_ARGS)
* *
* Return the Julian day which corresponds to the first day (Monday) of the given ISO 8601 year and week. * Return the Julian day which corresponds to the first day (Monday) of the given ISO 8601 year and week.
* Julian days are used to convert between ISO week dates and Gregorian dates. * Julian days are used to convert between ISO week dates and Gregorian dates.
*
* XXX: This function has integer overflow hazards, but restructuring it to
* work with the soft-error handling that its callers do is likely more
* trouble than it's worth.
*/ */
int int
isoweek2j(int year, int week) isoweek2j(int year, int week)

View File

@ -117,6 +117,22 @@ pg_mul_s16_overflow(int16 a, int16 b, int16 *result)
#endif #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 static inline uint16
pg_abs_s16(int16 a) pg_abs_s16(int16 a)
{ {
@ -185,6 +201,22 @@ pg_mul_s32_overflow(int32 a, int32 b, int32 *result)
#endif #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 static inline uint32
pg_abs_s32(int32 a) pg_abs_s32(int32 a)
{ {
@ -300,6 +332,22 @@ pg_mul_s64_overflow(int64 a, int64 b, int64 *result)
#endif #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 static inline uint64
pg_abs_s64(int64 a) pg_abs_s64(int64 a)
{ {

View File

@ -1528,6 +1528,8 @@ select make_date(2013, 13, 1);
ERROR: date field value out of range: 2013-13-01 ERROR: date field value out of range: 2013-13-01
select make_date(2013, 11, -1); select make_date(2013, 11, -1);
ERROR: date field value out of range: 2013-11--1 ERROR: date field value out of range: 2013-11--1
SELECT make_date(-2147483648, 1, 1);
ERROR: date field value out of range: -2147483648-01-01
select make_time(10, 55, 100.1); select make_time(10, 55, 100.1);
ERROR: time field value out of range: 10:55:100.1 ERROR: time field value out of range: 10:55:100.1
select make_time(24, 0, 2.1); select make_time(24, 0, 2.1);

View File

@ -3754,6 +3754,14 @@ SELECT to_timestamp('2015-02-11 86000', 'YYYY-MM-DD SSSSS'); -- ok
SELECT to_timestamp('2015-02-11 86400', 'YYYY-MM-DD SSSSS'); SELECT to_timestamp('2015-02-11 86400', 'YYYY-MM-DD SSSSS');
ERROR: date/time field value out of range: "2015-02-11 86400" ERROR: date/time field value out of range: "2015-02-11 86400"
SELECT to_timestamp('1000000000,999', 'Y,YYY');
ERROR: value for "Y,YYY" in source string is out of range
SELECT to_timestamp('0.-2147483648', 'SS.MS');
ERROR: date/time field value out of range: "0.-2147483648"
SELECT to_timestamp('613566758', 'W');
ERROR: date/time field value out of range: "613566758"
SELECT to_timestamp('2024 613566758 1', 'YYYY WW D');
ERROR: date/time field value out of range: "2024 613566758 1"
SELECT to_date('2016-13-10', 'YYYY-MM-DD'); SELECT to_date('2016-13-10', 'YYYY-MM-DD');
ERROR: date/time field value out of range: "2016-13-10" ERROR: date/time field value out of range: "2016-13-10"
SELECT to_date('2016-02-30', 'YYYY-MM-DD'); SELECT to_date('2016-02-30', 'YYYY-MM-DD');
@ -3794,6 +3802,14 @@ SELECT to_date('0000-02-01','YYYY-MM-DD'); -- allowed, though it shouldn't be
02-01-0001 BC 02-01-0001 BC
(1 row) (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"
SELECT to_date('-2147483648 01', 'CC YY');
ERROR: date/time field value out of range: "-2147483648 01"
SELECT to_date('2147483647 01', 'CC YY');
ERROR: date/time field value out of range: "2147483647 01"
-- to_char's TZ format code produces zone abbrev if known -- 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'); SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
to_char to_char

View File

@ -371,5 +371,6 @@ select make_date(0, 7, 15);
select make_date(2013, 2, 30); select make_date(2013, 2, 30);
select make_date(2013, 13, 1); select make_date(2013, 13, 1);
select make_date(2013, 11, -1); select make_date(2013, 11, -1);
SELECT make_date(-2147483648, 1, 1);
select make_time(10, 55, 100.1); select make_time(10, 55, 100.1);
select make_time(24, 0, 2.1); select make_time(24, 0, 2.1);

View File

@ -651,6 +651,10 @@ SELECT to_timestamp('2015-02-11 86000', 'YYYY-MM-DD SSSS'); -- ok
SELECT to_timestamp('2015-02-11 86400', 'YYYY-MM-DD SSSS'); SELECT to_timestamp('2015-02-11 86400', 'YYYY-MM-DD SSSS');
SELECT to_timestamp('2015-02-11 86000', 'YYYY-MM-DD SSSSS'); -- ok SELECT to_timestamp('2015-02-11 86000', 'YYYY-MM-DD SSSSS'); -- ok
SELECT to_timestamp('2015-02-11 86400', 'YYYY-MM-DD SSSSS'); SELECT to_timestamp('2015-02-11 86400', 'YYYY-MM-DD SSSSS');
SELECT to_timestamp('1000000000,999', 'Y,YYY');
SELECT to_timestamp('0.-2147483648', 'SS.MS');
SELECT to_timestamp('613566758', 'W');
SELECT to_timestamp('2024 613566758 1', 'YYYY WW D');
SELECT to_date('2016-13-10', 'YYYY-MM-DD'); SELECT to_date('2016-13-10', 'YYYY-MM-DD');
SELECT to_date('2016-02-30', 'YYYY-MM-DD'); SELECT to_date('2016-02-30', 'YYYY-MM-DD');
SELECT to_date('2016-02-29', 'YYYY-MM-DD'); -- ok SELECT to_date('2016-02-29', 'YYYY-MM-DD'); -- ok
@ -661,6 +665,10 @@ SELECT to_date('2016 365', 'YYYY DDD'); -- ok
SELECT to_date('2016 366', 'YYYY DDD'); -- ok SELECT to_date('2016 366', 'YYYY DDD'); -- ok
SELECT to_date('2016 367', 'YYYY DDD'); 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('0000-02-01','YYYY-MM-DD'); -- allowed, though it shouldn't be
SELECT to_date('100000000', 'CC');
SELECT to_date('-100000000', 'CC');
SELECT to_date('-2147483648 01', 'CC YY');
SELECT to_date('2147483647 01', 'CC YY');
-- to_char's TZ format code produces zone abbrev if known -- 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'); SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');