Description:
InnoDB's ha_innobase::open() frees and rebuilds the virtual column template (vc_templ->vtempl[]) when a table's ref_count drops to 1. A concurrent FK cascade
DELETE reads vtempl[] entries via innobase_get_computed_value() without holding dict_sys_mutex, creating a use-after-free race: one thread frees the template
while another is still reading from it.
The FK cascade accesses the child table through foreign->foreign_table (a direct pointer) without incrementing ref_count. This means another session's open() can
observe ref_count == 1 and trigger the free+rebuild at any time during the cascade.
What was done
A DELETE statement on a parent table with an ON DELETE CASCADE foreign key was executed while a concurrent SELECT on the child table (which has an indexed virtual
generated column) was running.
What was expected
The DELETE should complete successfully — the cascade should delete child rows and update secondary indexes on the virtual column without interference from
concurrent reads.
What was obtained (release build behavior)
In a release build, the cascade reads from freed memory. Depending on allocator behavior:
- If the freed memory is reused by the allocator for the rebuilt template, the cascade reads valid-looking data from the new allocation — the UAF is silent, but
the virtual column values may be computed from stale field offsets, producing incorrect index entries (data corruption).
- If the freed memory is not reused, the cascade reads garbage values for mysql_col_offset and mysql_col_len — this can cause a segfault (server crash) when the
garbage offset is used to access the MySQL row buffer.
In a debug build, the ut_error assertion on ROW_NOT_FOUND (triggered by the corrupted virtual column value) fires and the server crashes with SIGABRT.
How to repeat:
-- Setup: parent + child with FK CASCADE + indexed virtual column
CREATE TABLE parent (id INT PRIMARY KEY, val INT) ENGINE=InnoDB;
CREATE TABLE child (
id INT PRIMARY KEY,
parent_id INT,
val INT,
g6 VARCHAR(24) GENERATED ALWAYS AS (CONCAT(val, val)) VIRTUAL,
INDEX idx_g6 (g6),
CONSTRAINT fk FOREIGN KEY (parent_id)
REFERENCES parent(id) ON DELETE CASCADE
) ENGINE=InnoDB;
INSERT INTO parent VALUES (1, 10), (2, 20), (3, 30);
INSERT INTO child (id, parent_id, val) VALUES (1, 1, 10), (2, 2, 20), (3, 3, 30);
FLUSH TABLES;
Then in two concurrent sessions:
Session 1:
DELETE FROM parent WHERE id = 1;
Session 2 (immediately after, while Session 1's cascade is running):
SELECT COUNT(*) FROM child;
The race window is between the cascade reading vtempl[i] in innobase_get_computed_value() and using it — the concurrent SELECT triggers ha_innobase::open() which
frees the template mid-read.
An ASAN-instrumented build reliably detects this as heap-use-after-free at innobase_get_computed_value() (ha_innodb.cc). A debug build with DEBUG_SYNC
instrumentation can reproduce it deterministically without ASAN.
Description: InnoDB's ha_innobase::open() frees and rebuilds the virtual column template (vc_templ->vtempl[]) when a table's ref_count drops to 1. A concurrent FK cascade DELETE reads vtempl[] entries via innobase_get_computed_value() without holding dict_sys_mutex, creating a use-after-free race: one thread frees the template while another is still reading from it. The FK cascade accesses the child table through foreign->foreign_table (a direct pointer) without incrementing ref_count. This means another session's open() can observe ref_count == 1 and trigger the free+rebuild at any time during the cascade. What was done A DELETE statement on a parent table with an ON DELETE CASCADE foreign key was executed while a concurrent SELECT on the child table (which has an indexed virtual generated column) was running. What was expected The DELETE should complete successfully — the cascade should delete child rows and update secondary indexes on the virtual column without interference from concurrent reads. What was obtained (release build behavior) In a release build, the cascade reads from freed memory. Depending on allocator behavior: - If the freed memory is reused by the allocator for the rebuilt template, the cascade reads valid-looking data from the new allocation — the UAF is silent, but the virtual column values may be computed from stale field offsets, producing incorrect index entries (data corruption). - If the freed memory is not reused, the cascade reads garbage values for mysql_col_offset and mysql_col_len — this can cause a segfault (server crash) when the garbage offset is used to access the MySQL row buffer. In a debug build, the ut_error assertion on ROW_NOT_FOUND (triggered by the corrupted virtual column value) fires and the server crashes with SIGABRT. How to repeat: -- Setup: parent + child with FK CASCADE + indexed virtual column CREATE TABLE parent (id INT PRIMARY KEY, val INT) ENGINE=InnoDB; CREATE TABLE child ( id INT PRIMARY KEY, parent_id INT, val INT, g6 VARCHAR(24) GENERATED ALWAYS AS (CONCAT(val, val)) VIRTUAL, INDEX idx_g6 (g6), CONSTRAINT fk FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE CASCADE ) ENGINE=InnoDB; INSERT INTO parent VALUES (1, 10), (2, 20), (3, 30); INSERT INTO child (id, parent_id, val) VALUES (1, 1, 10), (2, 2, 20), (3, 3, 30); FLUSH TABLES; Then in two concurrent sessions: Session 1: DELETE FROM parent WHERE id = 1; Session 2 (immediately after, while Session 1's cascade is running): SELECT COUNT(*) FROM child; The race window is between the cascade reading vtempl[i] in innobase_get_computed_value() and using it — the concurrent SELECT triggers ha_innobase::open() which frees the template mid-read. An ASAN-instrumented build reliably detects this as heap-use-after-free at innobase_get_computed_value() (ha_innodb.cc). A debug build with DEBUG_SYNC instrumentation can reproduce it deterministically without ASAN.