From 10d2dfa2101a2609007f29f4f837fafc3a221906 Mon Sep 17 00:00:00 2001 From: Abhishek Bansal Date: Tue, 3 Mar 2026 11:55:25 +0530 Subject: [PATCH 1/2] MDEV-38474: ASAN heap-use-after-free in st_select_lex_unit::cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cleanup_stranded_units() was added at the start of st_select_lex_unit::cleanup() by 34a8209d6657. This causes a use-after-free when nested subqueries are merged into their parent unit. With nested subqueries like: SELECT * FROM t1 WHERE a IN (SELECT b FROM t2 WHERE a IN (SELECT c FROM t3 WHERE FALSE HAVING c < 0)); the stranded_clean_list chains the units as: Unit1 -> Unit2 -> Unit3. Because cleanup_stranded_units() was called first, Unit1->cleanup() would recursively trigger Unit2->cleanup(), which in turn would trigger Unit3->cleanup(). Unit3's cleanup frees its heap-allocated join structures. But since Unit3 was merged into Unit2, Unit2 still holds references to Unit3's structures (e.g., st_join_table). When control returns to Unit2 for its own local cleanup, it accesses already-freed memory. Fix: move cleanup_stranded_units() to the end of cleanup(). This way, each unit completes its own local cleanup first—clearing its references to any child structures—before triggering cleanup of its stranded (child) units. This enforces a parent-first cleanup order. --- mysql-test/main/merge.result | 93 +++++++++++++++++++++++++++++++++++ mysql-test/main/merge.test | 95 ++++++++++++++++++++++++++++++++++++ sql/sql_lex.cc | 2 +- sql/sql_union.cc | 8 ++- 4 files changed, 195 insertions(+), 3 deletions(-) diff --git a/mysql-test/main/merge.result b/mysql-test/main/merge.result index 0e9a2cb9896ab..f98c0f4d28472 100644 --- a/mysql-test/main/merge.result +++ b/mysql-test/main/merge.result @@ -3970,3 +3970,96 @@ DROP TABLE t1; # # End of 10.11 tests # +# +# MDEV-30088 Assertion `cond_selectivity <= 1.0' failed in get_range_limit_read_cost +# +CREATE TABLE t1 (a TIMESTAMP, KEY(a)) ENGINE=MRG_MyISAM; +explain SELECT a, COUNT(*) FROM t1 WHERE a >= '2000-01-01 00:00:00' GROUP BY a; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE NULL NULL NULL NULL NULL NULL NULL Impossible WHERE noticed after reading const tables +SELECT a, COUNT(*) FROM t1 WHERE a >= '2000-01-01 00:00:00' GROUP BY a; +a COUNT(*) +DROP TABLE t1; +# +# MDEV-30525: Assertion `ranges > 0' fails in IO_AND_CPU_COST handler::keyread_time +# +CREATE TABLE t1 (a INT, KEY(a)) ENGINE=MyISAM; +CREATE TABLE t2 (a INT, KEY(a)) ENGINE=MyISAM; +CREATE TABLE tm (a INT, KEY(a)) ENGINE=MRG_MyISAM UNION=(t1,t2); +SELECT DISTINCT a FROM tm WHERE a > 50; +a +DROP TABLE tm, t1, t2; +# Testcase 2: +CREATE TABLE t1 (a INT, KEY(a)) ENGINE=MyISAM; +CREATE TABLE t2 (a INT, KEY(a)) ENGINE=MyISAM; +CREATE TABLE tm (a INT, KEY(a)) ENGINE=MERGE UNION = (t1, t2) INSERT_METHOD=FIRST; +ANALYZE TABLE tm PERSISTENT FOR ALL; +Table Op Msg_type Msg_text +test.tm analyze status Engine-independent statistics collected +test.tm analyze note The storage engine for the table doesn't support analyze +SELECT DISTINCT a FROM (SELECT * FROM tm WHERE a iS NOT NULL) AS sq; +a +DROP TABLE tm, t1, t2; +# +# MDEV-30568 Assertion `cond_selectivity <= 1.000000001' failed in get_range_limit_read_cost +# +CREATE TABLE t1 (f INT, KEY(f)) ENGINE=MyISAM; +CREATE TABLE t2 (f INT, KEY(f)) ENGINE=MyISAM; +CREATE TABLE tm (f INT, KEY(f)) ENGINE=MERGE UNION = (t1, t2); +SELECT DISTINCT f FROM tm WHERE f IN (47, 126, 97, 48, 73, 0); +f +DROP TABLE tm, t1, t2; +# +# MDEV-30786 SIGFPE in cost_group_min_max on EXP +# +SET use_stat_tables='preferably'; +CREATE TABLE t1 (a INT,b INT,KEY i1 (a),KEY i2 (b)) ENGINE=MRG_MyISAM; +ANALYZE LOCAL TABLE t1; +Table Op Msg_type Msg_text +test.t1 analyze status Engine-independent statistics collected +test.t1 analyze note The storage engine for the table doesn't support analyze +EXPLAIN SELECT DISTINCT a FROM t1; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 range NULL i1 5 NULL 1 Using index for group-by +drop table t1; +set use_stat_tables=default; +# +# End of 11.0 tests +# +# +# MDEV-29174: UPDATE of view that uses MERGE table +# +CREATE TABLE t1 (a int) ENGINE=MERGE; +CREATE VIEW v1 AS SELECT a FROM t1; +UPDATE v1 SET a=0; +DROP VIEW v1; +DROP TABLE t1; +# End of 11.1 tests +# +# MDEV-38474 Double free or corruption, ASAN heap-use-after-free in st_join_table::cleanup +# +# Test case 1, fails on 10.11+ +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (b INT); +CREATE TABLE t3 (c INT); +# Inserts are optional, fails with and without data +INSERT INTO t1 VALUES (1),(2); +INSERT INTO t2 VALUES (3),(4); +INSERT INTO t3 VALUES (5),(6); +EXPLAIN SELECT * FROM t1 WHERE a IN (SELECT b FROM t2 WHERE a IN ((SELECT c FROM t3 WHERE FALSE HAVING c < 0))); +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY NULL NULL NULL NULL NULL NULL NULL Impossible WHERE noticed after reading const tables +3 MATERIALIZED NULL NULL NULL NULL NULL NULL NULL Impossible WHERE +DROP TABLE t1, t2, t3; +# Test case 2, fails on 11.4 but not on 10.11 +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (b INT); +CREATE TABLE t3 (c INT); +CREATE TABLE t4 (d INT PRIMARY KEY); +SET SQL_SAFE_UPDATES=1; +UPDATE t1 STRAIGHT_JOIN t2 SET a = 89 WHERE 9 IN (SELECT c FROM t3 WHERE c IN (SELECT MAX(d) FROM t4)); +ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column +DROP TABLE t1, t2, t3, t4; +# +# End of 11.4 tests +# diff --git a/mysql-test/main/merge.test b/mysql-test/main/merge.test index e66b3ea1dc44e..16a5a6853a4c7 100644 --- a/mysql-test/main/merge.test +++ b/mysql-test/main/merge.test @@ -2905,3 +2905,98 @@ DROP TABLE t1; --echo # --echo # End of 10.11 tests --echo # + +--echo # +--echo # MDEV-30088 Assertion `cond_selectivity <= 1.0' failed in get_range_limit_read_cost +--echo # + +CREATE TABLE t1 (a TIMESTAMP, KEY(a)) ENGINE=MRG_MyISAM; +explain SELECT a, COUNT(*) FROM t1 WHERE a >= '2000-01-01 00:00:00' GROUP BY a; +SELECT a, COUNT(*) FROM t1 WHERE a >= '2000-01-01 00:00:00' GROUP BY a; +DROP TABLE t1; + +--echo # +--echo # MDEV-30525: Assertion `ranges > 0' fails in IO_AND_CPU_COST handler::keyread_time +--echo # +CREATE TABLE t1 (a INT, KEY(a)) ENGINE=MyISAM; +CREATE TABLE t2 (a INT, KEY(a)) ENGINE=MyISAM; +CREATE TABLE tm (a INT, KEY(a)) ENGINE=MRG_MyISAM UNION=(t1,t2); +SELECT DISTINCT a FROM tm WHERE a > 50; +DROP TABLE tm, t1, t2; + +--echo # Testcase 2: +CREATE TABLE t1 (a INT, KEY(a)) ENGINE=MyISAM; +CREATE TABLE t2 (a INT, KEY(a)) ENGINE=MyISAM; +CREATE TABLE tm (a INT, KEY(a)) ENGINE=MERGE UNION = (t1, t2) INSERT_METHOD=FIRST; +ANALYZE TABLE tm PERSISTENT FOR ALL; +SELECT DISTINCT a FROM (SELECT * FROM tm WHERE a iS NOT NULL) AS sq; +DROP TABLE tm, t1, t2; + +--echo # +--echo # MDEV-30568 Assertion `cond_selectivity <= 1.000000001' failed in get_range_limit_read_cost +--echo # +CREATE TABLE t1 (f INT, KEY(f)) ENGINE=MyISAM; +CREATE TABLE t2 (f INT, KEY(f)) ENGINE=MyISAM; +CREATE TABLE tm (f INT, KEY(f)) ENGINE=MERGE UNION = (t1, t2); +SELECT DISTINCT f FROM tm WHERE f IN (47, 126, 97, 48, 73, 0); +DROP TABLE tm, t1, t2; + +--echo # +--echo # MDEV-30786 SIGFPE in cost_group_min_max on EXP +--echo # +SET use_stat_tables='preferably'; +CREATE TABLE t1 (a INT,b INT,KEY i1 (a),KEY i2 (b)) ENGINE=MRG_MyISAM; +ANALYZE LOCAL TABLE t1; +EXPLAIN SELECT DISTINCT a FROM t1; +drop table t1; +set use_stat_tables=default; + +--echo # +--echo # End of 11.0 tests +--echo # +--echo # +--echo # MDEV-29174: UPDATE of view that uses MERGE table +--echo # + +CREATE TABLE t1 (a int) ENGINE=MERGE; +CREATE VIEW v1 AS SELECT a FROM t1; +UPDATE v1 SET a=0; +DROP VIEW v1; +DROP TABLE t1; + +--echo # End of 11.1 tests + +--echo # +--echo # MDEV-38474 Double free or corruption, ASAN heap-use-after-free in st_join_table::cleanup +--echo # + +--echo # Test case 1, fails on 10.11+ +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (b INT); +CREATE TABLE t3 (c INT); + +--echo # Inserts are optional, fails with and without data +INSERT INTO t1 VALUES (1),(2); +INSERT INTO t2 VALUES (3),(4); +INSERT INTO t3 VALUES (5),(6); + +EXPLAIN SELECT * FROM t1 WHERE a IN (SELECT b FROM t2 WHERE a IN ((SELECT c FROM t3 WHERE FALSE HAVING c < 0))); + +DROP TABLE t1, t2, t3; + +--echo # Test case 2, fails on 11.4 but not on 10.11 +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (b INT); +CREATE TABLE t3 (c INT); +CREATE TABLE t4 (d INT PRIMARY KEY); + +SET SQL_SAFE_UPDATES=1; +--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE +UPDATE t1 STRAIGHT_JOIN t2 SET a = 89 WHERE 9 IN (SELECT c FROM t3 WHERE c IN (SELECT MAX(d) FROM t4)); + +DROP TABLE t1, t2, t3, t4; + +--echo # +--echo # End of 11.4 tests +--echo # + diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index 43615249ecb90..baffd3261e1dc 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -2975,7 +2975,7 @@ void st_select_lex_node::init_query_common() into the front of the stranded_clean_list: before: root -> B -> A after: root -> this -> B -> A - During cleanup, the stranded units are cleaned in FIFO order. + During cleanup, the stranded units are cleaned in LIFO order (parent-first). */ void st_select_lex_unit::remember_my_cleanup() { diff --git a/sql/sql_union.cc b/sql/sql_union.cc index dda95677332c2..0d87c830adf38 100644 --- a/sql/sql_union.cc +++ b/sql/sql_union.cc @@ -2597,8 +2597,6 @@ bool st_select_lex_unit::exec_recursive() bool st_select_lex_unit::cleanup() { - cleanup_stranded_units(); - bool error= 0; DBUG_ENTER("st_select_lex_unit::cleanup"); @@ -2684,6 +2682,12 @@ bool st_select_lex_unit::cleanup() } } + /* + Cleanup stranded units only after this unit has completed its own + cleanup, ensuring a parent-first (LIFO) cleanup order for merged tables. + */ + cleanup_stranded_units(); + DBUG_RETURN(error); } From b8672c7b81fda01476657e646e30e99237b5aa6d Mon Sep 17 00:00:00 2001 From: Dave Gosselin Date: Wed, 1 Apr 2026 09:55:11 -0400 Subject: [PATCH 2/2] MDEV-39209: use iterative cleanup for merged units to avoid stack overflow Query optimization can merge derived tables (VIEWs being a type of derived table) into outer queries, leaving behind stranded st_select_lex_unit objects ("stranded units") for post-query cleanup. Previously, these were cleaned up recursively. For queries with many merged derived tables, the deep recursion over the list of stranded units could exhaust the stack. This change replaces the recursive cleanup with an iterative loop to prevent stack overflows. --- sql/sql_lex.cc | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index baffd3261e1dc..670c719555ae0 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -2994,11 +2994,16 @@ void st_select_lex_unit::remember_my_cleanup() void st_select_lex_unit::cleanup_stranded_units() { - if (!stranded_clean_list) - return; - - stranded_clean_list->cleanup(); + st_select_lex_unit *cur= stranded_clean_list; stranded_clean_list= nullptr; + + while (cur) + { + st_select_lex_unit *next= cur->stranded_clean_list; + cur->stranded_clean_list= nullptr; + cur->cleanup(); + cur= next; + } }