修复 disable myisam 插件的 crash 问题

之前写了一个 禁止创建 MyISAM 表的插件blog),有报 issue,在 MySQL 重启后 uninstall plugin 会引发 crash。

测试了一下果然可以复现。挂上 gdb 后,backstace 如下:

(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=parser_state@entry=0x7fc3c61b02f0) 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

进一步追踪这个 column_bitmaps_set 的调用

  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->column_bitmaps_signal 这一步出错,反汇编以后,也确实是在 callq 指令处。注意到反汇编后,gdb 在 callq 指令所调用函数的名字没有能显示出来,说明这个地址很可能已经无效。这里 file 成员变量即是插件生成的替换了 create 操作的 ha_mysiam 的 wrapper。打印其内容发现 _vptr.handler = 0x7fc3d44768d0 ,这里也未能指出指向的类名。

阅读了 sql_handler.cc 中 mysql_uninstall_plugin 的代码后,明白了原因。卸载插件会删除 mysql.plugin 表中的相应记录。在 mysql_uninstall_plugin 中,先打开了 mysql.plugin 表,然后卸载插件,然后删除相应记录。问题就出在 plugin 表也是一个 MyISAM 表。如果打开的时候插件是启用的,那么其对应的 handler 是插件中的 wrapper。而插件卸载以后,对应地址的代码已经不复存在,因此就造成了 crash。那么既然是这个原因,又想到了,如果在插件加载期间打开过表,卸载插件后访问这些表应该也会出错。试了一下果然如此,一阵冷汗。

接着就是考虑如何解决这个问题。既然插件卸载后无法内存,那就需要在卸载时将原有的指向 wrapper 的 handler 都换回原有的 ha_myisam。handler 本身的内存是由 mysql 管理的,并不会因为插件卸载而释放。所以可以修改。wrapper 是 ha_myisam 的派生类,而且没有增加成员,因此内存布局和 ha_myisam 是完全相同的。唯一差异就在虚函数表。C++ 似乎没有这种强制转换到父类的语法。于是就考虑直接替换虚函数表指针。而 C++ 虚函数指针中总是对象最前面。于是直接建一个真的 ha_myisam 对象,然后把它的虚函数指针复制回去。试下来果然可以。接着就是在 wrapper 的构造和析构函数中维持一个当前现有 handler 的集合,用于在卸载时处理即可。

Leave a Reply

Your email address will not be published. Required fields are marked *