Skip to content

Commit

Permalink
MDEV-28530: Revoking privileges from a non-existing user on a master …
Browse files Browse the repository at this point in the history
…breaks replication on the slave in the presence of replication filters

Problem:
========
Replication can break while applying a query log event if its
respective command errors on the primary, but is ignored by the
replication filter within Grant_tables on the replica. The bug
reported by MDEV-28530 shows this with REVOKE ALL PRIVILEGES using a
non-existent user. The primary will binlog the REVOKE command with
an error code, and the replica will think the command executed with
success because the replication filter will ignore the command while
accessing the Grant_tables classes. When the replica performs an
error check, it sees the difference between the error codes, and
replication breaks.

Solution:
========
If the replication filter check done by Grant_tables logic ignores
the tables, reset thd->slave_expected_error to 0 so that
Query_log_event::do_apply_event() can be made aware that the
underlying query was ignored when it compares errors.

Note that this bug also effects DROP USER if not all users exist
in the provided list, and the patch fixes and tests this case.

Reviewed By:
============
andrei.elkin@mariadb.com
  • Loading branch information
bnestere authored and andrelkin committed Sep 3, 2022
1 parent e4cffc9 commit 4781201
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 0 deletions.
39 changes: 39 additions & 0 deletions mysql-test/suite/rpl/r/rpl_filter_revoke_missing_user.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
include/master-slave.inc
[connection master]
#
# Set replica to ignore system tables
connection slave;
include/stop_slave.inc
SET @@GLOBAL.replicate_wild_ignore_table="mysql.%";
include/start_slave.inc
#
# Trying to execute REVOKE ALL PRIVILEGES on a non-existent user and
# DROP USER on a list of users where not all users exist should error
# and be written into the binary log
connection master;
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'nonexistentuser'@'%';
ERROR HY000: Can't revoke all privileges for one or more of the requested users
CREATE USER 'testuser'@'localhost' IDENTIFIED by '';
DROP USER 'testuser'@'localhost', 'nonexistentuser'@'%';
ERROR HY000: Operation DROP USER failed for 'nonexistentuser'@'%'
#
# Ensure the events exist in the primary's binary log
FLUSH BINARY LOGS;
# MYSQL_BINLOG MYSQLD_DATADIR/binlog_file > MYSQL_TMP_DIR/mysqlbinlog_out.sql
# There should be three Query events: REVOKE, CREATE USER, and DROP USER
FOUND 3 /Query/ in mysqlbinlog_out.sql
FOUND 1 /REVOKE ALL PRIVILEGES/ in mysqlbinlog_out.sql
FOUND 1 /CREATE USER/ in mysqlbinlog_out.sql
FOUND 1 /DROP USER/ in mysqlbinlog_out.sql
#
# Ensure that the replica receives the event without error
connection slave;
Last_SQL_Error =
Last_SQL_Errno = 0
#
# Clean up
connection slave;
include/stop_slave.inc
SET @@GLOBAL.replicate_wild_ignore_table="";
include/start_slave.inc
include/rpl_end.inc
92 changes: 92 additions & 0 deletions mysql-test/suite/rpl/t/rpl_filter_revoke_missing_user.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#
# Purpose:
# This test ensures that a binlogged Query_log_event which failed on the
# primary server does not break replication if it is ignored by Grant_tables
# on the replica. The bug reported by MDEV-28530 shows this with
# REVOKE ALL PRIVILEGES.. using a non-existent user. The primary will binlog
# the REVOKE command with an error code, and the replica will think the command
# executed with success because the replication filter will ignore the command
# while accessing the Grant_tables classes. When the replica performs an error
# check, it sees the difference between the error codes, and replication
# breaks.
#
# Methodology:
# Using a replica configured with replicate_wild_ignore_table="schema.%",
# on the primary, execute REVOKE ALL PRVILEGES using a non-existent user and
# DROP USER using a list of users where not all users exist, and ensure that
# the replica acknowledges and ignores the events without erroring.
#
# References:
# MDEV-28530: Revoking privileges from a non-existing user on a master breaks
# replication on the slave in the presence of replication filters
#

source include/master-slave.inc;
source include/have_binlog_format_statement.inc;

--echo #
--echo # Set replica to ignore system tables
connection slave;
let $old_filter= query_get_value(SHOW SLAVE STATUS, Replicate_Wild_Ignore_Table, 1);
source include/stop_slave.inc;
SET @@GLOBAL.replicate_wild_ignore_table="mysql.%";
source include/start_slave.inc;


--echo #
--echo # Trying to execute REVOKE ALL PRIVILEGES on a non-existent user and
--echo # DROP USER on a list of users where not all users exist should error
--echo # and be written into the binary log
--connection master

--error 1269
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'nonexistentuser'@'%';

CREATE USER 'testuser'@'localhost' IDENTIFIED by '';
--error 1396
DROP USER 'testuser'@'localhost', 'nonexistentuser'@'%';
--save_master_pos


--echo #
--echo # Ensure the events exist in the primary's binary log
--let $MYSQLD_DATADIR= `select @@datadir`
--let $binlog_file=query_get_value(SHOW MASTER STATUS, File, 1)
FLUSH BINARY LOGS;
--echo # MYSQL_BINLOG MYSQLD_DATADIR/binlog_file > MYSQL_TMP_DIR/mysqlbinlog_out.sql
--exec $MYSQL_BINLOG $MYSQLD_DATADIR/$binlog_file > $MYSQL_TMP_DIR/mysqlbinlog_out.sql

--echo # There should be three Query events: REVOKE, CREATE USER, and DROP USER
--let SEARCH_FILE= $MYSQL_TMP_DIR/mysqlbinlog_out.sql

--let SEARCH_PATTERN= Query
--source include/search_pattern_in_file.inc

--let SEARCH_PATTERN= REVOKE ALL PRIVILEGES
--source include/search_pattern_in_file.inc

--let SEARCH_PATTERN= CREATE USER
--source include/search_pattern_in_file.inc

--let SEARCH_PATTERN= DROP USER
--source include/search_pattern_in_file.inc


--echo #
--echo # Ensure that the replica receives the event without error
connection slave;
--sync_with_master
let $error= query_get_value(SHOW SLAVE STATUS, Last_SQL_Error, 1);
--echo Last_SQL_Error = $error
let $errno= query_get_value(SHOW SLAVE STATUS, Last_SQL_Errno, 1);
--echo Last_SQL_Errno = $errno


--echo #
--echo # Clean up
--connection slave
source include/stop_slave.inc;
--eval SET @@GLOBAL.replicate_wild_ignore_table="$old_filter"
source include/start_slave.inc;

--source include/rpl_end.inc
7 changes: 7 additions & 0 deletions sql/log_event.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5703,6 +5703,13 @@ int Query_log_event::do_apply_event(rpl_group_info *rgi,
thd->update_server_status();
log_slow_statement(thd);
thd->lex->restore_set_statement_var();

/*
When THD::slave_expected_error gets reset inside execution stack
that is the case of to be ignored event. In this case the expected
error must change to the reset value as well.
*/
expected_error= thd->slave_expected_error;
}

thd->variables.option_bits&= ~OPTION_MASTER_SQL_ERROR;
Expand Down
3 changes: 3 additions & 0 deletions sql/sql_acl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,10 @@ class Grant_tables
Rpl_filter *rpl_filter= thd->system_thread_info.rpl_sql_info->rpl_filter;
if (rpl_filter->is_on() &&
!rpl_filter->tables_ok(0, &first_table_in_list->tl))
{
thd->slave_expected_error= 0;
DBUG_RETURN(1);
}
}
DBUG_RETURN(0);
}
Expand Down
2 changes: 2 additions & 0 deletions sql/sql_class.h
Original file line number Diff line number Diff line change
Expand Up @@ -3209,6 +3209,8 @@ class THD :public Statement,
/*
In case of a slave, set to the error code the master got when executing
the query. 0 if no error on the master.
The stored into variable master error code may get reset inside
execution stack when the event turns out to be ignored.
*/
int slave_expected_error;
enum_sql_command last_sql_command; // Last sql_command exceuted in mysql_execute_command()
Expand Down

0 comments on commit 4781201

Please sign in to comment.