Commit message: Bug#24493829 SETTING THE NULL_AUDIT_EVENT_RECORD CAUSES SEGMENTATION FAULT

Post-push fix. Memory leak fixed (reported by Valgrind).
CVE: CVE-2017-3460
CWE: NVD-CWE-noinfo
Vulnerability description: Vulnerability in the MySQL Server component of Oracle MySQL (subcomponent: Server: Audit Plug-in). Supported versions that are affected are 5.7.17 and earlier. Easily "exploitable" vulnerability allows high privileged attacker with network access via multiple protocols to compromise MySQL Server. Successful attacks of this vulnerability can result in unauthorized ability to cause a hang or frequently repeatable crash (complete DOS) of MySQL Server. CVSS 3.0 Base Score 4.9 (Availability impacts). CVSS Vector: (CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H).

Project: mysql-server
File: audit_null.c

static int audit_null_plugin_deinit(void *arg MY_ATTRIBUTE((unused)))
{
  return(0);
}

Project: mysql-server
File: audit_null.c

static int audit_null_notify(MYSQL_THD thd,
                             mysql_event_class_t event_class,
                             const void *event)
{
  char buffer[2000]= { 0, };
  int buffer_data= 0;
  unsigned long event_subclass= (unsigned long)*(int *)event;
  const char *order_str= (const char *)THDVAR(thd, event_order_check);
  int event_order_started= (int)THDVAR(thd, event_order_started);
  int exact_check= (int)THDVAR(thd, event_order_check_exact);
  LEX_CSTRING event_name= event_to_str(event_class, event_subclass);
  LEX_CSTRING event_token= get_token(&order_str);
  LEX_CSTRING event_data= get_token(&order_str);
  LEX_CSTRING event_command= get_token(&order_str);
  my_bool consume_event= TRUE;

  /* prone to races, oh well */
  number_of_calls++;

  if (event_class == MYSQL_AUDIT_GENERAL_CLASS)
  {
    const struct mysql_event_general *event_general=
                                    (const struct mysql_event_general *)event;

    switch (event_general->event_subclass)
    {
    case MYSQL_AUDIT_GENERAL_LOG:
      number_of_calls_general_log++;
      break;
    case MYSQL_AUDIT_GENERAL_ERROR:
      number_of_calls_general_error++;
      break;
    case MYSQL_AUDIT_GENERAL_RESULT:
      number_of_calls_general_result++;
      break;
    case MYSQL_AUDIT_GENERAL_STATUS:
      number_of_calls_general_status++;
      break;
    default:
      break;
    }
  }
  else if (event_class == MYSQL_AUDIT_CONNECTION_CLASS)
  {
    const struct mysql_event_connection *event_connection=
                                (const struct mysql_event_connection *) event;

    switch (event_connection->event_subclass)
    {
    case MYSQL_AUDIT_CONNECTION_CONNECT:
      number_of_calls_connection_connect++;
      break;
    case MYSQL_AUDIT_CONNECTION_DISCONNECT:
      number_of_calls_connection_disconnect++;
      break;
    case MYSQL_AUDIT_CONNECTION_CHANGE_USER:
      number_of_calls_connection_change_user++;
      break;
    case MYSQL_AUDIT_CONNECTION_PRE_AUTHENTICATE:
      number_of_calls_connection_pre_authenticate++;
        break;
    default:
      break;
    }
  }
  else if (event_class == MYSQL_AUDIT_PARSE_CLASS)
  {
    const struct mysql_event_parse *event_parse =
                                      (const struct mysql_event_parse *)event;

    switch (event_parse->event_subclass)
    {
    case MYSQL_AUDIT_PARSE_PREPARSE:
      number_of_calls_parse_preparse++;
      break;
    case MYSQL_AUDIT_PARSE_POSTPARSE:
      number_of_calls_parse_postparse++;
      break;
    default:
      break;
    }
  }
  /**
    Currently events not active.

  else if (event_class == MYSQL_AUDIT_AUTHORIZATION_CLASS)
  {
    const struct mysql_event_authorization *event_grant =
                             (const struct mysql_event_authorization *)event;

    buffer_data= sprintf(buffer, "db=\"%s\" table=\"%s\" object=\"%s\" "
                         "requested=\"0x%08x\" granted=\"0x%08x\"",
                         event_grant->database.str ? event_grant->database.str : "<NULL>",
                         event_grant->table.str ? event_grant->table.str : "<NULL>",
                         event_grant->object.str ? event_grant->object.str : "<NULL>",
                         event_grant->requested_privilege,
                         event_grant->granted_privilege);

    switch (event_grant->event_subclass)
    {
    case MYSQL_AUDIT_AUTHORIZATION_USER:
      number_of_calls_authorization_user++;
      break;
    case MYSQL_AUDIT_AUTHORIZATION_DB:
      number_of_calls_authorization_db++;
      break;
    case MYSQL_AUDIT_AUTHORIZATION_TABLE:
      number_of_calls_authorization_table++;
      break;
    case MYSQL_AUDIT_AUTHORIZATION_COLUMN:
      number_of_calls_authorization_column++;
      break;
    case MYSQL_AUDIT_AUTHORIZATION_PROCEDURE:
      number_of_calls_authorization_procedure++;
      break;
    case MYSQL_AUDIT_AUTHORIZATION_PROXY:
      number_of_calls_authorization_proxy++;
      break;
    default:
      break;
    }
  }
  */
  else if (event_class == MYSQL_AUDIT_SERVER_STARTUP_CLASS)
  {
    /* const struct mysql_event_server_startup *event_startup=
       (const struct mysql_event_server_startup *) event; */
    number_of_calls_server_startup++;
  }
  else if (event_class == MYSQL_AUDIT_SERVER_SHUTDOWN_CLASS)
  {
    /* const struct mysql_event_server_shutdown *event_startup=
       (const struct mysql_event_server_shutdown *) event; */
    number_of_calls_server_shutdown++;
  }
  else if (event_class == MYSQL_AUDIT_COMMAND_CLASS)
  {
    const struct mysql_event_command *event_command=
                                    (const struct mysql_event_command *)event;

    buffer_data= sprintf(buffer, "command_id=\"%d\"", event_command->command_id);

    switch (event_command->event_subclass)
    {
    case MYSQL_AUDIT_COMMAND_START:
      number_of_calls_command_start++;
      break;
    case MYSQL_AUDIT_COMMAND_END:
      number_of_calls_command_end++;
      break;
    default:
      break;
    }
  }
  else if (event_class == MYSQL_AUDIT_QUERY_CLASS)
  {
    const struct mysql_event_query *event_query=
                                      (const struct mysql_event_query *)event;

    buffer_data= sprintf(buffer, "sql_command_id=\"%d\"",
                         (int) event_query->sql_command_id);

    switch (event_query->event_subclass)
    {
    case MYSQL_AUDIT_QUERY_START:
      number_of_calls_query_start++;
      break;
    case MYSQL_AUDIT_QUERY_NESTED_START:
      number_of_calls_query_nested_start++;
      break;
    case MYSQL_AUDIT_QUERY_STATUS_END:
      number_of_calls_query_status_end++;
      break;
    case MYSQL_AUDIT_QUERY_NESTED_STATUS_END:
      number_of_calls_query_nested_status_end++;
      break;
    default:
      break;
    }
  }
  else if (event_class == MYSQL_AUDIT_TABLE_ACCESS_CLASS)
  {
    const struct mysql_event_table_access *event_table=
                               (const struct mysql_event_table_access *)event;

    buffer_data= sprintf(buffer, "db=\"%s\" table=\"%s\"",
                         event_table->table_database.str,
                         event_table->table_name.str);

    switch (event_table->event_subclass)
    {
    case MYSQL_AUDIT_TABLE_ACCESS_INSERT:
      number_of_calls_table_access_insert++;
      break;
    case MYSQL_AUDIT_TABLE_ACCESS_DELETE:
      number_of_calls_table_access_delete++;
      break;
    case MYSQL_AUDIT_TABLE_ACCESS_UPDATE:
      number_of_calls_table_access_update++;
      break;
    case MYSQL_AUDIT_TABLE_ACCESS_READ:
      number_of_calls_table_access_read++;
      break;
    default:
      break;
    }
  }
  else if (event_class == MYSQL_AUDIT_GLOBAL_VARIABLE_CLASS)
  {
    const struct mysql_event_global_variable *event_gvar =
                            (const struct mysql_event_global_variable *)event;

    /* Copy the variable content into the buffer. We do not guarantee that the
       variable value will fit into buffer. The buffer should be large enough
       to be used for the test purposes. */
    buffer_data= sprintf(buffer, "name=\"%.*s\"",
                         MY_MIN((int) event_gvar->variable_name.length,
                                (int) (sizeof(buffer) - 8)),
                          event_gvar->variable_name.str);

    buffer_data+= sprintf(buffer + buffer_data, " value=\"%.*s\"",
                         MY_MIN((int) event_gvar->variable_value.length,
                                (int) (sizeof(buffer) - 16)),
                          event_gvar->variable_value.str);
    buffer[buffer_data]= '\0';

    switch (event_gvar->event_subclass)
    {
    case MYSQL_AUDIT_GLOBAL_VARIABLE_GET:
      number_of_calls_global_variable_get++;
      break;
    case MYSQL_AUDIT_GLOBAL_VARIABLE_SET:
      number_of_calls_global_variable_set++;
      break;
    default:
      break;
    }
  }

  process_event_record(thd, event_name, buffer, buffer_data);

  if (my_charset_latin1.coll->strnncoll(&my_charset_latin1,
                                        (const uchar *)event_name.str,
                                        event_name.length,
                                        (const uchar *)event_token.str,
                                        event_token.length, 0))
  {
    /* Clear event command. */
    event_command.str= NULL;
    event_command.length= 0;

    if (exact_check == 1 && event_order_started == 1)
    {
      if (!(event_class == MYSQL_AUDIT_GENERAL_CLASS &&
            event_subclass == MYSQL_AUDIT_GENERAL_ERROR))
      {
        strxnmov(buffer, sizeof(buffer), event_name.str, " instead of ",
                 event_token.str, NullS);
        my_message(ER_AUDIT_API_ABORT, buffer, MYF(0));
      }

      THDVAR(thd, event_order_started)= 0;
      THDVAR(thd, event_order_check)= 0;

      return 1;
    }
  }
  else
  {
    LEX_CSTRING ignore= { C_STRING_WITH_LEN("<IGNORE>") };

    /* When we are not in the event order check, check if the specified
       data corresponds to the actual event data. */
    if (my_charset_latin1.coll->strnncoll(&my_charset_latin1,
                                          (const uchar *)event_data.str,
                                          event_data.length,
                                          (const uchar *) ignore.str,
                                          ignore.length, 0) &&
        my_charset_latin1.coll->strnncoll(&my_charset_latin1,
                                          (const uchar *) event_data.str,
                                          event_data.length,
                                          (const uchar *)buffer,
                                          (size_t)buffer_data, 0))
    {
      if (exact_check == 1 && event_order_started == 1)
      {
        char invalid_data_buffer[sizeof(buffer)]= { 0, };
        LEX_CSTRING status= { C_STRING_WITH_LEN("EVENT-ORDER-INVALID-DATA") };
        LEX_CSTRING order_cstr;

        lex_cstring_set(&order_cstr,
                        (const char *)THDVAR(thd, event_order_check));

        memmove((char *)order_cstr.str,
                (void *)status.str, status.length + 1);

        strxnmov(invalid_data_buffer, sizeof(invalid_data_buffer),
                 "Invalid data for '", event_name.str, "' -> ", buffer, NullS);
        my_message(ER_AUDIT_API_ABORT, invalid_data_buffer, MYF(0));

        THDVAR(thd, event_order_started)= 0;
        THDVAR(thd, event_order_check)= (char *)order_cstr.str;

        return 1;
      }

      /* Clear event command. */
      event_command.str= NULL;
      event_command.length= 0;
    }
    else
    {
      LEX_CSTRING order_cstr;
      ulong consume= THDVAR(thd, event_order_check_consume_ignore_count);
      lex_cstring_set(&order_cstr,
                      (const char *)THDVAR(thd, event_order_check));

      THDVAR(thd, event_order_started)= 1;

      if (consume)
      {
        /*
          Do not consume event this time. Just decrease value and wait until
          the next event is matched.
        */
        THDVAR(thd, event_order_check_consume_ignore_count)= consume - 1;
        consume_event= FALSE;
      }
      else
      {
        /* Consume matched event. */
        memmove((char*)order_cstr.str, (void*)order_str,
          order_cstr.length - (order_str - order_cstr.str) + 1);

        /* Count new length. */
        lex_cstring_set(&order_cstr, order_cstr.str);

        if (order_cstr.length == 0)
        {
          LEX_CSTRING status = { C_STRING_WITH_LEN("EVENT-ORDER-OK") };

          memmove((char *)order_cstr.str,
                  (void *)status.str, status.length + 1);

          /* event_order_started contains message. Do not verify it. */
          THDVAR(thd, event_order_started)= 0;
        }
      }
    }
  }

  return process_command(thd, event_command, consume_event);
}

Project: mysql-server
File: audit_null.c

static int audit_null_plugin_init(void *arg MY_ATTRIBUTE((unused)))
{
  struct st_mysql_show_var *var;

  for (var= simple_status; var->value != 0; var++)
  {
    *((int*)var->value) = 0;
  }

  return(0);
}

Project: mysql-server
File: audit_null.c

static int process_command(MYSQL_THD thd, LEX_CSTRING event_command,
                           my_bool consume_event)
{
  LEX_CSTRING abort_ret_command= { C_STRING_WITH_LEN("ABORT_RET") };

  if (!my_charset_latin1.coll->strnncoll(&my_charset_latin1,
                                         (const uchar *)event_command.str,
                                         event_command.length,
                                         (const uchar *)abort_ret_command.str,
                                         abort_ret_command.length, 0))
  {
    int ret_code = (int)THDVAR(thd, abort_value);
    const char *err_message = (const char *)THDVAR(thd, abort_message);
    LEX_CSTRING status = { C_STRING_WITH_LEN("EVENT-ORDER-ABORT") };
    LEX_CSTRING order_cstr;

    lex_cstring_set(&order_cstr, 
                    (const char *)THDVAR(thd, event_order_check));

    /* Do not replace order string yet. */
    if (consume_event)
    {
      memmove((char *) order_cstr.str,
              (void *) status.str, status.length + 1);

      THDVAR(thd, abort_value)= 1;
      THDVAR(thd, abort_message)= 0;
    }

    if (err_message)
    {
      my_message(ER_AUDIT_API_ABORT, err_message, MYF(0));
      THDVAR(thd, event_order_check)= (char *)order_cstr.str;
    }

    return ret_code;
  }

  return 0;
}

Project: mysql-server
File: audit_null.c

mysql_declare_plugin(audit_null)
{
  MYSQL_AUDIT_PLUGIN,         /* type                            */
  &audit_null_descriptor,     /* descriptor                      */
  "NULL_AUDIT",               /* name                            */
  "Oracle Corp",              /* author                          */
  "Simple NULL Audit",        /* description                     */
  PLUGIN_LICENSE_GPL,
  audit_null_plugin_init,     /* init function (when loaded)     */
  audit_null_plugin_deinit,   /* deinit function (when unloaded) */
  0x0003,                     /* version                         */
  simple_status,              /* status variables                */
  system_variables,           /* system variables                */
  NULL,
  0,
}

Project: mysql-server
File: audit_null.c

static void add_event(MYSQL_THD thd, const char *var, LEX_CSTRING event,
                      const char *data, size_t data_length)
{
  LEX_CSTRING str;
  size_t size;
  char *buffer;

  lex_cstring_set(&str, var);

  size= str.length + event.length + data_length + 4;
  buffer= (char *)my_malloc(PSI_NOT_INSTRUMENTED, size, MYF(MY_FAE));

  my_snprintf(buffer, size, "%s%s%s;%s;", var, str.length == 0 ? "" : "\n",
              event.str, data);

  buffer[size - (str.length == 0 ? 2 : 1)] = '\0';

  THDVAR(thd, event_record)= buffer;
}

Project: mysql-server
File: audit_null.c

static LEX_CSTRING event_to_str(unsigned int event_class,
                                unsigned long event_subclass)
{
  int count;
  for (count= 0; event_subclass; count++, event_subclass >>= 1);

  return event_names[event_class][count - 1];
}

Project: mysql-server
File: audit_null.c

static void process_event_record(MYSQL_THD thd, LEX_CSTRING event_name,
                                 const char *data, size_t data_length)
{
  const char *record_str = (const char *)THDVAR(thd, event_record_def);
  LEX_CSTRING record_begin = get_token(&record_str);
  LEX_CSTRING record_end = get_token(&record_str);

  if (record_str == NULL)
  {
    return;
  }

  if (record_end.length == 0)
  {
    /* We are already in the consuming phase. Add a new event name into
       a record variable */

    const char *buffer= THDVAR(thd, event_record);

    /* Add event. */
    add_event(thd, buffer, event_name, data, data_length);

    my_free((void *)(buffer));

    if (!my_charset_latin1.coll->strnncoll(&my_charset_latin1,
                                           (const uchar *)record_begin.str,
                                           record_begin.length,
                                           (const uchar *)event_name.str,
                                           event_name.length, FALSE))
    {
      /* Do not expect any more events. */
      THDVAR(thd, event_record_def)= 0;
    }
  }
  else
  {
    const char *buffer;

    /* We have not started recording of events yet. */
    if (my_charset_latin1.coll->strnncoll(&my_charset_latin1,
                                          (const uchar *)record_begin.str,
                                          record_begin.length,
                                          (const uchar *)event_name.str,
                                          event_name.length, FALSE))
    {
      /* Event not matching. */
      return;
    }

    buffer= THDVAR(thd, event_record);

    my_free((void *)(buffer));

    THDVAR(thd, event_record)= 0;

    add_event(thd, "", event_name, data, data_length);

    /* Add event. */

    record_str = (const char *)THDVAR(thd, event_record_def);

    /* Remove starting event. */
    memmove((char *)record_str, (void *)record_end.str, record_end.length + 1);
  }
}

Project: mysql-server
File: audit_null.c

static LEX_CSTRING get_token(const char **str)
{
  LEX_CSTRING ret = { NULL, 0 };

  if (*str != NULL)
  {
    ret.str= *str;

    if (*ret.str != '\0')
    {
      /* Find a param delimiter. */
      while (**str && **str != ';') (*str)++;

      ret.length= *str - ret.str;

      /* Skip a delimiter. */
      if (**str == ';') (*str)++;
    }
  }

  return ret;
}