Skip to content

Commit 69ac9e4

Browse files
Dag Wanvikdahlerlend
authored andcommitted
Bug#36075756 ASAN crash on MaterializeIterator<Profiler>::load_HF_row_into_hash_map() [1/2 noclose]
To see this error, we needed to run with ASAN compiled in and --sanitize on the repro test case. It didn't trigger an ASAN error as such, but hit an assert. Possibly this happens only with ASAN because space usage increases on the heap due to ASAN padding before and after allocated objects. The error is another instance of the third issue solved in Bug#35686098 Assertion `n < size()' failed in Element_type& Mem_root_array_YY: (quote): "we ran out of space in the dedicated hash table upon re-reading one of the on-disk chunks into the hash table." The fix there was to use the general mem_root instead of the dedicated one for the hash set operation for that particular corner case. Specifically, we used the general mem_root when allocating the hash map *payload*. In this bug, we see the same issue: we can't fit the last row (#10 out of 10 long varchar strings, i.e. blobs in the repro) into the hash table mem_root: this time we run out when we allocate space for the hash map *key*, in check_unique_fields_hash_map prior to calling ImmutableStringWithLength::Encode(<hash key>). The solution is the same, use the general mem_root for this case: i.e. we generalize the solution from Bug#35686098 to cover running out of space for both the key and payload. Change-Id: Id869e6afad7c996950779c008dcf650e21b17027
1 parent d49eb7d commit 69ac9e4

File tree

6 files changed

+212
-34
lines changed

6 files changed

+212
-34
lines changed

mysql-test/include/query_expression_debug.inc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,21 @@ SET SESSION tmp_table_size=default;
173173
SET SESSION set_operations_buffer_size=default;
174174
SET SESSION optimizer_trace="enabled=default";
175175

176+
--echo #
177+
--echo # Bug#36075756 ASAN crash on MaterializeIterator<Profiler>::load_HF_row_into_hash_map()
178+
--echo #
179+
--echo # Simulate overflow during SS_READING_RIGHT_HF
180+
SET SESSION set_operations_buffer_size = 16384;
181+
SET SESSION debug_set_operations_secondary_overflow_at='0 0 100 right_operand';
182+
SET SESSION optimizer_trace="enabled=on";
183+
SELECT * FROM t INTERSECT SELECT * FROM t ORDER BY i LIMIT 1;
184+
let $show_trace=
185+
SELECT JSON_PRETTY(JSON_EXTRACT(trace, '$.steps[*].join_execution.steps[1]."materialize for intersect".steps[0]."de-duplicate with hash table".steps[0]'))
186+
FROM information_schema.optimizer_trace;
187+
--echo # Should show overflow injection:
188+
eval $show_trace;
189+
SET SESSION set_operations_buffer_size=default;
190+
SET SESSION optimizer_trace="enabled=default";
191+
SET SESSION debug_set_operations_secondary_overflow_at=default;
192+
176193
DROP TABLE t;

mysql-test/r/query_expression-bugs.result

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,3 +441,34 @@ EXPLAIN
441441

442442
SET optimizer_switch="hash_set_operations=default";
443443
DROP TABLE t;
444+
#
445+
# Bug#36075756 ASAN crash on
446+
# MaterializeIterator<Profiler>::load_HF_row_into_hash_map()
447+
#
448+
CREATE TABLE t1 (c1 INT, c2 TIME);
449+
INSERT INTO t1 VALUES (4,'09:29:08'), (NULL,'19:11:10'), (2,'11:57:26'),
450+
(6,'00:39:46'), (6,'03:28:15'), (8,'06:44:18'),
451+
(2,'14:36:39'), (6,'18:42:45'), (8,'02:57:29'),
452+
(3,'16:46:13');
453+
CREATE VIEW view_t1 AS SELECT * FROM t1;
454+
# Used to assert with -DWITH_DEBUG=1 -DWITH_ASAN=1 (both required)
455+
# build on Oracle Linux Server 8
456+
SELECT SUBSTRING(t.rep, 1, 4) AS sub, LENGTH(t.rep) AS len
457+
FROM
458+
( SELECT REPEAT(c1, c2) AS rep FROM view_t1
459+
EXCEPT DISTINCT
460+
SELECT DISTINCT RANK() OVER () FROM t1 ) AS t
461+
ORDER BY len;
462+
sub len
463+
NULL NULL
464+
6666 3946
465+
8888 25729
466+
6666 32815
467+
8888 64418
468+
4444 92908
469+
2222 115726
470+
2222 143639
471+
3333 164613
472+
6666 184245
473+
DROP VIEW view_t1;
474+
DROP TABLE t1;

mysql-test/r/query_expression_debug.result

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,28 @@ SET SESSION debug_set_operations_secondary_overflow_at= default;
286286
SET SESSION tmp_table_size=default;
287287
SET SESSION set_operations_buffer_size=default;
288288
SET SESSION optimizer_trace="enabled=default";
289+
#
290+
# Bug#36075756 ASAN crash on MaterializeIterator<Profiler>::load_HF_row_into_hash_map()
291+
#
292+
# Simulate overflow during SS_READING_RIGHT_HF
293+
SET SESSION set_operations_buffer_size = 16384;
294+
SET SESSION debug_set_operations_secondary_overflow_at='0 0 100 right_operand';
295+
SET SESSION optimizer_trace="enabled=on";
296+
SELECT * FROM t INTERSECT SELECT * FROM t ORDER BY i LIMIT 1;
297+
i d c
298+
0 2022-04-30 abracadabra
299+
# Should show overflow injection:
300+
SELECT JSON_PRETTY(JSON_EXTRACT(trace, '$.steps[*].join_execution.steps[1]."materialize for intersect".steps[0]."de-duplicate with hash table".steps[0]'))
301+
FROM information_schema.optimizer_trace;
302+
JSON_PRETTY(JSON_EXTRACT(trace, '$.steps[*].join_execution.steps[1]."materialize for intersect".steps[0]."de-duplicate with hash table".steps[0]'))
303+
[
304+
{
305+
"injected overflow: SS_READING_RIGHT_HF": {}
306+
}
307+
]
308+
SET SESSION set_operations_buffer_size=default;
309+
SET SESSION optimizer_trace="enabled=default";
310+
SET SESSION debug_set_operations_secondary_overflow_at=default;
289311
DROP TABLE t;
290312
#
291313
# Debug build companion of query_expression.inc
@@ -573,4 +595,26 @@ SET SESSION debug_set_operations_secondary_overflow_at= default;
573595
SET SESSION tmp_table_size=default;
574596
SET SESSION set_operations_buffer_size=default;
575597
SET SESSION optimizer_trace="enabled=default";
598+
#
599+
# Bug#36075756 ASAN crash on MaterializeIterator<Profiler>::load_HF_row_into_hash_map()
600+
#
601+
# Simulate overflow during SS_READING_RIGHT_HF
602+
SET SESSION set_operations_buffer_size = 16384;
603+
SET SESSION debug_set_operations_secondary_overflow_at='0 0 100 right_operand';
604+
SET SESSION optimizer_trace="enabled=on";
605+
SELECT * FROM t INTERSECT SELECT * FROM t ORDER BY i LIMIT 1;
606+
i d c
607+
0 2022-04-30 abracadabra
608+
# Should show overflow injection:
609+
SELECT JSON_PRETTY(JSON_EXTRACT(trace, '$.steps[*].join_execution.steps[1]."materialize for intersect".steps[0]."de-duplicate with hash table".steps[0]'))
610+
FROM information_schema.optimizer_trace;
611+
JSON_PRETTY(JSON_EXTRACT(trace, '$.steps[*].join_execution.steps[1]."materialize for intersect".steps[0]."de-duplicate with hash table".steps[0]'))
612+
[
613+
{
614+
"injected overflow: SS_READING_RIGHT_HF": {}
615+
}
616+
]
617+
SET SESSION set_operations_buffer_size=default;
618+
SET SESSION optimizer_trace="enabled=default";
619+
SET SESSION debug_set_operations_secondary_overflow_at=default;
576620
DROP TABLE t;

mysql-test/t/query_expression-bugs.test

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,27 @@ eval EXPLAIN ANALYZE $query;
194194
SET optimizer_switch="hash_set_operations=default";
195195

196196
DROP TABLE t;
197+
198+
--echo #
199+
--echo # Bug#36075756 ASAN crash on
200+
--echo # MaterializeIterator<Profiler>::load_HF_row_into_hash_map()
201+
--echo #
202+
CREATE TABLE t1 (c1 INT, c2 TIME);
203+
204+
INSERT INTO t1 VALUES (4,'09:29:08'), (NULL,'19:11:10'), (2,'11:57:26'),
205+
(6,'00:39:46'), (6,'03:28:15'), (8,'06:44:18'),
206+
(2,'14:36:39'), (6,'18:42:45'), (8,'02:57:29'),
207+
(3,'16:46:13');
208+
CREATE VIEW view_t1 AS SELECT * FROM t1;
209+
210+
--echo # Used to assert with -DWITH_DEBUG=1 -DWITH_ASAN=1 (both required)
211+
--echo # build on Oracle Linux Server 8
212+
SELECT SUBSTRING(t.rep, 1, 4) AS sub, LENGTH(t.rep) AS len
213+
FROM
214+
( SELECT REPEAT(c1, c2) AS rep FROM view_t1
215+
EXCEPT DISTINCT
216+
SELECT DISTINCT RANK() OVER () FROM t1 ) AS t
217+
ORDER BY len;
218+
219+
DROP VIEW view_t1;
220+
DROP TABLE t1;

sql/iterators/composite_iterators.cc

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -870,12 +870,15 @@ class SpillState {
870870
bool spill() { return m_spill_read_state != ReadingState::SS_NONE; }
871871

872872
#ifndef NDEBUG
873-
bool simulated_secondary_overflow(bool *spill);
873+
/// If left operand overflow simulation, spill will get set, if right
874+
/// operand overflow simulation, \c rop_overflow will get set.
875+
bool simulated_secondary_overflow(bool *spill, bool *rop_overflow);
874876

875877
private:
876878
size_t m_simulated_set_idx{std::numeric_limits<size_t>::max()};
877879
size_t m_simulated_chunk_idx{std::numeric_limits<size_t>::max()};
878880
size_t m_simulated_row_no{std::numeric_limits<size_t>::max()};
881+
bool m_right_hf{false};
879882

880883
public:
881884
#endif
@@ -1430,8 +1433,7 @@ class MaterializeIterator final : public TableRowIterator {
14301433
bool *spill);
14311434
void backup_or_restore_blob_pointers(bool backup);
14321435
void update_row_in_hash_map();
1433-
enum Operand_type { LEFT_OPERAND, RIGHT_OPERAND };
1434-
bool store_row_in_hash_map(Operand_type type = LEFT_OPERAND);
1436+
bool store_row_in_hash_map();
14351437
bool handle_hash_map_full(const Operand &operand, ha_rows *stored_rows);
14361438
bool process_row(const Operand &operand, Operands &operands, TABLE *t,
14371439
uchar *set_counter_0, uchar *set_counter_1, bool *read_next);
@@ -1959,7 +1961,7 @@ bool MaterializeIterator<Profiler>::load_HF_row_into_hash_map() {
19591961
return true;
19601962
}
19611963

1962-
spill = store_row_in_hash_map(RIGHT_OPERAND);
1964+
spill = store_row_in_hash_map();
19631965
if (spill) {
19641966
// It fit before, should fit now
19651967
assert(false);
@@ -2031,19 +2033,18 @@ bool MaterializeIterator<Profiler>::check_unique_fields_hash_map(TABLE *t,
20312033
bool *found,
20322034
bool *spill) {
20332035
const size_t max_mem_available = thd()->variables.set_operations_buffer_size;
2034-
20352036
*spill = false;
20362037

20372038
#ifndef NDEBUG
2039+
bool right_op_overflow = false;
20382040
if (m_spill_state.spill()) {
2039-
if (write && // Only inject error for left operand: can only happen there
2040-
m_spill_state.read_state() ==
2041-
SpillState::ReadingState::SS_READING_LEFT_IF &&
2042-
m_spill_state.simulated_secondary_overflow(spill))
2041+
if (write &&
2042+
m_spill_state.simulated_secondary_overflow(spill, &right_op_overflow))
20432043
return true;
20442044
if (*spill) return false;
20452045
}
20462046
#endif
2047+
20472048
if (m_mem_root == nullptr) {
20482049
assert(write);
20492050
m_mem_root = make_unique_destroy_only<MEM_ROOT>(
@@ -2086,17 +2087,52 @@ bool MaterializeIterator<Profiler>::check_unique_fields_hash_map(TABLE *t,
20862087
m_mem_root->ForceNewBlock(required_key_bytes);
20872088
block = m_mem_root->Peek();
20882089
}
2090+
20892091
size_t bytes_to_commit = 0;
2090-
if (static_cast<size_t>(block.second - block.first) >= required_key_bytes) {
2092+
2093+
if (static_cast<size_t>(block.second - block.first) >= required_key_bytes
2094+
#ifndef NDEBUG
2095+
&& !right_op_overflow
2096+
#endif
2097+
) {
2098+
// Normal case, we have enough space
20912099
char *ptr = block.first;
20922100
secondary_hash_key = ImmutableStringWithLength::Encode(
20932101
pointer_cast<const char *>(&primary_hash), sizeof(primary_hash), &ptr);
20942102
assert(ptr < block.second);
20952103
bytes_to_commit = ptr - block.first;
20962104
} else if (write) {
2097-
// spill to disk
2098-
*spill = true;
2099-
return false;
2105+
// out of space in dedicated mem_root
2106+
if (m_spill_state.read_state() ==
2107+
SpillState::ReadingState::SS_READING_RIGHT_HF) {
2108+
// When re-reading chunk rows, different row order may cause heap
2109+
// fragmentation to differ so even though chunk fit in m_mem_root the
2110+
// first time (as left operand chunk), it may fail to fit when we
2111+
// re-read. This should typically only affect the last (few) rows in a
2112+
// chunk, so use m_overflow_mem_root.
2113+
char *ptr =
2114+
static_cast<char *>(m_overflow_mem_root->Alloc(required_key_bytes));
2115+
if (ptr == nullptr) {
2116+
my_error(ER_OUTOFMEMORY, MYF(ME_FATALERROR),
2117+
thd()->variables.set_operations_buffer_size);
2118+
return true;
2119+
}
2120+
secondary_hash_key = ImmutableStringWithLength::Encode(
2121+
pointer_cast<const char *>(&primary_hash), sizeof(primary_hash),
2122+
&ptr);
2123+
#ifndef NDEBUG
2124+
if (right_op_overflow) {
2125+
Opt_trace_context *trace = &thd()->opt_trace;
2126+
Opt_trace_object trace_wrapper(trace);
2127+
Opt_trace_object trace_exec(trace,
2128+
"injected overflow: SS_READING_RIGHT_HF");
2129+
}
2130+
#endif
2131+
} else {
2132+
// spill to disk
2133+
*spill = true;
2134+
return false;
2135+
}
21002136
}
21012137

21022138
*found = false;
@@ -2228,12 +2264,10 @@ bool MaterializeIterator<Profiler>::handle_hash_map_full(const Operand &operand,
22282264
positioned on it. Links any existing entry behind it, i.e. we insert at
22292265
front of the hash bucket, cf. StoreLinkedImmutableStringFromTableBuffers.
22302266
Update \c m_rows_in_hash_map.
2231-
@param type indicates whether we are processing the left operand or one of the
2232-
right operands in the set operation
22332267
@returns true on error
22342268
*/
22352269
template <typename Profiler>
2236-
bool MaterializeIterator<Profiler>::store_row_in_hash_map(Operand_type type) {
2270+
bool MaterializeIterator<Profiler>::store_row_in_hash_map() {
22372271
// Save the contents of all columns and make the hash map iterator's value
22382272
// field ("->second") point to it.
22392273
bool dummy = false;
@@ -2250,12 +2284,14 @@ bool MaterializeIterator<Profiler>::store_row_in_hash_map(Operand_type type) {
22502284
// processing the left operand, this would only happen for a minority of
22512285
// chunks and then typically only for the last row.
22522286
assert(m_overflow_mem_root != nullptr);
2287+
const bool is_right_operand = m_spill_state.read_state() ==
2288+
SpillState::ReadingState::SS_READING_RIGHT_HF;
2289+
22532290
LinkedImmutableString last_row_stored =
22542291
StoreLinkedImmutableStringFromTableBuffers(
2255-
m_mem_root.get(),
2256-
(type == RIGHT_OPERAND ? m_overflow_mem_root : nullptr),
2292+
m_mem_root.get(), (is_right_operand ? m_overflow_mem_root : nullptr),
22572293
m_table_collection, m_next_ptr, m_row_size_upper_bound,
2258-
(type == RIGHT_OPERAND ? &dummy : nullptr));
2294+
(is_right_operand ? &dummy : nullptr));
22592295
if (last_row_stored == nullptr) {
22602296
return true;
22612297
}
@@ -3346,7 +3382,11 @@ bool SpillState::write_completed_HFs(THD *thd, const Operands &operands,
33463382
}
33473383

33483384
#ifndef NDEBUG
3349-
bool SpillState::simulated_secondary_overflow(bool *spill) {
3385+
bool SpillState::simulated_secondary_overflow(bool *spill, bool *rop_overflow) {
3386+
if (read_state() != SpillState::ReadingState::SS_READING_LEFT_IF &&
3387+
read_state() != SpillState::ReadingState::SS_READING_RIGHT_HF)
3388+
return false;
3389+
33503390
const char *const common_msg =
33513391
"in debug_set_operations_secondary_overflow_at too high: should be "
33523392
"lower than or equal to:";
@@ -3355,12 +3395,13 @@ bool SpillState::simulated_secondary_overflow(bool *spill) {
33553395
m_simulated_set_idx == std::numeric_limits<size_t>::max()) {
33563396
// Parse out variables with
33573397
// syntax: <set-idx:integer 0-based> <chunk-idx:integer 0-based>
3358-
// <row_no:integer 1-based>
3359-
int tokens [[maybe_unused]] =
3360-
sscanf(m_thd->variables.debug_set_operations_secondary_overflow_at,
3361-
"%zu %zu %zu", &m_simulated_set_idx, &m_simulated_chunk_idx,
3362-
&m_simulated_row_no);
3363-
if (tokens != 3 || m_simulated_row_no < 1 ||
3398+
// <row_no:integer 1-based> [ right_operand ]
3399+
int tokens = 0;
3400+
char buff[31];
3401+
tokens = sscanf(m_thd->variables.debug_set_operations_secondary_overflow_at,
3402+
"%zu %zu %zu %30s", &m_simulated_set_idx,
3403+
&m_simulated_chunk_idx, &m_simulated_row_no, buff);
3404+
if (tokens < 3 || m_simulated_row_no < 1 ||
33643405
m_simulated_chunk_idx >= m_chunk_files.size()) {
33653406
my_error(ER_SIMULATED_INJECTION_ERROR, MYF(0), "Chunk number", common_msg,
33663407
m_chunk_files.size() - 1);
@@ -3372,23 +3413,40 @@ bool SpillState::simulated_secondary_overflow(bool *spill) {
33723413
m_no_of_chunk_file_sets - 1);
33733414
return true;
33743415
}
3416+
m_right_hf = tokens == 4;
3417+
if (m_right_hf && read_state() == ReadingState::SS_READING_LEFT_IF)
3418+
return false;
33753419
}
33763420

33773421
if (m_simulated_set_idx <
33783422
std::numeric_limits<size_t>::max()) { // initialized
33793423
if (m_current_chunk_file_set == m_simulated_set_idx &&
33803424
m_current_chunk_idx == m_simulated_chunk_idx) {
3381-
if (m_simulated_row_no >
3382-
m_row_counts[m_current_chunk_idx][m_current_chunk_file_set]
3383-
.IF_count) {
3384-
my_error(ER_SIMULATED_INJECTION_ERROR, MYF(0), "Row number", common_msg,
3385-
m_row_counts[m_current_chunk_idx][m_current_chunk_file_set]
3386-
.IF_count);
3387-
return true;
3425+
const size_t chunk_rows =
3426+
(!m_right_hf
3427+
? m_row_counts[m_current_chunk_idx][m_current_chunk_file_set]
3428+
.IF_count
3429+
:
3430+
3431+
m_row_counts[m_current_chunk_idx][m_current_chunk_file_set]
3432+
.HF_count);
3433+
if ((m_right_hf &&
3434+
read_state() == SpillState::ReadingState::SS_READING_RIGHT_HF) ||
3435+
!m_right_hf) {
3436+
if (m_simulated_row_no > chunk_rows) {
3437+
my_error(ER_SIMULATED_INJECTION_ERROR, MYF(0), "Row number",
3438+
common_msg, chunk_rows);
3439+
return true;
3440+
}
33883441
}
33893442

33903443
if (m_current_row_in_chunk == static_cast<size_t>(m_simulated_row_no)) {
3391-
*spill = true;
3444+
if (!m_right_hf) {
3445+
*spill = true;
3446+
} else if (read_state() ==
3447+
SpillState::ReadingState::SS_READING_RIGHT_HF) {
3448+
*rop_overflow = true;
3449+
}
33923450
}
33933451
}
33943452
}

sql/sys_vars.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7455,10 +7455,14 @@ static Sys_var_ulonglong Sys_set_operations_buffer_size(
74557455
// a) set index, cf. explanation in comments for class SpillState
74567456
// b) chunk index
74577457
// c) row number
7458+
// d) optional string: "right_operand"
74587459
// Syntax: <set-idx:integer 0-based> <chunk-idx:integer 0-based>
74597460
// <row_no:integer 1-based>
74607461
// Example:
74617462
// SET SESSION debug_set_operations_secondary_overflow_at = '1 5 7';
7463+
// SET SESSION debug_set_operations_secondary_overflow_at =
7464+
// '1 5 7 right_operand';
7465+
//
74627466
// If the numbers given are outside range on the high side, they will never
74637467
// trigger any secondary spill.
74647468
static Sys_var_charptr Sys_debug_set_operations_secondary_overflow_at(

0 commit comments

Comments
 (0)