背景
新版本正式服上线当天上午, 收到了DS Crash的报警, 因为DS没有开启生成Core文件, 所以只有Log中的堆栈信息:
查看对应的代码:
该段代码在体验服没有发生过变更, 且体验服没有发现与此相关的Crash.
背景结论
- 这是一个偶现的DS Crash.
- 空指针读内存失败导致Crash.
静态分析
通过查看项目源码和glibc源码进行静态分析.
项目代码错误
double
类型在日志格式化输出时, 错误使用了%llf
.
根据标准, double
应当用%f
, long double
应当用%Lf
, %llf
是未定义的.
我们服务器的glibc
对应版本是glibc2.17
, 通过查看对应源码vfprintf.c 将llf
识别为long double
类型.
崩溃堆栈分析
对照日志中的堆栈信息和libc.so的反汇编代码可知, 函数崩溃在:
000000000004c730 <__guess_grouping>:
4c730: 0f b6 06 movzbl (%rsi),%eax
对应在__printf_fp 中的代码是:
由此可知是: *grouping
操作导致了read memory at address 0x0000000000000000
.
但是搜索源码发现, 两处调用__guess_grouping
的地方都有指针判空.
静态分析结论
- 项目误将
double
类型使用long double
类型格式化输出 - 确定程序崩溃在函数
__guess_grouping
, 调用该函数的地方是在将double
或long double
转为字符串 - 调用
__guess_grouping
的地方均有判空, 但出现了空指针访问
动态分析
静态分析陷入了死胡同, 于是搭建环境重现问题.
使用假大厅每100ms启动一个DS, 最多并存200个DS, 每个DS存活1分钟, 累计启动40808次, 其中8次Crash. (平均崩溃率:1.96‱)
看了多个core文件后, 发现的寄存器rsi
的值都是:
rsi 0x4460068200e6b654 4926945147773957716
这是一个无效的内存地址, 故而发生判空没问题, 但是读内存失败的问题.
通过查看文档mpx-linux64-abi P20
2. If the class is INTEGER or POINTER, the next available register of the sequence %rdi, %rsi, %rdx, %rcx, %r8 and %r9 is used.
根据第二条可知: %rdi
对应的是 intdig_max
, %rsi
对应的是 grouping
.
虽然%rsi
是一个无效的地址, 但%rdi
是对的:
rdi 0x1 1
通过GDB查看上一层写入%rsi
的内存, 发现其值也是也是该无效地址.
动态分析结论
- 堆栈没有被破坏, 没有异常的函数调用
grouping
在上一层中指针是一个无效指针, 导致判空失败和传入函数__guess_grouping
后崩溃- 支线任务 为何无效的指针在信号捕获时传参为空指针?
- 支线任务 错误的传参实际读到的是什么?
假设/验证
假设当传入特定long double
时, vswprintf
会引起Crash.
验证思路:long double
是128位的, 用union
将其转化为两个int64_t
, 先对int64_t
进行输出, 然后再调用%Lf
输出long double
.
验证结果: 成功找到一些数值, 可以将崩溃率从1.96‱提高到了30%, 以下为其中一个数值的演示代码:
由此可以得出, glibc2.17存在bug, 在特定long double
数值下, printf
会导致程序Crash.
在glibc官网有这样一个Bug 4586 - printf crashes on some ‘long double’ values
该问题于2007-06-02
被人发现过, 并且于2007-06-08
被修复了
* [BZ #4586]
2007-06-06 Jakub Jelinek <jakub@redhat.com>
BZ #4586
* sysdeps/i386/ldbl2mpn.c (__mpn_extract_long_double): Treat
pseudo-zeros as zero.
* sysdeps/x86_64/ldbl2mpn.c: New file.
* sysdeps/ia64/ldbl2mpn.c: New file.
而我们的glibc
版本是libc-2.17.so
发布于2012-12-25
?!
验证结论
- 当特定的
long double
进行格式化输出时,glibc
会导致崩溃 - 该问题已于2007年修复, 但我们使用的版本是2012年发布的, 为什么还会Crash?
- 导致Crash的long double是有效的浮点数吗?
- 支线任务 为什么会导致随机Crash(使用gdb运行不会Crash)
- 支线任务 glibc在哪次提交修复了该问题
验证不同版本的glibc
使用docker
针对不同版本的glibc
做实验:
libc2.17
:tlinux
(libc2.17
-157.tl2.2
)会崩溃libc2.17
:centos7
(libc2.17
-292.el7
)会崩溃(centos7最新版)libc2.17
:ubuntu:13.10
(libc2.17
)会崩溃libc2.19
:ubuntu:14.04
(libc2.19
)会崩溃libc2.21
:ubuntu:15.04
(libc2.21
)不会崩溃libc2.21
:ubuntu:15.10
(libc2.21
)不会崩溃libc2.28
:centos8
(libc2.28
-42.el8.1
)不会崩溃
由此可知, BZ #4586 并没有在2007年被修复, 是在2.19(发布于2014-02-08)-2.21(发布于2015-02-06)的某个版本被修复的,
BZ #4586
有这样一行记录:fweimer 2014-07-04 16:25:35 UTC CC可以佐证这一假设.
导致Crash的long double
是有效的浮点数吗?
根据long double的数据结构, 对导致Crash的70多个数值进行分析, 发现其exponent
为0, 则浮点数的指数E等于1-16383(十进制 6.909499226981e-310#DEN double),是一个非常小的浮点数, 是合法的。
总体结论
由于错误使用了格式化字符串, 导致触发了glibc
老版本存在的bug.