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

Replication Breaks from Mal-copied Binlog Name on Malformed Format_description Event

    XMLWordPrintable

Details

    • Can result in hang or crash
    • Fix crash in Rotate event when followed by a Format Description event describing no Rotate event support
    • Q3/2026 Replic. Development

    Description

      A master that installs a Format_description event whose post_header_len
      array is shorter than four entries, and then sends a Rotate, makes the
      slave index that array out of bounds. That value becomes the Rotate
      event's post-header length, which decides both the position the slave
      records and the offset it copies the new binlog name from. Either way
      replication stops. If the value exceeds the event length the constructor
      abandons the event, is_valid() fails, and the IO thread stops with a
      relay log write failure. If it is small the slave adopts a name taken
      from the wrong offset, which is the low byte of the event's position
      field followed by a NUL, so the next reconnect asks the master for a
      binlog that does not exist and fails with error 1236.

      Log_event::read_log_event() has the check that would prevent it, and states
      why (sql/log_event.cc:1098):

        if (event_type > fdle->number_of_event_types &&
            event_type != FORMAT_DESCRIPTION_EVENT)
          /* It is unsafe to use the fdle if its post_header_len
             array does not include the event type. */
      

      Grepping number_of_event_types across sql/ shows that is the only
      validation of an incoming event type against the installed descriptor. And
      queue_event() bypasses it for ROTATE_EVENT by constructing directly
      (sql/slave.cc:6608), while Rotate_log_event's constructor indexes
      unconditionally (sql/log_event.cc:2588):

        uint8 post_header_len= description_event->post_header_len[ROTATE_EVENT-1];
      

      header_is_valid() requires no minimum on number_of_event_types, and
      my_malloc bumps a zero-length request to one byte and returns non-NULL, so
      post_header_len != NULL holds and such a descriptor is installed.

      Of the event types queue_event() builds without the general path,
      ROTATE_EVENT is the only one that indexes post_header_len.
      HEARTBEAT_LOG_EVENT and GTID_EVENT use common_header_len, and
      Gtid_log_event::peek() bounds event_len itself.
      FORMAT_DESCRIPTION_EVENT and GTID_LIST_EVENT go through
      Log_event::read_log_event() and so are already covered.

      To reproduce:

      Reaching this end to end takes two master-side steps, because the short
      descriptor has to be installed before the Rotate arrives.

      The descriptor is truncated in the sender, after the sender has parsed its own
      copy, reusing the fix_checksum() pattern that
      send_format_descriptor_event() already uses for its log_pos and
      created patches. 77 bytes is the size to aim for: number_of_event_types
      is event_len - 76, and one more byte is then taken off it for the checksum
      algorithm descriptor, so the array is memdup'd at one byte and index
      ROTATE_EVENT-1 is three bytes past it. That single byte is also what
      get_checksum_alg() reads on the way in and what becomes the descriptor's own
      checksum_alg, so setting it to CRC32 keeps checksum handling correct on both
      sides and leaves the length as the only anomaly.

      Patch file:

      --- a/sql/sql_repl.cc
      +++ b/sql/sql_repl.cc
      @@ static int send_format_descriptor_event(...)
      +  DBUG_EXECUTE_IF("binlog_sender_short_post_header_len",
      +  {
      +    char *ev= (char*) packet->ptr() + ev_offset;
      +    ulong short_len= LOG_EVENT_MINIMAL_HEADER_LEN +
      +                     ST_COMMON_HEADER_LEN_OFFSET + 2 + BINLOG_CHECKSUM_LEN;
      +    ev[LOG_EVENT_MINIMAL_HEADER_LEN + ST_COMMON_HEADER_LEN_OFFSET + 1]=
      +      (char) BINLOG_CHECKSUM_ALG_CRC32;
      +    packet->length(ev_offset + short_len);
      +    int4store(ev + EVENT_LEN_OFFSET, short_len);
      +    fix_checksum(info->current_checksum_alg, packet, ev_offset);
      +  });
      +
         /* send it */
      

      Test file:

      --source include/have_debug.inc
      --source include/have_binlog_format_row.inc
      --source include/master-slave.inc
       
      --connection master
      create table t1 (a int);
      insert into t1 values (1);
      --source include/save_master_gtid.inc
      --connection slave
      --source include/sync_with_master_gtid.inc
       
      --connection master
      set @@global.debug_dbug= "+d,binlog_sender_short_post_header_len";
       
      # Reconnect so the source resends its descriptor. The slave is caught up, so
      # the Rotate below is the first event to arrive after it. queue_event() builds
      # that Rotate directly and reads post_header_len[3] from a one byte array.
      --connection slave
      --source include/stop_slave.inc
      --source include/start_slave.inc
       
      --connection master
      FLUSH LOGS;
       
      --connection slave
      --source include/sync_with_master_gtid.inc
      

      The slave has to be caught up before the injection, so that the Rotate is the
      first event after the descriptor. With number_of_event_types at 0 every
      other event type is above it and read_log_event() rejects it, which is also
      what happens to the events following the Rotate.

      Attachments

        Issue Links

          Activity

            People

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

              Dates

                Created:
                Updated:
                Resolved:

                Time Tracking

                  Estimated:
                  Original Estimate - 1d
                  1d
                  Remaining:
                  Remaining Estimate - 0d
                  0d
                  Logged:
                  Time Spent - 7h 25m Time Not Required
                  7h 25m

                  Git Integration

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