From 45c4e94dacbc39e4c6fbb064879441a7cedb8544 Mon Sep 17 00:00:00 2001 From: Rex Johnston Date: Fri, 23 Jan 2026 07:22:03 +1300 Subject: [PATCH] MDEV-31632 Unresolvable outer reference causes null pointer exception MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SELECT 1 union select 2 UNION SELECT 1 from a JOIN a b ON (SELECT 1 FROM dual WHERE AAA) Crashes during fix_outer_field while resolving field item AAA In our resolver, once we have determined that a field item isn't local to our select, we call Item::fix_outer_field(), which iterates outwards towards the top level select, looking for where our Item_field might be resolvable. In our example here, the item isn't resolvable and we expose fragility in the loop, which i will detail here. After we initialize the variable 'outer_context' (to a context containing /* select#3 */ select 1 AS `1` from (a join a b on ((subquery#4))) ) we enter a loop │ 5927 for (; │ 5928 outer_context; │ 5929 outer_context= outer_context->outer_context) │ 5930 { │ 5931 select= outer_context->select_lex; │ 5932 Item_subselect *prev_subselect_item= │ 5933 last_checked_context->select_lex->master_unit()->item; │ 5934 last_checked_context= outer_context; here 'last_checked_context' is the context inner to the current 'outer_context', and we initialize prev_subselect_item to the Item enclosing the unit containing this inner select. So for the first iteration of the loop, select: select #3 last_checked_context: from select #4 to select #3. prev_subselect_item: item enclosing select #4 (where field item AAA is defined) The rest of the loop calls find_field_in_tables() / resolve_ref_in_select_and_group() in an attempt to resolve this item with this 'outer_context'. After the item fails resolution, we move to an outer context select: select #4294967295 (fake_select_lex) last_checked_context: from select #3 to the fake select lex containing the union (i.e. outermost) prev_subselect_item: null, there is no Item that contains this, it is the outermost select. We still need to execute the rest of the loop to determine whether AAA is resolvable here, but executing │ 5937 place= prev_subselect_item->parsing_place; We are now following a null pointer. We introduce a test for this null pointer, indicating that we are now evaluating the outermost select and we are not to try accessing the enclosing subselect item. Approved by: Oleksandr "Sanja" Byelkin (sanja@mariadb.com) --- mysql-test/main/subselect4.result | 6 ++++++ mysql-test/main/subselect4.test | 9 +++++++++ sql/item.cc | 30 +++++++++++++++++++++++------- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/mysql-test/main/subselect4.result b/mysql-test/main/subselect4.result index 696ba7de59279..b347101e25f09 100644 --- a/mysql-test/main/subselect4.result +++ b/mysql-test/main/subselect4.result @@ -3372,6 +3372,12 @@ drop table t1,t2,t3; CREATE TABLE t(c INT); SELECT (SELECT 0 GROUP BY c HAVING (SELECT c)) FROM t GROUP BY c; (SELECT 0 GROUP BY c HAVING (SELECT c)) +# +# MDEV-31632 Unresolvable outer reference causes null pointer exception +# +SELECT 1 union select 2 UNION SELECT 1 from t JOIN t b ON +(SELECT 1 FROM dual WHERE AAA); +ERROR 42S22: Unknown column 'AAA' in 'WHERE' DROP TABLE t; # # MDEV-38476 Wrong Result (Empty Set) with derived_merge=on using AVG() on text column in HAVING clause diff --git a/mysql-test/main/subselect4.test b/mysql-test/main/subselect4.test index 303d39c93f344..1ccfb2bbdab4a 100644 --- a/mysql-test/main/subselect4.test +++ b/mysql-test/main/subselect4.test @@ -2693,6 +2693,15 @@ drop table t1,t2,t3; CREATE TABLE t(c INT); SELECT (SELECT 0 GROUP BY c HAVING (SELECT c)) FROM t GROUP BY c; + +--echo # +--echo # MDEV-31632 Unresolvable outer reference causes null pointer exception +--echo # + +--error ER_BAD_FIELD_ERROR +SELECT 1 union select 2 UNION SELECT 1 from t JOIN t b ON + (SELECT 1 FROM dual WHERE AAA); + DROP TABLE t; --echo # diff --git a/sql/item.cc b/sql/item.cc index d745b3cd36f58..a003f9075ec6e 100644 --- a/sql/item.cc +++ b/sql/item.cc @@ -5920,10 +5920,19 @@ Item_field::fix_outer_field(THD *thd, Field **from_field, Item **reference) select= outer_context->select_lex; Item_subselect *prev_subselect_item= last_checked_context->select_lex->master_unit()->item; + /* + We have merged to the top select and this field is no longer outer, + or we have reached the outermost select without resolution + */ + bool at_top= !prev_subselect_item; last_checked_context= outer_context; upward_lookup= TRUE; - place= prev_subselect_item->parsing_place; + if (!at_top) + place= prev_subselect_item->parsing_place; + else + place= NO_MATTER; + /* If outer_field is set, field was already found by first call to find_field_in_tables(). Only need to find appropriate context. @@ -5978,8 +5987,11 @@ Item_field::fix_outer_field(THD *thd, Field **from_field, Item **reference) } if (*from_field != view_ref_found) { - prev_subselect_item->used_tables_cache|= (*from_field)->table->map; - prev_subselect_item->const_item_cache= 0; + if (!at_top) + { + prev_subselect_item->used_tables_cache|= (*from_field)->table->map; + prev_subselect_item->const_item_cache= 0; + } set_field(*from_field); if (!last_checked_context->select_lex->having_fix_field && select->group_list.elements && @@ -6028,7 +6040,8 @@ Item_field::fix_outer_field(THD *thd, Field **from_field, Item **reference) else { Item::Type ref_type= (*reference)->type(); - prev_subselect_item->used_tables_and_const_cache_join(*reference); + if (!at_top) + prev_subselect_item->used_tables_and_const_cache_join(*reference); mark_as_dependent(thd, last_checked_context->select_lex, context->select_lex, this, ((ref_type == REF_ITEM || ref_type == FIELD_ITEM) ? @@ -6076,7 +6089,7 @@ Item_field::fix_outer_field(THD *thd, Field **from_field, Item **reference) if (refref->fixed()) (*ref)->fix_fields_if_needed( thd, reference); } - if ((*ref)->fixed()) + if (!at_top && (*ref)->fixed()) prev_subselect_item->used_tables_and_const_cache_join(*ref); break; @@ -6088,8 +6101,11 @@ Item_field::fix_outer_field(THD *thd, Field **from_field, Item **reference) outer select (or we just trying to find wrong identifier, in this case it does not matter which used tables bits we set) */ - prev_subselect_item->used_tables_cache|= OUTER_REF_TABLE_BIT; - prev_subselect_item->const_item_cache= 0; + if (!at_top) + { + prev_subselect_item->used_tables_cache|= OUTER_REF_TABLE_BIT; + prev_subselect_item->const_item_cache= 0; + } } DBUG_ASSERT(ref != 0);