Bug #121259 Use-after-free in InnoDB virtual column template during FK cascade DELETE
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.
Portions of this website are copyright © 2001, 2002 The PHP Group
Page generated in 0.038 sec. using MySQL 8.4.7-u6-cloud
Timestamp references displayed by the system are UTC.
Content reproduced on this site is the property of the
respective copyright holders.
It is not reviewed in advance
by Oracle and does not necessarily represent the opinion of
Oracle or any other party.
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.