From fef56af873866edd95d02ee90ff053ef6e83767a Mon Sep 17 00:00:00 2001 From: mariadb-PranavTiwari Date: Mon, 5 Oct 2026 10:37:17 +0530 Subject: [PATCH] MDEV-39343 Add Grant_tables_intact status variable Add a status variable that reflects whether the grant tables' structure matches what this server expects, recomputed on startup and FLUSH PRIVILEGES. Also widen roles_mapping.Role and proxies_priv.Proxied_host in mysql_system_tables_fix.sql, which were never widened before. --- libmysqld/CMakeLists.txt | 2 +- mysql-test/include/load_dump_and_upgrade.inc | 9 + mysql-test/main/grant_4332.result | 1 + mysql-test/main/grant_4332.test | 4 + mysql-test/main/grant_lowercase.result | 1 + mysql-test/main/grant_lowercase.test | 3 + mysql-test/main/grant_tables_intact.result | 112 ++++++ mysql-test/main/grant_tables_intact.test | 95 +++++ mysql-test/main/mysql_upgrade.result | 12 + sql/CMakeLists.txt | 1 + sql/grant_table_defs.cc | 384 +++++++++++++++++++ sql/grant_table_defs.h | 34 ++ sql/mysqld.cc | 1 + sql/sql_acl.cc | 88 +++++ sql/sql_acl.h | 2 +- 15 files changed, 747 insertions(+), 2 deletions(-) create mode 100644 mysql-test/main/grant_tables_intact.result create mode 100644 mysql-test/main/grant_tables_intact.test create mode 100644 sql/grant_table_defs.cc create mode 100644 sql/grant_table_defs.h diff --git a/libmysqld/CMakeLists.txt b/libmysqld/CMakeLists.txt index 845efbd6759..f392193d09c 100644 --- a/libmysqld/CMakeLists.txt +++ b/libmysqld/CMakeLists.txt @@ -56,7 +56,7 @@ SET(SQL_EMBEDDED_SOURCES emb_qcache.cc libmysqld.c lib_sql.cc ../sql/password.c ../sql/discover.cc ../sql/derror.cc ../sql/field.cc ../sql/field_conv.cc ../sql/field_comp.cc ../sql/filesort_utils.cc ../sql/sql_digest.cc - ../sql/filesort.cc ../sql/grant.cc + ../sql/filesort.cc ../sql/grant.cc ../sql/grant_table_defs.cc ../sql/gstream.cc ../sql/slave.cc ../sql/signal_handler.cc ../sql/handler.cc ../sql/hash_filo.cc ../sql/hostname.cc diff --git a/mysql-test/include/load_dump_and_upgrade.inc b/mysql-test/include/load_dump_and_upgrade.inc index 52351ea473c..5fe01a34d34 100644 --- a/mysql-test/include/load_dump_and_upgrade.inc +++ b/mysql-test/include/load_dump_and_upgrade.inc @@ -11,6 +11,15 @@ # On 10.4 the local root was additionally given an empty password, # to avoid using unix_socket. +# Older dumps have genuinely narrower grant tables than this server expects +# (e.g. 10.4's roles_mapping/proxies_priv columns are shorter than today's), +# and dropping Delete_history_priv further below also shrinks mysql.db by +# one column - FLUSH PRIVILEGES during the upgrade will correctly log both +# message shapes for each, which Table_check_intact/Grant_tables_intact are +# designed to catch, not something gone wrong. +call mtr.add_suppression("is wrong. Expected"); +call mtr.add_suppression("Incorrect definition of table"); + # Backup the existing mysql schema, to restore it later --exec $MYSQL_DUMP mysql > $MYSQL_TMP_DIR/mysql_database_backup diff --git a/mysql-test/main/grant_4332.result b/mysql-test/main/grant_4332.result index 6a8e3d3f279..bf779014d42 100644 --- a/mysql-test/main/grant_4332.result +++ b/mysql-test/main/grant_4332.result @@ -1,6 +1,7 @@ # switching from mysql.global_priv to mysql.user set global sql_mode=""; set local sql_mode=""; +call mtr.add_suppression("Incorrect definition of table"); alter table mysql.user modify User char(16) binary not null default ''; alter table mysql.db modify User char(16) binary not null default ''; alter table mysql.tables_priv modify User char(16) binary not null default ''; diff --git a/mysql-test/main/grant_4332.test b/mysql-test/main/grant_4332.test index 4d79ddaf5ad..f4846b81326 100644 --- a/mysql-test/main/grant_4332.test +++ b/mysql-test/main/grant_4332.test @@ -10,6 +10,10 @@ set global sql_mode=""; set local sql_mode=""; +# The narrowed User columns below make Table_check_intact log a mismatch +# on the FLUSH PRIVILEGES just after - expected, not a new bug. +call mtr.add_suppression("Incorrect definition of table"); + alter table mysql.user modify User char(16) binary not null default ''; alter table mysql.db modify User char(16) binary not null default ''; alter table mysql.tables_priv modify User char(16) binary not null default ''; diff --git a/mysql-test/main/grant_lowercase.result b/mysql-test/main/grant_lowercase.result index 6bea15b11e2..7e27dbda906 100644 --- a/mysql-test/main/grant_lowercase.result +++ b/mysql-test/main/grant_lowercase.result @@ -9,6 +9,7 @@ connection default; disconnect conn1; drop user user1@localhost; call mtr.add_suppression("Incorrect database name"); +call mtr.add_suppression("Incorrect definition of table"); alter table mysql.db modify User char(16) default "", modify Db varchar(200) default ""; insert mysql.db set db=concat('=>', repeat(_utf8 'й', 200)); Warnings: diff --git a/mysql-test/main/grant_lowercase.test b/mysql-test/main/grant_lowercase.test index ee85bb592d3..7e28df23ba6 100644 --- a/mysql-test/main/grant_lowercase.test +++ b/mysql-test/main/grant_lowercase.test @@ -18,6 +18,9 @@ drop user user1@localhost; # in acl_load() call mtr.add_suppression("Incorrect database name"); +# The narrowed columns below also make Table_check_intact log a mismatch +# on the FLUSH PRIVILEGES just after - expected, not a new bug. +call mtr.add_suppression("Incorrect definition of table"); alter table mysql.db modify User char(16) default "", modify Db varchar(200) default ""; insert mysql.db set db=concat('=>', repeat(_utf8 'й', 200)); flush privileges; # shouldn't crash here diff --git a/mysql-test/main/grant_tables_intact.result b/mysql-test/main/grant_tables_intact.result new file mode 100644 index 00000000000..8635a53c51a --- /dev/null +++ b/mysql-test/main/grant_tables_intact.result @@ -0,0 +1,112 @@ +# +# MDEV-39343 Grant_tables_intact status variable +# +# Grant_tables_intact reflects whether mysql.db, mysql.roles_mapping, +# mysql.proxies_priv, mysql.tables_priv, mysql.columns_priv, +# mysql.procs_priv and mysql.global_priv structurally match this +# server's compiled-in column definitions. It is recomputed in +# acl_reload()/grant_reload(), i.e. on every FLUSH PRIVILEGES. +# +# Both message shapes below are expected - they're exactly what +# Table_check_intact logs when it finds the mismatches this test +# deliberately introduces. +call mtr.add_suppression("is wrong. Expected"); +call mtr.add_suppression("Incorrect definition of table"); +# Baseline: a freshly bootstrapped server must report intact. +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact ON +# +# mysql.db has real historical tolerance (min_columns=9 against a +# current 23 columns), so dropping one privilege column still lets +# the table load - it just no longer matches exactly. +# +ALTER TABLE mysql.db DROP COLUMN Delete_history_priv; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact OFF +ALTER TABLE mysql.db ADD COLUMN Delete_history_priv enum('N','Y') COLLATE utf8mb3_general_ci DEFAULT 'N' NOT NULL AFTER Trigger_priv; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact ON +# +# mysql.roles_mapping, mysql.proxies_priv, mysql.tables_priv, +# mysql.columns_priv, mysql.procs_priv and mysql.global_priv all +# have min_columns pinned to their current column count (no +# tolerance at all), so dropping a column would instead trip the +# pre-existing ER_CANNOT_LOAD_FROM_TABLE_V2 check in +# Grant_table_base::set_table() before Grant_tables_intact's own +# logic ever runs (see grant_repair.test for that mechanism). +# A type change on Host, instead, leaves the column count and key +# structure untouched, so the table still loads and our check +# actually gets exercised. +# +# mysql.roles_mapping +ALTER TABLE mysql.roles_mapping MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact OFF +ALTER TABLE mysql.roles_mapping MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact ON +# mysql.proxies_priv +ALTER TABLE mysql.proxies_priv MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact OFF +ALTER TABLE mysql.proxies_priv MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact ON +# mysql.tables_priv +ALTER TABLE mysql.tables_priv MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact OFF +ALTER TABLE mysql.tables_priv MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact ON +# mysql.columns_priv +ALTER TABLE mysql.columns_priv MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact OFF +ALTER TABLE mysql.columns_priv MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact ON +# mysql.procs_priv +ALTER TABLE mysql.procs_priv MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact OFF +ALTER TABLE mysql.procs_priv MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact ON +# mysql.global_priv +ALTER TABLE mysql.global_priv MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact OFF +ALTER TABLE mysql.global_priv MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +Variable_name Value +Grant_tables_intact ON +# End of 10.11 tests diff --git a/mysql-test/main/grant_tables_intact.test b/mysql-test/main/grant_tables_intact.test new file mode 100644 index 00000000000..6a0aed13447 --- /dev/null +++ b/mysql-test/main/grant_tables_intact.test @@ -0,0 +1,95 @@ +--echo # +--echo # MDEV-39343 Grant_tables_intact status variable +--echo # +--echo # Grant_tables_intact reflects whether mysql.db, mysql.roles_mapping, +--echo # mysql.proxies_priv, mysql.tables_priv, mysql.columns_priv, +--echo # mysql.procs_priv and mysql.global_priv structurally match this +--echo # server's compiled-in column definitions. It is recomputed in +--echo # acl_reload()/grant_reload(), i.e. on every FLUSH PRIVILEGES. +--echo # + +source include/not_embedded.inc; + +--echo # Both message shapes below are expected - they're exactly what +--echo # Table_check_intact logs when it finds the mismatches this test +--echo # deliberately introduces. +call mtr.add_suppression("is wrong. Expected"); +call mtr.add_suppression("Incorrect definition of table"); + +--echo # Baseline: a freshly bootstrapped server must report intact. +SHOW STATUS LIKE 'Grant_tables_intact'; + +--echo # +--echo # mysql.db has real historical tolerance (min_columns=9 against a +--echo # current 23 columns), so dropping one privilege column still lets +--echo # the table load - it just no longer matches exactly. +--echo # +ALTER TABLE mysql.db DROP COLUMN Delete_history_priv; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +ALTER TABLE mysql.db ADD COLUMN Delete_history_priv enum('N','Y') COLLATE utf8mb3_general_ci DEFAULT 'N' NOT NULL AFTER Trigger_priv; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; + +--echo # +--echo # mysql.roles_mapping, mysql.proxies_priv, mysql.tables_priv, +--echo # mysql.columns_priv, mysql.procs_priv and mysql.global_priv all +--echo # have min_columns pinned to their current column count (no +--echo # tolerance at all), so dropping a column would instead trip the +--echo # pre-existing ER_CANNOT_LOAD_FROM_TABLE_V2 check in +--echo # Grant_table_base::set_table() before Grant_tables_intact's own +--echo # logic ever runs (see grant_repair.test for that mechanism). +--echo # A type change on Host, instead, leaves the column count and key +--echo # structure untouched, so the table still loads and our check +--echo # actually gets exercised. +--echo # + +--echo # mysql.roles_mapping +ALTER TABLE mysql.roles_mapping MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +ALTER TABLE mysql.roles_mapping MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; + +--echo # mysql.proxies_priv +ALTER TABLE mysql.proxies_priv MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +ALTER TABLE mysql.proxies_priv MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; + +--echo # mysql.tables_priv +ALTER TABLE mysql.tables_priv MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +ALTER TABLE mysql.tables_priv MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; + +--echo # mysql.columns_priv +ALTER TABLE mysql.columns_priv MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +ALTER TABLE mysql.columns_priv MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; + +--echo # mysql.procs_priv +ALTER TABLE mysql.procs_priv MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +ALTER TABLE mysql.procs_priv MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; + +--echo # mysql.global_priv +ALTER TABLE mysql.global_priv MODIFY COLUMN Host char(60) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; +ALTER TABLE mysql.global_priv MODIFY COLUMN Host char(255) binary NOT NULL DEFAULT ''; +FLUSH PRIVILEGES; +SHOW STATUS LIKE 'Grant_tables_intact'; + +--echo # End of 10.11 tests diff --git a/mysql-test/main/mysql_upgrade.result b/mysql-test/main/mysql_upgrade.result index a645c16d432..7d74cc22992 100644 --- a/mysql-test/main/mysql_upgrade.result +++ b/mysql-test/main/mysql_upgrade.result @@ -2522,6 +2522,8 @@ name dl call mtr.add_suppression("InnoDB: Fetch of persistent statistics requested for table `mysql`\\.`gtid_slave_pos` but the required system tables mysql\\.innodb_table_stats and mysql\\.innodb_index_stats are not present or have unexpected structure"); call mtr.add_suppression("InnoDB: Unexpected length of mysql\\.innodb_table_stats\\.table_name"); call mtr.add_suppression("Column count of mysql.proc is wrong. Expected 21, found 20."); +call mtr.add_suppression("is wrong. Expected"); +call mtr.add_suppression("Incorrect definition of table"); # # Upgrade from version 5.5 # @@ -2531,6 +2533,8 @@ call mtr.add_suppression("Column count of mysql.proc is wrong. Expected 21, foun SELECT COUNT(*) > 0 AS `mysql.user has data` FROM mysql.user; mysql.user has data 1 +call mtr.add_suppression("is wrong. Expected"); +call mtr.add_suppression("Incorrect definition of table"); # # Upgrade from version 10.0 # @@ -2540,6 +2544,8 @@ mysql.user has data SELECT COUNT(*) > 0 AS `mysql.user has data` FROM mysql.user; mysql.user has data 1 +call mtr.add_suppression("is wrong. Expected"); +call mtr.add_suppression("Incorrect definition of table"); # # Upgrade from version 10.1 # @@ -2549,6 +2555,8 @@ mysql.user has data SELECT COUNT(*) > 0 AS `mysql.user has data` FROM mysql.user; mysql.user has data 1 +call mtr.add_suppression("is wrong. Expected"); +call mtr.add_suppression("Incorrect definition of table"); # # Upgrade from version 10.2 # @@ -2558,6 +2566,8 @@ mysql.user has data SELECT COUNT(*) > 0 AS `mysql.user has data` FROM mysql.user; mysql.user has data 1 +call mtr.add_suppression("is wrong. Expected"); +call mtr.add_suppression("Incorrect definition of table"); # # Upgrade from version 10.3 # @@ -2567,6 +2577,8 @@ mysql.user has data SELECT COUNT(*) > 0 AS `mysql.user has data` FROM mysql.user; mysql.user has data 1 +call mtr.add_suppression("is wrong. Expected"); +call mtr.add_suppression("Incorrect definition of table"); # # Upgrade from version 10.4 # diff --git a/sql/CMakeLists.txt b/sql/CMakeLists.txt index 80b28200a02..3130c37628a 100644 --- a/sql/CMakeLists.txt +++ b/sql/CMakeLists.txt @@ -150,6 +150,7 @@ SET (SQL_SOURCE # added in MariaDB: grant.cc + grant_table_defs.cc sql_explain.cc sql_analyze_stmt.cc sql_join_cache.cc diff --git a/sql/grant_table_defs.cc b/sql/grant_table_defs.cc new file mode 100644 index 00000000000..37ec42c3ece --- /dev/null +++ b/sql/grant_table_defs.cc @@ -0,0 +1,384 @@ +/* + Copyright (c) 2026, MariaDB + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA +*/ + +/* + Expected structure of the grant tables in the 'mysql' schema. + + These definitions are used by Table_check_intact, when the grant tables + are loaded (acl_reload() / grant_reload()), to compute the status + variable Grant_tables_intact. + + IMPORTANT: they must be kept in sync with the CREATE TABLE statements in + scripts/mysql_system_tables.sql (and the upgrade statements in + scripts/mysql_system_tables_fix.sql). Whoever adds, removes or changes a + column (name, type or character set) of one of the tables below must + update the matching definition here, otherwise a freshly installed server + will report its own grant tables as not intact. +*/ + +#include "mariadb.h" +#include "sql_acl.h" // MYSQL_DB_FIELD_COUNT +#include "grant_table_defs.h" + +static const +TABLE_FIELD_TYPE mysql_db_table_fields[MYSQL_DB_FIELD_COUNT] = { + { + { STRING_WITH_LEN("Host") }, + { STRING_WITH_LEN("char(255)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Db") }, + { STRING_WITH_LEN("char(64)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("User") }, + { STRING_WITH_LEN("char(128)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Select_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Insert_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Update_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Delete_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Create_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Drop_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Grant_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("References_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Index_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Alter_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Create_tmp_table_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Lock_tables_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Create_view_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Show_view_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Create_routine_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Alter_routine_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Execute_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Event_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Trigger_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Delete_history_priv") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + } +}; + +const TABLE_FIELD_DEF +mysql_db_table_def= {MYSQL_DB_FIELD_COUNT, mysql_db_table_fields, 0, (uint*) 0 }; + +static const +TABLE_FIELD_TYPE mysql_roles_mapping_table_fields[4] = { + { + { STRING_WITH_LEN("Host") }, + { STRING_WITH_LEN("char(255)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("User") }, + { STRING_WITH_LEN("char(128)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Role") }, + { STRING_WITH_LEN("char(128)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Admin_option") }, + { STRING_WITH_LEN("enum('N','Y')") }, + { STRING_WITH_LEN("utf8") } + } +}; + +const TABLE_FIELD_DEF +mysql_roles_mapping_table_def= {4, mysql_roles_mapping_table_fields, 0, (uint*) 0 }; + +static const +TABLE_FIELD_TYPE mysql_proxies_priv_table_fields[7] = { + { + { STRING_WITH_LEN("Host") }, + { STRING_WITH_LEN("char(255)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("User") }, + { STRING_WITH_LEN("char(128)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Proxied_host") }, + { STRING_WITH_LEN("char(255)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Proxied_user") }, + { STRING_WITH_LEN("char(128)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("With_grant") }, + { STRING_WITH_LEN("tinyint(1)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Grantor") }, + { STRING_WITH_LEN("varchar(384)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Timestamp") }, + { STRING_WITH_LEN("timestamp") }, + {NULL, 0} + } +}; + +const TABLE_FIELD_DEF +mysql_proxies_priv_table_def= {7, mysql_proxies_priv_table_fields, 0, (uint*) 0 }; + +static const +TABLE_FIELD_TYPE mysql_tables_priv_table_fields[8] = { + { + { STRING_WITH_LEN("Host") }, + { STRING_WITH_LEN("char(255)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Db") }, + { STRING_WITH_LEN("char(64)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("User") }, + { STRING_WITH_LEN("char(128)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Table_name") }, + { STRING_WITH_LEN("char(64)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Grantor") }, + { STRING_WITH_LEN("varchar(384)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Timestamp") }, + { STRING_WITH_LEN("timestamp") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Table_priv") }, + { STRING_WITH_LEN("set('Select','Insert','Update','Delete','Create','Drop','Grant','References','Index','Alter','Create View','Show view','Trigger','Delete versioning rows')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Column_priv") }, + { STRING_WITH_LEN("set('Select','Insert','Update','References')") }, + { STRING_WITH_LEN("utf8") } + } +}; + +const TABLE_FIELD_DEF +mysql_tables_priv_table_def= {8, mysql_tables_priv_table_fields, 0, (uint*) 0 }; + +static const +TABLE_FIELD_TYPE mysql_columns_priv_table_fields[7] = { + { + { STRING_WITH_LEN("Host") }, + { STRING_WITH_LEN("char(255)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Db") }, + { STRING_WITH_LEN("char(64)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("User") }, + { STRING_WITH_LEN("char(128)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Table_name") }, + { STRING_WITH_LEN("char(64)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Column_name") }, + { STRING_WITH_LEN("char(64)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Timestamp") }, + { STRING_WITH_LEN("timestamp") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Column_priv") }, + { STRING_WITH_LEN("set('Select','Insert','Update','References')") }, + { STRING_WITH_LEN("utf8") } + } +}; + +const TABLE_FIELD_DEF +mysql_columns_priv_table_def= {7, mysql_columns_priv_table_fields, 0, (uint*) 0 }; + +static const +TABLE_FIELD_TYPE mysql_procs_priv_table_fields[8] = { + { + { STRING_WITH_LEN("Host") }, + { STRING_WITH_LEN("char(255)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Db") }, + { STRING_WITH_LEN("char(64)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("User") }, + { STRING_WITH_LEN("char(128)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Routine_name") }, + { STRING_WITH_LEN("char(64)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Routine_type") }, + { STRING_WITH_LEN("enum('FUNCTION','PROCEDURE','PACKAGE','PACKAGE BODY')") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Grantor") }, + { STRING_WITH_LEN("varchar(384)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Proc_priv") }, + { STRING_WITH_LEN("set('Execute','Alter Routine','Grant')") }, + { STRING_WITH_LEN("utf8") } + }, + { + { STRING_WITH_LEN("Timestamp") }, + { STRING_WITH_LEN("timestamp") }, + {NULL, 0} + } +}; + +const TABLE_FIELD_DEF +mysql_procs_priv_table_def= {8, mysql_procs_priv_table_fields, 0, (uint*) 0 }; + +static const +TABLE_FIELD_TYPE mysql_global_priv_table_fields[3] = { + { + { STRING_WITH_LEN("Host") }, + { STRING_WITH_LEN("char(255)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("User") }, + { STRING_WITH_LEN("char(128)") }, + {NULL, 0} + }, + { + { STRING_WITH_LEN("Priv") }, + { STRING_WITH_LEN("longtext") }, + {NULL, 0} + } +}; + +const TABLE_FIELD_DEF +mysql_global_priv_table_def= {3, mysql_global_priv_table_fields, 0, (uint*) 0 }; diff --git a/sql/grant_table_defs.h b/sql/grant_table_defs.h new file mode 100644 index 00000000000..f6eee4099ee --- /dev/null +++ b/sql/grant_table_defs.h @@ -0,0 +1,34 @@ +/* + Copyright (c) 2026, MariaDB + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#ifndef GRANT_TABLE_DEFS_INCLUDED +#define GRANT_TABLE_DEFS_INCLUDED + +#include "table.h" // TABLE_FIELD_DEF + +/* + Expected structure of the grant tables, see grant_table_defs.cc. + Keep the definitions there in sync with scripts/mysql_system_tables.sql. +*/ +extern const TABLE_FIELD_DEF mysql_db_table_def; +extern const TABLE_FIELD_DEF mysql_roles_mapping_table_def; +extern const TABLE_FIELD_DEF mysql_proxies_priv_table_def; +extern const TABLE_FIELD_DEF mysql_tables_priv_table_def; +extern const TABLE_FIELD_DEF mysql_columns_priv_table_def; +extern const TABLE_FIELD_DEF mysql_procs_priv_table_def; +extern const TABLE_FIELD_DEF mysql_global_priv_table_def; + +#endif /* GRANT_TABLE_DEFS_INCLUDED */ diff --git a/sql/mysqld.cc b/sql/mysqld.cc index 52938056f0b..aabd9ce3f43 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -7613,6 +7613,7 @@ SHOW_VAR status_vars[]= { {"Aborted_connects", (char*) &aborted_connects, SHOW_LONG}, {"Aborted_connects_preauth", (char*) &aborted_connects_preauth, SHOW_LONG}, {"Acl", (char*) acl_statistics, SHOW_ARRAY}, + {"Grant_tables_intact", (char*) &grant_tables_intact, SHOW_MY_BOOL}, {"Access_denied_errors", (char*) offsetof(STATUS_VAR, access_denied_errors), SHOW_LONG_STATUS}, {"Binlog_bytes_written", (char*) offsetof(STATUS_VAR, binlog_bytes_written), SHOW_LONGLONG_STATUS}, {"Binlog_cache_disk_use", (char*) &binlog_cache_disk_use, SHOW_LONG}, diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index 267e360fa14..bd27630a1e6 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -28,6 +28,7 @@ #include "mariadb.h" /* NO_EMBEDDED_ACCESS_CHECKS */ #include "sql_priv.h" #include "sql_acl.h" // MYSQL_DB_FIELD_COUNT, ACL_ACCESS +#include "grant_table_defs.h" #include "sql_base.h" // close_mysql_tables #include "key.h" // key_copy, key_cmp_if_same, key_restore #include "sql_show.h" // append_identifier @@ -64,6 +65,17 @@ bool mysql_user_table_is_in_short_password_format= false; bool using_global_priv_table= true; +/* + Set at the end of acl_reload()/grant_reload() (so recomputed on every + server startup and every FLUSH PRIVILEGES) to reflect whether the + on-disk grant tables' structure matches what this server binary + expects. Exposed read-only via SHOW STATUS (see acl_statistics / + status_vars in mysqld.cc) so a remote mariadb-upgrade can check it + without local filesystem access. +*/ +my_bool grant_tables_intact= FALSE; +static Table_check_intact_log_error table_intact; + // set that from field length in acl_load? #ifndef NO_EMBEDDED_ACCESS_CHECKS const uint max_hostname_length= HOSTNAME_LENGTH; @@ -2973,6 +2985,34 @@ void acl_free(bool end) } +/* + Check that a grant table has the structure this server expects. + + A table that wasn't opened is reported in the error log and treated as + not intact, the same as a table with a wrong structure. + Table_check_intact::check() must not be called with a NULL TABLE. + + @param table Grant table, as returned by Grant_tables + @param name Name of the table in the mysql schema, for the message + @param table_def Expected structure (see grant_table_defs.cc) + + @retval true Table is intact + @retval false Table is missing or its structure is wrong +*/ + +static bool grant_table_is_intact(const Grant_table_base &table, + const char *name, + const TABLE_FIELD_DEF *table_def) +{ + if (!table.table_exists()) + { + sql_print_error("Table mysql.%s is missing", name); + return false; + } + return !table_intact.check(table.table(), table_def); +} + + /* Forget current user/db-level privileges and read new privileges from the privilege tables. @@ -3071,6 +3111,33 @@ bool acl_reload(THD *thd) delete_dynamic_with_callback(&old_acl_users, free_acl_user); delete_dynamic(&old_acl_proxy_users); my_hash_free(&old_acl_roles_mappings); + + /* + Check every table, even if an earlier one isn't intact, so that all + the problems are written to the error log. + + The 'user' view is never opened here, it is only a client-facing + compatibility layer. Instead user_table() is mysql.global_priv, or the + old tabular mysql.user table on installations that haven't been + upgraded to global_priv yet (using_global_priv_table is false, set in + User_table_tabular::setup_sysvars()). Only global_priv has a + definition to compare with, so the legacy table is not checked. + */ + bool db_intact= + grant_table_is_intact(tables.db_table(), "db", &mysql_db_table_def); + bool roles_mapping_intact= + grant_table_is_intact(tables.roles_mapping_table(), "roles_mapping", + &mysql_roles_mapping_table_def); + bool proxies_priv_intact= + grant_table_is_intact(tables.proxies_priv_table(), "proxies_priv", + &mysql_proxies_priv_table_def); + bool global_priv_intact= + (!using_global_priv_table || + grant_table_is_intact(tables.user_table(), "global_priv", + &mysql_global_priv_table_def)); + + grant_tables_intact= (db_intact && roles_mapping_intact && + proxies_priv_intact && global_priv_intact); } mysql_mutex_unlock(&acl_cache->lock); end: @@ -8388,6 +8455,27 @@ bool grant_reload(THD *thd) my_hash_free(&old_package_spec_priv_hash); my_hash_free(&old_package_body_priv_hash); free_root(&old_mem,MYF(0)); + + /* + acl_reload() always runs immediately before grant_reload(), both at + server startup and on FLUSH PRIVILEGES so it's safe to AND our tables' + results onto the value it just set rather than overwrite it. + + Check every table, even if an earlier one isn't intact, so that all + the problems are written to the error log. + */ + bool tables_priv_intact= + grant_table_is_intact(tables.tables_priv_table(), "tables_priv", + &mysql_tables_priv_table_def); + bool columns_priv_intact= + grant_table_is_intact(tables.columns_priv_table(), "columns_priv", + &mysql_columns_priv_table_def); + bool procs_priv_intact= + grant_table_is_intact(tables.procs_priv_table(), "procs_priv", + &mysql_procs_priv_table_def); + + grant_tables_intact= (grant_tables_intact && tables_priv_intact && + columns_priv_intact && procs_priv_intact); } mysql_mutex_lock(&acl_cache->lock); diff --git a/sql/sql_acl.h b/sql/sql_acl.h index a478ccdaa1e..ca538df7c26 100644 --- a/sql/sql_acl.h +++ b/sql/sql_acl.h @@ -51,7 +51,6 @@ enum mysql_db_table_field MYSQL_DB_FIELD_COUNT }; -extern const TABLE_FIELD_DEF mysql_db_table_def; extern bool mysql_user_table_is_in_short_password_format; extern LEX_CSTRING host_not_specified; @@ -284,6 +283,7 @@ int acl_set_default_role(THD *thd, const char *host, const char *user, const char *rolename); extern SHOW_VAR acl_statistics[]; +extern my_bool grant_tables_intact; /* Check if a role is granted to a user/role.