Description:
A MySQL replica crashes with a segfault in `set_and_validate_user_attributes()` during relay log recovery when the relay log contains an `ALTER USER` or `GRANT` statement for a user that does not exist on the replica.
The root cause is a startup ordering issue in `mysqld.cc`. `ReplicaInitializer` (which triggers relay log recovery and can apply relay log events) executes before `update_authentication_policy()` populates the global `authentication_policy_list` vector.
If relay log recovery applies an `ALTER USER` or `GRANT` for a non-existent user, the code path in `set_and_validate_user_attributes()` (`sql/auth/sql_user.cc`) reaches:
```
if (authentication_policy_list[0].compare("*") == 0)
```
Since `authentication_policy_list` is still empty at this point, `operator[](0)` is an out-of-bounds access. On release builds this manifests as a segfault in `std::string::compare(const char*)`. On debug builds it triggers a vector bounds-check assertion.
The crash is deterministic on restart — the same relay log event causes the crash every time, putting the replica into a restart loop that cannot self-recover.
How to repeat:
Requires a source-replica GTID replication setup. The key is to have a pending relay log event (`ALTER USER` or `GRANT`) for a user that does not exist on the replica, then restart the replica with `relay_log_recovery=ON`.
```
-- Setup: source (server-id=1, port=3306) and replica (server-id=2, port=3307)
-- with GTID replication. Replica uses relay_log_recovery=ON.
-- 1. On source: create a user and let it replicate to the replica
CREATE USER 'testuser'@'%' IDENTIFIED WITH mysql_native_password BY 'pass';
-- Wait for replication to sync
-- 2. On replica: stop replication and drop the user locally
STOP REPLICA;
DROP USER 'testuser'@'%';
-- 3. On source: execute ALTER USER (this goes into the binlog)
ALTER USER 'testuser'@'%' ACCOUNT LOCK;
-- (A GRANT statement also triggers the same crash)
-- 4. Kill the replica process (simulate crash)
-- kill -9 <replica_mysqld_pid>
-- 5. Restart the replica with --relay-log-recovery=ON
-- Relay log recovery applies the ALTER USER statement.
-- Since 'testuser' does not exist on the replica, the code enters
-- the "user does not exist" path in set_and_validate_user_attributes()
-- and accesses authentication_policy_list[0] before it has been populated.
-- Result: SEGFAULT (release) or assertion failure (debug)
```
Note: Because relay log recovery and `authentication_policy_list` initialization happen close together during startup, a timing-dependent race exists. To reliably reproduce, add a delay after `ReplicaInitializer` in `mysqld.cc`:
```
DBUG_EXECUTE_IF("delay_after_replica_init", { sleep(10); });
```
Then start the replica with `--debug='+d,delay_after_replica_init'`.
Stack trace (release build):
```
#0 std::__cxx11::basic_string<...>::compare(char const*) const
#1 set_and_validate_user_attributes(...) at sql/auth/sql_user.cc:1563
#2 mysql_alter_user(...) at sql/auth/sql_user.cc:3807
#3 mysql_execute_command(...) at sql/sql_parse.cc
#4 dispatch_sql_command(...) at sql/sql_parse.cc
#5 Query_log_event::do_apply_event(...) at sql/log_event.cc
#6 handle_slave_sql at sql/rpl_replica.cc
```
Suggested fix:
Move `update_authentication_policy()` to execute before `ReplicaInitializer` in the server startup sequence in `mysqld.cc`.
Alternatively, add a bounds check before accessing `authentication_policy_list[0]` in `set_and_validate_user_attributes()`, falling back to `default_auth_plugin_name` when the list is empty.
Description: A MySQL replica crashes with a segfault in `set_and_validate_user_attributes()` during relay log recovery when the relay log contains an `ALTER USER` or `GRANT` statement for a user that does not exist on the replica. The root cause is a startup ordering issue in `mysqld.cc`. `ReplicaInitializer` (which triggers relay log recovery and can apply relay log events) executes before `update_authentication_policy()` populates the global `authentication_policy_list` vector. If relay log recovery applies an `ALTER USER` or `GRANT` for a non-existent user, the code path in `set_and_validate_user_attributes()` (`sql/auth/sql_user.cc`) reaches: ``` if (authentication_policy_list[0].compare("*") == 0) ``` Since `authentication_policy_list` is still empty at this point, `operator[](0)` is an out-of-bounds access. On release builds this manifests as a segfault in `std::string::compare(const char*)`. On debug builds it triggers a vector bounds-check assertion. The crash is deterministic on restart — the same relay log event causes the crash every time, putting the replica into a restart loop that cannot self-recover. How to repeat: Requires a source-replica GTID replication setup. The key is to have a pending relay log event (`ALTER USER` or `GRANT`) for a user that does not exist on the replica, then restart the replica with `relay_log_recovery=ON`. ``` -- Setup: source (server-id=1, port=3306) and replica (server-id=2, port=3307) -- with GTID replication. Replica uses relay_log_recovery=ON. -- 1. On source: create a user and let it replicate to the replica CREATE USER 'testuser'@'%' IDENTIFIED WITH mysql_native_password BY 'pass'; -- Wait for replication to sync -- 2. On replica: stop replication and drop the user locally STOP REPLICA; DROP USER 'testuser'@'%'; -- 3. On source: execute ALTER USER (this goes into the binlog) ALTER USER 'testuser'@'%' ACCOUNT LOCK; -- (A GRANT statement also triggers the same crash) -- 4. Kill the replica process (simulate crash) -- kill -9 <replica_mysqld_pid> -- 5. Restart the replica with --relay-log-recovery=ON -- Relay log recovery applies the ALTER USER statement. -- Since 'testuser' does not exist on the replica, the code enters -- the "user does not exist" path in set_and_validate_user_attributes() -- and accesses authentication_policy_list[0] before it has been populated. -- Result: SEGFAULT (release) or assertion failure (debug) ``` Note: Because relay log recovery and `authentication_policy_list` initialization happen close together during startup, a timing-dependent race exists. To reliably reproduce, add a delay after `ReplicaInitializer` in `mysqld.cc`: ``` DBUG_EXECUTE_IF("delay_after_replica_init", { sleep(10); }); ``` Then start the replica with `--debug='+d,delay_after_replica_init'`. Stack trace (release build): ``` #0 std::__cxx11::basic_string<...>::compare(char const*) const #1 set_and_validate_user_attributes(...) at sql/auth/sql_user.cc:1563 #2 mysql_alter_user(...) at sql/auth/sql_user.cc:3807 #3 mysql_execute_command(...) at sql/sql_parse.cc #4 dispatch_sql_command(...) at sql/sql_parse.cc #5 Query_log_event::do_apply_event(...) at sql/log_event.cc #6 handle_slave_sql at sql/rpl_replica.cc ``` Suggested fix: Move `update_authentication_policy()` to execute before `ReplicaInitializer` in the server startup sequence in `mysqld.cc`. Alternatively, add a bounds check before accessing `authentication_policy_list[0]` in `set_and_validate_user_attributes()`, falling back to `default_auth_plugin_name` when the list is empty.