Bug #121287 SIGSEGV in Shared_multi_map<Routine>::Autolocker::~Autolocker() due to unlock-before-delete race condition
Submitted: 15 Sep 11:14
Reporter: nanzhou lin Email Updates:
Status: Open Impact on me:
None 
Category:MySQL Server: Data Dictionary Severity:S3 (Non-critical)
Version: OS:Any
Assigned to: CPU Architecture:Any

[15 Sep 11:14] nanzhou lin
Description:
MySQL server crashes with SIGSEGV (signal 11) in the data dictionary cache infrastructure, specifically in `dd::cache::Shared_multi_map<dd::Routine>::Autolocker::~Autolocker()`. The crash occurs due to a race condition in the `Autolocker` destructor, which releases the mutex **before** executing deferred `delete` operations on evicted DD cache objects. This creates a window where another thread can access, evict, or re-delete the same objects, leading to use-after-free or double-free.

The root cause is in `sql/dd/impl/cache/shared_multi_map.h`, lines 160-173. The `Autolocker` destructor executes in this order:

1. `mysql_mutex_unlock(&m_map->m_lock)` — releases the lock
2. `delete` deferred objects — **executed without lock protection**
3. `delete` deferred elements — **executed without lock protection**

Between step 1 and step 2, another thread may acquire the lock and access the same `Shared_multi_map` instance (via `get()`, `put()`, `release()`, or `drop()`), potentially operating on the same cache elements that are pending deletion. This leads to use-after-free or double-free, causing SIGSEGV.

## Crash Backtrace

```
stack_bottom = 7fc81e617460 thread_stack 0x100000
/usr/local/engine/bin/mysqld(my_print_stacktrace(unsigned char const*, unsigned long)+0x3d) [0x557ebc5620bd]
/usr/local/engine/bin/mysqld(print_fatal_signal(int)+0x314) [0x557ebafa00a4]
/usr/local/engine/bin/mysqld(handle_fatal_signal+0x84) [0x557ebafa0134]
/lib64/libpthread.so.0(+0x13280) [0x7fce32ab8280]
/usr/local/engine/bin/mysqld(dd::cache::Shared_multi_map<dd::Routine>::Autolocker::~Autolocker()+0x66) [0x557ebc324b56]
/usr/local/engine/bin/mysqld(dd::cache::Shared_multi_map<dd::Routine>::release(dd::cache::Cache_element<dd::Routine>*)+0x6f) [0x557ebc33317f]
/usr/local/engine/bin/mysqld(dd::cache::Dictionary_client::Auto_releaser::~Auto_releaser()+0x16b8) [0x557ebac577d8]
/usr/local/engine/bin/mysqld(+0x184e254) [0x557ebb33d254]
/usr/local/engine/bin/mysqld(sp_cache_routine(THD*, enum_sp_type, sp_name const*, bool, sp_head**)+0xb4) [0x557ebb33ce84]
/usr/local/engine/bin/mysqld(sp_cache_routine(THD*, Sroutine_hash_entry*, bool, sp_head**)+0x61) [0x557ebb33cda1]
/usr/local/engine/bin/mysqld(+0x12e35ad) [0x557ebadd25ad]
/usr/local/engine/bin/mysqld(open_tables(THD*, TABLE_LIST**, unsigned int*, unsigned int, Prelocking_strategy*)+0xa96) [0x557ebab8c9f6]
/usr/local/engine/bin/mysqld(open_tables_for_query(THD*, TABLE_LIST*, unsigned int)+0x99) [0x557ebab91339]
/usr/local/engine/bin/mysqld(Sql_cmd_dml::prepare(THD*)+0x521) [0x557ebabc5b51]
/usr/local/engine/bin/mysqld(Sql_cmd_dml::execute(THD*)+0xff) [0x557ebabc4b2f]
/usr/local/engine/bin/mysqld(mysql_execute_command(THD*, bool)+0x3de) [0x557ebabb301e]
/usr/local/engine/bin/mysqld(dispatch_sql_command(THD*, Parser_state*)+0x4d0) [0x557ebabba2e0]
/usr/local/engine/bin/mysqld(dispatch_command(THD*, COM_DATA const*, enum_server_command)+0x558) [0x557ebabbb6a8]
/usr/local/engine/bin/mysqld(do_command(THD*)+0x11b) [0x557ebabbafab]
/usr/local/engine/bin/mysqld(threadpool_process_request(THD*)+0x98) [0x557ebad25718]
/usr/local/engine/bin/mysqld(+0x1237219) [0x557ebad26219]
/usr/local/engine/bin/mysqld(+0x3017be6) [0x557ebcb06be6]
/lib64/libpthread.so.0(+0x8f4b) [0x7fce32aadf4b]
/lib64/libc.so.6(clone+0x3f) [0x7fce3258f7ef]
```

How to repeat:
 High-concurrency workload with stored routines (procedures/functions)

Suggested fix:
// Unlock the multi map when being deleted (e.g. going out of scope)
// and delete the objects and elements.
~Autolocker() {
    mysql_mutex_unlock(&m_map->m_lock);     // Step 1: Lock released
    // Delete the objects.
    for (typename Object_list_type::const_iterator it =
             m_objects_to_delete.begin();
         it != m_objects_to_delete.end(); ++it)
      delete *it;                            // Step 2: Delete without lock

    // Delete the elements.
    for (typename Element_list_type::const_iterator it =
             m_elements_to_delete.begin();
         it != m_elements_to_delete.end(); ++it)
      delete *it;                            // Step 3: Delete without lock
}