Uploaded image for project: 'MariaDB Server'
  1. MariaDB Server
  2. MDEV-41549

Date and datetime strings take year/month/day/hour/minute/second modulo 2^32 (and TIME strings minute/second modulo 2^64) without a warning: CAST('4294969296-11-21' AS DATE) = 2000-11-21

    XMLWordPrintable

Details

    Description

      A date or datetime string whose year, month, day, hour, minute or second field
      has 10 or more digits is accepted with that field taken modulo 2^32, and gives a
      valid, unrelated date with no warning. A field one digit shorter is rejected
      with "Incorrect datetime value":

      SELECT CAST('4294969296-11-21' AS DATE);        -- 2000-11-21    (4294969296 = 2^32 + 2000)
      SELECT CAST('20001-11-21' AS DATE);             -- NULL + 1292 Incorrect datetime value
      SELECT CAST('2004-4294967297-01' AS DATE);      -- 2004-01-01    (month 2^32 + 1)
      SELECT CAST('2004-01-4294967297' AS DATE);      -- 2004-01-01    (day 2^32 + 1)
      SELECT CAST('2004-01-32' AS DATE);              -- NULL + 1292
      SELECT CAST('2000-01-01 12:4294967316:00' AS DATETIME);   -- 2000-01-01 12:20:00
      SELECT CAST('2000-01-01 4294967308:00:00' AS DATETIME);   -- 2000-01-01 12:00:00
      

      Every function that parses a date string is affected, e.g.

      SELECT DATE_FORMAT('4294969300-01-01', '%Y-%m-%d');            -- '2004-01-01'
      SELECT GREATEST(DATE '1995-05-05', '10-10-4294967306');        -- 2010-10-10
      SELECT TIMESTAMPDIFF(MONTH, '4294969297-02-01', '2001-05-01'); -- 3
      

      and the wrapped value is stored, even in strict mode:

      CREATE TABLE t (d DATE);
      SET sql_mode = 'STRICT_ALL_TABLES';
      INSERT INTO t VALUES ('2004-4294967297-01');   -- succeeds, stores 2004-01-01
      INSERT INTO t VALUES ('2004-13-01');           -- ERROR 1292 Incorrect date value
      

      MySQL 26.10.0 and TiDB reject every one of these date strings with warning 1292
      (NULL, except GREATEST, where they fall back to another value with the warning) and
      reject the INSERT with ERROR 1292.

      Where (main, 82964737): sql-common/my_time.c, get_number(), lines 135-152, which
      str_to_datetime_or_date_body() uses for all six fields (lines 581-596):

      static int get_number(uint *val, uint *number_of_fields, const char **str,
                            const char *end)
      {
        ...
        *val= *s++ - '0';
        for (; s < end && my_isdigit(&my_charset_latin1, *s); s++)
          *val= *val * 10 + *s - '0';
      

      `*val` is a 32-bit uint and there is no overflow check, so the later range checks
      (month <= 12, day <= 31, year <= 9999, ...) see the wrapped value.

      TIME strings: minute and second fields wrap at 2^64

      The TIME parser has the same problem one width up. It guards the first (hour)
      field but not the minute and second fields:

      SELECT CAST('19:40:18446744073709551643' AS TIME);   -- 19:40:27   (second = 2^64 + 27)
      SELECT CAST('19:18446744073709551656:27' AS TIME);   -- 19:40:27   (minute = 2^64 + 40)
      SELECT TIME_TO_SEC('00:00:18446744073709551617');    -- 1
      SELECT CAST('19:40:4294967323' AS TIME);             -- NULL + 1292 Incorrect time value
      SELECT CAST('19:40:60' AS TIME);                     -- NULL + 1292
      SELECT CAST('18446744073709551635:40:27' AS TIME);   -- NULL + 1292 (hour is checked)
      

      sql-common/my_time.c, str_to_DDhhmmssff_internal() (line numbers on main 82964737):

        943   for (value=0; str != end && my_isdigit(&my_charset_latin1,*str) ; str++)
        944   {
        945     value=value*10L + (long) (*str - '0');
        946     if (value >= 42949672955959ULL) /* i.e. UINT_MAX32 : 59 : 59 */
        ...
        995     for (value=0; str != end && my_isdigit(&my_charset_latin1,*str) ; str++)
        996       value=value*10L + (long) (*str - '0');
        997     date[state++]= (ulong) value;
      

      The loop at 995-996 accumulates in a ulonglong with no check, so a field of 2^64+k
      is k. (A field of 2^32+k is not wrapped – ulong is 64-bit here – and is rejected
      by the later "> 59" check, which is why '19:40:4294967323' is NULL.)

      Not the same code path as MDEV-40971 / MDEV-40987 (JSON path indexes, PERIOD_ADD,
      SUBSTRING_INDEX, ELT): this is the date/time string parser, not an integer argument of a function, in get_number() and str_to_DDhhmmssff_internal() (sql-common/my_time.c);
      a fix there does not change this one.

      ================================================================================
      STEPS TO REPRODUCE
      ================================================================================

      SELECT CAST('4294969296-11-21' AS DATE);                    -- 1
      SELECT CAST('2004-4294967297-01' AS DATE);                  -- 2
      SELECT CAST('2004-01-4294967297' AS DATE);                  -- 3
      SELECT CAST('2000-01-01 12:4294967316:00' AS DATETIME);     -- 4
      SELECT DATE_FORMAT('4294969300-01-01', '%Y-%m-%d');         -- 5
      SELECT CAST('19:40:18446744073709551643' AS TIME);          -- 6
      SELECT CAST('19:18446744073709551656:27' AS TIME);          -- 7
      

      ================================================================================
      EXPECTED
      ================================================================================

      1.-7.  NULL, with warning 1292 Incorrect datetime value / Incorrect time value
      

      – the answer MariaDB already gives when the same field is one digit shorter, or
      (for TIME) 2^32 + k instead of 2^64 + k.

      ================================================================================
      ACTUAL (13.2.0 main 82964737; identical on 13.0.2, 11.8.9, 10.11.19, 10.6.28)
      ================================================================================

      1.  2000-11-21
      2.  2004-01-01
      3.  2004-01-01
      4.  2000-01-01 12:20:00
      5.  '2004-01-01'
      6.  19:40:27
      7.  19:40:27
      

      No warnings.

      ================================================================================
      SUGGESTED FIX
      ================================================================================

      In get_number(), stop (return an error) once *val would exceed a bound larger than
      any valid field, e.g. 999999 or UINT_MAX32 / 10, before the multiplication; in
      str_to_DDhhmmssff_internal(), apply to the minute/second loop the same kind of
      guard the hour loop already has (any value above 59 can stop the loop, the range
      check rejects it anyway).

      ================================================================================
      ATTACHMENTS
      ================================================================================

      repro.py (pymysql; exits 1 while the recorded wrong answers are returned).
      VERIFY-main-82964737-2026-10-14.txt: transcript on main and the four release images.

      Attachments

        Issue Links

          Activity

            People

              raghunandan.bhat Raghunandan Bhat
              kehan5800 Ke Han
              Votes:
              0 Vote for this issue
              Watchers:
              3 Start watching this issue

              Dates

                Created:
                Updated:

                Git Integration

                  Error rendering 'com.xiplink.jira.git.jira_git_plugin:git-issue-webpanel'. Please contact your Jira administrators.