From 17b0e7f6457c9d38243363a3c142896a1995d2ea Mon Sep 17 00:00:00 2001 From: Fariha Shaikh Date: Wed, 20 May 2026 23:03:01 +0000 Subject: [PATCH] Bug#120480 Fix crash during relay log recovery ReplicaInitializer starts replication threads before update_authentication_policy() populates authentication_policy_list. If the SQL thread applies an ALTER USER or GRANT for a non-existent user during this window, set_and_validate_user_attributes() accesses authentication_policy_list[0] on an empty vector, causing a crash. Fix by moving update_authentication_policy() before ReplicaInitializer in the startup sequence. Also add a bounds check before accessing authentication_policy_list[0] as defense-in-depth. Add test rpl_relay_log_recovery_auth_policy_crash in the rpl suite. This contribution is under the OCA signed by Amazon and covering submissions to the MySQL project. --- ...elay_log_recovery_auth_policy_crash.result | 22 ++++++ ..._relay_log_recovery_auth_policy_crash.test | 74 +++++++++++++++++++ sql/auth/sql_user.cc | 7 +- sql/mysqld.cc | 19 ++--- 4 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 mysql-test/suite/rpl/r/rpl_relay_log_recovery_auth_policy_crash.result create mode 100644 mysql-test/suite/rpl/t/rpl_relay_log_recovery_auth_policy_crash.test diff --git a/mysql-test/suite/rpl/r/rpl_relay_log_recovery_auth_policy_crash.result b/mysql-test/suite/rpl/r/rpl_relay_log_recovery_auth_policy_crash.result new file mode 100644 index 000000000000..99672f2859ed --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_relay_log_recovery_auth_policy_crash.result @@ -0,0 +1,22 @@ +include/master-slave.inc +Warnings: +Note #### Sending passwords in plain text without SSL/TLS is extremely insecure. +Note #### Storing MySQL user name or password information in the connection metadata repository is not secure and is therefore not recommended. Please consider using the USER and PASSWORD connection options for START REPLICA; see the 'START REPLICA Syntax' in the MySQL Manual for more information. +[connection master] +call mtr.add_suppression("Replica SQL for channel.*"); +call mtr.add_suppression(".*coordinator and worker threads are stopped.*"); +CREATE USER 'testuser'@'%' IDENTIFIED WITH mysql_native_password BY 'pass'; +include/sync_slave_sql_with_master.inc +include/stop_slave_sql.inc +DROP USER 'testuser'@'%'; +SET GLOBAL DEBUG='+d,empty_authentication_policy_list'; +ALTER USER 'testuser'@'%' ACCOUNT LOCK; +START REPLICA SQL_THREAD; +include/wait_for_slave_sql_error.inc [errno=1396] +SET GLOBAL DEBUG='-d,empty_authentication_policy_list'; +include/stop_slave.inc +include/rpl_reset_slave.inc +include/start_slave.inc +DROP USER 'testuser'@'%'; +include/sync_slave_sql_with_master.inc +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_relay_log_recovery_auth_policy_crash.test b/mysql-test/suite/rpl/t/rpl_relay_log_recovery_auth_policy_crash.test new file mode 100644 index 000000000000..d247e118926f --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_relay_log_recovery_auth_policy_crash.test @@ -0,0 +1,74 @@ +# ==== Purpose ==== +# +# Verify that the server does not crash when applying an ALTER USER +# statement for a non-existent user while authentication_policy_list +# is empty. +# +# ==== Implementation ==== +# +# 1. Create a user on source and sync to replica. +# 2. Stop replication and drop the user on replica only. +# 3. On replica, set debug flag to clear authentication_policy_list +# at the point of access. +# 4. Execute ALTER USER ... ACCOUNT LOCK on source. +# 5. Start replication on replica and verify it does not crash. +# +# ==== References ==== +# +# Bug#120480 Crash in set_and_validate_user_attributes during relay +# log recovery + +--source include/not_group_replication_plugin.inc +--source include/have_debug.inc +--source include/have_binlog_format_row.inc +--source include/not_have_privilege_checks_user.inc +--source include/master-slave.inc + +--connection slave +call mtr.add_suppression("Replica SQL for channel.*"); +call mtr.add_suppression(".*coordinator and worker threads are stopped.*"); + +# 1. Create user on source and sync to replica + +--connection master +CREATE USER 'testuser'@'%' IDENTIFIED WITH mysql_native_password BY 'pass'; +--source include/sync_slave_sql_with_master.inc + +# 2. Stop replication and drop the user on replica only + +--connection slave +--source include/stop_slave_sql.inc +DROP USER 'testuser'@'%'; + +# 3. Set debug flag to simulate empty authentication_policy_list + +SET GLOBAL DEBUG='+d,empty_authentication_policy_list'; + +# 4. Execute ALTER USER on source + +--connection master +ALTER USER 'testuser'@'%' ACCOUNT LOCK; + +# 5. Start SQL thread on replica. Without the fix, this crashes due to +# accessing authentication_policy_list[0] on an empty vector. + +--connection slave +START REPLICA SQL_THREAD; + +--let $slave_sql_errno= 1396 +--source include/wait_for_slave_sql_error.inc + +# Cleanup + +SET GLOBAL DEBUG='-d,empty_authentication_policy_list'; +--source include/stop_slave.inc +--let $rpl_skip_change_master= 1 +--let $rpl_skip_start_slave= 1 +--source include/rpl_reset_slave.inc +--source include/start_slave.inc + +--connection master +DROP USER 'testuser'@'%'; +--source include/sync_slave_sql_with_master.inc + +--source include/rpl_end.inc diff --git a/sql/auth/sql_user.cc b/sql/auth/sql_user.cc index 9ca086b5723f..5d5d05c0788e 100644 --- a/sql/auth/sql_user.cc +++ b/sql/auth/sql_user.cc @@ -1110,7 +1110,7 @@ static bool check_for_authentication_policy(THD *thd, LEX_USER *user_name, (mfa ? mfa->get_multi_factor_auth_list() : nullptr); DBUG_TRACE; - assert(!auth_policy_list.empty()); + if (auth_policy_list.empty()) return false; uint nth_factor = user_name->first_factor_auth_info.nth_factor; /* check 1FA method */ @@ -1581,7 +1581,10 @@ bool set_and_validate_user_attributes( */ if (!Str->first_factor_auth_info.uses_identified_with_clause) { mysql_mutex_lock(&LOCK_authentication_policy); - if (authentication_policy_list[0].compare("*") == 0) + DBUG_EXECUTE_IF("empty_authentication_policy_list", + { authentication_policy_list.clear(); }); + if (authentication_policy_list.empty() || + authentication_policy_list[0].compare("*") == 0) Str->first_factor_auth_info.plugin = default_auth_plugin_name; else lex_string_strmake(thd->mem_root, &Str->first_factor_auth_info.plugin, diff --git a/sql/mysqld.cc b/sql/mysqld.cc index e176b83b17e6..895fb59bcc6c 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -8147,6 +8147,16 @@ int mysqld_main(int argc, char **argv) binlog_unsafe_map_init(); + if (opt_authentication_policy && + validate_authentication_policy(opt_authentication_policy)) { + /* --authentication_policy is set to invalid value */ + LogErr(ERROR_LEVEL, ER_INVALID_AUTHENTICATION_POLICY); + return 1; + } else { + /* update the value */ + update_authentication_policy(); + } + ReplicaInitializer replica_initializer(opt_initialize, opt_skip_replica_start, rpl_channel_filters, &opt_replica_skip_errors); @@ -8172,15 +8182,6 @@ int mysqld_main(int argc, char **argv) // Start signal handler thread. start_signal_handler(); #endif - if (opt_authentication_policy && - validate_authentication_policy(opt_authentication_policy)) { - /* --authentication_policy is set to invalid value */ - LogErr(ERROR_LEVEL, ER_INVALID_AUTHENTICATION_POLICY); - return 1; - } else { - /* update the value */ - update_authentication_policy(); - } /* set all persistent options */ if (persisted_variables_cache.set_persisted_options(false)) { LogErr(ERROR_LEVEL, ER_CANT_SET_UP_PERSISTED_VALUES);