From 34d9e4e39983a5482b56528e6ff4f442589ecfc9 Mon Sep 17 00:00:00 2001 From: Thirunarayanan Balathandayuthapani Date: Tue, 14 Jul 2026 16:05:38 +0530 Subject: [PATCH] MDEV-40332 InnoDB: Defragmentation of BASE_IDX in SYS_VIRTUAL failed: Data structure corruption Problem: ======== - While shrinking the system tablespace, InnoDB defragments the system tables to move used pages out of the extents near the end of the file so the tail can be truncated. defragment_level() relocates every page of a to-be-moved extent and, to do so, rewrites the node pointer in the page's parent. A B-tree root page has no parent node pointer, so it is never recorded in m_parent_pages. When the root of a system-table index happens to reside in an extent that was selected for relocation, the parent lookup fails and defragmentation is aborted with DB_CORRUPTION even though nothing is corrupt. Solution: ========= Because a root page cannot move, the system tablespace cannot shrink below the highest root page, and relocating any page at or below it cannot reduce the file size. Exclude that region from relocation up front. SpaceDefragmenter::max_root_extent(): New function returning the highest extent that holds a root page of a system-table index SpaceDefragmenter::find_new_extents(): Use max_root_extent() together with the minimum tablespace size as the lower bound (floor) of the relocation scan, so no extent at or below the highest root is ever added to the relocation map. This avoids the false corruption and also avoids reserving a destination extent for a move that would never happen. The free tail above the highest root is still reclaimed by truncation. --- .../suite/innodb/r/sys_defragment_root.result | 44 +++++++++++++ .../suite/innodb/t/sys_defragment_root.opt | 2 + .../suite/innodb/t/sys_defragment_root.test | 66 +++++++++++++++++++ storage/innobase/dict/dict0crea.cc | 11 +++- storage/innobase/fsp/fsp0fsp.cc | 40 ++++++++++- storage/innobase/include/dict0dict.h | 12 ++-- 6 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 mysql-test/suite/innodb/r/sys_defragment_root.result create mode 100644 mysql-test/suite/innodb/t/sys_defragment_root.opt create mode 100644 mysql-test/suite/innodb/t/sys_defragment_root.test diff --git a/mysql-test/suite/innodb/r/sys_defragment_root.result b/mysql-test/suite/innodb/r/sys_defragment_root.result new file mode 100644 index 0000000000000..986bed791b0cc --- /dev/null +++ b/mysql-test/suite/innodb/r/sys_defragment_root.result @@ -0,0 +1,44 @@ +# +# MDEV-40332 InnoDB: Defragmentation of BASE_IDX in +# SYS_VIRTUAL failed: Data structure corruption +# +# restart: --innodb-data-home-dir=MYSQLTEST_VARDIR/tmp/root_datadir --innodb-log-group-home-dir=MYSQLTEST_VARDIR/tmp/root_datadir --innodb_undo_directory=MYSQLTEST_VARDIR/tmp/root_datadir --debug_dbug=+d,defer_sys_virtual +# SYS_VIRTUAL is absent for now +SELECT NAME FROM INFORMATION_SCHEMA.INNODB_SYS_TABLES +WHERE SPACE=0 AND NAME='SYS_VIRTUAL'; +NAME +SET GLOBAL innodb_file_per_table= 0; +Warnings: +Warning 1287 '@@innodb_file_per_table' is deprecated and will be removed in a future release +SET GLOBAL innodb_limit_optimistic_insert_debug= 2; +CREATE TABLE t1(f1 INT NOT NULL, f2 INT NOT NULL, f3 INT NOT NULL, +INDEX(f1), INDEX(f2), INDEX(f3)) +STATS_PERSISTENT=0 ENGINE=InnoDB; +BEGIN; +INSERT INTO t1 SELECT seq, seq, seq FROM seq_1_to_16384; +INSERT INTO t1 SELECT seq, seq, seq FROM seq_1_to_16384; +COMMIT; +# Restart without the flag: SYS_VIRTUAL is now created +# into the grown tablespace, so its BASE_IDX root page +# lands in a high extent. +# restart: --innodb-data-home-dir=MYSQLTEST_VARDIR/tmp/root_datadir --innodb-log-group-home-dir=MYSQLTEST_VARDIR/tmp/root_datadir --innodb_undo_directory=MYSQLTEST_VARDIR/tmp/root_datadir +# SYS_VIRTUAL now exists, and its BASE_IDX root landed +# above the 10M (640-page) shrink floor +SELECT NAME FROM INFORMATION_SCHEMA.INNODB_SYS_TABLES +WHERE SPACE=0 AND NAME='SYS_VIRTUAL'; +NAME +SYS_VIRTUAL +SELECT PAGE_NO > 640 AS root_above_shrink_floor +FROM INFORMATION_SCHEMA.INNODB_SYS_INDEXES WHERE SPACE=0 AND NAME='BASE_IDX'; +root_above_shrink_floor +1 +SET GLOBAL innodb_limit_optimistic_insert_debug= 2; +INSERT INTO t1 SELECT seq, seq, seq FROM seq_1_to_32; +DROP TABLE t1; +InnoDB 0 transactions not purged +# restart: --innodb-data-home-dir=MYSQLTEST_VARDIR/tmp/root_datadir --innodb-log-group-home-dir=MYSQLTEST_VARDIR/tmp/root_datadir --innodb_undo_directory=MYSQLTEST_VARDIR/tmp/root_datadir --innodb_data_file_path=ibdata1:10M:autoextend:autoshrink --innodb_fast_shutdown=0 +SELECT FILE_SIZE < $fsize AS shrunk +FROM INFORMATION_SCHEMA.INNODB_SYS_TABLESPACES WHERE SPACE=0; +shrunk +1 +# restart diff --git a/mysql-test/suite/innodb/t/sys_defragment_root.opt b/mysql-test/suite/innodb/t/sys_defragment_root.opt new file mode 100644 index 0000000000000..dece6a18311d8 --- /dev/null +++ b/mysql-test/suite/innodb/t/sys_defragment_root.opt @@ -0,0 +1,2 @@ +--innodb_page_size=16k +--innodb_sys_tablespaces diff --git a/mysql-test/suite/innodb/t/sys_defragment_root.test b/mysql-test/suite/innodb/t/sys_defragment_root.test new file mode 100644 index 0000000000000..90da05003ecee --- /dev/null +++ b/mysql-test/suite/innodb/t/sys_defragment_root.test @@ -0,0 +1,66 @@ +--source include/have_innodb.inc +--source include/have_debug.inc +--source include/have_sequence.inc +--source include/not_embedded.inc +--source include/not_windows.inc +--source include/not_valgrind.inc + +--echo # +--echo # MDEV-40332 InnoDB: Defragmentation of BASE_IDX in +--echo # SYS_VIRTUAL failed: Data structure corruption +--echo # + +let bugdir= $MYSQLTEST_VARDIR/tmp/root_datadir; +--mkdir $bugdir +let $dirs= --innodb-data-home-dir=$bugdir --innodb-log-group-home-dir=$bugdir --innodb_undo_directory=$bugdir; + +let $restart_parameters=$dirs --debug_dbug=+d,defer_sys_virtual; +--source include/restart_mysqld.inc + +--echo # SYS_VIRTUAL is absent for now +SELECT NAME FROM INFORMATION_SCHEMA.INNODB_SYS_TABLES +WHERE SPACE=0 AND NAME='SYS_VIRTUAL'; + +SET GLOBAL innodb_file_per_table= 0; +SET GLOBAL innodb_limit_optimistic_insert_debug= 2; +CREATE TABLE t1(f1 INT NOT NULL, f2 INT NOT NULL, f3 INT NOT NULL, + INDEX(f1), INDEX(f2), INDEX(f3)) + STATS_PERSISTENT=0 ENGINE=InnoDB; +BEGIN; +INSERT INTO t1 SELECT seq, seq, seq FROM seq_1_to_16384; +INSERT INTO t1 SELECT seq, seq, seq FROM seq_1_to_16384; +COMMIT; + +--echo # Restart without the flag: SYS_VIRTUAL is now created +--echo # into the grown tablespace, so its BASE_IDX root page +--echo # lands in a high extent. +let $restart_parameters=$dirs; +--source include/restart_mysqld.inc + +--echo # SYS_VIRTUAL now exists, and its BASE_IDX root landed +--echo # above the 10M (640-page) shrink floor +SELECT NAME FROM INFORMATION_SCHEMA.INNODB_SYS_TABLES +WHERE SPACE=0 AND NAME='SYS_VIRTUAL'; + +SELECT PAGE_NO > 640 AS root_above_shrink_floor +FROM INFORMATION_SCHEMA.INNODB_SYS_INDEXES WHERE SPACE=0 AND NAME='BASE_IDX'; + +SET GLOBAL innodb_limit_optimistic_insert_debug= 2; +INSERT INTO t1 SELECT seq, seq, seq FROM seq_1_to_32; +DROP TABLE t1; +--source include/wait_all_purged.inc + +let $fsize=`SELECT FILE_SIZE FROM INFORMATION_SCHEMA.INNODB_SYS_TABLESPACES + WHERE SPACE=0`; + +let $restart_parameters=$dirs --innodb_data_file_path=ibdata1:10M:autoextend:autoshrink --innodb_fast_shutdown=0; +--source include/restart_mysqld.inc + +let SEARCH_FILE= $MYSQLTEST_VARDIR/log/mysqld.1.err; + +evalp SELECT FILE_SIZE < $fsize AS shrunk +FROM INFORMATION_SCHEMA.INNODB_SYS_TABLESPACES WHERE SPACE=0; + +let $restart_parameters=; +--source include/restart_mysqld.inc +--rmdir $bugdir diff --git a/storage/innobase/dict/dict0crea.cc b/storage/innobase/dict/dict0crea.cc index 333d84f7d967c..47c3bd53068c3 100644 --- a/storage/innobase/dict/dict0crea.cc +++ b/storage/innobase/dict/dict0crea.cc @@ -1445,6 +1445,7 @@ dberr_t dict_sys_t::create_or_check_sys_tables() noexcept } if (!sys_virtual) { + DBUG_EXECUTE_IF("defer_sys_virtual", goto commit_trx;); error= que_eval_sql(nullptr, "PROCEDURE CREATE_VIRTUAL() IS\n" "BEGIN\n" "CREATE TABLE\n" @@ -1480,7 +1481,9 @@ dberr_t dict_sys_t::create_or_check_sys_tables() noexcept goto err_exit; } }); - +#ifndef DBUG_OFF +commit_trx: +#endif /* !DBUG_OFF */ trx->commit(); row_mysql_unlock_data_dictionary(trx); trx->clear_and_free(); @@ -1509,6 +1512,12 @@ dberr_t dict_sys_t::create_or_check_sys_tables() noexcept else prevent_eviction(sys_foreign_cols); + DBUG_EXECUTE_IF("defer_sys_virtual", + { + unlock(); + return DB_SUCCESS; + }); + if (sys_virtual); else if (!(sys_virtual= load_table(SYS_TABLE[SYS_VIRTUAL]))) { diff --git a/storage/innobase/fsp/fsp0fsp.cc b/storage/innobase/fsp/fsp0fsp.cc index 73d47e5624514..8bda103026e07 100644 --- a/storage/innobase/fsp/fsp0fsp.cc +++ b/storage/innobase/fsp/fsp0fsp.cc @@ -5317,6 +5317,32 @@ class SpaceDefragmenter final return FIL_NULL; } + /** A B-tree root page cannot be relocated: its page number + is recorded in SYS_INDEXES and the dictionary cache, and it + stores the FSEG segment headers(PAGE_BTR_SEG_LEAF and + PAGE_BTR_SEG_TOP). The system tablespace cannot shrink below + the highest root page, and relocating any page at or below + it would not reduce the tablespace size. + @return the highest extent that holds such a root page, or 0 */ + uint32_t max_root_extent() noexcept + { + uint32_t max_extent= 0; + for (dict_table_t *table : + {dict_sys.sys_tables, dict_sys.sys_columns, dict_sys.sys_indexes, + dict_sys.sys_fields, dict_sys.sys_foreign, + dict_sys.sys_foreign_cols, dict_sys.sys_virtual}) + { + if (!table) + continue; + for (dict_index_t *index= dict_table_get_first_index(table); + index; index= dict_table_get_next_index(index)) + if (index->page != FIL_NULL) + max_extent= std::max(max_extent, + (index->page / m_extent_size) * m_extent_size); + } + return max_extent; + } + /** Defragment the indexes */ dberr_t defragment_index(dict_index_t &index) noexcept { @@ -5327,6 +5353,9 @@ class SpaceDefragmenter final /** Defragment the table */ dberr_t defragment_table(const dict_table_t *table) noexcept { + if (!table) + return DB_SUCCESS; + for (dict_index_t *index= dict_table_get_first_index(table); index; index= dict_table_get_next_index(index)) { @@ -5358,7 +5387,11 @@ class SpaceDefragmenter final uint32_t free_limit= fil_system.sys_space->free_limit; uint32_t fixed_size= srv_sys_space.get_min_size(); - while (free_limit > fixed_size) + /* A root page cannot be relocated, so the tablespace cannot + shrink below the highest root. Never map an extent at or + below that floor */ + uint32_t floor= std::max(fixed_size, max_root_extent()); + while (free_limit > floor) { uint32_t state= m_extent_info[free_limit]; @@ -5566,6 +5599,11 @@ dberr_t IndexDefragmenter::defragment_level( } uint32_t new_extent= space_defrag->get_new_extent(cur_extent); + /* The root page has no parent node pointer cannot be relocated. + find_new_extents() never maps an extent at or below the highest + root, so a root is never scheduled for relocation. */ + ut_ad(cur_page_no != m_index.page || new_extent == cur_extent); + /* There is no need for extent to be changed */ if (new_extent == cur_extent) { diff --git a/storage/innobase/include/dict0dict.h b/storage/innobase/include/dict0dict.h index a2ea4ad2fff5b..091f2e87a0f29 100644 --- a/storage/innobase/include/dict0dict.h +++ b/storage/innobase/include/dict0dict.h @@ -1332,7 +1332,11 @@ class dict_sys_t /** @return whether all non-hard-coded system tables exist */ bool sys_tables_exist() const - { return UNIV_LIKELY(sys_foreign && sys_foreign_cols && sys_virtual); } + { + DBUG_EXECUTE_IF("defer_sys_virtual", + return sys_foreign && sys_foreign_cols;); + return UNIV_LIKELY(sys_foreign && sys_foreign_cols && sys_virtual); + } /** list of persistent tables that can be evicted */ UT_LIST_BASE_NODE_T(dict_table_t) table_LRU; @@ -1508,9 +1512,9 @@ class dict_sys_t bool is_sys_table(table_id_t table_id) const noexcept { return (table_id > 0 && table_id <= 4) || - table_id == sys_foreign->id || - table_id == sys_foreign_cols->id || - table_id == sys_virtual->id; + (sys_foreign && table_id == sys_foreign->id) || + (sys_foreign_cols && table_id == sys_foreign_cols->id) || + (sys_virtual && table_id == sys_virtual->id); } };