Skip to content

Commit ef9adb5

Browse files
committed
MDEV-32694: ASAN errors in Binary_string::alloced_length / reset_stmt_params
Anonymous block is represented internally by the class sp_head, so every statement inside an anonymous block is a SP instruction. On the other hand, the anonymous block specified in the FROM clause of the PREPARE statement is treated as a single statement. In result, all parameter markers (represented by the character ?) are parts of the anonymous block specified in the prepared statement and at the same time parameter are markers, internally represented by instances of the class Item_param and distributed among SP instructions representing SQL statements (every SQL statement is represented by an instance of the class sp_instr_stmt) In case table metadata changed on running an anonymous block in prepared statement mode, only SP instruction's statement is re-parsed. Before re-parsing a SP's statement, all items are cleaned up including instances of the class Item_param that represent positional parameters. Unfortunately, this leads to presence of a dangling pointer in Prepared_statement::param_array that references to the deleted Item_param while invoking reset_stmt_params happening on every execution of a prepared statement. To fix the issue, no instances of Item_param created on re-parsings a statement for failed SP instruction, rather instances of Item_param left from first time parsing are re-used. As a consequence, all pointers to instances of the class Item_param stored in the array Prepared_statememt::param_array and possibly spread along the code base (e.g. select_lex->limit_params.select_limit) still point to valid Items.
1 parent c742cc9 commit ef9adb5

File tree

7 files changed

+266
-12
lines changed

7 files changed

+266
-12
lines changed

mysql-test/main/ps.result

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6014,4 +6014,66 @@ INSERT INTO t1 VALUES (1,11);
60146014
CREATE TRIGGER tr BEFORE INSERT ON t1 FOR EACH ROW SET @x = NULL;
60156015
EXECUTE IMMEDIATE "UPDATE t1 SET a = ?" USING DEFAULT;
60166016
DROP TABLE t1;
6017+
#
6018+
# MDEV-32694: ASAN errors in Binary_string::alloced_length / reset_stmt_params
6019+
#
6020+
CREATE TABLE t1 (a INT);
6021+
INSERT INTO t1 VALUES (1), (2), (3), (4), (5), (6), (7);
6022+
PREPARE stmt FROM 'BEGIN NOT ATOMIC SELECT * FROM t1 LIMIT ?; SELECT * FROM t1 LIMIT ?; END';
6023+
# Expected output is row (1) produced by the first query
6024+
# the rows (1), (2), (3) produced by the second one
6025+
EXECUTE stmt USING 1, 3;
6026+
a
6027+
1
6028+
a
6029+
1
6030+
2
6031+
3
6032+
ALTER TABLE t1 ADD COLUMN f INT;
6033+
# Because metadata of the table t1 has been changed,
6034+
# the second execution of the same prepared statement should result in
6035+
# re-compilation of the first statement enclosed in the BEGIN / END block
6036+
# and since different actual values are provided to positional parameters
6037+
# on the second execution, the exepected output is the row (1), (2), (3)
6038+
# produced by the first query and the rows (1), (2), (3), (4), (5)
6039+
# produced by the second one
6040+
EXECUTE stmt USING 3, 5;
6041+
a f
6042+
1 NULL
6043+
2 NULL
6044+
3 NULL
6045+
a f
6046+
1 NULL
6047+
2 NULL
6048+
3 NULL
6049+
4 NULL
6050+
5 NULL
6051+
DEALLOCATE PREPARE stmt;
6052+
DROP TABLE t1;
6053+
# Check that the order of parameters preserved after re-compilation of a
6054+
# failed statement inside anonymous BEGIN / END block.
6055+
CREATE TABLE t1 (a INT);
6056+
INSERT INTO t1 VALUES (1);
6057+
PREPARE stmt FROM 'BEGIN NOT ATOMIC SELECT a, ?, ? FROM t1; END';
6058+
# Expected output is the row (1, 10, 20)
6059+
EXECUTE stmt USING 10, 20;
6060+
a ? ?
6061+
1 10 20
6062+
ALTER TABLE t1 ADD COLUMN b INT;
6063+
# Expected output is the row (1, 300, 400)
6064+
EXECUTE stmt USING 300, 400;
6065+
a ? ?
6066+
1 300 400
6067+
ALTER TABLE t1 DROP COLUMN b;
6068+
# Expected output is the row (1, 500, 700)
6069+
EXECUTE stmt USING 500, 700;
6070+
a ? ?
6071+
1 500 700
6072+
ALTER TABLE t1 ADD COLUMN b INT;
6073+
# Expected output is the row (1, 700, 900)
6074+
EXECUTE stmt USING 700, 900;
6075+
a ? ?
6076+
1 700 900
6077+
DEALLOCATE PREPARE stmt;
6078+
DROP TABLE t1;
60176079
# End of 11.4 tests

mysql-test/main/ps.test

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5479,4 +5479,51 @@ EXECUTE IMMEDIATE "UPDATE t1 SET a = ?" USING DEFAULT;
54795479
# Cleanup
54805480
DROP TABLE t1;
54815481

5482+
--echo #
5483+
--echo # MDEV-32694: ASAN errors in Binary_string::alloced_length / reset_stmt_params
5484+
--echo #
5485+
5486+
CREATE TABLE t1 (a INT);
5487+
INSERT INTO t1 VALUES (1), (2), (3), (4), (5), (6), (7);
5488+
5489+
PREPARE stmt FROM 'BEGIN NOT ATOMIC SELECT * FROM t1 LIMIT ?; SELECT * FROM t1 LIMIT ?; END';
5490+
--echo # Expected output is row (1) produced by the first query
5491+
--echo # the rows (1), (2), (3) produced by the second one
5492+
EXECUTE stmt USING 1, 3;
5493+
ALTER TABLE t1 ADD COLUMN f INT;
5494+
5495+
--echo # Because metadata of the table t1 has been changed,
5496+
--echo # the second execution of the same prepared statement should result in
5497+
--echo # re-compilation of the first statement enclosed in the BEGIN / END block
5498+
--echo # and since different actual values are provided to positional parameters
5499+
--echo # on the second execution, the exepected output is the row (1), (2), (3)
5500+
--echo # produced by the first query and the rows (1), (2), (3), (4), (5)
5501+
--echo # produced by the second one
5502+
EXECUTE stmt USING 3, 5;
5503+
5504+
# Cleanup
5505+
DEALLOCATE PREPARE stmt;
5506+
DROP TABLE t1;
5507+
5508+
--echo # Check that the order of parameters preserved after re-compilation of a
5509+
--echo # failed statement inside anonymous BEGIN / END block.
5510+
CREATE TABLE t1 (a INT);
5511+
INSERT INTO t1 VALUES (1);
5512+
PREPARE stmt FROM 'BEGIN NOT ATOMIC SELECT a, ?, ? FROM t1; END';
5513+
--echo # Expected output is the row (1, 10, 20)
5514+
EXECUTE stmt USING 10, 20;
5515+
ALTER TABLE t1 ADD COLUMN b INT;
5516+
--echo # Expected output is the row (1, 300, 400)
5517+
EXECUTE stmt USING 300, 400;
5518+
ALTER TABLE t1 DROP COLUMN b;
5519+
--echo # Expected output is the row (1, 500, 700)
5520+
EXECUTE stmt USING 500, 700;
5521+
ALTER TABLE t1 ADD COLUMN b INT;
5522+
--echo # Expected output is the row (1, 700, 900)
5523+
EXECUTE stmt USING 700, 900;
5524+
5525+
# Cleanup
5526+
DEALLOCATE PREPARE stmt;
5527+
DROP TABLE t1;
5528+
54825529
--echo # End of 11.4 tests

sql/sp_instr.cc

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,42 @@ static int cmp_rqp_locations(const void *a_, const void *b_)
2424
}
2525

2626

27+
/**
28+
Traverse the list of Item_param instances created on the fist parsing of
29+
SP instruction's statement and put them back into sp_inst_lex->free list
30+
for releasing them on deallocating statement's resources to avoid
31+
memory leaks.
32+
*/
33+
34+
void
35+
sp_lex_instr::put_back_item_params(THD *thd, LEX *lex,
36+
const List<Item_param>& param_values)
37+
{
38+
/*
39+
Instance of Item_param must be ignored on re-parsing a statement
40+
of failed SP instruction, therefore lex->param_list must be empty.
41+
Instance of the class Item_param created on first (initial) parsing of
42+
Prepared Statement is used for whole its life.
43+
*/
44+
DBUG_ASSERT(lex->param_list.is_empty());
45+
46+
for (auto it= param_values.begin();
47+
it != param_values.end(); ++it)
48+
{
49+
/*
50+
Put retained instances of Item_param back into sp_lex_inst::free_list
51+
to avoid leaking them. Original ordering of Item_param objects
52+
are preserved since param_values contains items in reverse order.
53+
*/
54+
Item_param *param_for_adding_to_free_list= it.operator->();
55+
56+
Item *prev_head= free_list;
57+
free_list= param_for_adding_to_free_list;
58+
param_for_adding_to_free_list->next= prev_head;
59+
}
60+
}
61+
62+
2763
/*
2864
StoredRoutinesBinlogging
2965
This paragraph applies only to statement-based binlogging. Row-based
@@ -592,14 +628,30 @@ void sp_lex_instr::get_query(String *sql_query) const
592628
}
593629

594630

595-
void sp_lex_instr::cleanup_before_parsing(enum_sp_type sp_type)
631+
List<Item_param>
632+
sp_lex_instr::cleanup_before_parsing(enum_sp_type sp_type)
596633
{
597634
Item *current= free_list;
635+
List<Item_param> param_values{};
598636

599637
while (current)
600638
{
601639
Item *next= current->next;
602-
current->delete_self();
640+
641+
if (current->is_stored_routine_parameter())
642+
/*
643+
`current` points to an instance of the class Item_param.
644+
Place an instance of the class Item_param into the list `param_values`
645+
and skip the item in free_list (don't invoke the method delete_self()
646+
on it). Since the `free_list` stores items in reverse order of creation
647+
(that is the last created item is the one pointed by the `free_list`),
648+
place items in the list `param_values` using push_front to save
649+
original ordering of items
650+
*/
651+
param_values.push_front((Item_param*)current);
652+
else
653+
current->delete_self();
654+
603655
current= next;
604656
}
605657

@@ -612,6 +664,8 @@ void sp_lex_instr::cleanup_before_parsing(enum_sp_type sp_type)
612664
dangling references.
613665
*/
614666
m_cur_trigger_stmt_items.empty();
667+
668+
return param_values;
615669
}
616670

617671

@@ -770,10 +824,13 @@ LEX* sp_lex_instr::parse_expr(THD *thd, sp_head *sp, LEX *sp_instr_lex)
770824
m_cur_trigger_stmt_items.first->next_trig_field_list;
771825

772826
/*
773-
Clean up items owned by this SP instruction.
827+
Clean up items owned by this SP instruction except instances of Item_param.
828+
`sp_statement_param_values` stores instances of the class Item_param
829+
associated with the SP instruction's statement before the statement
830+
has been re-parsed.
774831
*/
775-
cleanup_before_parsing(sp->m_handler->type());
776-
832+
List<Item_param> sp_statement_param_values=
833+
cleanup_before_parsing(sp->m_handler->type());
777834
DBUG_ASSERT(mem_root != thd->mem_root);
778835
/*
779836
Back up the current free_list pointer and reset it to nullptr.
@@ -812,18 +869,29 @@ LEX* sp_lex_instr::parse_expr(THD *thd, sp_head *sp, LEX *sp_instr_lex)
812869
if (parser_state.init(thd, sql_query.c_ptr(), sql_query.length()))
813870
return nullptr;
814871

872+
/*
873+
Direct the parser to handle the '?' symbol in special way, that is as
874+
a positional parameter inside a prepared statement.
875+
*/
876+
parser_state.m_lip.stmt_prepare_mode= true;
877+
815878
// Create a new LEX and initialize it.
816879

817880
LEX *lex_saved= thd->lex;
818881
Item **cursor_free_list= nullptr;
882+
st_lex_local *lex_local= nullptr;
819883

820884
/*
821885
sp_instr_lex != nullptr for cursor relating SP instructions (sp_instr_cpush,
822886
sp_instr_cursor_copy_struct) and in some cases for sp_instr_set.
823887
*/
824888
if (sp_instr_lex == nullptr)
825889
{
826-
thd->lex= new (thd->mem_root) st_lex_local;
890+
lex_local= new (thd->mem_root) st_lex_local;
891+
thd->lex= lex_local;
892+
893+
lex_local->sp_statement_param_values= std::move(sp_statement_param_values);
894+
lex_local->param_values_it= lex_local->sp_statement_param_values.begin();
827895
lex_start(thd);
828896
if (sp->m_handler->type() == SP_TYPE_TRIGGER)
829897
{
@@ -892,7 +960,17 @@ LEX* sp_lex_instr::parse_expr(THD *thd, sp_head *sp, LEX *sp_instr_lex)
892960
const char *m_tmp_query_bak= sp->m_tmp_query;
893961
sp->m_tmp_query= sql_query.c_ptr();
894962

963+
/*
964+
Hint the parser that re-parsing of a failed SP instruction is in progress
965+
and instances of the class Item_param associated with SP instruction
966+
should be handled carefully (re-used on re-parsing the instruction's
967+
statement).
968+
@sa param_push_or_clone
969+
@sa LEX::add_placeholder
970+
*/
971+
thd->reparsing_sp_stmt= true;
895972
bool parsing_failed= parse_sql(thd, &parser_state, nullptr);
973+
thd->reparsing_sp_stmt= false;
896974

897975
sp->m_tmp_query= m_tmp_query_bak;
898976
thd->m_digest= parent_digest;
@@ -915,12 +993,17 @@ LEX* sp_lex_instr::parse_expr(THD *thd, sp_head *sp, LEX *sp_instr_lex)
915993
*/
916994
*cursor_free_list= thd->free_list;
917995
else
996+
{
918997
/*
919998
Assign the list of items created on re-parsing the statement to
920999
the current stored routine's instruction.
9211000
*/
9221001
free_list= thd->free_list;
9231002

1003+
put_back_item_params(thd, thd->lex,
1004+
lex_local->sp_statement_param_values);
1005+
}
1006+
9241007
thd->free_list= nullptr;
9251008
}
9261009

sql/sp_instr.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,8 +500,12 @@ class sp_lex_instr : public sp_instr
500500

501501
/**
502502
Clean up items previously created on behalf of the current instruction.
503+
504+
@return a list of Item_param instances representing position parameters
505+
specified for the instruction that is a part of a prepared
506+
statement
503507
*/
504-
void cleanup_before_parsing(enum_sp_type sp_type);
508+
List<Item_param> cleanup_before_parsing(enum_sp_type sp_type);
505509

506510

507511
/**
@@ -524,6 +528,10 @@ class sp_lex_instr : public sp_instr
524528

525529
bool setup_memroot_for_reparsing(sp_head *sphead,
526530
bool *new_memroot_allocated);
531+
532+
void put_back_item_params(THD *thd, LEX *lex,
533+
const List<Item_param>& param_values);
534+
527535
};
528536

529537

sql/sql_class.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5972,6 +5972,8 @@ class THD: public THD_count, /* this must be first */
59725972
return (lex->sphead != 0 &&
59735973
!(in_sub_stmt & (SUB_STMT_FUNCTION | SUB_STMT_TRIGGER)));
59745974
}
5975+
5976+
bool reparsing_sp_stmt= {false};
59755977
};
59765978

59775979

sql/sql_lex.cc

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8176,9 +8176,19 @@ Item *LEX::make_item_sysvar(THD *thd,
81768176

81778177
static bool param_push_or_clone(THD *thd, LEX *lex, Item_param *item)
81788178
{
8179-
return !lex->clone_spec_offset ?
8180-
lex->param_list.push_back(item, thd->mem_root) :
8181-
item->add_as_clone(thd);
8179+
if (lex->clone_spec_offset)
8180+
return item->add_as_clone(thd);
8181+
else
8182+
{
8183+
if (thd->reparsing_sp_stmt)
8184+
/*
8185+
Don't put an instance of Item_param in case a SP statement
8186+
being re-parsed.
8187+
*/
8188+
return false;
8189+
8190+
return lex->param_list.push_back(item, thd->mem_root);
8191+
}
81828192
}
81838193

81848194

@@ -8196,8 +8206,40 @@ Item_param *LEX::add_placeholder(THD *thd, const LEX_CSTRING *name,
81968206
return NULL;
81978207
}
81988208
Query_fragment pos(thd, sphead, start, end);
8199-
Item_param *item= new (thd->mem_root) Item_param(thd, name,
8200-
pos.pos(), pos.length());
8209+
Item_param *item;
8210+
/*
8211+
Check whether re-parsing of a failed SP instruction is in progress.
8212+
In context of the method LEX::add_placeholder, the failed instruction
8213+
being re-parsed is a part of compound statement enclosed into
8214+
the BEGIN/END clauses.
8215+
*/
8216+
if (thd->reparsing_sp_stmt)
8217+
{
8218+
/*
8219+
Get a saved Item_param and reuse it instead of creating a new one.
8220+
st_lex_local stores instances of the class Item_param that were saved
8221+
before cleaning up SP instruction's free_list. So, the same instance of
8222+
Item_param will be used on every re-parsing of failed SP instruction
8223+
for each specific positional parameter.
8224+
*/
8225+
st_lex_local *lex= (st_lex_local*)this;
8226+
DBUG_ASSERT(lex->param_values_it != lex->sp_statement_param_values.end());
8227+
/*
8228+
For release build emit internal error in case the assert condition
8229+
fails
8230+
*/
8231+
if (lex->param_values_it == lex->sp_statement_param_values.end())
8232+
{
8233+
my_error(ER_INTERNAL_ERROR, MYF(0), "no more Item_param for re-bind");
8234+
return nullptr;
8235+
}
8236+
8237+
item= lex->param_values_it.operator ->();
8238+
lex->param_values_it++;
8239+
}
8240+
else
8241+
item= new (thd->mem_root) Item_param(thd, name,
8242+
pos.pos(), pos.length());
82018243
if (unlikely(!item) || unlikely(param_push_or_clone(thd, this, item)))
82028244
{
82038245
my_error(ER_OUT_OF_RESOURCES, MYF(0));

0 commit comments

Comments
 (0)