干活的时候, 同事又塞了个 coredump, 咋一看比较直观 r14 寄存器为 0, 导致 SIGSEGV.
0x00007f54b0335cf0 <+0>: push r15
0x00007f54b0335cf2 <+2>: push r14
0x00007f54b0335cf4 <+4>: push r13
0x00007f54b0335cf6 <+6>: push r12
0x00007f54b0335cf8 <+8>: push rbx
0x00007f54b0335cf9 <+9>: mov r14,QWORD PTR [rdi]
0x00007f54b0335cfc <+12>: test r14,r14
0x00007f54b0335cff <+15>: je 0x7f54b0335d07 <A+23>
0x00007f54b0335d01 <+17>: add QWORD PTR [r14],0xffffffffffffffff
0x00007f54b0335d05 <+21>: je 0x7f54b0335d11 <A+33>
0x00007f54b0335d07 <+23>: pop rbx
0x00007f54b0335d08 <+24>: pop r12
0x00007f54b0335d0a <+26>: pop r13
0x00007f54b0335d0c <+28>: pop r14
0x00007f54b0335d0e <+30>: pop r15
0x00007f54b0335d10 <+32>: ret
=> 0x00007f54b0335d11 <+33>: mov rbx,QWORD PTR [r14+0x30]
(gdb) i r r14
r14 0x0 0
习惯性地将对应汇编使用 as2cfg 转换为控制流图:
就发现很奇怪的地方, 如控制流图所示, crash 所处控制流块只有一个入边, 即当 crash 在 rip=0x7f54b0335d11 时, 当时 CPU 的情况一定是:
- 执行了
0x7f54b0335d01 add QWORD PTR [r14],0xffffffffffffffff
- 执行了
0x7f54b0335d05 je 0x7f54b0335d11
; - 执行
0x7f54b0335d11 mov rbx,QWORD PTR [r14+0x30]
;
在执行 0x7f54b0335d11 时由于 r14 = 0 crash 了. 问题是如果 r14 = 0, 在执行 0x7f54b0335d01 时就应该 crash 啊! 我第一反应是 gdb 出 bug 了?! 众所周知, gdb 是通过读取 coredump NOTE segment 来得到 crash 点各个线程各个寄存器的信息; 所以我使用了 lief project 直接解析下 coredump, 还顺便帮 lief project 修复了个 bug Use Segment::file_offset() instead of Binary::virtual_address_to_offset() for parsing note segment. lief 读取出来的信息如下所示:
Name: CORE
Type: PRSTATUS
Description: [0b 00 00 00 00 00 00 00 00 00 00 00 0b 00 00 00 ...]
Siginfo: 11 - 0 - 0
Current Signal: 11
Pending signal: 0
Signal held: 0
PID: 64
PPID: 1
PGRP: 1
SID: 1
utime: 1900:826000
stime: 36:865000
cutime: 23225:806195
cstime: 23782:825399
Registers:
X86_64_R15 : 0x7f5536455510
X86_64_R14 : 0
X86_64_R13 : 0x7f5520e18030
X86_64_R12 : 0x7f5520e18030
X86_64_RBP : 0x7f4e906a89f0
X86_64_RBX : 0x7f4e906a89c0
X86_64_R11 : 0
X86_64_R10 : 0x7f4df48b8f00
X86_64_R9 : 0x7f54b2456a80
X86_64_R8 : 0x7f53f744ce90
X86_64_RAX : 0x7f54b0cd9a08
X86_64_RCX : 0x2
X86_64_RDX : 0x7f4e906a8b30
X86_64_RSI : 0x7f4e90803bf0
X86_64_RDI : 0x7f4e906a8a38
X86_64__ : 0xffffffffffffffff
X86_64_RIP : 0x7f54b0335d11
X86_64_CS : 0x33
X86_64_EFLAGS : 0x10246
X86_64_RSP : 0x7f55227fcd10
X86_64_SS : 0x2b
r14 确实为 0, 我有点慌了. 不过 lief 确实显示了一个值得注意的地方:
Name: CORE
Type: SIGINFO
Description: [0b 00 00 00 00 00 00 00 fa ff ff ff 00 00 00 00 ...]
Signo: 11
Code: 0 # 这里 lief 把 Code/Errno 搞混了, 实际上 Errno = 0, Code = -6
Errno: -6
# Description 的全部内容:
$od -A d -t xI ~/tmp/hos-lw-core/siginfo.desc.txt
0000000 0000000b 00000000 fffffffa 00000000
0000016 00000019 00000000 00000000 00000000
0000032 00000000 00000000 00000000 00000000
*
0000128
从 kernel code 中可以看到 Core = -6
意味着信号是用户触发的, 如上 00000019 00000000
记录了发送方 pid=0x19, uid=0x0.
/*
* si_code values
* Digital reserves positive values for kernel-generated signals.
*/
#define SI_USER 0 /* sent by kill, sigsend, raise */
#define SI_KERNEL 0x80 /* sent by the kernel from somewhere */
#define SI_QUEUE -1 /* sent by sigqueue */
#define SI_TIMER -2 /* sent by timer expiration */
#define SI_MESGQ -3 /* sent by real time mesq state change */
#define SI_ASYNCIO -4 /* sent by AIO completion */
#define SI_SIGIO -5 /* sent by queued SIGIO */
#define SI_TKILL -6 /* sent by tkill system call */
#define SI_DETHREAD -7 /* sent by execve() killing subsidiary threads */
#define SI_ASYNCNL -60 /* sent by glibc async name lookup completion */
#define SI_FROMUSER(siptr) ((siptr)->si_code <= 0)
考虑到 linux pid namespace 的存在, 单单这里记录的 pid=0x19, 由于缺少对应的 pid namespace, 所以并不能确认是哪个进程. 根据 kernel code 可知这里 pid=0x19 是调用 tkill 的进程当前的 pid namespace X. 设 crash 进程所处 namespace 为 Y, 根据 kernel kill 以及 pid 可见性规则, 这里 X 要么是 Y, 要么是 Y 的祖先. 如果 X = Y, 那么 pid=25 就是 crash 进程本身, crash 进程确实有个线程 pid = 25. 如果 X 是 Y 的祖先, 那么就对应着 cpuhp 这个内核线程. 不想写了, 现在一堆的疑问:
-
具体是谁发送了 SIGSEGV 信号???
-
无论是谁发送了 SIGSEGV 信号, rip=0x7f54b0335d11, r14=0 这种组合都不应该出现啊!
后记
在发到群里面分享的时候, 在小伙伴们的不断脑暴启发下, 我进一步确认了下我们 SIGSEGV handler 实现(这是一块很久很久很久无人寻迹过的代码了), 然后发现这里面居然不是我以为的 abort,,, 而是 raise(sig):
/* Pass on the signal (so that a core file is produced). */
struct sigaction sa;
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(sig, &sa, NULL);
raise(sig);
这就解释了为啥 core note segment SIGINFO 说 SIGSEGV 是用户态发送的. 它确实是用户态发起的, 并且确实是 crash 进程自己发起的. 至于另外一个 (rip, r14) 诡异组合问题, 等我把上面那个 SIGSEGV handler 移除之后再观察, 我也确实修复过一个 write-after-free/double free 问题, 估计是这个问题的副作用.
(大家真是太棒了! 这个问题两个月前出现, 我那时进行了一次冲锋, 以失败告终; 今天兴致又起看了下, 居然有了点结论!