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

Slave Crashes in table_def::table_def() on Malformed Table_map

    XMLWordPrintable

Details

    • Can result in hang or crash
    • Q3/2026 Replic. Development

    Description

      A Table_map event that declares more columns than its field metadata covers
      makes the applier read past the metadata buffer. Three consequences follow.

      • The walk runs up to 1,908,874,270 bytes past the end of the block, so the
        replica takes a SIGSEGV.
      • The bytes read become the source's declared column types, and
        show_sql_type() renders them into ER_SLAVE_CONVERSION_FAILED
        (sql/rpl_utility_server.cc:1000), so replica heap bytes surface as column
        lengths in Last_Error and in the error log.
      • When the decoded types happen to look compatible with the replica's own,
        table_def::calc_field_size() uses them to decide how many row-image bytes
        to consume. The replica then reads further out of bounds while unpacking the
        Rows_log_event and applies shifted values to its own tables, rather than
        failing.

      table_def::table_def() walks m_size columns and indexes
      field_metadata[index] unconditionally (sql/rpl_utility.cc:203 to
      :269). The loop is guarded only by if (m_size && metadata_size);
      metadata_size is never compared against index, and there is not even
      the assert(index <= metadata_size) that MySQL's pre-fix code carried.
      Seven column types advance index by two bytes (SET, ENUM, STRING, BIT,
      VARCHAR, VARCHAR_COMPRESSED, NEWDECIMAL) and eleven by one, so the walk can
      demand up to 2 * m_colcnt bytes.

      The event reader enforces only an upper bound
      (sql/log_event.cc:3769):

        if (m_field_metadata_size <= (m_colcnt * 2))   /* no lower bound */
      

      then allocates exactly m_field_metadata_size bytes and copies exactly
      that many. A Table_map declaring many MYSQL_TYPE_STRING columns while
      encoding m_field_metadata_size of 1 satisfies that check, satisfies the
      event-length checks MDEV-39689 added, and satisfies
      if (m_size && metadata_size). is_valid() is only
      m_memory != NULL, so the event is accepted and
      Table_map_log_event::do_apply_event() (sql/log_event_server.cc:6624)
      builds the table_def.

      With slave_max_allowed_packet at its 1 GB default, the byte budget allows
      954,437,139 columns, so the loop reads 1,908,874,278 bytes. int index
      peaks just below INT_MAX, so no signed overflow intervenes.

      Two further paths reach the same loop. mysqlbinlog v or -flashback
      on a crafted file, through Table_map_log_event::create_table_def(), which
      needs no server at all and is the most accessible. And BINLOG, gated by
      BINLOG_REPLAY_ACL or SUPER_ACL.

      Note that MDEV-39689 does not cover this. That commit touches
      sql/log_event.cc and sql/log_event_server.cc only, and every bound it
      added asks whether a declared length fits inside the event. None asks whether
      m_field_metadata_size is large enough for the metadata the
      m_coltype bytes demand, and the attack makes it smaller, not larger.
      sql/rpl_utility.cc has never carried a bound. All maintained branches are
      affected.

      To reproduce:

      Patch file:

      --- a/sql/log_event_server.cc
      +++ b/sql/log_event_server.cc
      @@ Table_map_log_event::Table_map_log_event(THD *thd, TABLE *tbl, ...)
         m_field_metadata_size= save_field_metadata();
         DBUG_ASSERT(m_field_metadata_size <= (m_colcnt * 2));
       
      +  DBUG_EXECUTE_IF("corrupt_table_map_shrink_field_metadata",
      +                  m_field_metadata_size= 1;);
      

      Test file:

      --source include/have_debug.inc
      --source include/have_binlog_format_row.inc
      --source include/master-slave.inc
       
      --connection slave
      call mtr.add_suppression("Slave SQL: Column 0 of table .test.t1. cannot be converted");
      call mtr.add_suppression("Error running query, slave SQL thread aborted");
       
      # Every column is MYSQL_TYPE_STRING, so the decode loop wants 2 bytes each
      # while the event carries 1 byte in total.
      --connection master
      create table t1 (c1 char(10), c2 char(10), c3 char(10), c4 char(10),
                       c5 char(10), c6 char(10), c7 char(10), c8 char(10),
                       c9 char(10), c10 char(10), c11 char(10), c12 char(10),
                       c13 char(10), c14 char(10), c15 char(10), c16 char(10),
                       c17 char(10), c18 char(10), c19 char(10), c20 char(10),
                       c21 char(10), c22 char(10), c23 char(10), c24 char(10));
      set @@session.debug_dbug= "+d,corrupt_table_map_shrink_field_metadata";
      insert into t1 values ('a','b','c','d','e','f','g','h',
                             'i','j','k','l','m','n','o','p',
                             'q','r','s','t','u','v','w','x');
      set @@session.debug_dbug= "";
      --source include/save_master_gtid.inc
       
      # table_def::table_def() reads 48 bytes from a 1-byte block.
      --connection slave
      --let $slave_sql_errno= 1677
      --source include/wait_for_slave_sql_error.inc
       
      # Both definitions are char(10), so a conversion failure is only reachable if
      # the second metadata byte of column 0 came from outside the event.
      --let $assert_text= The SQL thread reported a conversion failure for column 0
      --let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.2.err
      --let $assert_select= Column 0 of table .test.t1. cannot be converted from type
      --let $assert_count= 1
      --source include/assert_grep.inc
       
      # The target type in that message is the replica's own char(10 octets), so
      # this anchors on the source type the replica decoded.
      --let $assert_text= The decoded source type is not the char(10) the source sent
      --let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.2.err
      --let $assert_select= cannot be converted from type .char\(10 octets\)
      --let $assert_count= 0
      --source include/assert_grep.inc
      

      24 columns is the smallest count that leaves the allocation. The metadata block
      is the second of two in the reader's my_multi_malloc, ALIGN_SIZE rounds
      both to 8 bytes, and a debug build follows each with an 8-byte
      ALLOC_BARRIER, so the 32-byte allocation ends at field_metadata[16] and
      a walk of 16 bytes or fewer stays inside it and faults on nothing.

      A replica applying a Table_map that declares 24 char(10) columns and
      one metadata byte reports:

      [ERROR] Slave SQL: Column 0 of table 'test.t1' cannot be converted from type
      'char(165 octets)' to type 'char(10 octets) character set latin1', Gtid 0-1-2,
      Internal MariaDB error code: 1677
      

      Both definitions are char(10), so the error is itself the proof.
      x = field_metadata[0] << 8 | field_metadata[1]
      (sql/rpl_utility.cc:229) took 254, the real type byte the event carried,
      and 165 from past it. 165 is 0xA5, MariaDB's own TRASH_ALLOC fill, so that
      byte is uninitialized heap from the 8-byte ALIGN_SIZE rounding of the
      1-byte block, and it reached the error log.

      Attachments

        Issue Links

          Activity

            People

              bnestere Brandon Nesterenko
              bnestere Brandon Nesterenko
              Brandon Nesterenko Brandon Nesterenko
              Kristian Nielsen Kristian Nielsen
              Votes:
              0 Vote for this issue
              Watchers:
              2 Start watching this issue

              Dates

                Created:
                Updated:

                Time Tracking

                  Estimated:
                  Original Estimate - 1d Original Estimate - 1d
                  1d
                  Remaining:
                  Time Spent - 1h Remaining Estimate - 1d
                  1d
                  Logged:
                  Time Spent - 1h Remaining Estimate - 1d
                  1h

                  Git Integration

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