Skip to content

Commit 0db19ef

Browse files
roylysengkahatlen
authored andcommitted
Bug#31691060: Assertion `!is_open()' in Materialized_cursor::~Materialized_cursor
Bug#31781145: Sig 11 in setup_tmp_table_handler at sql/sql_tmp_table.CC:2067 The problem in both these bug reports is bad handling of an error during execution in cursor-based operations. This is manifested both with cursors in stored procedures and with cursors associated with prepared statements in the client API. Furthermore, some opportunity for specific problems due to execution error because of invalid metadata existed. Changes in this patch: sp_cursor::open(): - After mysql_open_cursor(), if execution failed, the cursor is closed (just in case, as it should already be closed), and the cursor pointer is unassigned. mysql_open_cursor(): - The cursor is returned regardless of the execution completion: If execution is unsuccessful, a cursor may still have been created. - When a cursor is created and execution is successful, the cursor is opened (as before). - When a cursor is created and execution fails, the cursor is closed but it is not deleted. Prepared_statement::close_cursor() and ~Prepared_statement(): - cursor->close() is always called since it is state-aware. Note also that the is_open() result is ambiguous: There are actually two potential "open" states for a cursor: 1) the cursor has been populated with a result, and 2) the cursor has been opened against that result and can be read from. In the latter case, the table access must be closed. In both cases, the temporary table holding the result must be deleted. Prepared_statement::swap_prepared_statement(): - The members result and cursor are also swapped, since they are associated with the statement. Without this, repreparation may cause access to deleted memory. Prepared_statement::execute(): - After an error from mysql_open_cursor(), the result object was destroyed unconditionally. Now it is destroyed only if it is not referenced by a cursor. Several tests have been added: - A test that creates a stored procedure with a cursor, for which the SQL statement is causing an execution error. Multiple calls are made to the procedure, to ensure that subsequent executions are not affected by prior errors. FLUSH TABLES is performed between executions to ensure recovery from error due to metadata invalidation. - A test that creates a stored procedure with a cursor that is invoked multiple times within the procedure, with FLUSH TABLES performed between executions. - A client test that creates a cursor that executes a prepared statement with an associated cursor. It is verified that errors in statement execution is handled correctly. - A client test that creates a cursor for a SHOW PRIVILEGES statement. When prepared, this statement does not create a cursor, so the client library simulates a cursor. It is also demonstrated that the statement can be executed repeatedly. Reviewed-by: Knut Anders Hatlen <knut.hatlen@oracle.com>
1 parent 8c8d369 commit 0db19ef

File tree

6 files changed

+338
-39
lines changed

6 files changed

+338
-39
lines changed

mysql-test/r/sp.result

+76
Original file line numberDiff line numberDiff line change
@@ -8465,3 +8465,79 @@ IF (COUNT(*) > 0, 'affected', 'not affected')
84658465
not affected
84668466
DROP FUNCTION ReturnFalse;
84678467
DROP TABLE t1;
8468+
# Bug#31691060: Assertion `!is_open()' in Materialized_cursor::~Materialized_cursor
8469+
CREATE TABLE t1(a INTEGER);
8470+
CREATE TABLE t2(a INTEGER, b INTEGER);
8471+
INSERT INTO t1 VALUES(0), (1), (2);
8472+
INSERT INTO t2 VALUES(1, 10), (2, 20), (2, 21);
8473+
CREATE PROCEDURE pc(val INTEGER)
8474+
BEGIN
8475+
DECLARE finished, col_a, col_b INTEGER DEFAULT 0;
8476+
DECLARE c CURSOR FOR
8477+
SELECT a, (SELECT b FROM t2 WHERE t1.a=t2.a) FROM t1 WHERE a = val;
8478+
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
8479+
SET finished = 0;
8480+
OPEN c;
8481+
loop1: LOOP
8482+
FETCH c INTO col_a, col_b;
8483+
IF finished = 1 THEN
8484+
LEAVE loop1;
8485+
END IF;
8486+
END LOOP loop1;
8487+
CLOSE c;
8488+
END //
8489+
CREATE PROCEDURE pc_with_flush()
8490+
BEGIN
8491+
DECLARE finished, col_a, col_b INTEGER DEFAULT 0;
8492+
DECLARE val INTEGER DEFAULT 0;
8493+
DECLARE c CURSOR FOR
8494+
SELECT a, (SELECT b FROM t2 WHERE t1.a=t2.a) FROM t1 WHERE a = val;
8495+
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
8496+
SET finished = 0;
8497+
OPEN c;
8498+
loop1: LOOP
8499+
FETCH c INTO col_a, col_b;
8500+
IF finished = 1 THEN
8501+
LEAVE loop1;
8502+
END IF;
8503+
END LOOP loop1;
8504+
CLOSE c;
8505+
FLUSH TABLES;
8506+
SET finished = 0;
8507+
SET val = 1;
8508+
OPEN c;
8509+
loop2: LOOP
8510+
FETCH c INTO col_a, col_b;
8511+
IF finished = 1 THEN
8512+
LEAVE loop2;
8513+
END IF;
8514+
END LOOP loop2;
8515+
CLOSE c;
8516+
FLUSH TABLES;
8517+
SET val = 2;
8518+
SET finished = 0;
8519+
OPEN c;
8520+
loop3: LOOP
8521+
FETCH c INTO col_a, col_b;
8522+
IF finished = 1 THEN
8523+
LEAVE loop3;
8524+
END IF;
8525+
END LOOP loop3;
8526+
CLOSE c;
8527+
END //
8528+
CALL pc(1);
8529+
CALL pc(2);
8530+
ERROR 21000: Subquery returns more than 1 row
8531+
FLUSH TABLES;
8532+
CALL pc(1);
8533+
CALL pc(2);
8534+
ERROR 21000: Subquery returns more than 1 row
8535+
FLUSH TABLES;
8536+
CALL pc(2);
8537+
ERROR 21000: Subquery returns more than 1 row
8538+
CALL pc(1);
8539+
CALL pc_with_flush();
8540+
ERROR 21000: Subquery returns more than 1 row
8541+
DROP PROCEDURE pc;
8542+
DROP PROCEDURE pc_with_flush;
8543+
DROP TABLE t1, t2;

mysql-test/t/sp.test

+80
Original file line numberDiff line numberDiff line change
@@ -9869,3 +9869,83 @@ SELECT IF (COUNT(*) > 0, 'affected', 'not affected') FROM t1
98699869

98709870
DROP FUNCTION ReturnFalse;
98719871
DROP TABLE t1;
9872+
9873+
--echo # Bug#31691060: Assertion `!is_open()' in Materialized_cursor::~Materialized_cursor
9874+
9875+
CREATE TABLE t1(a INTEGER);
9876+
CREATE TABLE t2(a INTEGER, b INTEGER);
9877+
INSERT INTO t1 VALUES(0), (1), (2);
9878+
INSERT INTO t2 VALUES(1, 10), (2, 20), (2, 21);
9879+
DELIMITER //;
9880+
CREATE PROCEDURE pc(val INTEGER)
9881+
BEGIN
9882+
DECLARE finished, col_a, col_b INTEGER DEFAULT 0;
9883+
DECLARE c CURSOR FOR
9884+
SELECT a, (SELECT b FROM t2 WHERE t1.a=t2.a) FROM t1 WHERE a = val;
9885+
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
9886+
SET finished = 0;
9887+
OPEN c;
9888+
loop1: LOOP
9889+
FETCH c INTO col_a, col_b;
9890+
IF finished = 1 THEN
9891+
LEAVE loop1;
9892+
END IF;
9893+
END LOOP loop1;
9894+
CLOSE c;
9895+
END //
9896+
CREATE PROCEDURE pc_with_flush()
9897+
BEGIN
9898+
DECLARE finished, col_a, col_b INTEGER DEFAULT 0;
9899+
DECLARE val INTEGER DEFAULT 0;
9900+
DECLARE c CURSOR FOR
9901+
SELECT a, (SELECT b FROM t2 WHERE t1.a=t2.a) FROM t1 WHERE a = val;
9902+
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
9903+
SET finished = 0;
9904+
OPEN c;
9905+
loop1: LOOP
9906+
FETCH c INTO col_a, col_b;
9907+
IF finished = 1 THEN
9908+
LEAVE loop1;
9909+
END IF;
9910+
END LOOP loop1;
9911+
CLOSE c;
9912+
FLUSH TABLES;
9913+
SET finished = 0;
9914+
SET val = 1;
9915+
OPEN c;
9916+
loop2: LOOP
9917+
FETCH c INTO col_a, col_b;
9918+
IF finished = 1 THEN
9919+
LEAVE loop2;
9920+
END IF;
9921+
END LOOP loop2;
9922+
CLOSE c;
9923+
FLUSH TABLES;
9924+
SET val = 2;
9925+
SET finished = 0;
9926+
OPEN c;
9927+
loop3: LOOP
9928+
FETCH c INTO col_a, col_b;
9929+
IF finished = 1 THEN
9930+
LEAVE loop3;
9931+
END IF;
9932+
END LOOP loop3;
9933+
CLOSE c;
9934+
END //
9935+
DELIMITER ;//
9936+
CALL pc(1);
9937+
--error ER_SUBQUERY_NO_1_ROW
9938+
CALL pc(2);
9939+
FLUSH TABLES;
9940+
CALL pc(1);
9941+
--error ER_SUBQUERY_NO_1_ROW
9942+
CALL pc(2);
9943+
FLUSH TABLES;
9944+
--error ER_SUBQUERY_NO_1_ROW
9945+
CALL pc(2);
9946+
CALL pc(1);
9947+
--error ER_SUBQUERY_NO_1_ROW
9948+
CALL pc_with_flush();
9949+
DROP PROCEDURE pc;
9950+
DROP PROCEDURE pc_with_flush;
9951+
DROP TABLE t1, t2;

sql/sp_rcontext.cc

+8-1
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,14 @@ bool sp_cursor::open(THD *thd) {
458458
return true;
459459
}
460460

461-
return mysql_open_cursor(thd, &m_result, &m_server_side_cursor);
461+
bool rc = mysql_open_cursor(thd, &m_result, &m_server_side_cursor);
462+
463+
// If execution failed, ensure that the cursor is closed.
464+
if (rc && m_server_side_cursor != nullptr) {
465+
m_server_side_cursor->close();
466+
m_server_side_cursor = nullptr;
467+
}
468+
return rc;
462469
}
463470

464471
bool sp_cursor::close() {

sql/sql_cursor.cc

+26-30
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,11 @@ class Query_result_materialize final : public Query_result_union {
138138
@param thd thread handle
139139
@param[in] result result class of the caller used as a destination
140140
for the rows fetched from the cursor
141-
@param[in,out] pcursor a pointer to store a pointer to cursor in
142-
If non-NULL on entry, use the supplied cursor.
143-
Must be NULL on first invocation.
141+
@param[in,out] pcursor a pointer to store a pointer to cursor in.
142+
The cursor is usually created on first call.
143+
Notice that a cursor may be returned even though
144+
execution causes an error. Cursor is open
145+
when execution is successful, closed otherwise.
144146
145147
@return Error status
146148
@@ -234,47 +236,41 @@ bool mysql_open_cursor(THD *thd, Query_result *result,
234236
DEBUG_SYNC(thd, "after_table_close");
235237
thd->m_statement_psi = parent_locker;
236238

237-
/*
238-
Possible options here:
239-
- a materialized cursor is open. In this case rc is 0 and
240-
result_materialize->materialized is not NULL
241-
- an error occurred during materialization.
242-
result_materialize->materialized_cursor is not NULL, but rc != 0
243-
- successful completion of mysql_execute_command without
244-
a cursor: rc is 0, result_materialize->materialized_cursor is NULL.
245-
This is possible if some command writes directly to the
246-
network, bypassing Query_result mechanism. An example of
247-
such command is SHOW VARIABLES or SHOW STATUS.
248-
*/
239+
Materialized_cursor *materialized_cursor =
240+
result_materialize != nullptr ? result_materialize->materialized_cursor
241+
: nullptr;
242+
243+
if (*pcursor == nullptr) *pcursor = materialized_cursor;
244+
249245
if (rc) {
250-
if (result_materialize->materialized_cursor) {
251-
/* Rollback metadata in the client-server protocol. */
246+
/*
247+
Execution ended in error. Notice that a cursor may have been
248+
created, in this case metadata in client-server protocol is rolled
249+
back and the cursor is closed (if it is open).
250+
*/
251+
if (materialized_cursor != nullptr) {
252252
result_materialize->abort_result_set(thd);
253-
254-
delete result_materialize->materialized_cursor;
255-
result_materialize->materialized_cursor = nullptr;
253+
materialized_cursor->close();
256254
}
257-
258255
return true;
259256
}
260257

261-
if (result_materialize != nullptr &&
262-
result_materialize->materialized_cursor) {
263-
Materialized_cursor *materialized_cursor =
264-
result_materialize->materialized_cursor;
265-
258+
/*
259+
Execution was successful. For most queries, a cursor has been created
260+
and must be opened, however for some queries, no cursor is used.
261+
This is possible if some command writes directly to the
262+
network, bypassing Query_result mechanism. An example of
263+
such command is SHOW PRIVILEGES.
264+
*/
265+
if (materialized_cursor != nullptr) {
266266
/*
267267
NOTE: close_thread_tables() has been called in
268268
mysql_execute_command(), so all tables except from the cursor
269269
temporary table have been closed.
270270
*/
271-
272271
if (materialized_cursor->open(thd)) {
273-
delete materialized_cursor;
274272
return true;
275273
}
276-
277-
if (*pcursor == nullptr) *pcursor = materialized_cursor;
278274
}
279275

280276
return false;

sql/sql_prepare.cc

+11-8
Original file line numberDiff line numberDiff line change
@@ -2288,7 +2288,7 @@ Prepared_statement::Prepared_statement(THD *thd_arg)
22882288

22892289
void Prepared_statement::close_cursor() {
22902290
if (cursor == nullptr) return;
2291-
if (cursor->is_open()) cursor->close();
2291+
cursor->close();
22922292
}
22932293

22942294
void Prepared_statement::setup_set_params() {
@@ -2347,7 +2347,7 @@ Prepared_statement::~Prepared_statement() {
23472347
DBUG_TRACE;
23482348
DBUG_PRINT("enter", ("stmt: %p cursor: %p", this, cursor));
23492349
if (cursor != nullptr) {
2350-
if (cursor->is_open()) close_cursor();
2350+
close_cursor();
23512351
if (result != nullptr) destroy(result);
23522352
cursor = nullptr;
23532353
result = nullptr;
@@ -3277,6 +3277,10 @@ void Prepared_statement::swap_prepared_statement(Prepared_statement *copy) {
32773277
std::swap(m_name, copy->m_name);
32783278
/* Ditto */
32793279
std::swap(m_db, copy->m_db);
3280+
// Need a new cursor-specific query result after repreparation
3281+
std::swap(result, copy->result);
3282+
// Need a new cursor, if requested
3283+
std::swap(cursor, copy->cursor);
32803284

32813285
DBUG_ASSERT(thd == copy->thd);
32823286
}
@@ -3439,9 +3443,11 @@ bool Prepared_statement::execute(String *expanded_query, bool open_cursor) {
34393443
if (!result) {
34403444
error = true; // OOM
34413445
} else if ((error = mysql_open_cursor(thd, result, &cursor))) {
3442-
// cursor is freed inside mysql_open_cursor
3443-
destroy(result);
3444-
result = nullptr;
3446+
// Destroy result if cursor was never created
3447+
if (cursor == nullptr) {
3448+
destroy(result);
3449+
result = nullptr;
3450+
}
34453451
} else {
34463452
lex->cleanup(thd, true);
34473453
}
@@ -3502,9 +3508,6 @@ bool Prepared_statement::execute(String *expanded_query, bool open_cursor) {
35023508
if (cur_db_changed)
35033509
mysql_change_db(thd, to_lex_cstring(saved_cur_db_name), true);
35043510

3505-
// Assert that if an error, the cursor and the result are deallocated.
3506-
DBUG_ASSERT(!error || (cursor == nullptr && result == nullptr));
3507-
35083511
cleanup_stmt();
35093512

35103513
/*

0 commit comments

Comments
 (0)