From 59dafb5f6c1b59b12d0d3b90190a4677dc41d62c Mon Sep 17 00:00:00 2001 From: Andrei Elkin Date: Tue, 12 May 2026 20:27:58 +0300 Subject: [PATCH] MDEV-39500 STRICT execution mode slave silently overwrites record even at mismatch The problem at hand is that the default STRICT slave execution mode is not strict enough. In row-based replication, if the slave applier locates a row for an UPDATE using a unique key, it silently applies the changes even if the before-image in the replication event does not match the local non-PK columns. This allows among other things data divergence to go undetected without throwing any errors. This is resolved by introducing a new slave execution mode: STRINGENT. When @@global.slave_exec_mode = STRINGENT is configured, the applier explicitly compares the event's before-image against the local record. If a mismatch is detected, it rejects the update and safely halts replication by throwing a new error, ER_INCONSISTENT_SLAVE_RECORD. --- .../suite/rpl/r/rpl_mismatch_detect.result | 66 ++++++++++++++ .../suite/rpl/t/rpl_mismatch_detect.test | 87 +++++++++++++++++++ sql/log_event_server.cc | 18 +++- sql/share/errmsg-utf8.txt | 2 + sql/sql_class.h | 3 +- sql/sys_vars.cc | 3 +- 6 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 mysql-test/suite/rpl/r/rpl_mismatch_detect.result create mode 100644 mysql-test/suite/rpl/t/rpl_mismatch_detect.test diff --git a/mysql-test/suite/rpl/r/rpl_mismatch_detect.result b/mysql-test/suite/rpl/r/rpl_mismatch_detect.result new file mode 100644 index 0000000000000..156beed3e53b8 --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_mismatch_detect.result @@ -0,0 +1,66 @@ +include/master-slave.inc +[connection master] +# +# MDEV-39500: Replicated UPDATE silently succeeds despite row divergence on slave +# +connection master; +CREATE TABLE t1 (a INT PRIMARY KEY, c VARCHAR(255)) ENGINE=InnoDB; +INSERT INTO t1 VALUES (1, 'initial_state'); +connection slave; +connection slave; +set @save_slave_exec_mode = @@global.slave_exec_mode; +set @@global.slave_exec_mode = STRICT; +UPDATE t1 SET c = 'diverged_to_pass'; +SELECT * FROM t1; +a c +1 diverged_to_pass +connection master; +UPDATE t1 SET c = 'master_update1'; +# +# Expect it to succeed (slave gets in sync to have applied it). +# Exposing MDEV-39500: The slave applier uses the PK to find the row but +# fails to reject the update despite the before-image mismatch, applying it +# silently without throwing HA_ERR_RECORD_CHANGED (or similar). +# +connection slave; +connection slave; +# The slave has silently synced and overwritten the local changes +SELECT * FROM t1; +a c +1 master_update1 +include/diff_tables.inc [master:t1, slave:t1] +connection slave; +set @@global.slave_exec_mode = STRINGENT; +call mtr.add_suppression("Slave: Replicated 'Update_rows_v1' record.s before-image on table"); +UPDATE t1 SET c = 'diverged_to_fail'; +SELECT * FROM t1; +a c +1 diverged_to_fail +connection master; +UPDATE t1 SET c = 'master_update2'; +connection slave; +# +# Expect it to fail. +# +include/wait_for_slave_sql_error.inc [errno=4264] +connection slave; +# the stopped slave deverges +SELECT * FROM t1; +a c +1 diverged_to_fail +set @@global.slave_exec_mode = STRICT; +include/start_slave.inc +Not anymore in STRICT mode +SELECT * FROM t1; +a c +1 master_update2 +connection master; +connection slave; +# +# Cleanup +# +connection master; +DROP TABLE t1; +connection slave; +set @@global.slave_exec_mode = @save_slave_exec_mode; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_mismatch_detect.test b/mysql-test/suite/rpl/t/rpl_mismatch_detect.test new file mode 100644 index 0000000000000..de7e6a5322fa4 --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_mismatch_detect.test @@ -0,0 +1,87 @@ +--source include/have_binlog_format_row.inc +--source include/have_innodb.inc +--source include/master-slave.inc + +--echo # +--echo # MDEV-39500: Replicated UPDATE silently succeeds despite row divergence on slave +--echo # + +# Having two slave_exec_mode modes conduct the following. +# 0. initially populate a PK-defined table with a record +# 1. makes slave data inconsistent with master via UPDATEing locally a +# replicated record, and run +# 2. a replicated UPDATE from master and expect, it to succeed +# in the default STRICT mode, or +# fail in the new STRINGENT. + + +connection master; +CREATE TABLE t1 (a INT PRIMARY KEY, c VARCHAR(255)) ENGINE=InnoDB; +INSERT INTO t1 VALUES (1, 'initial_state'); +sync_slave_with_master; + +connection slave; +set @save_slave_exec_mode = @@global.slave_exec_mode; +set @@global.slave_exec_mode = STRICT; +# Modify the non-PK columns to simulate divergence +UPDATE t1 SET c = 'diverged_to_pass'; +SELECT * FROM t1; + +connection master; +UPDATE t1 SET c = 'master_update1'; + +--echo # +--echo # Expect it to succeed (slave gets in sync to have applied it). +--echo # Exposing MDEV-39500: The slave applier uses the PK to find the row but +--echo # fails to reject the update despite the before-image mismatch, applying it +--echo # silently without throwing HA_ERR_RECORD_CHANGED (or similar). +--echo # +sync_slave_with_master; + +connection slave; +--echo # The slave has silently synced and overwritten the local changes +SELECT * FROM t1; + +--let $diff_tables= master:t1, slave:t1 +--source include/diff_tables.inc + +# the new mode +connection slave; +set @@global.slave_exec_mode = STRINGENT; +call mtr.add_suppression("Slave: Replicated 'Update_rows_v1' record.s before-image on table"); + +# Modify the non-PK columns to simulate divergence +UPDATE t1 SET c = 'diverged_to_fail'; +SELECT * FROM t1; + +connection master; +UPDATE t1 SET c = 'master_update2'; + +connection slave; +--echo # +--echo # Expect it to fail. +--echo # +# ER_INCONSISTENT_SLAVE_RECORD +--let $slave_sql_errno = 4264 +--source include/wait_for_slave_sql_error.inc + +connection slave; +--echo # the stopped slave deverges +SELECT * FROM t1; + +set @@global.slave_exec_mode = STRICT; +--source include/start_slave.inc +--echo Not anymore in STRICT mode +SELECT * FROM t1; + +connection master; +sync_slave_with_master; + +--echo # +--echo # Cleanup +--echo # +connection master; +DROP TABLE t1; +sync_slave_with_master; +set @@global.slave_exec_mode = @save_slave_exec_mode; +--source include/rpl_end.inc diff --git a/sql/log_event_server.cc b/sql/log_event_server.cc index aea8028db1029..a77a6f08a6071 100644 --- a/sql/log_event_server.cc +++ b/sql/log_event_server.cc @@ -8206,7 +8206,7 @@ int Rows_log_event::find_row(rpl_group_info *rgi) int error= 0; bool is_table_scan= false, is_index_scan= false; Check_level_instant_set clis(table->in_use, CHECK_FIELD_IGNORE); - + bool do_compare_records= slave_exec_mode == SLAVE_EXEC_MODE_STRINGENT; /* rpl_row_tabledefs.test specifies that if the extra field on the slave does not have a default value @@ -8272,6 +8272,10 @@ int Rows_log_event::find_row(rpl_group_info *rgi) error= row_not_found_error(rgi); table->file->print_error(error, MYF(0)); } + else + { + goto comp_rec; + } DBUG_RETURN(error); } @@ -8465,6 +8469,18 @@ int Rows_log_event::find_row(rpl_group_info *rgi) if (is_table_scan || is_index_scan) issue_long_find_row_warning(get_general_type_code(), m_table->alias.c_ptr(), is_index_scan, rgi); +comp_rec: + if (error == 0 && do_compare_records && rgi->rli->mi /* !online alter */) + { + if (record_compare(table, m_vers_from_plain)) + { + my_error(ER_INCONSISTENT_SLAVE_RECORD, MYF(0), + get_type_str(get_type_code()), + table->s->db.str, table->s->table_name.str); + error= 1; // not HA error, the caller will catch it as already reported + } + } + DBUG_RETURN(error); } diff --git a/sql/share/errmsg-utf8.txt b/sql/share/errmsg-utf8.txt index 884fc75c91379..9b8a4bad1e8db 100644 --- a/sql/share/errmsg-utf8.txt +++ b/sql/share/errmsg-utf8.txt @@ -12404,3 +12404,5 @@ ER_WARN_QB_NAME_PATH_VIEW_NOT_FOUND eng "Hint %s is ignored. `%s` required at element #%u of the path is not found in the target query block." ER_WARN_QB_NAME_PATH_NOT_SUPPORTED_INSIDE_VIEW eng "Hint %s is ignored. QB_NAME hints with path are not supported inside view definitions." +ER_INCONSISTENT_SLAVE_RECORD + eng "Replicated '%s' record's before-image on table `%s.%s` does not match the local record" diff --git a/sql/sql_class.h b/sql/sql_class.h index 03394f46307c0..99e79479a0682 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -119,7 +119,8 @@ enum enum_ha_read_modes { RFIRST, RNEXT, RPREV, RLAST, RKEY, RNEXT_SAME }; enum enum_duplicates { DUP_ERROR, DUP_REPLACE, DUP_UPDATE }; enum enum_delay_key_write { DELAY_KEY_WRITE_NONE, DELAY_KEY_WRITE_ON, DELAY_KEY_WRITE_ALL }; -enum enum_slave_exec_mode { SLAVE_EXEC_MODE_STRICT, +enum enum_slave_exec_mode { SLAVE_EXEC_MODE_STRINGENT, + SLAVE_EXEC_MODE_STRICT, SLAVE_EXEC_MODE_IDEMPOTENT, SLAVE_EXEC_MODE_LAST_BIT }; enum enum_slave_run_triggers_for_rbr { SLAVE_RUN_TRIGGERS_FOR_RBR_NO, diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index 3e0f1fa1cb795..3439907831a03 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -3684,7 +3684,8 @@ Sys_slave_compressed_protocol( DEFAULT(FALSE)); #ifdef HAVE_REPLICATION -static const char *slave_exec_mode_names[]= {"STRICT", "IDEMPOTENT", 0}; +static const char *slave_exec_mode_names[]= + {"STRINGENT", "STRICT", "IDEMPOTENT", 0}; static Sys_var_on_access_global Slave_exec_mode(