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).
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).