萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 程式設計 >  >> 資料庫 >> MySQL 教學

修復 MySQL 插件協議 Crash:禁用 MyISAM 插件卸載時的虛函數表指針懸空問題

開發「禁止創建 MyISAM 表」插件時發現:MySQL 重啟後執行 UNINSTALL PLUGIN 導致服務器 Crash。根因是插件封裝的 ha_myisam handler 虛函數表指針在插件卸載後懸空,而 mysql.plugin 表(MyISAM 引擎)打開時持有該 handler,卸載流程中訪問已釋放的代碼地址。本文詳解 Crash 堆棧分析、根因定位與虛函數表指針修復方案。

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

根因定位

  1. mysql_uninstall_plugin 流程:打開 mysql.plugin 表 → 卸載插件(釋放插件代碼內存) → 刪除 mysql.plugin 記錄
  2. mysql.plugin 是 MyISAM 表,打開時關聯的 handler 是插件提供的 ha_myisam_wrapper
  3. 插件卸載後,wrapper 類的虛函數表地址失效
  4. 後續操作 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 vptr

核心思路

  • 插件的 ha_myisam_wrapper 繼承自 ha_myisam,**無新增成員變量**,內存佈局完全一致
  • 唯一差異是虛函數表指針(vptr)指向 wrapper 的 vtable
  • 卸載前,遍歷所有持有 wrapper 的 handler 實例,**將 vptr 替換回原生 ha_myisam 的 vtable**

實現步驟

1. 插件加載時註冊 handler 實例

// 插件內部維護一個集合
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);
}

2. 卸載插件前的清理函數

// 獲取原生 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;
}

3. 插件 deinit 接口調用清理

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 表。

最佳實踐總結

  • 插件開發:handler 封裝類**保持與基類相同內存佈局**,不添加成員變量
  • 插件生命週期:在 deinit 階段主動恢復所有 wrapper 的 vptr
  • 註冊機制:維護全局 wrapper 實例集合,便於批量恢復
  • 測試驗證:必須測試「安裝插件 → 重啟 MySQL → 卸載插件」完整鏈路
  • 生產謹慎:存儲引擎層插件風險極高,建議通過 --disabled-storage-engines 參數禁用而非自研插件
copyright © 萬盛學電腦網 all rights reserved