From 05594fc3d638d3d4636cc07a2cf792de738aa6ac Mon Sep 17 00:00:00 2001 From: Abhishek Bansal Date: Tue, 3 Mar 2026 11:55:25 +0530 Subject: [PATCH] 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/suite/merge/merge.result | 28 ++++++++++++++++++++++++ mysql-test/suite/merge/merge.test | 34 +++++++++++++++++++++++++++++ sql/sql_lex.cc | 2 +- sql/sql_union.cc | 7 ++++-- 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/mysql-test/suite/merge/merge.result b/mysql-test/suite/merge/merge.result index 19d948615bb47..2550199a24a5a 100644 --- a/mysql-test/suite/merge/merge.result +++ b/mysql-test/suite/merge/merge.result @@ -4031,3 +4031,31 @@ 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/suite/merge/merge.test b/mysql-test/suite/merge/merge.test index 4b752fcdaffd7..6e95dfe2b4017 100644 --- a/mysql-test/suite/merge/merge.test +++ b/mysql-test/suite/merge/merge.test @@ -2965,3 +2965,37 @@ 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 e3daab28648ba..83d318adc0ab1 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -3004,7 +3004,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 c9eebc7c05e55..21d5fa677f6a7 100644 --- a/sql/sql_union.cc +++ b/sql/sql_union.cc @@ -2688,8 +2688,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"); @@ -2778,6 +2776,11 @@ bool st_select_lex_unit::cleanup() delete pushdown_unit; pushdown_unit= nullptr; + /* + 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); }