From b66690e6eb878d2594940631100a9b4ccea12764 Mon Sep 17 00:00:00 2001 From: bsrikanth-mariadb Date: Wed, 9 Sep 2026 10:43:47 +0530 Subject: [PATCH] MDEV-36356 Server crash in Item::save_int_in_field with Window functions A tail (ORDER BY / LIMIT / locking clause) that follows a parenthesized query expression is parsed while the select inside the parentheses is the current one, so everything the tail registers - its window functions, the window specifications they introduce and the units of its subqueries - ends up registered in that inner select. When the parenthesized query expression already has a tail of its own, LEX::add_tail_to_query_expression_body_ext_parens() wraps it into a derived table and attaches the new tail to the wrapping select. The registrations were left behind in the inner select, so the ORDER BY of the wrapping select contained window function items that no select had registered: they never got a Window_funcs_sort, were never computed, and their result field was read unset. The subqueries of the tail kept the inner select as their master and name resolution context. Move these registrations to the wrapping select in the new move_tail_registrations(): - The window functions of the tail are the ones its ORDER BY items contain, looked up with Item::walk() and walk_subquery == FALSE so that a window function belonging to a subquery of the tail is not found and stays registered where it was parsed. Each one takes along the window specification it introduced; "OVER win_name" has none of its own, it is looked up by name at fix_fields() time and never reaches window_specs. - Item::walk() does not descend into a window specification, so the items of a moved specification's PARTITION BY and ORDER BY lists are not re-targeted by Lex_order_limit_lock::set_to() and are pointed at the wrapping select's name resolution context here. - The subquery units of the tail are the ones registered in front of the first unit the inner select had when the tail started to be parsed; the grammar remembers it in a mid-rule action. There is no equivalent of the walk above for them: a unit records neither the clause it came from nor its position in the parsed text. They are excluded from the inner select, re-registered in the wrapping select and their Item_subselect::parent_select is updated. Per-select parse state that is not a registration (n_sum_items, with_sum_func, with_rownum, ftfunc_list, uncacheable, and the select_n_where_fields of the tail's own fields) is deliberately left behind, where it is merely over-counted. Also remove the unreachable !unit check - unit is dereferenced on entry - and fix the indentation of the surrounding block. --- mysql-test/main/win_orderby.result | 153 +++++++++++++++++++++++++++++ mysql-test/main/win_orderby.test | 81 +++++++++++++++ sql/sql_lex.cc | 122 +++++++++++++++++++++-- sql/sql_lex.h | 3 +- sql/sql_yacc.yy | 15 ++- 5 files changed, 364 insertions(+), 10 deletions(-) diff --git a/mysql-test/main/win_orderby.result b/mysql-test/main/win_orderby.result index 31694f91a5c12..e007f290d11fe 100644 --- a/mysql-test/main/win_orderby.result +++ b/mysql-test/main/win_orderby.result @@ -222,5 +222,158 @@ id select_type table type possible_keys key key_len ref rows Extra NULL UNION RESULT ALL NULL NULL NULL NULL NULL drop table t; # +# MDEV-36356 Server crash in Item::save_int_in_field and UBSAN null-pointer-use +# +(SELECT 1 FROM dual LIMIT 5) ORDER BY ROW_NUMBER() OVER (PARTITION BY 1); +1 +1 +(SELECT 1 FROM dual LIMIT 5) ORDER BY ROW_NUMBER() OVER (); +1 +1 +(SELECT 5 a FROM dual LIMIT 1) ORDER BY AVG(a) OVER (ORDER BY 1); +a +5 +(SELECT CAST('1973' AS DATETIME) a FROM dual LIMIT 1) ORDER BY FIRST_VALUE(a) OVER (ORDER BY 1); +a +NULL +Warnings: +Warning 1292 Incorrect datetime value: '1973' +(SELECT CAST('1973' AS CHAR) a FROM dual LIMIT 1) ORDER BY FIRST_VALUE(a) OVER (ORDER BY 1); +a +1973 +SET @save_sql_mode= @@sql_mode; +SET sql_mode=''; +(SELECT CAST('' AS CHAR) a LIMIT 1) ORDER BY FIRST_VALUE(a) OVER (ORDER BY 1); +a + +SET sql_mode= @save_sql_mode; +((SELECT 1) FOR UPDATE WAIT + 1) ORDER BY PERCENTILE_DISC(COUNT(*)) WITHIN GROUP(ORDER BY 1) OVER(); +1 +1 +((SELECT 1) FOR UPDATE WAIT + 1) ORDER BY PERCENTILE_DISC(COUNT(*)) WITHIN GROUP(ORDER BY CURRENT_TIMESTAMP) OVER(); +1 +1 +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (3),(1),(2); +CREATE TABLE t2 (a INT, b INT); +INSERT INTO t2 VALUES (3,1),(1,2),(2,1); +SELECT * FROM ((SELECT a FROM t1 UNION SELECT 9 LIMIT 5) ORDER BY ROW_NUMBER() OVER ()) x ORDER BY x.a; +a +1 +2 +3 +9 +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY (SELECT MAX(a) FROM t1))) x ORDER BY x.a; +a +1 +2 +3 +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER () + (SELECT MAX(a) FROM t1)) x ORDER BY x.a; +a +1 +2 +3 +# This query used to crash, now it correctly fails with a bad field reference error +(SELECT a FROM t1 LIMIT 5) ORDER BY (SELECT a FROM t2 WHERE t2.a = t1.a LIMIT 1); +ERROR 42S22: Unknown column 't1.a' in 'WHERE' +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER ()) x, ((SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY a)) y ORDER BY x.a, y.a; +a a +1 1 +1 2 +1 3 +2 1 +2 2 +2 3 +3 1 +3 2 +3 3 +# The value of the window function must be the one of the derived table form +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (PARTITION BY a)) x ORDER BY x.a; +a +1 +2 +3 +SELECT * FROM (SELECT * FROM (SELECT a FROM t1 LIMIT 5) dt ORDER BY ROW_NUMBER() OVER (PARTITION BY a)) x ORDER BY x.a; +a +1 +2 +3 +# A query expression nested in the ORDER BY must not hide the window +# function that precedes it: all of these must sort descending +(SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY a) + (SELECT 0 FROM t1 LIMIT 1) DESC; +a +3 +2 +1 +(SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY a) + (SELECT 0 FROM t1 ORDER BY a LIMIT 1) DESC; +a +3 +2 +1 +(SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY a) + (SELECT MAX(d.a)*0 FROM (SELECT a FROM t1 ORDER BY a) d) DESC; +a +3 +2 +1 +(SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY a) + (SELECT 0 FROM t1 ORDER BY a LIMIT 1) + RANK() OVER (ORDER BY a) DESC; +a +3 +2 +1 +# A window function referring to a window defined inside the parentheses +# is not visible from the ORDER BY that follows them +(SELECT a FROM t1 WINDOW w AS (ORDER BY a) LIMIT 5) ORDER BY ROW_NUMBER() OVER w; +ERROR HY000: Window specification with name 'w' is not defined +# A window function of a subquery of the ORDER BY stays in that subquery +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY (SELECT ROW_NUMBER() OVER () FROM t1 LIMIT 1)) x ORDER BY x.a; +a +1 +2 +3 +# Correlated subqueries of the ORDER BY are re-registered in the wrapper +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY EXISTS (SELECT 1 FROM t2 WHERE t2.a=a AND t2.b=a)) x ORDER BY x.a; +a +1 +2 +3 +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY a IN (SELECT b FROM t2 WHERE t2.a=a)) x ORDER BY x.a; +a +1 +2 +3 +DROP TABLE t1, t2; +CREATE TABLE t1 (c1 INT, c2 TEXT); +INSERT INTO t1 (c1, c2) VALUES (0, 'a'); +SELECT * FROM +{ +ta1 t1 AS ta2 NATURAL RIGHT OUTER JOIN +( +( +SELECT * FROM +{ +ta3 t1 AS ta4 NATURAL STRAIGHT_JOIN t1 AS ta5 +} +LIMIT 1234567890 ROWS EXAMINED 1234567890 LOCK IN SHARE MODE SKIP LOCKED +) +ORDER BY FALSE <=> + INTERVAL NOT FALSE=FALSE IN +( +SELECT FALSE <=> FALSE IN (SELECT 'string') +) +SECOND_MICROSECOND + TRUE <=> TRUE IN (SELECT 'string') << ROW_NUMBER () OVER +( +PARTITION BY NOT TRUE <=> FALSE IN (SELECT 'string') DESC +) +IN (SELECT 'string') +)=ta6 +NATURAL JOIN t1 AS ta7 +}; +c1 c2 +0 a +Warnings: +Warning 1292 Truncated incorrect DECIMAL value: 'string' +Warning 1292 Truncated incorrect DECIMAL value: 'string' +Warning 1292 Incorrect datetime value: '0000-00-00 00:00:00' +DROP TABLE t1; +# # End of 10.11 tests # diff --git a/mysql-test/main/win_orderby.test b/mysql-test/main/win_orderby.test index 2700271d47ed5..90b6c512982bf 100644 --- a/mysql-test/main/win_orderby.test +++ b/mysql-test/main/win_orderby.test @@ -175,6 +175,87 @@ explain update t set c=1 where exists ( from t order by x=avg(row_number() over t) over (order by 1)); drop table t; +--echo # +--echo # MDEV-36356 Server crash in Item::save_int_in_field and UBSAN null-pointer-use +--echo # +(SELECT 1 FROM dual LIMIT 5) ORDER BY ROW_NUMBER() OVER (PARTITION BY 1); +(SELECT 1 FROM dual LIMIT 5) ORDER BY ROW_NUMBER() OVER (); +(SELECT 5 a FROM dual LIMIT 1) ORDER BY AVG(a) OVER (ORDER BY 1); +(SELECT CAST('1973' AS DATETIME) a FROM dual LIMIT 1) ORDER BY FIRST_VALUE(a) OVER (ORDER BY 1); +(SELECT CAST('1973' AS CHAR) a FROM dual LIMIT 1) ORDER BY FIRST_VALUE(a) OVER (ORDER BY 1); +SET @save_sql_mode= @@sql_mode; +SET sql_mode=''; +(SELECT CAST('' AS CHAR) a LIMIT 1) ORDER BY FIRST_VALUE(a) OVER (ORDER BY 1); +SET sql_mode= @save_sql_mode; +((SELECT 1) FOR UPDATE WAIT + 1) ORDER BY PERCENTILE_DISC(COUNT(*)) WITHIN GROUP(ORDER BY 1) OVER(); +((SELECT 1) FOR UPDATE WAIT + 1) ORDER BY PERCENTILE_DISC(COUNT(*)) WITHIN GROUP(ORDER BY CURRENT_TIMESTAMP) OVER(); + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (3),(1),(2); +CREATE TABLE t2 (a INT, b INT); +INSERT INTO t2 VALUES (3,1),(1,2),(2,1); + +SELECT * FROM ((SELECT a FROM t1 UNION SELECT 9 LIMIT 5) ORDER BY ROW_NUMBER() OVER ()) x ORDER BY x.a; +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY (SELECT MAX(a) FROM t1))) x ORDER BY x.a; +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER () + (SELECT MAX(a) FROM t1)) x ORDER BY x.a; + +--echo # This query used to crash, now it correctly fails with a bad field reference error +--error ER_BAD_FIELD_ERROR +(SELECT a FROM t1 LIMIT 5) ORDER BY (SELECT a FROM t2 WHERE t2.a = t1.a LIMIT 1); +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER ()) x, ((SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY a)) y ORDER BY x.a, y.a; + +--echo # The value of the window function must be the one of the derived table form +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (PARTITION BY a)) x ORDER BY x.a; +SELECT * FROM (SELECT * FROM (SELECT a FROM t1 LIMIT 5) dt ORDER BY ROW_NUMBER() OVER (PARTITION BY a)) x ORDER BY x.a; + +--echo # A query expression nested in the ORDER BY must not hide the window +--echo # function that precedes it: all of these must sort descending +(SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY a) + (SELECT 0 FROM t1 LIMIT 1) DESC; +(SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY a) + (SELECT 0 FROM t1 ORDER BY a LIMIT 1) DESC; +(SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY a) + (SELECT MAX(d.a)*0 FROM (SELECT a FROM t1 ORDER BY a) d) DESC; +(SELECT a FROM t1 LIMIT 5) ORDER BY ROW_NUMBER() OVER (ORDER BY a) + (SELECT 0 FROM t1 ORDER BY a LIMIT 1) + RANK() OVER (ORDER BY a) DESC; + +--echo # A window function referring to a window defined inside the parentheses +--echo # is not visible from the ORDER BY that follows them +--error ER_WRONG_WINDOW_SPEC_NAME +(SELECT a FROM t1 WINDOW w AS (ORDER BY a) LIMIT 5) ORDER BY ROW_NUMBER() OVER w; + +--echo # A window function of a subquery of the ORDER BY stays in that subquery +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY (SELECT ROW_NUMBER() OVER () FROM t1 LIMIT 1)) x ORDER BY x.a; + +--echo # Correlated subqueries of the ORDER BY are re-registered in the wrapper +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY EXISTS (SELECT 1 FROM t2 WHERE t2.a=a AND t2.b=a)) x ORDER BY x.a; +SELECT * FROM ((SELECT a FROM t1 LIMIT 5) ORDER BY a IN (SELECT b FROM t2 WHERE t2.a=a)) x ORDER BY x.a; + +DROP TABLE t1, t2; + +CREATE TABLE t1 (c1 INT, c2 TEXT); +INSERT INTO t1 (c1, c2) VALUES (0, 'a'); +SELECT * FROM +{ + ta1 t1 AS ta2 NATURAL RIGHT OUTER JOIN + ( + ( + SELECT * FROM + { + ta3 t1 AS ta4 NATURAL STRAIGHT_JOIN t1 AS ta5 + } + LIMIT 1234567890 ROWS EXAMINED 1234567890 LOCK IN SHARE MODE SKIP LOCKED + ) + ORDER BY FALSE <=> + INTERVAL NOT FALSE=FALSE IN + ( + SELECT FALSE <=> FALSE IN (SELECT 'string') + ) + SECOND_MICROSECOND + TRUE <=> TRUE IN (SELECT 'string') << ROW_NUMBER () OVER + ( + PARTITION BY NOT TRUE <=> FALSE IN (SELECT 'string') DESC + ) + IN (SELECT 'string') + )=ta6 + NATURAL JOIN t1 AS ta7 +}; +DROP TABLE t1; + --echo # --echo # End of 10.11 tests --echo # diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index c5ef832284686..e6a985667ae14 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -10686,13 +10686,121 @@ SELECT_LEX_UNIT *LEX::add_tail_to_query_expression_body(SELECT_LEX_UNIT *unit, } +static bool order_list_contains(SQL_I_List *order_list, Item *item) +{ + for (ORDER *ord= order_list->first; ord; ord= ord->next) + { + if ((*ord->item)->walk(&Item::find_item_processor, FALSE, item)) + return true; + } + return false; +} + + +static bool remove_window_spec(SELECT_LEX *sel, Window_spec *win_spec) +{ + List_iterator it(sel->window_specs); + Window_spec *cur; + while ((cur= it++)) + { + if (cur == win_spec) + { + it.remove(); + return true; + } + } + return false; +} + + +/* + Move to another select what the tail of a query expression registered + + The tail (ORDER BY / LIMIT / locking clause) that follows a parenthesized + query expression is parsed while the select inside the parentheses is the + current one, so its window functions, their window specifications and its + subqueries all get registered there. When the query expression is wrapped + into a derived table the tail belongs to the wrapping select, and these + registrations have to follow it. + + The tail's window functions are the ones its ORDER BY expressions contain: + they are looked up with Item::walk() and walk_subquery == FALSE, so a window + function that belongs to a subquery of the tail is not found and stays + registered in the select it was parsed in. Each of them takes along the + window specification it introduced; "OVER win_name" has none of its own, it + is looked up by name at fix_fields() time and never reaches window_specs. + + The tail's subquery units are the ones registered in front of 'first_unit', + the first unit 'from' had when the tail started to be parsed. There is no + equivalent of the walk above for them: a unit records neither the clause it + came from nor its position in the parsed text. + + Note that per-select parse state that is not a registration is deliberately + left behind on 'from', where it is merely over-counted: n_sum_items, + with_sum_func, with_rownum, ftfunc_list, uncacheable and the + select_n_where_fields of the tail's own fields. +*/ +static bool move_tail_registrations(THD *thd, SELECT_LEX *from, SELECT_LEX *to, + SQL_I_List *order_list, + SELECT_LEX_UNIT *first_unit) +{ + if (order_list) + { + List_iterator it(from->window_funcs); + Item_window_func *win_func; + while ((win_func= it++)) + { + if (!order_list_contains(order_list, win_func)) + continue; + it.remove(); + if (to->window_funcs.push_back(win_func, thd->mem_root)) + return true; + to->fields_in_window_functions+= + win_func->window_func()->argument_count(); + + Window_spec *win_spec= win_func->window_spec; + if (!win_spec || !remove_window_spec(from, win_spec)) + continue; + if (to->window_specs.push_back(win_spec, thd->mem_root)) + return true; + to->fields_in_window_functions+= win_spec->partition_list->elements + + win_spec->order_list->elements; + /* + Item::walk() does not descend into a window specification, so the + items of these lists are not re-targeted by + Lex_order_limit_lock::set_to(). + */ + for (ORDER *o= win_spec->partition_list->first; o; o= o->next) + (*o->item)->walk(&Item::change_context_processor, FALSE, &to->context); + for (ORDER *o= win_spec->order_list->first; o; o= o->next) + (*o->item)->walk(&Item::change_context_processor, FALSE, &to->context); + } + } + + st_select_lex_unit *next_unit; + for (st_select_lex_unit *unit= from->first_inner_unit(); + unit != first_unit; unit= next_unit) + { + next_unit= unit->next_unit(); + unit->exclude_from_tree(); + unit->cut_next(); // so add_statistics() stops here + to->add_statistics(unit); + to->register_unit(unit, &to->context); + if (unit->item) + unit->item->parent_select= to; + } + return false; +} + + /** Add non-empty tail to a parenthesized query primary */ SELECT_LEX_UNIT * LEX::add_tail_to_query_expression_body_ext_parens(SELECT_LEX_UNIT *unit, - Lex_order_limit_lock *l) + Lex_order_limit_lock *l, + SELECT_LEX_UNIT *first_unit) { SELECT_LEX *sel= unit->first_select()->next_select() ? unit->fake_select_lex : unit->first_select(); @@ -10706,14 +10814,16 @@ LEX::add_tail_to_query_expression_body_ext_parens(SELECT_LEX_UNIT *unit, l->order_list= &sel->order_list; else { - if (!unit) - return NULL; + SELECT_LEX *inner_sel= sel; sel= wrap_unit_into_derived(unit); if (!sel) return NULL; - if (!create_unit(sel)) - return NULL; - } + if (!create_unit(sel)) + return NULL; + if (move_tail_registrations(thd, inner_sel, sel, l->order_list, + first_unit)) + return NULL; + } } l->set_to(sel); return sel->master_unit(); diff --git a/sql/sql_lex.h b/sql/sql_lex.h index 0eea5ae771245..b1c8e5dc0daed 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -4826,7 +4826,8 @@ struct LEX: public Query_tables_list Lex_order_limit_lock *l); SELECT_LEX_UNIT * add_tail_to_query_expression_body_ext_parens(SELECT_LEX_UNIT *unit, - Lex_order_limit_lock *l); + Lex_order_limit_lock *l, + SELECT_LEX_UNIT *first_unit); SELECT_LEX_UNIT *parsed_body_ext_parens_primary(SELECT_LEX_UNIT *unit, SELECT_LEX *primary, enum sub_select_type unit_type, diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index eabd28ba35e63..d72ff9b028855 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -8742,12 +8742,21 @@ query_expression_body_ext: } | query_expression_body_ext_parens { - Lex->push_select(!$1->first_select()->next_select() ? - $1->first_select() : $1->fake_select_lex); + SELECT_LEX *sel= !$1->first_select()->next_select() ? + $1->first_select() : $1->fake_select_lex; + /* + Remember the subqueries this select already has: the ones the + tail registers in front of them have to follow it when the + query expression is wrapped into a derived table. See + LEX::add_tail_to_query_expression_body_ext_parens(). + */ + $$= sel->first_inner_unit(); + Lex->push_select(sel); } query_expression_tail { - if (!($$= Lex->add_tail_to_query_expression_body_ext_parens($1, $3))) + if (!($$= Lex->add_tail_to_query_expression_body_ext_parens( + $1, $3, $2))) MYSQL_YYABORT; } ;