From cd79f102110a3543bc78ef4bec4dbeadaf1f1a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Wed, 29 Nov 2023 18:57:57 +0200 Subject: [PATCH] MDEV-31441 BLOB corruption on UPDATE of PRIMARY KEY with FOREIGN KEY row_upd_clust_rec_by_insert(): If we are resuming from a lock wait, reset the 'disowned' flag of the BLOB pointers in 'entry' that we copied from 'rec' on which we had invoked btr_cur_disown_inherited_fields() before the lock wait started. In this way, the inserted record with the updated PRIMARY KEY value will have the BLOB ownership associated with itself, like it is supposed to be. Note: If the lock wait had been aborted, then rollback would have invoked btr_cur_unmark_extern_fields() and no corruption would be possible. Reviewed by: Vladislav Lesin Tested by: Matthias Leich --- mysql-test/suite/innodb/r/foreign_key.result | 27 ++++++++++++++-- mysql-test/suite/innodb/t/foreign_key.test | 34 ++++++++++++++++++-- storage/innobase/row/row0upd.cc | 25 +++++++++++++- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/mysql-test/suite/innodb/r/foreign_key.result b/mysql-test/suite/innodb/r/foreign_key.result index f87aeb56b77..769af9cc220 100644 --- a/mysql-test/suite/innodb/r/foreign_key.result +++ b/mysql-test/suite/innodb/r/foreign_key.result @@ -727,7 +727,9 @@ pk a b 13 0 1 14 0 1 15 1 0 -disconnect con1; +connection con1; +COMMIT; +connection default; InnoDB 0 transactions not purged CHECK TABLE t1; Table Op Msg_type Msg_text @@ -906,5 +908,26 @@ CONSTRAINT FK_t1_id FOREIGN KEY (t1_id) REFERENCES t1 (id) ALTER TABLE t1 MODIFY id INT unsigned AUTO_INCREMENT; DROP TABLE t1,t2; # -# End of 10.4 tests +# MDEV-31441 BLOB corruption on UPDATE of PRIMARY KEY with FOREIGN KEY # +CREATE TABLE t1 (pk INT PRIMARY KEY, t TEXT) ENGINE=InnoDB; +CREATE TABLE t2 (pk INT PRIMARY KEY, FOREIGN KEY (pk) REFERENCES t1(pk)) +ENGINE=InnoDB; +SET @blob = REPEAT('A', @@innodb_page_size / 2); +INSERT INTO t1 SET pk=1, t=@blob; +INSERT INTO t2 SET pk=1; +connection con1; +BEGIN; +DELETE FROM t2; +connection default; +UPDATE t1 SET pk=12; +connection con1; +COMMIT; +disconnect con1; +connection default; +UPDATE t1 SET pk=1; +SELECT pk,t=@blob FROM t1; +pk t=@blob +1 1 +DROP TABLE t2, t1; +# End of 10.4 tests diff --git a/mysql-test/suite/innodb/t/foreign_key.test b/mysql-test/suite/innodb/t/foreign_key.test index 032bab2408d..9de72eb4681 100644 --- a/mysql-test/suite/innodb/t/foreign_key.test +++ b/mysql-test/suite/innodb/t/foreign_key.test @@ -732,7 +732,9 @@ SELECT a FROM t1 FORCE INDEX(a); # the "goto rollback_to_savept" in row_mysql_handle_errors() is reverted. SELECT * FROM t1; # Allow purge to continue by closing the read view. -disconnect con1; +connection con1; +COMMIT; +connection default; # Wait for purge. With the fix reverted, the server would crash here. --source include/wait_all_purged.inc @@ -954,7 +956,35 @@ ALTER TABLE t1 MODIFY id INT unsigned AUTO_INCREMENT; DROP TABLE t1,t2; --echo # ---echo # End of 10.4 tests +--echo # MDEV-31441 BLOB corruption on UPDATE of PRIMARY KEY with FOREIGN KEY --echo # +CREATE TABLE t1 (pk INT PRIMARY KEY, t TEXT) ENGINE=InnoDB; +CREATE TABLE t2 (pk INT PRIMARY KEY, FOREIGN KEY (pk) REFERENCES t1(pk)) +ENGINE=InnoDB; + +SET @blob = REPEAT('A', @@innodb_page_size / 2); +INSERT INTO t1 SET pk=1, t=@blob; +INSERT INTO t2 SET pk=1; +--connection con1 +BEGIN; +DELETE FROM t2; +--connection default +# The following will be blocked by a FOREIGN KEY check on pk=1 in t2. +--send +UPDATE t1 SET pk=12; +--connection con1 +let $wait_condition= +SELECT count(*) > 0 FROM INFORMATION_SCHEMA.PROCESSLIST WHERE state='Updating'; +--source include/wait_condition.inc +COMMIT; +--disconnect con1 +--connection default +--reap +UPDATE t1 SET pk=1; +SELECT pk,t=@blob FROM t1; +DROP TABLE t2, t1; + +--echo # End of 10.4 tests + --source include/wait_until_count_sessions.inc diff --git a/storage/innobase/row/row0upd.cc b/storage/innobase/row/row0upd.cc index 0700792e441..58c93e4db0f 100644 --- a/storage/innobase/row/row0upd.cc +++ b/storage/innobase/row/row0upd.cc @@ -2698,6 +2698,25 @@ row_upd_clust_rec_by_insert_inherit_func( return(inherit); } +/** Mark 'disowned' BLOBs as 'owned' and 'inherited' again, +after resuming from a lock wait. +@param entry clustered index entry */ +static ATTRIBUTE_COLD void row_upd_reown_inherited_fields(dtuple_t *entry) +{ + for (ulint i= 0; i < entry->n_fields; i++) + { + const dfield_t *dfield= dtuple_get_nth_field(entry, i); + if (dfield_is_ext(dfield)) + { + byte *blob_len= static_cast(dfield->data) + + dfield->len - (BTR_EXTERN_FIELD_REF_SIZE - BTR_EXTERN_LEN); + ut_ad(*blob_len & BTR_EXTERN_OWNER_FLAG); + *blob_len= byte(*blob_len & ~BTR_EXTERN_OWNER_FLAG) | + BTR_EXTERN_INHERITED_FLAG; + } + } +} + /***********************************************************//** Marks the clustered index record deleted and inserts the updated version of the record to the index. This function should be used when the ordering @@ -2776,12 +2795,16 @@ row_upd_clust_rec_by_insert( /* If the clustered index record is already delete marked, then we are here after a DB_LOCK_WAIT. Skip delete marking clustered index and disowning - its blobs. */ + its blobs. Mark the BLOBs in the index entry + (which we copied from the already "disowned" rec) + as "owned", like it was on the previous call of + row_upd_clust_rec_by_insert(). */ ut_ad(row_get_rec_trx_id(rec, index, offsets) == trx->id); ut_ad(!trx_undo_roll_ptr_is_insert( row_get_rec_roll_ptr(rec, index, offsets))); + row_upd_reown_inherited_fields(entry); goto check_fk; }