Commit 634d3ff2 authored by unknown's avatar unknown
Browse files

Fixes a number of problems with time/datetime <-> string conversion functions:

- bug #11655 "Wrong time is returning from nested selects - maximum time exists
- input and output TIME values were not validated properly in several conversion functions
- bug #20927 "sec_to_time treats big unsigned as signed"
- integer overflows were not checked in several functions. As a result, input values like 2^32 or 3600*2^32 were treated as 0
- BIGINT UNSIGNED values were treated as SIGNED in several functions
- in cases where both input string truncation and out-of-range TIME value occur, only 'truncated incorrect time value' warning was produced


include/my_time.h:
  Added defines for the TIME limits
  Added defines for the warning flags set by str_to_time() and check_time_range()
  Added check_time_range() declaration
mysql-test/r/func_sapdb.result:
  Fixed testcases which relied on incorrect TIMEDIFF() behaviour
mysql-test/r/func_time.result:
  Fixed testcase which relied on incorrect behaviour
  Added testcases for out-of-range values in SEC_TO_TIME(), TIME_TO_SEC(), ADDTIME(), SUBTIME() and EXTRACT()
mysql-test/t/func_time.test:
  Added testcases for out-of-range values in SEC_TO_TIME(), TIME_TO_SEC(), ADDTIME(), SUBTIME() and EXTRACT()
sql-common/my_time.c:
  Added check_time_range() to be used from str_to_time() and item_timefunc.cc
  Added new out-of-range flag to str_to_time() warnings
  Use '%u' instead of '%d' in my_*_to_str() because the arguments are unsigned
sql/field.cc:
  Replaced out-of-range checks with checks for flags returned by str_to_time()
sql/item_timefunc.cc:
  Added wrappers over make_datetime() and make_time() which perform out-of-range checks on input values
  Moved common code in Item_func_sec_to_time::val_str() and Item_func_sec_to_time::val_int() into a separate function sec_to_time()
  Replaced calls to make_datetime() with make_datetime_with_warn() in Item_func_add_time and Item_func_timediff
  Checks for 'unsigned int' overflows in Item_func_maketime
  Use make_time_with_warn() instead of make_time() in Item_func_maketime
  Fixed incorrect sizeof() in Item_func_str_to_date::get_time()
sql/time.cc:
  Check for return value of str_to_time() along with warning flags
parent 7c8931d8
Loading
Loading
Loading
Loading
+13 −1
Original line number Diff line number Diff line
@@ -44,12 +44,24 @@ typedef long my_time_t;
#define TIME_FUZZY_DATE    1
#define TIME_DATETIME_ONLY 2

#define MYSQL_TIME_WARN_TRUNCATED    1
#define MYSQL_TIME_WARN_OUT_OF_RANGE 2

/* Limits for the TIME data type */
#define TIME_MAX_HOUR 838
#define TIME_MAX_MINUTE 59
#define TIME_MAX_SECOND 59
#define TIME_MAX_VALUE (TIME_MAX_HOUR*10000 + TIME_MAX_MINUTE*100 + \
                        TIME_MAX_SECOND)

enum enum_mysql_timestamp_type
str_to_datetime(const char *str, uint length, MYSQL_TIME *l_time,
                uint flags, int *was_cut);

bool str_to_time(const char *str,uint length, MYSQL_TIME *l_time,
                 int *was_cut);
                 int *warning);

int check_time_range(struct st_mysql_time *time, int *warning);

long calc_daynr(uint year,uint month,uint day);

+11 −4
Original line number Diff line number Diff line
@@ -97,7 +97,9 @@ subtime("02:01:01.999999", "01:01:01.999999")
01:00:00.000000
select timediff("1997-01-01 23:59:59.000001","1995-12-31 23:59:59.000002");
timediff("1997-01-01 23:59:59.000001","1995-12-31 23:59:59.000002")
8807:59:59.999999
838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '8807:59:59.999999'
select timediff("1997-12-31 23:59:59.000001","1997-12-30 01:01:01.000002");
timediff("1997-12-31 23:59:59.000001","1997-12-30 01:01:01.000002")
46:58:57.999999
@@ -208,13 +210,16 @@ NULL NULL
SELECT TIMEDIFF(t1,t4) As ttt, TIMEDIFF(t2, t3) As qqq from test;
ttt	qqq
-744:00:00	NULL
26305:01:02	22:58:58
-26305:01:02	-22:58:58
838:59:59	22:58:58
-838:59:59	-22:58:58
NULL	26:02:02
00:00:00	-26:02:02
NULL	NULL
NULL	NULL
00:00:00	-24:00:00
Warnings:
Warning	1292	Truncated incorrect time value: '26305:01:02'
Warning	1292	Truncated incorrect time value: '-26305:01:02'
drop table t1, test;
select addtime("-01:01:01.01", "-23:59:59.1") as a;
a
@@ -224,7 +229,9 @@ a
10000
select microsecond(19971231235959.01) as a;
a
10000
0
Warnings:
Warning	1292	Truncated incorrect time value: '19971231235959.01'
select date_add("1997-12-31",INTERVAL "10.09" SECOND_MICROSECOND) as a;
a
1997-12-31 00:00:10.090000
+91 −1
Original line number Diff line number Diff line
@@ -330,7 +330,9 @@ extract(DAY_MINUTE FROM "02 10:11:12")
21011
select extract(DAY_SECOND FROM "225 10:11:12");
extract(DAY_SECOND FROM "225 10:11:12")
225101112
8385959
Warnings:
Warning	1292	Truncated incorrect time value: '225 10:11:12'
select extract(HOUR FROM "1999-01-02 10:11:12");
extract(HOUR FROM "1999-01-02 10:11:12")
10
@@ -688,3 +690,91 @@ t1 CREATE TABLE `t1` (
  `from_unixtime(1) + 0` double(23,6) default NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1
drop table t1;
SELECT SEC_TO_TIME(3300000);
SEC_TO_TIME(3300000)
838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '3300000'
SELECT SEC_TO_TIME(3300000)+0;
SEC_TO_TIME(3300000)+0
8385959.000000
Warnings:
Warning	1292	Truncated incorrect time value: '3300000'
SELECT SEC_TO_TIME(3600 * 4294967296);
SEC_TO_TIME(3600 * 4294967296)
838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '15461882265600'
SELECT TIME_TO_SEC('916:40:00');
TIME_TO_SEC('916:40:00')
3020399
Warnings:
Warning	1292	Truncated incorrect time value: '916:40:00'
SELECT ADDTIME('500:00:00', '416:40:00');
ADDTIME('500:00:00', '416:40:00')
838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '916:40:00'
SELECT ADDTIME('916:40:00', '416:40:00');
ADDTIME('916:40:00', '416:40:00')
838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '916:40:00'
Warning	1292	Truncated incorrect time value: '1255:39:59'
SELECT SUBTIME('916:40:00', '416:40:00');
SUBTIME('916:40:00', '416:40:00')
422:19:59
Warnings:
Warning	1292	Truncated incorrect time value: '916:40:00'
SELECT SUBTIME('-916:40:00', '416:40:00');
SUBTIME('-916:40:00', '416:40:00')
-838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '-916:40:00'
Warning	1292	Truncated incorrect time value: '-1255:39:59'
SELECT MAKETIME(916,0,0);
MAKETIME(916,0,0)
838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '916:00:00'
SELECT MAKETIME(4294967296, 0, 0);
MAKETIME(4294967296, 0, 0)
838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '4294967296:00:00'
SELECT MAKETIME(-4294967296, 0, 0);
MAKETIME(-4294967296, 0, 0)
-838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '-4294967296:00:00'
SELECT MAKETIME(0, 4294967296, 0);
MAKETIME(0, 4294967296, 0)
NULL
SELECT MAKETIME(0, 0, 4294967296);
MAKETIME(0, 0, 4294967296)
NULL
SELECT MAKETIME(CAST(-1 AS UNSIGNED), 0, 0);
MAKETIME(CAST(-1 AS UNSIGNED), 0, 0)
838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '18446744073709551615:00:00'
SELECT EXTRACT(HOUR FROM '100000:02:03');
EXTRACT(HOUR FROM '100000:02:03')
838
Warnings:
Warning	1292	Truncated incorrect time value: '100000:02:03'
CREATE TABLE t1(f1 TIME);
INSERT INTO t1 VALUES('916:00:00 a');
Warnings:
Warning	1265	Data truncated for column 'f1' at row 1
Warning	1264	Data truncated; out of range for column 'f1' at row 1
SELECT * FROM t1;
f1
838:59:59
DROP TABLE t1;
SELECT SEC_TO_TIME(CAST(-1 AS UNSIGNED));
SEC_TO_TIME(CAST(-1 AS UNSIGNED))
838:59:59
Warnings:
Warning	1292	Truncated incorrect time value: '18446744073709551615'
End of 4.1 tests
+44 −1
Original line number Diff line number Diff line
@@ -358,4 +358,47 @@ create table t1 select now() - now(), curtime() - curtime(),
show create table t1;
drop table t1;

# End of 4.1 tests
#
# Bug #11655: Wrong time is returning from nested selects - maximum time exists
#
# check if SEC_TO_TIME() handles out-of-range values correctly
SELECT SEC_TO_TIME(3300000);
SELECT SEC_TO_TIME(3300000)+0;
SELECT SEC_TO_TIME(3600 * 4294967296);

# check if TIME_TO_SEC() handles out-of-range values correctly
SELECT TIME_TO_SEC('916:40:00');

# check if ADDTIME() handles out-of-range values correctly
SELECT ADDTIME('500:00:00', '416:40:00');
SELECT ADDTIME('916:40:00', '416:40:00');

# check if SUBTIME() handles out-of-range values correctly
SELECT SUBTIME('916:40:00', '416:40:00');
SELECT SUBTIME('-916:40:00', '416:40:00');

# check if MAKETIME() handles out-of-range values correctly
SELECT MAKETIME(916,0,0);
SELECT MAKETIME(4294967296, 0, 0);
SELECT MAKETIME(-4294967296, 0, 0);
SELECT MAKETIME(0, 4294967296, 0);
SELECT MAKETIME(0, 0, 4294967296);
SELECT MAKETIME(CAST(-1 AS UNSIGNED), 0, 0);

# check if EXTRACT() handles out-of-range values correctly
SELECT EXTRACT(HOUR FROM '100000:02:03');

# check if we get proper warnings if both input string truncation
# and out-of-range value occur
CREATE TABLE t1(f1 TIME);
INSERT INTO t1 VALUES('916:00:00 a');
SELECT * FROM t1;
DROP TABLE t1;

#
# Bug #20927: sec_to_time treats big unsigned as signed
#
# check if SEC_TO_TIME() handles BIGINT UNSIGNED values correctly
SELECT SEC_TO_TIME(CAST(-1 AS UNSIGNED));

--echo End of 4.1 tests
+69 −22
Original line number Diff line number Diff line
@@ -402,8 +402,10 @@ str_to_datetime(const char *str, uint length, MYSQL_TIME *l_time,
                        There may be an optional [.second_part] after seconds
   length               Length of str
   l_time               Store result here
   was_cut              Set to 1 if value was cut during conversion or to 0
                        otherwise.
   warning              Set MYSQL_TIME_WARN_TRUNCATED flag if the input string
                        was cut during conversion, and/or
                        MYSQL_TIME_WARN_OUT_OF_RANGE flag, if the value is
                        out of range.

   NOTES
     Because of the extra days argument, this function can only
@@ -414,16 +416,16 @@ str_to_datetime(const char *str, uint length, MYSQL_TIME *l_time,
     1  error
*/

bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
                 int *was_cut)
bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time, int *warning)
{
  long date[5],value;
  ulong date[5];
  ulonglong value;
  const char *end=str+length, *end_of_days;
  bool found_days,found_hours;
  uint state;

  l_time->neg=0;
  *was_cut= 0;
  *warning= 0;
  for (; str != end && my_isspace(&my_charset_latin1,*str) ; str++)
    length--;
  if (str != end && *str == '-')
@@ -438,13 +440,16 @@ bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
  /* Check first if this is a full TIMESTAMP */
  if (length >= 12)
  {                                             /* Probably full timestamp */
    int was_cut;
    enum enum_mysql_timestamp_type
      res= str_to_datetime(str, length, l_time,
                           (TIME_FUZZY_DATE | TIME_DATETIME_ONLY), was_cut);
                           (TIME_FUZZY_DATE | TIME_DATETIME_ONLY), &was_cut);
    if ((int) res >= (int) MYSQL_TIMESTAMP_ERROR)
    {
      if (was_cut)
        *warning|= MYSQL_TIME_WARN_TRUNCATED;
      return res == MYSQL_TIMESTAMP_ERROR;
    /* We need to restore was_cut flag since str_to_datetime can modify it */
    *was_cut= 0;
    }
  }

  /* Not a timestamp. Try to get this as a DAYS_TO_SECOND string */
@@ -524,7 +529,7 @@ bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
    if (field_length > 0)
      value*= (long) log_10_int[field_length];
    else if (field_length < 0)
      *was_cut= 1;
      *warning|= MYSQL_TIME_WARN_TRUNCATED;
    date[4]=value;
  }
  else
@@ -538,10 +543,7 @@ bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
       ((str[1] == '-' || str[1] == '+') &&
        (end - str) > 2 &&
        my_isdigit(&my_charset_latin1, str[2]))))
  {
    *was_cut= 1;
    return 1;
  }

  if (internal_format_positions[7] != 255)
  {
@@ -560,12 +562,12 @@ bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
    }
  }

  /* Some simple checks */
  if (date[2] >= 60 || date[3] >= 60)
  {
    *was_cut= 1;
  /* Integer overflow checks */
  if (date[0] > UINT_MAX || date[1] > UINT_MAX ||
      date[2] > UINT_MAX || date[3] > UINT_MAX ||
      date[4] > UINT_MAX)
    return 1;
  }
  
  l_time->year=         0;                      /* For protocol::store_time */
  l_time->month=        0;
  l_time->day=          date[0];
@@ -575,6 +577,10 @@ bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
  l_time->second_part=  date[4];
  l_time->time_type= MYSQL_TIMESTAMP_TIME;

  /* Check if the value is valid and fits into TIME range */
  if (check_time_range(l_time, warning))
    return 1;
  
  /* Check if there is garbage at end of the TIME specification */
  if (str != end)
  {
@@ -582,7 +588,7 @@ bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
    {
      if (!my_isspace(&my_charset_latin1,*str))
      {
        *was_cut= 1;
        *warning|= MYSQL_TIME_WARN_TRUNCATED;
        break;
      }
    } while (++str != end);
@@ -591,6 +597,47 @@ bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
}


/*
  Check 'time' value to lie in the TIME range

  SYNOPSIS:
    check_time_range()
    time     pointer to TIME value
    warning  set MYSQL_TIME_WARN_OUT_OF_RANGE flag if the value is out of range

  DESCRIPTION
  If the time value lies outside of the range [-838:59:59, 838:59:59],
  set it to the closest endpoint of the range and set
  MYSQL_TIME_WARN_OUT_OF_RANGE flag in the 'warning' variable.

  RETURN
    0        time value is valid, but was possibly truncated
    1        time value is invalid
*/

int check_time_range(struct st_mysql_time *time, int *warning) 
{
  longlong hour;

  if (time->minute >= 60 || time->second >= 60)
    return 1;

  hour= time->hour + (24*time->day);
  if (hour <= TIME_MAX_HOUR &&
      (hour != TIME_MAX_HOUR || time->minute != TIME_MAX_MINUTE ||
       time->second != TIME_MAX_SECOND || !time->second_part))
    return 0;

  time->day= 0;
  time->hour= TIME_MAX_HOUR;
  time->minute= TIME_MAX_MINUTE;
  time->second= TIME_MAX_SECOND;
  time->second_part= 0;
  *warning|= MYSQL_TIME_WARN_OUT_OF_RANGE;
  return 0;
}


/*
  Prepare offset of system time zone from UTC for my_system_gmt_sec() func.

@@ -776,7 +823,7 @@ void set_zero_time(MYSQL_TIME *tm, enum enum_mysql_timestamp_type time_type)
int my_time_to_str(const MYSQL_TIME *l_time, char *to)
{
  uint extra_hours= 0;
  return my_sprintf(to, (to, "%s%02d:%02d:%02d",
  return my_sprintf(to, (to, "%s%02u:%02u:%02u",
                         (l_time->neg ? "-" : ""),
                         extra_hours+ l_time->hour,
                         l_time->minute,
@@ -785,7 +832,7 @@ int my_time_to_str(const MYSQL_TIME *l_time, char *to)

int my_date_to_str(const MYSQL_TIME *l_time, char *to)
{
  return my_sprintf(to, (to, "%04d-%02d-%02d",
  return my_sprintf(to, (to, "%04u-%02u-%02u",
                         l_time->year,
                         l_time->month,
                         l_time->day));
@@ -793,7 +840,7 @@ int my_date_to_str(const MYSQL_TIME *l_time, char *to)

int my_datetime_to_str(const MYSQL_TIME *l_time, char *to)
{
  return my_sprintf(to, (to, "%04d-%02d-%02d %02d:%02d:%02d",
  return my_sprintf(to, (to, "%04u-%02u-%02u %02u:%02u:%02u",
                         l_time->year,
                         l_time->month,
                         l_time->day,
Loading