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.