copyright_author: zsl
copyright_author_href: https://github.com/zslxiu
copyright_info: 此文章版权归 zsl 所有,如有转载,请注明来自原作者
copyright_url: https://wayawbott0.f.mioffice.cn/docx/doxk4r1xwdh71gbnAvW6R1FY5sL


一、问题分析

1.1 dmesg_TZ.txt

Kernel log:
[41525.700710][T23239] Kernel panic - not syncing: stack-protector: Kernel stack is corrupted in: mi_binder_wait4_hook$835a4498e5ea78c0e09ecbebc9fc5a14+0x2f8/0x2fc [millet_binder]
[41525.700740][T23239] CPU: 5 PID: 23239 Comm: Recents-TaskRes Tainted: G        WC O      5.10.198-android12-9-00085-g226a9632f13d-ab11136126 #1
[41525.700753][T23239] Hardware name: Qualcomm Technologies, Inc. Parrot QRD, RUAN based on SM7435 (DT)
[41525.700761][T23239] Call trace:
[41525.700778][T23239]  dump_backtrace.cfi_jt+0x0/0x8
[41525.700788][T23239]  dump_stack_lvl+0xdc/0x138
[41525.700800][T23239]  panic+0x188/0x46c
[41525.700810][T23239]  printk_nmi_enter+0x0/0xc4
[41525.700822][T23239]  mi_binder_wait4_hook$835a4498e5ea78c0e09ecbebc9fc5a14+0x2f8/0x2fc [millet_binder]
[41525.700845][T23239] SMP: stopping secondary CPUs

之前以为这边 printk_nmi_enter(),是 panic 流程中的一部分,只是没有打出关键的函数,但是实际上printk_nmi_enter 是完全错误的,不存在打印的不是关键函数的情况

1.2 进程栈解析

mi_binder_wait_for_work()的汇编:

0xffffffe3f7f6f524 <mi_binder_wait_for_work>:        paciasp
0xffffffe3f7f6f528 <mi_binder_wait_for_work+4>:        str        x30, [x18], #8
0xffffffe3f7f6f52c <mi_binder_wait_for_work+8>:        stp        x29, x30, [sp, #-64]!
0xffffffe3f7f6f530 <mi_binder_wait_for_work+12>:        str        x23, [sp, #16]
0xffffffe3f7f6f534 <mi_binder_wait_for_work+16>:        stp        x22, x21, [sp, #32]
0xffffffe3f7f6f538 <mi_binder_wait_for_work+20>:        stp        x20, x19, [sp, #48]
0xffffffe3f7f6f53c <mi_binder_wait_for_work+24>:        mov        x29, sp

mi_binder_wait4_hook()的汇编:

0xffffffe3f73f5914 <mi_binder_wait4_hook>:        paciasp
0xffffffe3f73f5918 <mi_binder_wait4_hook+4>:        sub        sp, sp, #0xf0
0xffffffe3f73f591c <mi_binder_wait4_hook+8>:        str        x30, [x18], #8
0xffffffe3f73f5920 <mi_binder_wait4_hook+12>:        stp        x29, x30, [sp, #160]
0xffffffe3f73f5924 <mi_binder_wait4_hook+16>:        str        x25, [sp, #176]
0xffffffe3f73f5928 <mi_binder_wait4_hook+20>:        stp        x24, x23, [sp, #192]
0xffffffe3f73f592c <mi_binder_wait4_hook+24>:        stp        x22, x21, [sp, #208]
0xffffffe3f73f5930 <mi_binder_wait4_hook+28>:        stp        x20, x19, [sp, #224]
0xffffffe3f73f5934 <mi_binder_wait4_hook+32>:        add        x29, sp, #0xa0

一般就这两种形式:

  1. stp x29, x30, [sp, #-64]!

将 x29 存入 sp - 0x40 这个地址,将 x30 存入 sp - 0x40 + 0x8 这个地址,并且 sp = sp - 0x40,这就是为这个函数开辟了 4 行栈空间作为这个函数的栈帧,此时 sp 就指向了这个函数的栈顶。

  1. sub sp, sp, #0xf0

sp = sp - 0xf0,为此函数开辟 15 行栈空间作为这个函数的栈帧。

寄存器入栈完成后,会有 mov x29, sp 和 add x29, sp, #0xa0 将 fp lr 这行对应的内存地址赋值给 x29,以便跳转到下一个函数时,将 x29 入栈记录为新的 fp。

栈帧中是怎么存放数据的,存放了哪些数据?

str        x23, [sp, #16]
stp        x29, x30, [sp, #-64]!

str 操作一个寄存器,stp 操作两个寄存器。都是入栈指令,将对应寄存器的值存入对应的地址。

sp 指向栈帧的栈顶,表示一行的内存地址,sp + #16,也就是 sp + 0x10,可以观察到前面的内存地址,每行都差 0x10,所以 sp + 0x10 就是到了 sp 的下一行。

存放的数据需要关注的如下:

x29:栈帧指针 fp 寄存器,记录上一个函数的 fp lr 那一行的内存地址。

x30:lr 寄存器,记录上一个函数跳转指令的下一条指令的内存地址。

x19 - x28:这些寄存器在跳转之前有的会记录传递的参数,因此可以直接读取对应地址的值来看参数值

__stack_chk_fail() 的下一步是 panic(),panic() 栈帧中记录的 lr 应该是 __stack_chk_fail() 中跳转指令的下一条指令的内存地址,也就是 0xffffffe3fd73c6b4 + 0x4 = 0xffffffe3fd73c6b8。

dis 0xffffffe3fd73c6b8 可以看到是函数 printk_nmi_enter() 了,所以这是错误的。

个人认为 __stack_chk_fail() 最后应该填充一句 nop 用来防止这个问题。

1.3 trace32查看进程栈

__stack_chk_fail() 是 mi_binder_wait4_hook() 的下一步,从 mi_binder_wait4_hook() 的汇编看:

0xffffffe3f73f5914 <mi_binder_wait4_hook$835a4498e5ea78c0e09ecbebc9fc5a14>:        paciasp
...
0xffffffe3f73f5bc0 <mi_binder_wait4_hook+684>:        adrp        x9, 0xffffffe3fe2e5000 <llcp_rawsock_ops+8>
0xffffffe3f73f5bc4 <mi_binder_wait4_hook+688>:        ldur        x8, [x29, #-8]
0xffffffe3f73f5bc8 <mi_binder_wait4_hook+692>:        ldr        x9, [x9, #3792]
0xffffffe3f73f5bcc <mi_binder_wait4_hook+696>:        cmp        x9, x8
0xffffffe3f73f5bd0 <mi_binder_wait4_hook+700>:        b.ne        0xffffffe3f73f5c08 <mi_binder_wait4_hook$835a4498e5ea78c0e09ecbebc9fc5a14+756>  // b.any
0xffffffe3f73f5bd4 <mi_binder_wait4_hook+704>:        ldp        x29, x30, [sp, #160]
0xffffffe3f73f5bd8 <mi_binder_wait4_hook+708>:        ldp        x20, x19, [sp, #224]
0xffffffe3f73f5bdc <mi_binder_wait4_hook+712>:        ldp        x22, x21, [sp, #208]
0xffffffe3f73f5be0 <mi_binder_wait4_hook+716>:        ldp        x24, x23, [sp, #192]
0xffffffe3f73f5be4 <mi_binder_wait4_hook+720>:        ldr        x25, [sp, #176]
0xffffffe3f73f5be8 <mi_binder_wait4_hook+724>:        ldr        x30, [x18, #-8]!
0xffffffe3f73f5bec <mi_binder_wait4_hook+728>:        add        sp, sp, #0xf0
0xffffffe3f73f5bf0 <mi_binder_wait4_hook+732>:        autiasp
0xffffffe3f73f5bf4 <mi_binder_wait4_hook+736>:        ret
0xffffffe3f73f5bf8 <mi_binder_wait4_hook+740>:        mov        w1, #0x3                           // #3
0xffffffe3f73f5bfc <mi_binder_wait4_hook+744>:        mov        x0, x20
0xffffffe3f73f5c00 <mi_binder_wait4_hook+748>:        bl        0xffffffe3fc9dd0b4 <refcount_warn_saturate>
0xffffffe3f73f5c04 <mi_binder_wait4_hook+752>:        b        0xffffffe3f73f5bc0 <mi_binder_wait4_hook$835a4498e5ea78c0e09ecbebc9fc5a14+684>
0xffffffe3f73f5c08 <mi_binder_wait4_hook+756>:        bl        0xffffffe3fd73c670 <__stack_chk_fail>

怎么确定函数汇编最后跑到了哪里?

1.mi_binder_wait4_hook$835a4498e5ea78c0e09ecbebc9fc5a14+0x2f8/0x2fc 最后的 +0x2f8/0x2fc,0x2f8 表示

运行指令的偏移,0x2fc 表示函数汇编的长度。(如果不是最后发生 panic 的函数,这个偏移就是跳转指令的下一行。)

2.通过进程栈记录的 lr,也可以确定。

看 __stack_chk_fail() 函数内容,和报错对应的,panic 是在这个函数中触发的。

1.4 问题地址推导

mi_binder_wait4_hook() 函数首地址 0xffffffe3f73f5914 + 0x2f8 = 0xffffffe3f73f5c0c,也就是运行到了这行:

bl        0xffffffe3fd73c670 <__stack_chk_fail>

从汇编看,这行肯定是跳转过来的,可以看到这行:

b.ne        0xffffffe3f73f5c08 <mi_binder_wait4_hook$+756>  // b.anyb.ne        0xffffffe3f73f5c08 <mi_binder_wait4_hook$+756>  // b.any

跳转的原因是因为 cmp x9,x8;x9 不等于 x8,所以要看下 x8、x9 的赋值。

x9 的赋值(adrp 指令等于mov):

adrp        x9, 0xffffffe3fe2e5000 <llcp_rawsock_ops+8>
ldr        x9, [x9, #3792] // x9 = bee1da79a4be8600

x8 的赋值,函数开始开辟15行栈帧,把 x29 赋值为 sp - 0xf0 + 0xa0 的位置,计算出 x8 的值(adrp 指令等于mov)后,存入 x29 - 0x8 的位置,最后 x8 也是从 x29 - 0x8 的位置去拿值:

sub        sp, sp, #0xf0
add        x29, sp, #0xa0
adrp        x8, 0xffffffe3fe2e5000 <llcp_rawsock_ops+8>
ldr        x8, [x8, #3792]
stur        x8, [x29, #-8]
ldur        x8, [x29, #-8]

x9 计算出为 bee1da79a4be8600,rd ffffffc0337eb9b8 也是 bee1da79a4be8600,此时问题出现了,在从 x29 - 0x8 这个位置读值的时候,x29 发生了 bitflip,导致了从错误的地址读值,造成 x8 和 x9 不相等,从而运行到了

__stack_chk_fail(),触发了 panic。

2. 根本原因

函数编译开始时会把 x29 入栈,入栈后会执行 add x29, sp, #0xa0;然后将 sp + 0xa0,也就是 fp lr 这行的地址赋值给 x29,以便执行到下一个函数时入栈 x29,以记录 fp。

因此可以通过下一个函数栈帧记录的 fp 来查看 x29 的值,显然,x29 变心了。

ffffffc0337eb9c0 变成了 ffffffc0334eb9c0

ffffffc0337eb9c0 二进制:

1111 1111 1111 1111 1111 1111 1100 0000 0011 0011 0111 1110 1011 1001 1100 0000

ffffffc0334eb9c0 二进制:

1111 1111 1111 1111 1111 1111 1100 0000 0011 0011 0100 1110 1011 1001 1100 0000

大概率就是这两位发生 bitflip。