mirror of
https://github.com/MariaDB/server.git
synced 2025-07-30 16:24:05 +03:00
A fix and a test case for Bug#16365 "Prepared Statements: DoS with
too many open statements". The patch adds a new global variable @@max_prepared_stmt_count. This variable limits the total number of prepared statements in the server. The default value of @@max_prepared_stmt_count is 16382. 16382 small statements (a select against 3 tables with GROUP, ORDER and LIMIT) consume 100MB of RAM. Once this limit has been reached, the server will refuse to prepare a new statement and return ER_UNKNOWN_ERROR (unfortunately, we can't add new errors to 4.1 without breaking 5.0). The limit is changeable after startup and can accept any value from 0 to 1 million. In case the new value of the limit is less than the current statement count, no new statements can be added, while the old still can be used. Additionally, the current count of prepared statements is now available through a global read-only variable @@prepared_stmt_count.
This commit is contained in:
119
sql/sql_class.cc
119
sql/sql_class.cc
@ -1616,27 +1616,128 @@ Statement_map::Statement_map() :
|
||||
NULL,MYF(0));
|
||||
}
|
||||
|
||||
int Statement_map::insert(Statement *statement)
|
||||
/*
|
||||
Insert a new statement to the thread-local statement map.
|
||||
|
||||
DESCRIPTION
|
||||
If there was an old statement with the same name, replace it with the
|
||||
new one. Otherwise, check if max_prepared_stmt_count is not reached yet,
|
||||
increase prepared_stmt_count, and insert the new statement. It's okay
|
||||
to delete an old statement and fail to insert the new one.
|
||||
|
||||
POSTCONDITIONS
|
||||
All named prepared statements are also present in names_hash.
|
||||
Statement names in names_hash are unique.
|
||||
The statement is added only if prepared_stmt_count < max_prepard_stmt_count
|
||||
last_found_statement always points to a valid statement or is 0
|
||||
|
||||
RETURN VALUE
|
||||
0 success
|
||||
1 error: out of resources or max_prepared_stmt_count limit has been
|
||||
reached. An error is sent to the client, the statement is deleted.
|
||||
*/
|
||||
|
||||
int Statement_map::insert(THD *thd, Statement *statement)
|
||||
{
|
||||
int rc= my_hash_insert(&st_hash, (byte *) statement);
|
||||
if (rc == 0)
|
||||
last_found_statement= statement;
|
||||
if (my_hash_insert(&st_hash, (byte*) statement))
|
||||
{
|
||||
/*
|
||||
Delete is needed only in case of an insert failure. In all other
|
||||
cases hash_delete will also delete the statement.
|
||||
*/
|
||||
delete statement;
|
||||
my_error(ER_OUT_OF_RESOURCES, MYF(0));
|
||||
goto err_st_hash;
|
||||
}
|
||||
if (statement->name.str)
|
||||
{
|
||||
/*
|
||||
If there is a statement with the same name, remove it. It is ok to
|
||||
If there is a statement with the same name, remove it. It is ok to
|
||||
remove old and fail to insert new one at the same time.
|
||||
*/
|
||||
Statement *old_stmt;
|
||||
if ((old_stmt= find_by_name(&statement->name)))
|
||||
erase(old_stmt);
|
||||
if ((rc= my_hash_insert(&names_hash, (byte*)statement)))
|
||||
hash_delete(&st_hash, (byte*)statement);
|
||||
erase(old_stmt);
|
||||
if (my_hash_insert(&names_hash, (byte*) statement))
|
||||
{
|
||||
my_error(ER_OUT_OF_RESOURCES, MYF(0));
|
||||
goto err_names_hash;
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
pthread_mutex_lock(&LOCK_prepared_stmt_count);
|
||||
/*
|
||||
We don't check that prepared_stmt_count is <= max_prepared_stmt_count
|
||||
because we would like to allow to lower the total limit
|
||||
of prepared statements below the current count. In that case
|
||||
no new statements can be added until prepared_stmt_count drops below
|
||||
the limit.
|
||||
*/
|
||||
if (prepared_stmt_count >= max_prepared_stmt_count)
|
||||
{
|
||||
pthread_mutex_unlock(&LOCK_prepared_stmt_count);
|
||||
my_error(ER_UNKNOWN_ERROR, MYF(0));
|
||||
goto err_max;
|
||||
}
|
||||
prepared_stmt_count++;
|
||||
pthread_mutex_unlock(&LOCK_prepared_stmt_count);
|
||||
|
||||
last_found_statement= statement;
|
||||
return 0;
|
||||
|
||||
err_max:
|
||||
if (statement->name.str)
|
||||
hash_delete(&names_hash, (byte*) statement);
|
||||
err_names_hash:
|
||||
hash_delete(&st_hash, (byte*) statement);
|
||||
err_st_hash:
|
||||
send_error(thd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
void Statement_map::erase(Statement *statement)
|
||||
{
|
||||
if (statement == last_found_statement)
|
||||
last_found_statement= 0;
|
||||
if (statement->name.str)
|
||||
{
|
||||
hash_delete(&names_hash, (byte *) statement);
|
||||
}
|
||||
hash_delete(&st_hash, (byte *) statement);
|
||||
pthread_mutex_lock(&LOCK_prepared_stmt_count);
|
||||
DBUG_ASSERT(prepared_stmt_count > 0);
|
||||
prepared_stmt_count--;
|
||||
pthread_mutex_unlock(&LOCK_prepared_stmt_count);
|
||||
}
|
||||
|
||||
|
||||
void Statement_map::reset()
|
||||
{
|
||||
/* Must be first, hash_free will reset st_hash.records */
|
||||
pthread_mutex_lock(&LOCK_prepared_stmt_count);
|
||||
DBUG_ASSERT(prepared_stmt_count >= st_hash.records);
|
||||
prepared_stmt_count-= st_hash.records;
|
||||
pthread_mutex_unlock(&LOCK_prepared_stmt_count);
|
||||
|
||||
my_hash_reset(&names_hash);
|
||||
my_hash_reset(&st_hash);
|
||||
last_found_statement= 0;
|
||||
}
|
||||
|
||||
|
||||
Statement_map::~Statement_map()
|
||||
{
|
||||
/* Must go first, hash_free will reset st_hash.records */
|
||||
pthread_mutex_lock(&LOCK_prepared_stmt_count);
|
||||
DBUG_ASSERT(prepared_stmt_count >= st_hash.records);
|
||||
prepared_stmt_count-= st_hash.records;
|
||||
pthread_mutex_unlock(&LOCK_prepared_stmt_count);
|
||||
|
||||
hash_free(&names_hash);
|
||||
hash_free(&st_hash);
|
||||
|
||||
}
|
||||
|
||||
bool select_dumpvar::send_data(List<Item> &items)
|
||||
{
|
||||
List_iterator_fast<Item_func_set_user_var> li(vars);
|
||||
|
Reference in New Issue
Block a user