一次不安全的内存重叠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
正常binlog的null bit是0 112 96
比对现场的测试数据的before image的数值:
);
可以清晰的看出来总共是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
0
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
注意使用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.
执行该程序会有一定概率出现
转载请注明转自高孝鑫的博客!
后一篇:《将博客搬至CSDN》