開發「禁止創建 MyISAM 表」插件時發現:MySQL 重啟後執行 UNINSTALL PLUGIN 導致服務器 Crash。根因是插件封裝的 ha_myisam handler 虛函數表指針在插件卸載後懸空,而 mysql.plugin 表(MyISAM 引擎)打開時持有該 handler,卸載流程中訪問已釋放的代碼地址。本文詳解 Crash 堆棧分析、根因定位與虛函數表指針修復方案。
(gdb) bt
#0 0x00000000006f7658 in column_bitmaps_set (write_set_arg=0x189ad98, read_set_arg=0x189ad98, this=0x7fc3a800fa60)
at /root/mysql-5.6.24-tp/sql/table.h:1228
#1 use_all_columns (this=0x7fc3a800fa60) at /root/mysql-5.6.24-tp/sql/table.h:1238
#2 mysql_uninstall_plugin (thd=thd@entry=0x1d1b030, name=0x1d1d858) at /root/mysql-5.6.24-tp/sql/sql_plugin.cc:2077
#3 0x00000000006e8daf in mysql_execute_command (thd=thd@entry=0x1d1b030) at /root/mysql-5.6.24-tp/sql/sql_parse.cc:4910
#4 0x00000000006ed9d8 in mysql_parse (thd=thd@entry=0x1d1b030, rawbuf=..., length=..., parser_state=...)
at /root/mysql-5.6.24-tp/sql/sql_parse.cc:6391
#5 0x00000000006ef1cd in dispatch_command (command=COM_QUERY, thd=0x1d1b030, packet=..., packet_length=...)
at /root/mysql-5.6.24-tp/sql/sql_parse.cc:1340
#6 0x00000000006f0f24 in do_command (thd=...) at /root/mysql-5.6.24-tp/sql/sql_parse.cc:1037
#7 0x00000000006bd662 in do_handle_one_connection (thd_arg=thd_arg@entry=0x1d1b030) at /root/mysql-5.6.24-tp/sql/sql_connect.cc:982
#8 0x00000000006bd710 in handle_one_connection (arg=arg@entry=0x1d1b030) at /root/mysql-5.6.24-tp/sql/sql_connect.cc:898
#9 0x000000000095dea3 in pfs_spawn_thread (arg=0x1dbb410) at /root/mysql-5.6.24-tp/storage/perfschema/pfs.cc:1860
#10 0x00007fc3f7258182 in start_thread (arg=0x7fc3c61b1700) at pthread_create.c:312
#11 0x00007fc3f676530d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111
mysql_uninstall_plugin 流程:打開 mysql.plugin 表 → 卸載插件(釋放插件代碼內存) → 刪除 mysql.plugin 記錄mysql.plugin 是 MyISAM 表,打開時關聯的 handler 是插件提供的 ha_myisam_wrapperwrapper 類的虛函數表地址失效mysql.plugin 表(如刪除記錄)調用 handler::column_bitmaps_signal(),通過懸空的 vptr 跳轉到已卸載的代碼段 → **Segmentation Fault**// table.h - handler 基類
inline void column_bitmaps_set(MY_BITMAP *read_set_arg, MY_BITMAP *write_set_arg) {
read_set = read_set_arg;
write_set = write_set_arg;
if (file && created)
file->column_bitmaps_signal(); // 此處調用虛函數,file 為插件 wrapper 實例
}
ha_myisam_wrapper 繼承自 ha_myisam,**無新增成員變量**,內存佈局完全一致ha_myisam 的 vtable**// 插件內部維護一個集合
static std::set wrapped_handlers;
ha_myisam_wrapper::ha_myisam_wrapper(handlerton *hton, TABLE_SHARE *table_arg)
: ha_myisam(hton, table_arg) {
wrapped_handlers.insert(this);
}
ha_myisam_wrapper::~ha_myisam_wrapper() {
wrapped_handlers.erase(this);
}
// 獲取原生 ha_myisam 的 vptr
static void* get_native_ha_myisam_vptr() {
ha_myisam native(hton, nullptr); // 構造臨時原生對象
return *reinterpret_cast(&native); // 取對象首地址的 vptr
}
int myisam_disable_plugin_uninstall() {
void* native_vptr = get_native_ha_myisam_vptr();
for (handler* h : wrapped_handlers) {
// 直接覆蓋對象首部的 vptr
*reinterpret_cast(h) = native_vptr;
}
wrapped_handlers.clear();
return 0;
}
static int myisam_disable_deinit(void *p) {
myisam_disable_plugin_uninstall(); // 恢復 vptr
return 0;
}
mysql_declare_plugin(myisam_disable)
{
MYSQL_DAEMON_PLUGIN,
&myisam_disable_plugin,
"myisam_disable",
PLUGIN_AUTHOR_ORACLE,
"Disable MyISAM storage engine",
PLUGIN_LICENSE_GPL,
nullptr, // init
nullptr, // deinit (舊版本)
0x0100,
nullptr,
nullptr,
nullptr,
0,
}
mysql_declare_plugin_end;
// 5.7+ 使用 plugin_deinit 接口
static struct st_mysql_daemon myisam_disable_plugin = {
MYSQL_DAEMON_INTERFACE_VERSION
myisam_disable_deinit // deinit 回調
};
# 1. 安裝插件
INSTALL PLUGIN myisam_disable SONAME 'myisam_disable.so';
# 2. 嘗試創建 MyISAM 表(應失敗)
CREATE TABLE t (id INT) ENGINE=MyISAM;
-- ERROR 1286 (42000): Unknown storage engine 'MyISAM'
# 3. 重啟 MySQL
service mysqld restart
# 4. 卸載插件(修復前會 Crash,修復後正常)
UNINSTALL PLUGIN myisam_disable;
-- Query OK, 0 rows affected
# 5. 再次創建 MyISAM 表(恢復正常)
CREATE TABLE t (id INT) ENGINE=MyISAM;
-- Query OK, 0 rows affected
任何在插件加載期間打開過表的 handler,卸載插件後訪問該表均會 Crash。修復方案必須覆蓋所有已打開表的 handler 實例,而不僅僅是
mysql.plugin表。
deinit 階段主動恢復所有 wrapper 的 vptr--disabled-storage-engines 參數禁用而非自研插件