Merge lp:~gl-az/percona-server/ST-31919-5.6 into lp:percona-server/5.6

Proposed by George Ormond Lorch III
Status: Superseded
Proposed branch: lp:~gl-az/percona-server/ST-31919-5.6
Merge into: lp:percona-server/5.6
Diff against target: 470 lines (+231/-50)
4 files modified
Percona-Server/mysql-test/suite/federated/federated_bug_68354.result (+80/-0)
Percona-Server/mysql-test/suite/federated/federated_bug_68354.test (+79/-0)
Percona-Server/storage/federated/ha_federated.cc (+56/-43)
Percona-Server/storage/federated/ha_federated.h (+16/-7)
To merge this branch: bzr merge lp:~gl-az/percona-server/ST-31919-5.6
Reviewer Review Type Date Requested Status
Alexey Kopytov (community) Needs Fixing
Review via email: mp+169956@code.launchpad.net

This proposal has been superseded by a proposal from 2013-06-18.

Description of the change

Fix for upstream 68354 and lp 1182572:
  Server crashes on update/join FEDERATED + local table when only 1 local row

This issue seems to be the result of a poor or changed assumption in the order
in which handler calls are made. In this specific scenario (a join with a local
table that has exactly one matching record) there is an assumption in the code
that a call to position will always come after a 'rnd_init/index_init' and
'rnd_next/index_read' but before 'rnd_end/index_end' is called.

The failing call order is:
  ha_federated::index_init
  ha_federated::index_read
  ha_federated::index_end
  ha_federated::position
  ha_federated::rnd_init
  ha_federated::extra
  ha_federated::rnd_pos

So here you can see position is getting called _outside_ of the scope of an
_init.._end table and/or index scan. This does indicate a change/problem in
the server but that is outside the scope of this fix.

As results are fetched, they are added to an interal list of result sets called
'results'. The last result set/current position is also held in the
'stored_result' value. If 'index_end' or 'rnd_end' is called before 'position'
has been called, the 'stored_result' is zeroed out and that result is removed
from the 'stored_results' list. When 'position' is called, a NULL result is
returned, which is then passed back through 'rnd_pos', which then dereferences
the NULL value and tries to manipulate some of the result internals, crashing.

So seems that the intention of the whole 'stored_result' was to have a place
to track all results allocated for the purposes of keeping them valid for the
'position' and 'rnd_pos' calls.

My approch to fixing this is to restructure this somewhan and instead of
tracking all results allocated in the 'results' list and popping them out when
they are discovered to not be needed, I went the other way. The results only
get addes to a storage list when it has been proven that they are in fact
needed later. So now for each new result set, it is temporarily stored in a
new value called 'last_result'. Whenever a new last_result needs created, it is
set by calling a new function 'set_last_result'. This function will check to
see if 'position' has been called on that result and if so, add it to the
(renamed) list 'stored_results' and clears the 'position_called' flag, else it
will free the 'last_result'. It then sets the new 'last_result' value.

Because of the out of scan scope call of position, we can no longer clear the
'stored_results' lists on '_end'. It will be cleared now when 'reset' is
called.

I have created a test case (suite/federated/federated_bug_68354.test) and
verified that it fails without the fix. Everything tests out under the federated
suite and the issue appears fixed.

The only downside is that a lengthy query could use a lot of memory while
hanging onto old 'position'ed results but I can't see how that can be safely
avoided in this particular handler.

To post a comment you must log in.
Revision history for this message
George Ormond Lorch III (gl-az) wrote :
Revision history for this message
Alexey Kopytov (akopytov) wrote :

Same comments as in the 5.5 MP.

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'Percona-Server/mysql-test/suite/federated/federated_bug_68354.result'
2--- Percona-Server/mysql-test/suite/federated/federated_bug_68354.result 1970-01-01 00:00:00 +0000
3+++ Percona-Server/mysql-test/suite/federated/federated_bug_68354.result 2013-06-18 18:42:38 +0000
4@@ -0,0 +1,80 @@
5+CREATE DATABASE federated;
6+CREATE DATABASE federated;
7+SET @OLD_MASTER_CONCURRENT_INSERT= @@GLOBAL.CONCURRENT_INSERT;
8+SET @@GLOBAL.CONCURRENT_INSERT= 0;
9+SET @OLD_SLAVE_CONCURRENT_INSERT= @@GLOBAL.CONCURRENT_INSERT;
10+SET @@GLOBAL.CONCURRENT_INSERT= 0;
11+DROP TABLE IF EXISTS federated.t1;
12+Warnings:
13+Note 1051 Unknown table 'federated.t1'
14+CREATE TABLE federated.t1 (
15+`id` int(20) NOT NULL,
16+`dummy` int(20) DEFAULT NULL,
17+PRIMARY KEY (`id`))
18+ENGINE="MyISAM"
19+ DEFAULT CHARSET=latin1;
20+INSERT INTO federated.t1 (`id`) VALUES
21+(1001), (1002), (1003), (1004), (1005),
22+(1006), (1007), (1008), (1009), (1010);
23+CREATE TABLE federated.t1 (
24+`id` int(20) NOT NULL,
25+`dummy` int(20) DEFAULT NULL,
26+PRIMARY KEY (`id`))
27+ENGINE="FEDERATED"
28+ DEFAULT CHARSET=latin1
29+CONNECTION='mysql://root@127.0.0.1:SLAVE_PORT/federated/t1';
30+CREATE TABLE federated.t2 (
31+`id` int(20) NOT NULL,
32+`dummy` int(20) NOT NULL,
33+PRIMARY KEY (`id`))
34+ENGINE="MyISAM"
35+ DEFAULT CHARSET=latin1;
36+INSERT INTO federated.t2 (`id`, `dummy`) VALUES (1005, 5);
37+SELECT * FROM federated.t1;
38+id dummy
39+1001 NULL
40+1002 NULL
41+1003 NULL
42+1004 NULL
43+1005 NULL
44+1006 NULL
45+1007 NULL
46+1008 NULL
47+1009 NULL
48+1010 NULL
49+UPDATE federated.t1 ft1 INNER JOIN federated.t2 ft2 ON ft1.id = ft2.id SET ft1.dummy = ft2.dummy WHERE ft1.dummy IS NULL;
50+SELECT * FROM federated.t1;
51+id dummy
52+1001 NULL
53+1002 NULL
54+1003 NULL
55+1004 NULL
56+1005 5
57+1006 NULL
58+1007 NULL
59+1008 NULL
60+1009 NULL
61+1010 NULL
62+DELETE FROM federated.t1 WHERE id = 1005;
63+SELECT * FROM federated.t1 WHERE id = 1005;
64+id dummy
65+SELECT * FROM federated.t1;
66+id dummy
67+1001 NULL
68+1002 NULL
69+1003 NULL
70+1004 NULL
71+1006 NULL
72+1007 NULL
73+1008 NULL
74+1009 NULL
75+1010 NULL
76+DROP TABLE federated.t2;
77+DROP TABLE federated.t1;
78+DROP TABLE federated.t1;
79+SET @@GLOBAL.CONCURRENT_INSERT= @OLD_MASTER_CONCURRENT_INSERT;
80+SET @@GLOBAL.CONCURRENT_INSERT= @OLD_SLAVE_CONCURRENT_INSERT;
81+DROP TABLE IF EXISTS federated.t1;
82+DROP DATABASE federated;
83+DROP TABLE IF EXISTS federated.t1;
84+DROP DATABASE federated;
85
86=== added file 'Percona-Server/mysql-test/suite/federated/federated_bug_68354.test'
87--- Percona-Server/mysql-test/suite/federated/federated_bug_68354.test 1970-01-01 00:00:00 +0000
88+++ Percona-Server/mysql-test/suite/federated/federated_bug_68354.test 2013-06-18 18:42:38 +0000
89@@ -0,0 +1,79 @@
90+# Bug 1182572 68354: Server crashes on update/join FEDERATED + local table when
91+# only 1 local row.
92+#
93+# When updating a federated table with UPDATE ... JOIN, the server consistently
94+# crashes with Signal 11 when only 1 row exists in the local table involved in
95+# the join and that 1 row can be joined with a row in the federated table.
96+#
97+
98+--source suite/federated/include/federated.inc
99+
100+connection default;
101+
102+# Disable concurrent inserts to avoid test failures when reading
103+# data from concurrent connections (insert might return before
104+# the data is actually in the table).
105+SET @OLD_MASTER_CONCURRENT_INSERT= @@GLOBAL.CONCURRENT_INSERT;
106+SET @@GLOBAL.CONCURRENT_INSERT= 0;
107+
108+connection slave;
109+SET @OLD_SLAVE_CONCURRENT_INSERT= @@GLOBAL.CONCURRENT_INSERT;
110+SET @@GLOBAL.CONCURRENT_INSERT= 0;
111+DROP TABLE IF EXISTS federated.t1;
112+CREATE TABLE federated.t1 (
113+ `id` int(20) NOT NULL,
114+ `dummy` int(20) DEFAULT NULL,
115+ PRIMARY KEY (`id`))
116+ ENGINE="MyISAM"
117+ DEFAULT CHARSET=latin1;
118+
119+INSERT INTO federated.t1 (`id`) VALUES
120+ (1001), (1002), (1003), (1004), (1005),
121+ (1006), (1007), (1008), (1009), (1010);
122+
123+connection master;
124+--replace_result $SLAVE_MYPORT SLAVE_PORT
125+eval CREATE TABLE federated.t1 (
126+ `id` int(20) NOT NULL,
127+ `dummy` int(20) DEFAULT NULL,
128+ PRIMARY KEY (`id`))
129+ ENGINE="FEDERATED"
130+ DEFAULT CHARSET=latin1
131+ CONNECTION='mysql://root@127.0.0.1:$SLAVE_MYPORT/federated/t1';
132+
133+CREATE TABLE federated.t2 (
134+ `id` int(20) NOT NULL,
135+ `dummy` int(20) NOT NULL,
136+ PRIMARY KEY (`id`))
137+ ENGINE="MyISAM"
138+ DEFAULT CHARSET=latin1;
139+
140+INSERT INTO federated.t2 (`id`, `dummy`) VALUES (1005, 5);
141+
142+SELECT * FROM federated.t1;
143+
144+# master would crash here before bug fix
145+UPDATE federated.t1 ft1 INNER JOIN federated.t2 ft2 ON ft1.id = ft2.id SET ft1.dummy = ft2.dummy WHERE ft1.dummy IS NULL;
146+
147+# sanity test to make sure correct rows are being returned and server doesn't
148+# also crash during DELETE
149+SELECT * FROM federated.t1;
150+DELETE FROM federated.t1 WHERE id = 1005;
151+SELECT * FROM federated.t1 WHERE id = 1005;
152+
153+SELECT * FROM federated.t1;
154+
155+DROP TABLE federated.t2;
156+DROP TABLE federated.t1;
157+
158+connection slave;
159+DROP TABLE federated.t1;
160+
161+connection default;
162+
163+SET @@GLOBAL.CONCURRENT_INSERT= @OLD_MASTER_CONCURRENT_INSERT;
164+connection slave;
165+SET @@GLOBAL.CONCURRENT_INSERT= @OLD_SLAVE_CONCURRENT_INSERT;
166+
167+connection default;
168+--source suite/federated/include/federated_cleanup.inc
169
170=== modified file 'Percona-Server/storage/federated/ha_federated.cc'
171--- Percona-Server/storage/federated/ha_federated.cc 2013-05-12 06:24:46 +0000
172+++ Percona-Server/storage/federated/ha_federated.cc 2013-06-18 18:42:38 +0000
173@@ -1,4 +1,4 @@
174-/* Copyright (c) 2004, 2011, Oracle and/or its affiliates. All rights reserved.
175+/* Copyright (c) 2004, 2012, Oracle and/or its affiliates. All rights reserved.
176
177 This program is free software; you can redistribute it and/or modify
178 it under the terms of the GNU General Public License as published by
179@@ -900,7 +900,7 @@
180 ha_federated::ha_federated(handlerton *hton,
181 TABLE_SHARE *table_arg)
182 :handler(hton, table_arg),
183- mysql(0), stored_result(0)
184+ mysql(0), last_result(0), position_called(false)
185 {
186 trx_next= 0;
187 memset(&bulk_insert, 0, sizeof(bulk_insert));
188@@ -1653,7 +1653,7 @@
189 ref_length= sizeof(MYSQL_RES *) + sizeof(MYSQL_ROW_OFFSET);
190 DBUG_PRINT("info", ("ref_length: %u", ref_length));
191
192- my_init_dynamic_array(&results, sizeof(MYSQL_RES *), 4, 4);
193+ my_init_dynamic_array(&stored_results, sizeof(MYSQL_RES *), 4, 4);
194 reset();
195
196 DBUG_RETURN(0);
197@@ -1677,9 +1677,7 @@
198
199 DBUG_ENTER("ha_federated::close");
200
201- free_result();
202-
203- delete_dynamic(&results);
204+ delete_dynamic(&stored_results);
205
206 /* Disconnect from mysql */
207 mysql_close(mysql);
208@@ -2326,13 +2324,18 @@
209 uint key_len, ha_rkey_function find_flag)
210 {
211 int rc;
212+ MYSQL_RES* mysql_result;
213+
214 DBUG_ENTER("ha_federated::index_read");
215
216 MYSQL_INDEX_READ_ROW_START(table_share->db.str, table_share->table_name.str);
217- free_result();
218 rc= index_read_idx_with_result_set(buf, active_index, key,
219 key_len, find_flag,
220- &stored_result);
221+ &mysql_result);
222+
223+ if (rc == 0)
224+ set_last_result(mysql_result);
225+
226 MYSQL_INDEX_READ_ROW_DONE(rc);
227 DBUG_RETURN(rc);
228 }
229@@ -2363,7 +2366,6 @@
230 &mysql_result)))
231 DBUG_RETURN(retval);
232 mysql_free_result(mysql_result);
233- results.elements--;
234 DBUG_RETURN(0);
235 }
236
237@@ -2428,7 +2430,6 @@
238 if ((retval= read_next(buf, *result)))
239 {
240 mysql_free_result(*result);
241- results.elements--;
242 *result= 0;
243 table->status= STATUS_NOT_FOUND;
244 DBUG_RETURN(retval);
245@@ -2480,6 +2481,7 @@
246 bool eq_range_arg, bool sorted)
247 {
248 char sql_query_buffer[FEDERATED_QUERY_BUFFER_SIZE];
249+ MYSQL_RES *mysql_result;
250 int retval;
251 String sql_query(sql_query_buffer,
252 sizeof(sql_query_buffer),
253@@ -2501,13 +2503,15 @@
254 }
255 sql_query.length(0);
256
257- if (!(stored_result= store_result(mysql)))
258+ if (!(mysql_result= store_result(mysql)))
259 {
260 retval= HA_ERR_END_OF_FILE;
261 goto error;
262 }
263
264- retval= read_next(table->record[0], stored_result);
265+ set_last_result(mysql_result);
266+
267+ retval= read_next(table->record[0], mysql_result);
268 MYSQL_INDEX_READ_ROW_DONE(retval);
269 DBUG_RETURN(retval);
270
271@@ -2536,7 +2540,7 @@
272 DBUG_ENTER("ha_federated::index_next");
273 MYSQL_INDEX_READ_ROW_START(table_share->db.str, table_share->table_name.str);
274 ha_statistic_increment(&SSV::ha_read_next_count);
275- retval= read_next(buf, stored_result);
276+ retval= read_next(buf, get_last_result());
277 MYSQL_INDEX_READ_ROW_DONE(retval);
278 DBUG_RETURN(retval);
279 }
280@@ -2557,6 +2561,7 @@
281
282 int ha_federated::rnd_init(bool scan)
283 {
284+ MYSQL_RES *mysql_result;
285 DBUG_ENTER("ha_federated::rnd_init");
286 /*
287 The use of the 'scan' flag is incredibly important for this handler
288@@ -2596,8 +2601,9 @@
289 if (scan)
290 {
291 if (real_query(share->select_query, strlen(share->select_query)) ||
292- !(stored_result= store_result(mysql)))
293+ !(mysql_result= store_result(mysql)))
294 DBUG_RETURN(stash_remote_error());
295+ set_last_result(mysql_result);
296 }
297 DBUG_RETURN(0);
298 }
299@@ -2606,14 +2612,14 @@
300 int ha_federated::rnd_end()
301 {
302 DBUG_ENTER("ha_federated::rnd_end");
303- DBUG_RETURN(index_end());
304+ active_index= MAX_KEY;
305+ DBUG_RETURN(0);
306 }
307
308
309 int ha_federated::index_end(void)
310 {
311 DBUG_ENTER("ha_federated::index_end");
312- free_result();
313 active_index= MAX_KEY;
314 DBUG_RETURN(0);
315 }
316@@ -2644,7 +2650,7 @@
317 {
318 DBUG_ENTER("ha_federated::rnd_next_int");
319
320- if (stored_result == 0)
321+ if (get_last_result() == 0)
322 {
323 /*
324 Return value of rnd_init is not always checked (see records.cc),
325@@ -2653,7 +2659,7 @@
326 */
327 DBUG_RETURN(1);
328 }
329- DBUG_RETURN(read_next(buf, stored_result));
330+ DBUG_RETURN(read_next(buf, get_last_result()));
331 }
332
333
334@@ -2721,16 +2727,20 @@
335
336 void ha_federated::position(const uchar *record __attribute__ ((unused)))
337 {
338+ MYSQL_RES* mysql_result;
339 DBUG_ENTER("ha_federated::position");
340-
341- DBUG_ASSERT(stored_result);
342-
343- position_called= TRUE;
344+
345+ mysql_result= get_last_result();
346+
347+ DBUG_ASSERT(mysql_result);
348+
349 /* Store result set address. */
350- memcpy(ref, &stored_result, sizeof(MYSQL_RES *));
351+ memcpy(ref, &mysql_result, sizeof(MYSQL_RES *));
352 /* Store data cursor position. */
353 memcpy(ref + sizeof(MYSQL_RES *), &current_position,
354 sizeof(MYSQL_ROW_OFFSET));
355+
356+ position_called= true;
357 DBUG_VOID_RETURN;
358 }
359
360@@ -2966,14 +2976,20 @@
361 replace_duplicates= FALSE;
362
363 /* Free stored result sets. */
364- for (uint i= 0; i < results.elements; i++)
365+ for (uint i= 0; i < stored_results.elements; i++)
366 {
367 MYSQL_RES *result;
368- get_dynamic(&results, (uchar *) &result, i);
369+ get_dynamic(&stored_results, (uchar *) &result, i);
370 mysql_free_result(result);
371 }
372- reset_dynamic(&results);
373+ reset_dynamic(&stored_results);
374
375+ if (last_result)
376+ {
377+ mysql_free_result(last_result);
378+ last_result= 0;
379+ }
380+ position_called= false;
381 return 0;
382 }
383
384@@ -3261,31 +3277,28 @@
385
386 MYSQL_RES *ha_federated::store_result(MYSQL *mysql_arg)
387 {
388- MYSQL_RES *result= mysql_store_result(mysql_arg);
389 DBUG_ENTER("ha_federated::store_result");
390- if (result)
391- {
392- (void) insert_dynamic(&results, &result);
393- }
394- position_called= FALSE;
395- DBUG_RETURN(result);
396+ DBUG_RETURN(mysql_store_result(mysql_arg));
397 }
398
399
400-void ha_federated::free_result()
401+void ha_federated::set_last_result(MYSQL_RES *result)
402 {
403- DBUG_ENTER("ha_federated::free_result");
404- if (stored_result && !position_called)
405- {
406- mysql_free_result(stored_result);
407- stored_result= 0;
408- if (results.elements > 0)
409- results.elements--;
410- }
411+ DBUG_ENTER("ha_federated::set_last_result");
412+ if (position_called)
413+ {
414+ insert_dynamic(&stored_results, (uchar*) &last_result);
415+ position_called= false;
416+ }
417+ else
418+ {
419+ mysql_free_result(last_result);
420+ }
421+ last_result= result;
422 DBUG_VOID_RETURN;
423 }
424
425-
426+
427 int ha_federated::external_lock(THD *thd, int lock_type)
428 {
429 int error= 0;
430
431=== modified file 'Percona-Server/storage/federated/ha_federated.h'
432--- Percona-Server/storage/federated/ha_federated.h 2012-08-22 01:40:20 +0000
433+++ Percona-Server/storage/federated/ha_federated.h 2013-06-18 18:42:38 +0000
434@@ -79,11 +79,19 @@
435 THR_LOCK_DATA lock; /* MySQL lock */
436 FEDERATED_SHARE *share; /* Shared lock info */
437 MYSQL *mysql; /* MySQL connection */
438- MYSQL_RES *stored_result;
439- /**
440- Array of all stored results we get during a query execution.
441- */
442- DYNAMIC_ARRAY results;
443+ /*
444+ Last fetched result, freed if !position_called when set to next result.
445+ */
446+ MYSQL_RES *last_result;
447+ /*
448+ Array of stored results that exist because of position call.
449+ */
450+ DYNAMIC_ARRAY stored_results;
451+ /*
452+ Indicates if position has been called since last_result was set, used to
453+ determine if last_result needs pushed into stored_results when
454+ set_last_result is called.
455+ */
456 bool position_called;
457 uint fetch_num; // stores the fetch num
458 MYSQL_ROW_OFFSET current_position; // Current position used by ::position()
459@@ -258,8 +266,9 @@
460 bool get_error_message(int error, String *buf);
461
462 MYSQL_RES *store_result(MYSQL *mysql);
463- void free_result();
464-
465+ void set_last_result(MYSQL_RES *result);
466+ MYSQL_RES *get_last_result() const{ return last_result; }
467+
468 int external_lock(THD *thd, int lock_type);
469 int connection_commit();
470 int connection_rollback();

Subscribers

People subscribed via source and target branches