Commit fbcd70a4 authored by acurtis/antony@xiphis.org/ltamd64.xiphis.org's avatar acurtis/antony@xiphis.org/ltamd64.xiphis.org
Browse files

Bug#25679

  "Federated Denial of Service"
  Federated storage engine used to attempt to open connections within
  the ::create() and ::open() methods which are invoked while LOCK_open
  mutex is being held by mysqld. As a result, no other client sessions
  can open tables while Federated is attempting to open a connection.
  Long DNS lookup times would stall mysqld's operation and a rogue
  connection string which connects to a remote server which simply
  stalls during handshake can stall mysqld for a much longer period of
  time.
  This patch moves the opening of the connection much later, when the
  federated actually issues queries, by which time the LOCK_open mutex is
  no longer being held.
parent 3f06084e
Loading
Loading
Loading
Loading
+6 −5
Original line number Diff line number Diff line
@@ -40,17 +40,18 @@ CREATE TABLE federated.t1 (
    )
ENGINE="FEDERATED" DEFAULT CHARSET=latin1
CONNECTION='mysql://root@127.0.0.1:SLAVE_PORT/federated/t3';
ERROR HY000: Can't create federated table. Foreign data src error:  error: 1146  'Table 'federated.t3' doesn't exist'
SELECT * FROM federated.t1;
ERROR HY000: The foreign data source you are trying to reference does not exist. Data source error:  error: 1146  'Table 'federated.t3' doesn't exist'
DROP TABLE federated.t1;
CREATE TABLE federated.t1 (
`id` int(20) NOT NULL,
`name` varchar(32) NOT NULL default ''
    )
ENGINE="FEDERATED" DEFAULT CHARSET=latin1
CONNECTION='mysql://user:pass@127.0.0.1:SLAVE_PORT/federated/t1';
ERROR HY000: Unable to connect to foreign data source: database: 'federated'  username: 'user'  hostname: '127.0.0.1'
DROP TABLE IF EXISTS federated.t1;
Warnings:
Note	1051	Unknown table 't1'
SELECT * FROM federated.t1;
ERROR HY000: Unable to connect to foreign data source: Access denied for user 'user'@'localhost' (using password: YES)
DROP TABLE federated.t1;
CREATE TABLE federated.t1 (
`id` int(20) NOT NULL,
`name` varchar(32) NOT NULL default ''
+6 −3
Original line number Diff line number Diff line
@@ -30,25 +30,28 @@ CREATE TABLE federated.t1 (

# test non-existant table
--replace_result $SLAVE_MYPORT SLAVE_PORT
--error 1434
eval CREATE TABLE federated.t1 (
    `id` int(20) NOT NULL,
    `name` varchar(32) NOT NULL default ''
    )
  ENGINE="FEDERATED" DEFAULT CHARSET=latin1
  CONNECTION='mysql://root@127.0.0.1:$SLAVE_MYPORT/federated/t3';
--error 1431
SELECT * FROM federated.t1;
DROP TABLE federated.t1;

# test bad user/password 
--replace_result $SLAVE_MYPORT SLAVE_PORT
--error 1429
eval CREATE TABLE federated.t1 (
    `id` int(20) NOT NULL,
    `name` varchar(32) NOT NULL default ''
    )
  ENGINE="FEDERATED" DEFAULT CHARSET=latin1
  CONNECTION='mysql://user:pass@127.0.0.1:$SLAVE_MYPORT/federated/t1';
--error 1429
SELECT * FROM federated.t1;
DROP TABLE federated.t1;

DROP TABLE IF EXISTS federated.t1;
# # correct connection, same named tables
--replace_result $SLAVE_MYPORT SLAVE_PORT
eval CREATE TABLE federated.t1 (
+140 −155
Original line number Diff line number Diff line
@@ -497,105 +497,6 @@ static bool append_ident(String *string, const char *name, uint length,
}


/*
 Check (in create) whether the tables exists, and that it can be connected to

  SYNOPSIS
    check_foreign_data_source()
      share               pointer to FEDERATED share
      table_create_flag   tells us that ::create is the caller, 
                          therefore, return CANT_CREATE_FEDERATED_TABLE

  DESCRIPTION
    This method first checks that the connection information that parse url
    has populated into the share will be sufficient to connect to the foreign
    table, and if so, does the foreign table exist.
*/

static int check_foreign_data_source(FEDERATED_SHARE *share,
                                     bool table_create_flag)
{
  char query_buffer[FEDERATED_QUERY_BUFFER_SIZE];
  char error_buffer[FEDERATED_QUERY_BUFFER_SIZE];
  uint error_code;
  String query(query_buffer, sizeof(query_buffer), &my_charset_bin);
  MYSQL *mysql;
  DBUG_ENTER("ha_federated::check_foreign_data_source");

  /* Zero the length, otherwise the string will have misc chars */
  query.length(0);

  /* error out if we can't alloc memory for mysql_init(NULL) (per Georg) */
  if (!(mysql= mysql_init(NULL)))
    DBUG_RETURN(HA_ERR_OUT_OF_MEM);
  /* check if we can connect */
  if (!mysql_real_connect(mysql,
                          share->hostname,
                          share->username,
                          share->password,
                          share->database,
                          share->port,
                          share->socket, 0))
  {
    /*
      we want the correct error message, but it to return
      ER_CANT_CREATE_FEDERATED_TABLE if called by ::create
    */
    error_code= (table_create_flag ?
                 ER_CANT_CREATE_FEDERATED_TABLE :
                 ER_CONNECT_TO_FOREIGN_DATA_SOURCE);

    my_sprintf(error_buffer,
               (error_buffer,
                "database: '%s'  username: '%s'  hostname: '%s'",
                share->database, share->username, share->hostname));

    my_error(ER_CONNECT_TO_FOREIGN_DATA_SOURCE, MYF(0), error_buffer);
    goto error;
  }
  else
  {
    /*
      Since we do not support transactions at this version, we can let the 
      client API silently reconnect. For future versions, we will need more 
      logic to deal with transactions
    */
    mysql->reconnect= 1;
    /*
      Note: I am not using INORMATION_SCHEMA because this needs to work with 
      versions prior to 5.0
      
      if we can connect, then make sure the table exists 

      the query will be: SELECT * FROM `tablename` WHERE 1=0
    */
    query.append(FEDERATED_SELECT);
    query.append(FEDERATED_STAR);
    query.append(FEDERATED_FROM);
    append_ident(&query, share->table_name, share->table_name_length,
                 ident_quote_char);
    query.append(FEDERATED_WHERE);
    query.append(FEDERATED_FALSE);

    if (mysql_real_query(mysql, query.ptr(), query.length()))
    {
      error_code= table_create_flag ?
        ER_CANT_CREATE_FEDERATED_TABLE : ER_FOREIGN_DATA_SOURCE_DOESNT_EXIST;
      my_sprintf(error_buffer, (error_buffer, "error: %d  '%s'",
                                mysql_errno(mysql), mysql_error(mysql)));

      my_error(error_code, MYF(0), error_buffer);
      goto error;
    }
  }
  error_code=0;

error:
    mysql_close(mysql);
    DBUG_RETURN(error_code);
}


static int parse_url_error(FEDERATED_SHARE *share, TABLE *table, int error_num)
{
  char buf[FEDERATED_QUERY_BUFFER_SIZE];
@@ -1478,36 +1379,7 @@ int ha_federated::open(const char *name, int mode, uint test_if_locked)
    DBUG_RETURN(1);
  thr_lock_data_init(&share->lock, &lock, NULL);

  /* Connect to foreign database mysql_real_connect() */
  mysql= mysql_init(0);

  /*
    BUG# 17044 Federated Storage Engine is not UTF8 clean
    Add set names to whatever charset the table is at open
    of table
  */
  /* this sets the csname like 'set names utf8' */
  mysql_options(mysql,MYSQL_SET_CHARSET_NAME,
                this->table->s->table_charset->csname);

  if (!mysql || !mysql_real_connect(mysql,
                                   share->hostname,
                                   share->username,
                                   share->password,
                                   share->database,
                                   share->port,
                                   share->socket, 0))
  {
    free_share(share);
    DBUG_RETURN(stash_remote_error());
  }
  /*
    Since we do not support transactions at this version, we can let the client
    API silently reconnect. For future versions, we will need more logic to
    deal with transactions
  */

  mysql->reconnect= 1;
  DBUG_ASSERT(mysql == NULL);

  ref_length= (table->s->primary_key != MAX_KEY ?
               table->key_info[table->s->primary_key].key_length :
@@ -1543,8 +1415,8 @@ int ha_federated::close(void)
    stored_result= 0;
  }
  /* Disconnect from mysql */
  if (mysql)                                    // QQ is this really needed
  mysql_close(mysql);
  mysql= NULL;
  retval= free_share(share);
  DBUG_RETURN(retval);

@@ -1774,7 +1646,7 @@ int ha_federated::write_row(byte *buf)
    if (bulk_insert.length + values_string.length() + bulk_padding >
        mysql->net.max_packet_size && bulk_insert.length)
    {
      error= mysql_real_query(mysql, bulk_insert.str, bulk_insert.length);
      error= real_query(bulk_insert.str, bulk_insert.length);
      bulk_insert.length= 0;
    }
    else
@@ -1798,8 +1670,7 @@ int ha_federated::write_row(byte *buf)
  }  
  else
  {
    error= mysql_real_query(mysql, values_string.ptr(), 
                            values_string.length());
    error= real_query(values_string.ptr(), values_string.length());
  }
  
  if (error)
@@ -1847,6 +1718,13 @@ void ha_federated::start_bulk_insert(ha_rows rows)
  if (rows == 1)
    DBUG_VOID_RETURN;

  /*
    Make sure we have an open connection so that we know the 
    maximum packet size.
  */
  if (!mysql && real_connect())
    DBUG_VOID_RETURN;

  page_size= (uint) my_getpagesize();

  if (init_dynamic_string(&bulk_insert, NULL, page_size, page_size))
@@ -1875,7 +1753,7 @@ int ha_federated::end_bulk_insert()
  
  if (bulk_insert.str && bulk_insert.length)
  {
    if (mysql_real_query(mysql, bulk_insert.str, bulk_insert.length))
    if (real_query(bulk_insert.str, bulk_insert.length))
      error= stash_remote_error();
    else
    if (table->next_number_field)
@@ -1921,7 +1799,7 @@ int ha_federated::optimize(THD* thd, HA_CHECK_OPT* check_opt)
  append_ident(&query, share->table_name, share->table_name_length, 
               ident_quote_char);

  if (mysql_real_query(mysql, query.ptr(), query.length()))
  if (real_query(query.ptr(), query.length()))
  {
    DBUG_RETURN(stash_remote_error());
  }
@@ -1949,7 +1827,7 @@ int ha_federated::repair(THD* thd, HA_CHECK_OPT* check_opt)
  if (check_opt->sql_flags & TT_USEFRM)
    query.append(FEDERATED_USE_FRM);

  if (mysql_real_query(mysql, query.ptr(), query.length()))
  if (real_query(query.ptr(), query.length()))
  {
    DBUG_RETURN(stash_remote_error());
  }
@@ -2087,7 +1965,7 @@ int ha_federated::update_row(const byte *old_data, byte *new_data)
  if (!has_a_primary_key)
    update_string.append(FEDERATED_LIMIT1);

  if (mysql_real_query(mysql, update_string.ptr(), update_string.length()))
  if (real_query(update_string.ptr(), update_string.length()))
  {
    DBUG_RETURN(stash_remote_error());
  }
@@ -2152,7 +2030,7 @@ int ha_federated::delete_row(const byte *buf)
  delete_string.append(FEDERATED_LIMIT1);
  DBUG_PRINT("info",
             ("Delete sql: %s", delete_string.c_ptr_quick()));
  if (mysql_real_query(mysql, delete_string.ptr(), delete_string.length()))
  if (real_query(delete_string.ptr(), delete_string.length()))
  {
    DBUG_RETURN(stash_remote_error());
  }
@@ -2261,7 +2139,7 @@ int ha_federated::index_read_idx_with_result_set(byte *buf, uint index,
                        NULL, 0);
  sql_query.append(index_string);

  if (mysql_real_query(mysql, sql_query.ptr(), sql_query.length()))
  if (real_query(sql_query.ptr(), sql_query.length()))
  {
    my_sprintf(error_buffer, (error_buffer, "error: %d '%s'",
                              mysql_errno(mysql), mysql_error(mysql)));
@@ -2327,7 +2205,7 @@ int ha_federated::read_range_first(const key_range *start_key,
    mysql_free_result(stored_result);
    stored_result= 0;
  }
  if (mysql_real_query(mysql, sql_query.ptr(), sql_query.length()))
  if (real_query(sql_query.ptr(), sql_query.length()))
  {
    retval= ER_QUERY_ON_FOREIGN_DATA_SOURCE;
    goto error;
@@ -2427,9 +2305,7 @@ int ha_federated::rnd_init(bool scan)
      stored_result= 0;
    }

    if (mysql_real_query(mysql,
                         share->select_query,
                         strlen(share->select_query)))
    if (real_query(share->select_query, strlen(share->select_query)))
      goto error;

    stored_result= mysql_store_result(mysql);
@@ -2646,8 +2522,7 @@ int ha_federated::info(uint flag)
    append_ident(&status_query_string, share->table_name,
                 share->table_name_length, value_quote_char);

    if (mysql_real_query(mysql, status_query_string.ptr(),
                         status_query_string.length()))
    if (real_query(status_query_string.ptr(), status_query_string.length()))
      goto error;

    status_query_string.length(0);
@@ -2702,12 +2577,18 @@ int ha_federated::info(uint flag)
  DBUG_RETURN(0);

error:
  if (result)
  mysql_free_result(result);

  if (mysql)
  {
    my_sprintf(error_buffer, (error_buffer, ": %d : %s",
                              mysql_errno(mysql), mysql_error(mysql)));
    my_error(error_code, MYF(0), error_buffer);
  }
  else
  {
    error_code= remote_error_number;
    my_error(error_code, MYF(0), ER(error_code));
  }
  DBUG_RETURN(error_code);
}

@@ -2785,7 +2666,7 @@ int ha_federated::delete_all_rows()
  /*
    TRUNCATE won't return anything in mysql_affected_rows
  */
  if (mysql_real_query(mysql, query.ptr(), query.length()))
  if (real_query(query.ptr(), query.length()))
  {
    DBUG_RETURN(stash_remote_error());
  }
@@ -2874,8 +2755,7 @@ int ha_federated::create(const char *name, TABLE *table_arg,
  FEDERATED_SHARE tmp_share; // Only a temporary share, to test the url
  DBUG_ENTER("ha_federated::create");

  if (!(retval= parse_url(&tmp_share, table_arg, 1)))
    retval= check_foreign_data_source(&tmp_share, 1);
  retval= parse_url(&tmp_share, table_arg, 1);

  my_free((gptr) tmp_share.scheme, MYF(MY_ALLOW_ZERO_PTR));
  DBUG_RETURN(retval);
@@ -2883,9 +2763,114 @@ int ha_federated::create(const char *name, TABLE *table_arg,
}


int ha_federated::real_connect()
{
  char buffer[FEDERATED_QUERY_BUFFER_SIZE];
  String sql_query(buffer, sizeof(buffer), &my_charset_bin);
  DBUG_ENTER("ha_federated::real_connect");

  /* 
    Bug#25679
    Ensure that we do not hold the LOCK_open mutex while attempting
    to establish Federated connection to guard against a trivial
    Denial of Service scenerio.
  */
  safe_mutex_assert_not_owner(&LOCK_open);

  DBUG_ASSERT(mysql == NULL);

  if (!(mysql= mysql_init(NULL)))
  {
    remote_error_number= HA_ERR_OUT_OF_MEM;
    DBUG_RETURN(-1);
  }

  /*
    BUG# 17044 Federated Storage Engine is not UTF8 clean
    Add set names to whatever charset the table is at open
    of table
  */
  /* this sets the csname like 'set names utf8' */
  mysql_options(mysql,MYSQL_SET_CHARSET_NAME,
                this->table->s->table_charset->csname);

  sql_query.length(0);

  if (!mysql_real_connect(mysql,
                          share->hostname,
                          share->username,
                          share->password,
                          share->database,
                          share->port,
                          share->socket, 0))
  {
    stash_remote_error();
    mysql_close(mysql);
    mysql= NULL;
    my_error(ER_CONNECT_TO_FOREIGN_DATA_SOURCE, MYF(0), remote_error_buf);
    remote_error_number= -1;
    DBUG_RETURN(-1);
  }

  /*
    We have established a connection, lets try a simple dummy query just 
    to check that the table and expected columns are present.
  */
  sql_query.append(share->select_query);
  sql_query.append(FEDERATED_WHERE);
  sql_query.append(FEDERATED_FALSE);
  if (mysql_real_query(mysql, sql_query.ptr(), sql_query.length()))
  {
    sql_query.length(0);
    sql_query.append("error: ");
    sql_query.qs_append(mysql_errno(mysql));
    sql_query.append("  '");
    sql_query.append(mysql_error(mysql));
    sql_query.append("'");
    mysql_close(mysql);
    mysql= NULL;
    my_error(ER_FOREIGN_DATA_SOURCE_DOESNT_EXIST, MYF(0), sql_query.ptr());
    remote_error_number= -1;
    DBUG_RETURN(-1);
  }

  /* Just throw away the result, no rows anyways but need to keep in sync */
  mysql_free_result(mysql_store_result(mysql));

  /*
    Since we do not support transactions at this version, we can let the client
    API silently reconnect. For future versions, we will need more logic to
    deal with transactions
  */

  mysql->reconnect= 1;
  DBUG_RETURN(0);
}


int ha_federated::real_query(const char *query, uint length)
{
  int rc= 0;
  DBUG_ENTER("ha_federated::real_query");

  if (!mysql && (rc= real_connect()))
    goto end;

  if (!query || !length)
    goto end;

  rc= mysql_real_query(mysql, query, length);
  
end:
  DBUG_RETURN(rc);
}


int ha_federated::stash_remote_error()
{
  DBUG_ENTER("ha_federated::stash_remote_error()");
  if (!mysql)
    DBUG_RETURN(remote_error_number);
  remote_error_number= mysql_errno(mysql);
  strmake(remote_error_buf, mysql_error(mysql), sizeof(remote_error_buf)-1);
  if (remote_error_number == ER_DUP_ENTRY ||
+2 −0
Original line number Diff line number Diff line
@@ -182,6 +182,8 @@ class ha_federated: public handler
                                     uint key_len,
                                     ha_rkey_function find_flag,
                                     MYSQL_RES **result);
  int real_query(const char *query, uint length);
  int real_connect();
public:
  ha_federated(TABLE *table_arg);
  ~ha_federated()