Bug #120859 mysqld crashes with SIGSEGV in temptable::Block::can_accommodate() due to shared_block_pool use-after-free race conditio
Submitted: 3 Jul 9:14
Reporter: yuxiang jiang (OCA) Email Updates:
Status: Open Impact on me:
None 
Category:MySQL Server: InnoDB storage engine Severity:S3 (Non-critical)
Version:8.0.30 OS:Any
Assigned to: CPU Architecture:Any

[3 Jul 9:14] yuxiang jiang
Description:
mysqld crashes with SIGSEGV (signal 11) in temptable::Block::can_accommodate()
under high-concurrency workloads with frequent connection aborts (Aborted
connection / KILL).

The crash is a use-after-free race condition in the TempTable engine's
Lock_free_shared_block_pool. When a connection is abruptly terminated,
try_release() destroys the shared Block's underlying memory (via free/munmap)
while a concurrent Allocator::allocate() call on the same connection's slot
is still reading from that memory. This produces a SIGSEGV when
can_accommodate() dereferences the freed address.

The race window exists between the free() call inside Block::destroy() and
the read of m_offset+8 inside Block::can_accommodate(). Because glibc uses
mmap for allocations >= 128KB (TempTable blocks start at 1MiB), free()
immediately triggers munmap which invalidates the page table, causing any
subsequent access to fault with SIGSEGV.

The race:

  Thread-1 (execute query)               Thread-2 (connection cleanup/KILL)
  ────────────────────────               ───────────────────────────────────
  Allocator::allocate()
    m_shared_block->is_empty()
    → m_offset != nullptr → false          ha_close_connection()
    → (guard passed)                         shared_block_pool_release()
                                               try_release()
                                                 block.destroy()
                                                   free(m_offset)  ← munmap
                                                   m_offset = nullptr

    m_shared_block->can_accommodate()
      Header::block_size()
        read *(m_offset + 8)
        → *** SIGSEGV ***

The core problem: the Block object in the pool slot is concurrently accessed by
both the allocator (read) and try_release (write+free) without any mutual
exclusion. is_empty() only checks if m_offset == nullptr, so it cannot detect
the stale pointer state between free() and m_offset=nullptr.

How to repeat:
diff --git a/storage/temptable/include/temptable/allocator.h b/storage/temptable/include/temptable/allocator.h
index 46ea7e590be..df846b4a75b 100644
--- a/storage/temptable/include/temptable/allocator.h
+++ b/storage/temptable/include/temptable/allocator.h
@@ -35,6 +35,8 @@ TempTable custom allocator. */
 
 #include "my_dbug.h"
 #include "my_sys.h"
+#include "sql/current_thd.h"
+#include "sql/debug_sync.h"
 #include "sql/mysqld.h"  // temptable_max_ram, temptable_max_mmap
 #include "storage/temptable/include/temptable/block.h"
 #include "storage/temptable/include/temptable/chunk.h"
@@ -600,6 +602,32 @@ inline T *Allocator<T, AllocationScheme>::allocate(size_t n_elements) {
 
   Block *block;
 
+#ifndef NDEBUG
+  if (m_shared_block && m_shared_block->is_empty()) {
+    const size_t block_size =
+        AllocationScheme::block_size(0, n_bytes_requested);
+    *m_shared_block =
+        Block(block_size, AllocationScheme::block_source(block_size));
+    block = m_shared_block;
+  } else {
+    DBUG_EXECUTE_IF("temptable_simulate_shared_block_uaf", {
+      if (m_shared_block && !m_shared_block->is_empty()) {
+        if (m_shared_block->type() == Source::RAM) {
+          MemoryMonitor::RAM::decrease(m_shared_block->size());
+        } else if (m_shared_block->type() == Source::MMAP_FILE) {
+          MemoryMonitor::MMAP::decrease(m_shared_block->size());
+        }
+        m_shared_block->destroy();
+      }
+    });
+    if (m_shared_block &&
+        m_shared_block->can_accommodate(n_bytes_requested)) {
+      block = m_shared_block;
+    } else {
+      block = m_state->get_block_for_new_allocation(n_bytes_requested);
+    }
+  }
+#else
   if (m_shared_block && m_shared_block->is_empty()) {
     const size_t block_size =
         AllocationScheme::block_size(0, n_bytes_requested);
@@ -612,6 +640,7 @@ inline T *Allocator<T, AllocationScheme>::allocate(size_t n_elements) {
   } else {
     block = m_state->get_block_for_new_allocation(n_bytes_requested);
   }
+#endif
 
   /* temptable::Table is allowed to fit no more data than the given threshold
    * controlled through TableResourceMonitor abstraction. TableResourceMonitor
diff --git a/mysql-test/suite/temptable/t/shared_block_pool_uaf.test b/mysql-test/suite/temptable/t/shared_block_pool_uaf.test
new file mode 100644
index 00000000000..d14ffa4a41e
--- /dev/null
+++ b/mysql-test/suite/temptable/t/shared_block_pool_uaf.test
@@ -0,0 +1,43 @@
+--source include/have_debug.inc
+
+--echo #
+--echo # Bug: temptable shared_block_pool use-after-free crash
+--echo #
+--echo # The debug flag temptable_simulate_shared_block_uaf directly destroys
+--echo # the shared_block inside allocate() after is_empty() returns false,
+--echo # simulating the effect of a concurrent try_release(). This causes
+--echo # can_accommodate() to read freed memory.
+--echo #
+--echo # Without fix: SIGSEGV in Block::can_accommodate()
+--echo # With fix: no crash
+--echo #
+
+SET @saved_engine = @@internal_tmp_mem_storage_engine;
+SET GLOBAL internal_tmp_mem_storage_engine = TempTable;
+
+CREATE TABLE t1 (a INT, b VARCHAR(200));
+INSERT INTO t1 VALUES (1, REPEAT('x', 100)), (2, REPEAT('y', 100)),
+                      (3, REPEAT('z', 100)), (4, REPEAT('w', 100));
+INSERT INTO t1 SELECT a + 4, b FROM t1;
+INSERT INTO t1 SELECT a + 8, b FROM t1;
+
+--echo # Warm up: fill shared_block so it becomes non-empty
+SELECT DISTINCT a FROM t1 ORDER BY a;
+
+--echo # Enable debug flag: inside allocate(), after is_empty() returns false,
+--echo # the shared_block will be destroyed before can_accommodate() is called.
+--echo # This precisely simulates the UAF window.
+SET SESSION debug = '+d,temptable_simulate_shared_block_uaf';
+
+--echo # This query triggers allocate() on the non-empty shared_block.
+--echo # Without fix: SIGSEGV. With fix: no crash.
+--error 0,2013
+SELECT DISTINCT a, b FROM t1 ORDER BY a;
+
+SET SESSION debug = '-d,temptable_simulate_shared_block_uaf';
+
+--echo # If server is still alive, the fix works.
+SELECT 1;
+
+DROP TABLE t1;
+SET GLOBAL internal_tmp_mem_storage_engine = @saved_engine;
diff --git a/mysql-test/suite/temptable/r/shared_block_pool_uaf.result b/mysql-test/suite/temptable/r/shared_block_pool_uaf.result
new file mode 100644
index 00000000000..7decd69d6d6
--- /dev/null
+++ b/mysql-test/suite/temptable/r/shared_block_pool_uaf.result
@@ -0,0 +1,53 @@
+#
+# Bug: temptable shared_block_pool use-after-free crash
+#
+# The debug flag temptable_simulate_shared_block_uaf directly destroys
+# the shared_block inside allocate() after is_empty() returns false,
+# simulating the effect of a concurrent try_release(). This causes
+# can_accommodate() to read freed memory.
+#
+# Without fix: SIGSEGV in Block::can_accommodate()
+# With fix: no crash
+#
+# Warm up: fill shared_block so it becomes non-empty
+a
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+# Enable debug flag: inside allocate(), after is_empty() returns false,
+# the shared_block will be destroyed before can_accommodate() is called.
+# This precisely simulates the UAF window.
+# This query triggers allocate() on the non-empty shared_block.
+# Without fix: SIGSEGV. With fix: no crash.
+# If server is still alive, the fix works.
+1
+1

Suggested fix:
Add a per-slot std::atomic<uint32_t> flag to L1_dcache_aligned_block in
shared_block_pool.h as a lightweight CAS spinlock. Both Allocator::allocate()/
deallocate() and try_release() must acquire the flag (CAS FREE->IN_USE) before
operating on the Block, and release it (store FREE) afterward.

The flag is placed in the pool slot struct (not inside Block) because:
1. Block::destroy() frees the memory where any in-block flag would reside
2. Block is a value type; assignment would overwrite any embedded flag
3. Only pool slots need protection; AllocatorState::current_block is thread-private

Changes required (3 files):
  - storage/temptable/include/temptable/shared_block_pool.h
      Add flag field, BLOCK_FLAG_FREE/IN_USE constants, lock_block/unlock_block
      methods, global shared_block_lock/unlock declarations.
      Modify try_release() to spinlock before destroy().

  - storage/temptable/include/temptable/allocator.h
      Forward-declare shared_block_lock/unlock.
      Wrap allocate() and deallocate() shared_block access with lock/unlock.

  - storage/temptable/src/handler.cc
      Implement shared_block_lock/unlock delegating to pool singleton.

Alternatively, defer Block::destroy() from try_release() to the next
try_acquire() call (lazy destruction), eliminating the race by ensuring
destruction only occurs when the new owner has exclusive CAS ownership of
the slot (Oracle upstream patch approach).