加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

一次不安全的内存重叠memcpy引发的“”

(2022-10-06 16:08:25)
标签:

mysql

分类: 工作学习
很早之前团队编写了一个版本的binlog revert工具,最近在实际使用中发现翻转后的binlog event有概率使mysql crash。当然mysql crash本身是一个问题,只影响mysql5.7的版本,具体参见我们提给官方的bug: https://bugs.mysql.com/bug.php?id=108546

本文主要分析和讨论为何会随机生成一个有问题的binlog event。为了研究这个问题,我们先具体分析下有问题的binlog event和正常的binlog event的差异。

分析问题binlog event

因为原始的binlog是一个update的binlog event,所以对应的mysql内的代码入口是sql/log_event.cc的 do_index_scan_and_update,该函数在before image和after image解析前都会调用Rows_log_event::unpack_current_row 并最终调用sql/sql_record.cc的unpack_row函数,函数声明如下:

213 unpack_row(Relay_log_info const *rli,
214            TABLE *table, uint const colcnt,
215            uchar const *const row_data, MY_BITMAP const *cols,
216            uchar const **const current_row_end, ulong *const master_reclength,
217            uchar const *const row_end)

所以可以看出row_data指向的就是row image,所以在这里打断点就可以把整个row event打印出来。

如下分别比对了反转异常和反转正常的2个binlog event,可以明显看出最开始的2个字节不一样:
如下是异常的row image

0x7fff9c010550: 54 0 96 47 0 48 51 57
0x7fff9c010558: 99 54 97 51 50 45 100 99
0x7fff9c010560: 49 98 45 52 57 55 56 45
0x7fff9c010568: 98 102 54 99 45 100 102 54

如下是正常的row image

0x7fff9c010550: 0 112 96 47 0 48 51 57
0x7fff9c010558: 99 54 97 51 50 45 100 99
0x7fff9c010560: 49 98 45 52 57 55 56 45
0x7fff9c010568: 98 102 54 99 45 100 102 54



这个表有24列,所以null bit的是 (colums_num+ 7) / 8 = 3

所以:

问题binlog的null bit是54 0 96

  转换为二进制为:00110110  00000000   01100000

正常binlog的null bit是0 112 96

  转换为二进制为:00000000  01110000   01100000


比对现场的测试数据的before image的数值:

 INSERT INTO `t5k_trade_is_fields_rslt` values(
   '039c6a32-dc1b-4978-bf6c-df6ea0cd80d4-1662617934'    ,
   1                          
   0                          
   '1'                 ,
   'K.A.S. '            ,
   'K.S.A.'             ,
   1                              
   '625224'               ,
   'T03,T04'              ,
   '2'                    
   '1'                    
   '3:报文'               ,
   NULL                
   NULL               ,
   NULL               ,
   '2'                 ,
   'K.S.A. 表示沙特阿拉伯的国家缩写,不涉及相关名单' ,
   '4'                 ,
   '不涉及相关名单'           ,
   '4'                ,
   '6'                  
   NULL               ,
   NULL               ,
   ''
);

可以清晰的看出来总共是5个NULL,分别位于中间的8列和最后的8列,即与正常row image的null bit是对应的。
所以问题row image的null bit肯定写错了,即最开始的2个字节写错了。

分析工具代码问题

如下是工具代码中进行update row image翻转的代码

405 int MySQLBinlogRevertTool::revert_update_data(
406     MySQLRowEvent *event, MySQLTableMapEvent *table_map_event) {
407   int rc = -1;
408   uint64_t columns_len = 0;
409   unsigned char *rows_buf = NULL;
410   unsigned char *columns = NULL;
411   unsigned char *used_columns = NULL;
412   unsigned char *metadata = NULL;

...

457     if (!(length1 = sweep_one_row(value, columns_len,
458                                   used_columns,       
459                                   columns,           
460                                   metadata))) {       
461       goto end;
462     }
463     value += length1;
464 
465     size_t length2;
466     if (!(length2 = sweep_one_row(value, columns_len, used_columns, columns,
467                                   metadata))) {
468       goto end;
469     }
470     value += length2;
471 
472     LOG_INFO("Revert update row before image [%u], after image [%u]\n", length1,
473              length2);


...

483     
484     unsigned char *swap_buf1 = new unsigned char[length1];
485     memcpy(swap_buf1, start_pos, length1);
486 
487     memcpy(start_pos, start_pos + length1, length2);
488     memcpy(start_pos + length2, swap_buf1, length1);


整个逻辑是457行算出before image的长度length1,466行算出after image的长度length2

然后在487行和488行进行memcpy交换。

但487行的memcpy是有问题的,因为可能dst的(start_pos, start_pos+length2) 的内存区域与 src的(start_pos+length1, start_pos+length2+length1) 可能重叠,如果length2>length1。

参考文章https://www.cnblogs.com/splitfire/p/14135941.html

可知,内存重叠场景下的memcpy行为是有不确定性的,行为可能错误。



现场实际问题的binlog(翻转后的)before image长度是219:

(gdb) p pack_ptr
$3 = (const uchar *) 0x7fff9001062b ""
(gdb) x/10b pack_ptr
0x7fff9001062b: 0       112     112     47           48      51      57
0x7fff90010633: 99      54
(gdb) p row_data
$4 = (const uchar * const) 0x7fff90010550 ""
(gdb) p pack_ptr-row_data
$5 = 219



after image的长度是217:

(gdb) p pack_ptr
$6 = (const uchar *) 0x7fff90010704 "\t"
(gdb) p row_data
$7 = (const uchar * const) 0x7fff9001062b ""
(gdb) p pack_ptr-row_data
$9 = 217

因为是反转后的,所以dbscale在做revert反转时的before image长度length1是217,而after image 长度length2是219,length2>length1存在内存重叠,并且影响的长度是2,正好与问题binlog影响长度一致。


所以从拷贝安全的角度来说,上述487行需要避免直接重叠拷贝,如果length2>length1,那么需要先把after image拷贝到一个临时内存中,再从临时内存往start_pos拷贝。

验证memcpy不安全内存重叠拷贝问题的随机性

参考文章https://www.cnblogs.com/splitfire/p/14135941.html,使用如下程序对memcpy不安全的内存重叠场景进行测试,看是否问题是随机的。

#include
#include

    enum
    {
    eInvalid = 0,
    eMemcpy,
    eMemmove,
    eByteCopy,
    eMax,
    };
    
    bool memoryCopyTest(int copyType)
    {
    if (copyType <= eInvalid || copyType >= eMax)
    {
    return false;
    }
    
    using Juint8 = unsigned char;
    
    // 待处理数据包
    Juint8 packet[] = {
    0x68, 0x2f, 0x0c, 0x00, 0x06, 0x00, 0x74, 0x01, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02,
    0x04, 0x00, 0x16, 0x44, 0x50, 0x44, 0x38, 0x30, 0x30, 0x32, 0x30, 0x31, 0x39, 0x30, 0x35, 0x30,
    0x38, 0x31, 0x36, 0x35, 0x30, 0x2e, 0x64, 0x61, 0x74, 0xf7, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00,
    0x00,
    0x68, 0xe1, 0x0e, 0x00, 0x06, 0x00, 0x74, 0x01, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02,
    0x06, 0xf7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64,
    0x65, 0x09, 0x28, 0x24, 0x28, 0x49, 0x43, 0x43, 0x53, 0x44, 0x45, 0x56, 0x48, 0x4f, 0x4d, 0x45,
    0x29, 0x2f, 0x73, 0x72, 0x63, 0x2f, 0x68, 0x6d, 0x69, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e,
    0x73, 0x61, 0x75, 0x78, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x61, 0x75, 0x78, 0x2e,
    0x70, 0x72, 0x69, 0x29, 0x31, 0x0d, 0x0a, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x09, 0x28,
    0x24, 0x28, 0x49, 0x43, 0x43, 0x53, 0x44, 0x45, 0x56, 0x48, 0x4f, 0x4d, 0x45, 0x29, 0x2f, 0x73,
    0x72, 0x63, 0x2f, 0x68, 0x6d, 0x69, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x61, 0x75,
    0x78, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x61, 0x75, 0x78, 0x2e, 0x70, 0x72, 0x69,
    0x29, 0x32, 0x0d, 0x0a, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x09, 0x28, 0x24, 0x28, 0x49,
    0x43, 0x43, 0x53, 0x44, 0x45, 0x56, 0x48, 0x4f, 0x4d, 0x45, 0x29, 0x2f, 0x73, 0x72, 0x63, 0x2f,
    0x68, 0x6d, 0x69, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x61, 0x75, 0x78, 0x2f, 0x70,
    0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x61, 0x75, 0x78, 0x2e, 0x70, 0x72, 0x69, 0x29, 0x33,
    };
    // 数据包长度
    const size_t nPacketLen = sizeof(packet);
    
    // 接收缓冲区
    Juint8 readBuffer[1024] = {};
    memcpy(readBuffer, packet, nPacketLen);
    size_t nReadBufferDataLength = nPacketLen;
    
    // 已处理数据长度
    int processedDataLen = 49;
    nReadBufferDataLength -= processedDataLen;
    
    // 剩余需要处理的数据
    Juint8 dataRemainingAfterProcess[1024] = {};
    memcpy(dataRemainingAfterProcess, readBuffer + processedDataLen, nReadBufferDataLength);
    
    // 拷贝比较
    switch (copyType)
    {
    case eMemcpy:
    {
    memcpy(readBuffer, readBuffer + processedDataLen, nReadBufferDataLength);
    }
    break;
    case eMemmove:
    {
    memmove(readBuffer, readBuffer + processedDataLen, nReadBufferDataLength);
    }
    break;
    case eByteCopy:
    {
    for (size_t i = 0; i < nReadBufferDataLength; ++i)
    {
    readBuffer[i] = readBuffer[processedDataLen + i];
    }
    }
    break;
    default:
    break;
    }
    
    for (size_t i = 0; i < nReadBufferDataLength; ++i)
    {
    if (readBuffer[i] != dataRemainingAfterProcess[i])
    {
    std::cout << "memory corrupted!" << std::endl;
    return false;
    }
    }
    
    std::cout << "Copy success!" << std::endl;
    return true;
    }

    int main(int argc, char** argv)
    {
    memoryCopyTest(eMemcpy);
    return 0;
    }
注意使用centos7进行测试,因为这个问题和OS的内核与所带的glibc版本相关:

环境:ldd --version
ldd (GNU libc) 2.17
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
执行该程序会有一定概率出现


转载请注明转自高孝鑫的博客!

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有