Details
-
Bug
-
Status: Stalled (View Workflow)
-
Major
-
Resolution: Unresolved
-
10.11.19, 11.4.13, 11.8.9, 12.3.3, 13.0.2, 13.1.1
-
None
-
Q4/2026 Server Maintenance
Description
Summary
InnoDB records the location of a DATA DIRECTORY tablespace in two fields of dict_table_t:
- the DICT_TF_POS_DATA_DIR bit in flags, which is persisted in SYS_TABLES.TYPE and loaded with the table;
- the string data_dir_path, which is not persisted and is derived from the open tablespace.
srv_get_meta_data_filename(), the IMPORT code, ha_innobase::truncate() and ha_innobase::delete_table() assume that "flag set" implies "data_dir_path non-NULL and complete". Nothing maintains that, and the fields are written and read by different threads without a common lock.
The condition is checked by the debug assertion ut_ad(!DICT_TF_HAS_DATA_DIR(table->flags) || data_dir_path) in srv_get_meta_data_filename().
Specific scenarios
1. RENAME TABLE leaves the flag set but clears data_dir_path
row_rename_table_for_mysql() sets table->data_dir_path= NULL but leaves DICT_TF_POS_DATA_DIR set. While the tablespace is loaded, the next call to dict_get_and_save_data_dir_path() recovers the path from it. A DISCARDed table has no tablespace, so the state "flag set, path NULL" stays. The debug assertion ut_ad(!DICT_TF_HAS_DATA_DIR(table->flags) || data_dir_path) in srv_get_meta_data_filename() no longer holds, causing debug failures in certain scenarios (e.g. running {IMPORT TABLESPACE}}). In release builds the data directory is observably not set manifesting in the following scenarios:
- SHOW CREATE TABLE (and anything else built on update_create_info()) drops the DATA DIRECTORY clause.
- TRUNCATE TABLE recreates the table without DATA DIRECTORY (ha_innobase::truncate() passes data_dir_path on), and the new tablespace is created in the datadir.
- DROP TABLE (ha_innobase::delete_table()) builds the names of the .cfg, .ibd and .isl files to delete from the default location, so files that are in the DATA DIRECTORY are left behind.
- IMPORT TABLESPACE looks for the .cfg in the default location. If it is found there, handle_instant_metadata() hits ut_a(table->data_dir_path) and aborts the server. Without a .cfg file there, the import fails with a "Tablespace is missing" style error.
2. data_dir_path is published before the string is finished
dict_get_and_save_data_dir_path() assigns table->data_dir_path the full tablespace file name (.../db/t.ibd) and only afterwards calls os_file_make_data_dir_path(), which rewrites the string in place. Intermediate states are visible to anyone reading the pointer in between. Readers that can run concurrently with the writer (shared MDL) do not take table->lock_mutex: srv_get_meta_data_filename() (FLUSH TABLES ... FOR EXPORT) and ha_innobase::update_create_info() (SHOW CREATE TABLE, INFORMATION_SCHEMA.TABLES, ALTER preparation). Result: a wrong DATA DIRECTORY in SHOW CREATE TABLE, and potentially a wrong .cfg path for FLUSH TABLES ... FOR EXPORT. DROP TABLE and TRUNCATE TABLE hold an exclusive MDL and do not race with the writer.
Opening a table takes lock_mutex (statistics), which hides the window from sessions that have to open the table first. Sessions that already have it open (LOCK TABLES mode, cached handlers) can hit it.
3. Unsynchronized reads of flags and data_dir_path
srv_get_meta_data_filename() reads table->flags and then table->data_dir_path with no lock and no atomics. The writer stores the flag first and the pointer second, with no release/acquire ordering. On weakly ordered CPUs a reader can see the pointer without the flag (and use the default path), or the flag without the pointer. Callers hold only an MDL, and two sessions can hold a shared MDL on the same table at once, so it does not serialize them.
4. Memory leak in dict_get_and_save_data_dir_path()
!table->data_dir_path is tested before taking the lock, so two threads can both pass it. The second assignment overwrites the first, leaking the string, and a reader may still hold the old pointer. Aside from the leak itself there is no demonstrable defective behavior.
5. Bit-field read-modify-write outside the documented protection
flags is a bit-field that shares one 32-bit word with flags2, skip_alter_undo, file_unreadable and cached. dict0mem.h says that writes to bit-fields of that word require an exclusive MDL. dict_get_and_save_data_dir_path() updates flags under a shared MDL and only lock_mutex. The update is normally a no-op in value, because the flag is already set, but it is still a read-modify-write: on arm64 with clang (RelWithDebInfo) it compiles to a 64-bit load/or/store covering the whole word and the next one, so it writes back stale values of the neighbouring bit-fields.
Other writers of the same word do not take lock_mutex. For example file_unreadable is set at run time when a page of the table cannot be read (row_ins_clust_index_entry_low(), decryption failure, and similar paths). If that happens between the read and the write of the flags update, the file_unreadable marker is lost, and the table is treated as readable again until the bad page is read once more.
Problem 1 is the stable form of the breach, which stays until the table is evicted or the tablespace is loaded again. Problems 2 and 3 are the transient forms. The results are assertion failures and a server abort, a lost DATA DIRECTORY clause, and files created or left in the wrong place.
Attachments
Issue Links
- is blocked by
-
MDEV-41595 Tablespace relocation broken misfeature or spurious test
-
- Open
-
- relates to
-
MDEV-41592 DISCARD/IMPORT TABLESPACE does not work with DATA DIRECTORY
-
- Open
-
-
MDEV-12251 InnoDB: Failing assertion: table->data_dir_path
-
- Closed
-