Skip to content

Commit 6ae3bca

Browse files
author
kostja@bodhi.(none)
committed
Bug#27430 "Crash in subquery code when in PS and table DDL changed after
PREPARE", review fixes: - make the patch follow the specification of WL#4166 and remove the new error that was originally introduced. Now the client never gets an error from reprepare, unless it failed. I.e. even if the statement at hand returns a completely different result set, this is not considered a server error. The C API library, that can not handle this situation, was modified to return a client error. Added additional test coverage.
1 parent 2c0ce2a commit 6ae3bca

8 files changed

Lines changed: 229 additions & 64 deletions

File tree

include/errmsg.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ extern const char *client_errors[]; /* Error messages */
9696
#define CR_NOT_IMPLEMENTED 2054
9797
#define CR_SERVER_LOST_EXTENDED 2055
9898
#define CR_STMT_CLOSED 2056
99-
#define CR_ERROR_LAST /*Copy last error nr:*/ 2056
99+
#define CR_NEW_STMT_METADATA 2057
100+
#define CR_ERROR_LAST /*Copy last error nr:*/ 2057
100101
/* Add error numbers before CR_ERROR_LAST and change it accordingly. */
101102

include/mysql_com.h

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,19 +184,38 @@ enum enum_server_command
184184
#define SERVER_MORE_RESULTS_EXISTS 8 /* Multi query - next query exists */
185185
#define SERVER_QUERY_NO_GOOD_INDEX_USED 16
186186
#define SERVER_QUERY_NO_INDEX_USED 32
187-
/*
187+
/**
188188
The server was able to fulfill the clients request and opened a
189189
read-only non-scrollable cursor for a query. This flag comes
190190
in reply to COM_STMT_EXECUTE and COM_STMT_FETCH commands.
191191
*/
192192
#define SERVER_STATUS_CURSOR_EXISTS 64
193-
/*
193+
/**
194194
This flag is sent when a read-only cursor is exhausted, in reply to
195195
COM_STMT_FETCH command.
196196
*/
197197
#define SERVER_STATUS_LAST_ROW_SENT 128
198198
#define SERVER_STATUS_DB_DROPPED 256 /* A database was dropped */
199199
#define SERVER_STATUS_NO_BACKSLASH_ESCAPES 512
200+
/**
201+
Sent to the client if after a prepared statement reprepare
202+
we discovered that the new statement returns a different
203+
number of result set columns.
204+
*/
205+
#define SERVER_STATUS_METADATA_CHANGED 1024
206+
207+
/**
208+
Server status flags that must be cleared when starting
209+
execution of a new SQL statement.
210+
Flags from this set are only added to the
211+
current server status by the execution engine, but
212+
never removed -- the execution engine expects them
213+
to disappear automagically by the next command.
214+
*/
215+
#define SERVER_STATUS_CLEAR_SET (SERVER_QUERY_NO_GOOD_INDEX_USED| \
216+
SERVER_QUERY_NO_INDEX_USED|\
217+
SERVER_MORE_RESULTS_EXISTS|\
218+
SERVER_STATUS_METADATA_CHANGED)
200219

201220
#define MYSQL_ERRMSG_SIZE 512
202221
#define NET_READ_TIMEOUT 30 /* Timeout on read */
@@ -205,6 +224,7 @@ enum enum_server_command
205224

206225
#define ONLY_KILL_QUERY 1
207226

227+
208228
struct st_vio; /* Only C */
209229
typedef struct st_vio Vio;
210230

libmysql/errmsg.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const char *client_errors[]=
8484
"This feature is not implemented yet",
8585
"Lost connection to MySQL server at '%s', system error: %d",
8686
"Statement closed indirectly because of a preceeding %s() call",
87+
"The number of columns in the result set differs from the number of bound buffers. You must reset the statement, rebind the result set columns, and execute the statement again",
8788
""
8889
};
8990

@@ -149,6 +150,7 @@ const char *client_errors[]=
149150
"This feature is not implemented yet",
150151
"Lost connection to MySQL server at '%s', system error: %d",
151152
"Statement closed indirectly because of a preceeding %s() call",
153+
"The number of columns in the result set differs from the number of bound buffers. You must reset the statement, rebind the result set columns, and execute the statement again",
152154
""
153155
};
154156

@@ -212,6 +214,7 @@ const char *client_errors[]=
212214
"This feature is not implemented yet",
213215
"Lost connection to MySQL server at '%s', system error: %d",
214216
"Statement closed indirectly because of a preceeding %s() call",
217+
"The number of columns in the result set differs from the number of bound buffers. You must reset the statement, rebind the result set columns, and execute the statement again",
215218
""
216219
};
217220
#endif

libmysql/libmysql.c

Lines changed: 88 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1706,6 +1706,7 @@ static my_bool setup_one_fetch_function(MYSQL_BIND *, MYSQL_FIELD *field);
17061706
#define RESET_SERVER_SIDE 1
17071707
#define RESET_LONG_DATA 2
17081708
#define RESET_STORE_RESULT 4
1709+
#define RESET_CLEAR_ERROR 8
17091710

17101711
static my_bool reset_stmt_handle(MYSQL_STMT *stmt, uint flags);
17111712

@@ -2090,7 +2091,7 @@ mysql_stmt_prepare(MYSQL_STMT *stmt, const char *query, ulong length)
20902091
To be removed when all commands will fully support prepared mode.
20912092
*/
20922093

2093-
static unsigned int alloc_stmt_fields(MYSQL_STMT *stmt)
2094+
static void alloc_stmt_fields(MYSQL_STMT *stmt)
20942095
{
20952096
MYSQL_FIELD *fields, *field, *end;
20962097
MEM_ROOT *alloc= &stmt->mem_root;
@@ -2108,7 +2109,10 @@ static unsigned int alloc_stmt_fields(MYSQL_STMT *stmt)
21082109
!(stmt->bind= (MYSQL_BIND *) alloc_root(alloc,
21092110
sizeof(MYSQL_BIND) *
21102111
stmt->field_count)))
2111-
return 0;
2112+
{
2113+
set_stmt_error(stmt, CR_OUT_OF_MEMORY, unknown_sqlstate, NULL);
2114+
return;
2115+
}
21122116

21132117
for (fields= mysql->fields, end= fields+stmt->field_count,
21142118
field= stmt->fields;
@@ -2127,13 +2131,15 @@ static unsigned int alloc_stmt_fields(MYSQL_STMT *stmt)
21272131
field->def = fields->def ? strdup_root(alloc,fields->def): 0;
21282132
field->max_length= 0;
21292133
}
2130-
return stmt->field_count;
21312134
}
21322135

21332136

2134-
/*
2137+
/**
21352138
Update result set columns metadata if it was sent again in
21362139
reply to COM_STMT_EXECUTE.
2140+
2141+
@note If the new field count is different from the original one,
2142+
an error is set and no update is performed.
21372143
*/
21382144

21392145
static void update_stmt_fields(MYSQL_STMT *stmt)
@@ -2143,7 +2149,22 @@ static void update_stmt_fields(MYSQL_STMT *stmt)
21432149
MYSQL_FIELD *stmt_field= stmt->fields;
21442150
MYSQL_BIND *my_bind= stmt->bind_result_done ? stmt->bind : 0;
21452151

2146-
DBUG_ASSERT(stmt->field_count == stmt->mysql->field_count);
2152+
if (stmt->field_count != stmt->mysql->field_count)
2153+
{
2154+
/*
2155+
The tables used in the statement were altered,
2156+
and the query now returns a different number of columns.
2157+
There is no way to continue without reallocating the bind
2158+
array:
2159+
- if the number of columns increased, mysql_stmt_fetch()
2160+
will write beyond allocated memory
2161+
- if the number of columns decreased, some user-bound
2162+
buffers will be left unassigned without user knowing
2163+
that.
2164+
*/
2165+
set_stmt_error(stmt, CR_NEW_STMT_METADATA, unknown_sqlstate, NULL);
2166+
return;
2167+
}
21472168

21482169
for (; field < field_end; ++field, ++stmt_field)
21492170
{
@@ -2792,6 +2813,50 @@ my_bool STDCALL mysql_stmt_attr_get(MYSQL_STMT *stmt,
27922813
}
27932814

27942815

2816+
/**
2817+
Update statement result set metadata from with the new field
2818+
information sent during statement execute.
2819+
2820+
@pre mysql->field_count is not zero
2821+
2822+
@retval TRUE if error: out of memory or the new
2823+
result set has a different number of columns
2824+
@retval FALSE success
2825+
*/
2826+
2827+
static void reinit_result_set_metadata(MYSQL_STMT *stmt)
2828+
{
2829+
/* Server has sent result set metadata */
2830+
if (stmt->field_count == 0)
2831+
{
2832+
/*
2833+
This is 'SHOW'/'EXPLAIN'-like query. Current implementation of
2834+
prepared statements can't send result set metadata for these queries
2835+
on prepare stage. Read it now.
2836+
*/
2837+
alloc_stmt_fields(stmt);
2838+
}
2839+
else
2840+
{
2841+
/*
2842+
Update result set metadata if it for some reason changed between
2843+
prepare and execute, i.e.:
2844+
- in case of 'SELECT ?' we don't know column type unless data was
2845+
supplied to mysql_stmt_execute, so updated column type is sent
2846+
now.
2847+
- if data dictionary changed between prepare and execute, for
2848+
example a table used in the query was altered.
2849+
Note, that now (4.1.3) we always send metadata in reply to
2850+
COM_STMT_EXECUTE (even if it is not necessary), so either this or
2851+
previous branch always works.
2852+
TODO: send metadata only when it's really necessary and add a warning
2853+
'Metadata changed' when it's sent twice.
2854+
*/
2855+
update_stmt_fields(stmt);
2856+
}
2857+
}
2858+
2859+
27952860
/*
27962861
Send placeholders data to server (if there are placeholders)
27972862
and execute prepared statement.
@@ -2847,48 +2912,18 @@ int STDCALL mysql_stmt_execute(MYSQL_STMT *stmt)
28472912
DBUG_RETURN(1);
28482913
}
28492914

2850-
if (reset_stmt_handle(stmt, RESET_STORE_RESULT))
2915+
if (reset_stmt_handle(stmt, RESET_STORE_RESULT | RESET_CLEAR_ERROR))
28512916
DBUG_RETURN(1);
28522917
/*
28532918
No need to check for stmt->state: if the statement wasn't
28542919
prepared we'll get 'unknown statement handler' error from server.
28552920
*/
28562921
if (mysql->methods->stmt_execute(stmt))
28572922
DBUG_RETURN(1);
2858-
if (mysql->field_count)
2859-
{
2860-
/* Server has sent result set metadata */
2861-
if (stmt->field_count == 0)
2862-
{
2863-
/*
2864-
This is 'SHOW'/'EXPLAIN'-like query. Current implementation of
2865-
prepared statements can't send result set metadata for these queries
2866-
on prepare stage. Read it now.
2867-
*/
2868-
alloc_stmt_fields(stmt);
2869-
}
2870-
else
2871-
{
2872-
/*
2873-
Update result set metadata if it for some reason changed between
2874-
prepare and execute, i.e.:
2875-
- in case of 'SELECT ?' we don't know column type unless data was
2876-
supplied to mysql_stmt_execute, so updated column type is sent
2877-
now.
2878-
- if data dictionary changed between prepare and execute, for
2879-
example a table used in the query was altered.
2880-
Note, that now (4.1.3) we always send metadata in reply to
2881-
COM_STMT_EXECUTE (even if it is not necessary), so either this or
2882-
previous branch always works.
2883-
TODO: send metadata only when it's really necessary and add a warning
2884-
'Metadata changed' when it's sent twice.
2885-
*/
2886-
update_stmt_fields(stmt);
2887-
}
2888-
}
28892923
stmt->state= MYSQL_STMT_EXECUTE_DONE;
2890-
if (stmt->field_count)
2924+
if (mysql->field_count)
28912925
{
2926+
reinit_result_set_metadata(stmt);
28922927
if (stmt->server_status & SERVER_STATUS_CURSOR_EXISTS)
28932928
{
28942929
mysql->status= MYSQL_STATUS_READY;
@@ -2903,7 +2938,7 @@ int STDCALL mysql_stmt_execute(MYSQL_STMT *stmt)
29032938
network or b) is more efficient if all (few) result set rows are
29042939
precached on client and server's resources are freed.
29052940
*/
2906-
DBUG_RETURN(mysql_stmt_store_result(stmt));
2941+
mysql_stmt_store_result(stmt);
29072942
}
29082943
else
29092944
{
@@ -2912,7 +2947,7 @@ int STDCALL mysql_stmt_execute(MYSQL_STMT *stmt)
29122947
stmt->read_row_func= stmt_read_row_unbuffered;
29132948
}
29142949
}
2915-
DBUG_RETURN(0);
2950+
DBUG_RETURN(test(stmt->last_errno));
29162951
}
29172952

29182953

@@ -4766,6 +4801,12 @@ int STDCALL mysql_stmt_store_result(MYSQL_STMT *stmt)
47664801
DBUG_RETURN(1);
47674802
}
47684803

4804+
if (stmt->last_errno)
4805+
{
4806+
/* An attempt to use an invalid statement handle. */
4807+
DBUG_RETURN(1);
4808+
}
4809+
47694810
if (mysql->status == MYSQL_STATUS_READY &&
47704811
stmt->server_status & SERVER_STATUS_CURSOR_EXISTS)
47714812
{
@@ -4973,9 +5014,10 @@ static my_bool reset_stmt_handle(MYSQL_STMT *stmt, uint flags)
49735014
stmt->state= MYSQL_STMT_INIT_DONE;
49745015
return 1;
49755016
}
4976-
stmt_clear_error(stmt);
49775017
}
49785018
}
5019+
if (flags & RESET_CLEAR_ERROR)
5020+
stmt_clear_error(stmt);
49795021
stmt->state= MYSQL_STMT_PREPARE_DONE;
49805022
}
49815023
return 0;
@@ -4986,7 +5028,8 @@ my_bool STDCALL mysql_stmt_free_result(MYSQL_STMT *stmt)
49865028
DBUG_ENTER("mysql_stmt_free_result");
49875029

49885030
/* Free the client side and close the server side cursor if there is one */
4989-
DBUG_RETURN(reset_stmt_handle(stmt, RESET_LONG_DATA | RESET_STORE_RESULT));
5031+
DBUG_RETURN(reset_stmt_handle(stmt, RESET_LONG_DATA | RESET_STORE_RESULT |
5032+
RESET_CLEAR_ERROR));
49905033
}
49915034

49925035
/********************************************************************
@@ -5067,7 +5110,9 @@ my_bool STDCALL mysql_stmt_reset(MYSQL_STMT *stmt)
50675110
DBUG_RETURN(1);
50685111
}
50695112
/* Reset the client and server sides of the prepared statement */
5070-
DBUG_RETURN(reset_stmt_handle(stmt, RESET_SERVER_SIDE | RESET_LONG_DATA));
5113+
DBUG_RETURN(reset_stmt_handle(stmt,
5114+
RESET_SERVER_SIDE | RESET_LONG_DATA |
5115+
RESET_CLEAR_ERROR));
50715116
}
50725117

50735118
/*

sql/share/errmsg.txt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6130,6 +6130,3 @@ ER_LOG_PURGE_NO_FILE
61306130

61316131
ER_NEED_REPREPARE
61326132
eng "Prepared statement needs to be re-prepared"
6133-
6134-
ER_PS_REBIND
6135-
eng "Prepared statement result set has changed, a rebind needed"

sql/sql_parse.cc

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -914,8 +914,11 @@ bool dispatch_command(enum enum_server_command command, THD *thd,
914914
/* TODO: set thd->lex->sql_command to SQLCOM_END here */
915915
VOID(pthread_mutex_unlock(&LOCK_thread_count));
916916

917-
thd->server_status&=
918-
~(SERVER_QUERY_NO_INDEX_USED | SERVER_QUERY_NO_GOOD_INDEX_USED);
917+
/**
918+
Clear the set of flags that are expected to be cleared at the
919+
beginning of each command.
920+
*/
921+
thd->server_status&= ~SERVER_STATUS_CLEAR_SET;
919922
switch (command) {
920923
case COM_INIT_DB:
921924
{
@@ -5377,9 +5380,11 @@ void mysql_reset_thd_for_next_command(THD *thd)
53775380

53785381
thd->query_start_used= 0;
53795382
thd->is_fatal_error= thd->time_zone_used= 0;
5380-
thd->server_status&= ~ (SERVER_MORE_RESULTS_EXISTS |
5381-
SERVER_QUERY_NO_INDEX_USED |
5382-
SERVER_QUERY_NO_GOOD_INDEX_USED);
5383+
/*
5384+
Clear the status flag that are expected to be cleared at the
5385+
beginning of each SQL statement.
5386+
*/
5387+
thd->server_status&= ~SERVER_STATUS_CLEAR_SET;
53835388
/*
53845389
If in autocommit mode and not in a transaction, reset
53855390
OPTION_STATUS_NO_TRANS_UPDATE | OPTION_KEEP_LOG to not get warnings

sql/sql_prepare.cc

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3315,7 +3315,7 @@ Prepared_statement::reprepare()
33153315
@param[in] copy the re-prepared prepared statement to verify
33163316
the metadata of
33173317
3318-
@retval TRUE error, ER_PS_NEED_REBIND is reported
3318+
@retval TRUE error, ER_PS_REBIND is reported
33193319
@retval FALSE statement return no or compatible metadata
33203320
*/
33213321

@@ -3333,9 +3333,8 @@ bool Prepared_statement::validate_metadata(Prepared_statement *copy)
33333333
if (lex->select_lex.item_list.elements !=
33343334
copy->lex->select_lex.item_list.elements)
33353335
{
3336-
/** Column counts mismatch. */
3337-
my_error(ER_PS_REBIND, MYF(0));
3338-
return TRUE;
3336+
/** Column counts mismatch, update the client */
3337+
thd->server_status|= SERVER_STATUS_METADATA_CHANGED;
33393338
}
33403339

33413340
return FALSE;

0 commit comments

Comments
 (0)