Bug #121259 Use-after-free in InnoDB virtual column template during FK cascade DELETE

Submitted: 10 Sep 4:05
Reporter: Matheus Aires Email Updates:
Status: Open Impact on me:
None 
Category:MySQL Server: InnoDB storage engine Severity:S2 (Serious)
Version:8.0.45 OS:Any
Assigned to: CPU Architecture:Any
Tags: generated column, InnoDB Crash, memory corruption, race condition

[10 Sep 4:05] Matheus Aires
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.