ZygiskOnKernelSU 一直以来都存在着一个奇怪的 bug ,至今已经收到无数起汇报(可能最早的一例)。这些汇报的共同特征是:都来自 oplus 或者 coloros 系统;都出现了 zygiskd32 崩溃的现象;崩溃的堆栈基本上都来自 malloc ;都只有 release 构建出现问题。
早期的分析发现,崩溃的线程是处理 daemon socket 启动的线程,其中一个命令是根据不同 root 实现去查询某 uid 是否具有 root 权限,而问题似乎来自这里,因为每次执行完这个命令后,就会发生崩溃。当时一个看似可行的修复方法是将相关代码禁止内联。
而近期 root profile 推出后,zygisksu 也做出了相应修改,在这个提交中,never inline 被去掉,于是又出现了上述问题的汇报。
经过讨论 [1] ,怀疑是内核的 bool size 和 rust 中的 bool size 不一致的问题,rust 的 bool size 是 1,如果内核的 bool size 是 4,可能导致内存被破坏。而利用 prctl 查询 root 权限,需要传入一个 bool 指针,由内核改写返回值,问题可能就在这里。因此随后的一个 CI 进行了修改,并且有人汇报此 CI 的 release 版本正常 'https://t.me/KernelSU_group/3252/101247' rel='nofollow'>[2] 。
于是主线也做了类似的修正,不过没有用到上述 CI 的提交。但这个修正的 CI 的 release 仍有人汇报仍然出现崩溃。
实际上上述猜测是有问题的,因为随后的实验又发现,内核的 bool size 为 1 ,看起来和 rust 是一致的。后来又有人提出 copy_to_user 在长度小于 16 的时候可能出现覆盖现象。总之,问题看上去就是某种意外的内存覆写导致的。
然而想要解释问题的具体原因难度很大,因为 release 构建缺少调试信息,而且崩溃的堆栈回溯大部分帧都来自 libc ,难以从 zygiskd 自身的代码发现问题。不论如何,我还是发了一个带有 debug info 的 release 版本,等待有问题的用户参与调查。
就在最近有 ColorOS 用户积极参与了问题调查,真相似乎终于揭开。
为了验证 bool size 的问题,我给那个用户发了一个程序,检测 prctl 是否会覆写,结论是没有错误覆写 [3]。
虽然 bool 没有问题,但是种种迹象表明,应该就是这个 prctl 调用出现的问题,而且一定产生了意外的内存覆写。于是我看了看 kernelsu 的源码,发现了蹊跷之处。
在执行 CMD_UID_GRANTED_ROOT 的时候,kernelsu 会将 prctl 的 arg5 当作一个用户指针,尝试写入一个 reply_ok 返回值。这个返回值区别于 arg4 ,arg4 是命令的 bool 返回值(即 uid 是否具有 root)。
而 zygisksu 中忽略了 arg5 ,且调用 prctl 仅仅传了 4 个参数,因此可以在 dmesg 看到大量的 reply err ,因为 arg5 的地址非法。
不过调用的时候不写 arg5 ,不代表它就没有值了,32 位 arm 的调用约定是 r0~r3 作为前四个参数,之后的参数由调用者放到栈上。现在调用者没有把 arg5 放到栈上,因此取到的值就不是我们能控制的了,这种随机性也和问题报告中,崩溃点各不相同的现象相对应。
将 prctl 的参数列表补全后,release 版本终于能够正常使用。
虽然没有让参与调查者在自己的设备上跟踪有问题版本 zygiskd 的系统调用,不过我在自己的设备上测试,发现 32 位的 prctl 调用的第五个参数确实是一个合法地址,甚至是 libc malloc 内存区域的地址。当 zygiskd32 调用了 uid_granted_root 后,KernelSU 也没有打出 reply err 的日志,说明这个地址确实被覆写了,至于为什么没有崩溃,以我目前的能力尚不能解释,只能当作玄学了。可想而知,有多少看似能正常运行着 zygisksu 的系统,他们的 zygiskd32 的内存其实已经被破坏了。
可以说,zygisksu 到这个问题发现为止一直运行在 bug 上。
zygisksu 的教训告诉我们:调用 prctl 这样的函数时一定要将不用的参数填上 0 ,否则可能出现意想不到的未定义行为。
另外,从 arm64 的系统调用跟踪发现,arg5 是 0 ,也许是因为 64 位上这个参数来自寄存器。
早期的分析发现,崩溃的线程是处理 daemon socket 启动的线程,其中一个命令是根据不同 root 实现去查询某 uid 是否具有 root 权限,而问题似乎来自这里,因为每次执行完这个命令后,就会发生崩溃。当时一个看似可行的修复方法是将相关代码禁止内联。
而近期 root profile 推出后,zygisksu 也做出了相应修改,在这个提交中,never inline 被去掉,于是又出现了上述问题的汇报。
经过讨论 [1] ,怀疑是内核的 bool size 和 rust 中的 bool size 不一致的问题,rust 的 bool size 是 1,如果内核的 bool size 是 4,可能导致内存被破坏。而利用 prctl 查询 root 权限,需要传入一个 bool 指针,由内核改写返回值,问题可能就在这里。因此随后的一个 CI 进行了修改,并且有人汇报此 CI 的 release 版本正常 'https://t.me/KernelSU_group/3252/101247' rel='nofollow'>[2] 。
于是主线也做了类似的修正,不过没有用到上述 CI 的提交。但这个修正的 CI 的 release 仍有人汇报仍然出现崩溃。
实际上上述猜测是有问题的,因为随后的实验又发现,内核的 bool size 为 1 ,看起来和 rust 是一致的。后来又有人提出 copy_to_user 在长度小于 16 的时候可能出现覆盖现象。总之,问题看上去就是某种意外的内存覆写导致的。
然而想要解释问题的具体原因难度很大,因为 release 构建缺少调试信息,而且崩溃的堆栈回溯大部分帧都来自 libc ,难以从 zygiskd 自身的代码发现问题。不论如何,我还是发了一个带有 debug info 的 release 版本,等待有问题的用户参与调查。
就在最近有 ColorOS 用户积极参与了问题调查,真相似乎终于揭开。
为了验证 bool size 的问题,我给那个用户发了一个程序,检测 prctl 是否会覆写,结论是没有错误覆写 [3]。
虽然 bool 没有问题,但是种种迹象表明,应该就是这个 prctl 调用出现的问题,而且一定产生了意外的内存覆写。于是我看了看 kernelsu 的源码,发现了蹊跷之处。
在执行 CMD_UID_GRANTED_ROOT 的时候,kernelsu 会将 prctl 的 arg5 当作一个用户指针,尝试写入一个 reply_ok 返回值。这个返回值区别于 arg4 ,arg4 是命令的 bool 返回值(即 uid 是否具有 root)。
而 zygisksu 中忽略了 arg5 ,且调用 prctl 仅仅传了 4 个参数,因此可以在 dmesg 看到大量的 reply err ,因为 arg5 的地址非法。
不过调用的时候不写 arg5 ,不代表它就没有值了,32 位 arm 的调用约定是 r0~r3 作为前四个参数,之后的参数由调用者放到栈上。现在调用者没有把 arg5 放到栈上,因此取到的值就不是我们能控制的了,这种随机性也和问题报告中,崩溃点各不相同的现象相对应。
将 prctl 的参数列表补全后,release 版本终于能够正常使用。
虽然没有让参与调查者在自己的设备上跟踪有问题版本 zygiskd 的系统调用,不过我在自己的设备上测试,发现 32 位的 prctl 调用的第五个参数确实是一个合法地址,甚至是 libc malloc 内存区域的地址。当 zygiskd32 调用了 uid_granted_root 后,KernelSU 也没有打出 reply err 的日志,说明这个地址确实被覆写了,至于为什么没有崩溃,以我目前的能力尚不能解释,只能当作玄学了。可想而知,有多少看似能正常运行着 zygisksu 的系统,他们的 zygiskd32 的内存其实已经被破坏了。
可以说,zygisksu 到这个问题发现为止一直运行在 bug 上。
zygisksu 的教训告诉我们:调用 prctl 这样的函数时一定要将不用的参数填上 0 ,否则可能出现意想不到的未定义行为。
另外,从 arm64 的系统调用跟踪发现,arg5 是 0 ,也许是因为 64 位上这个参数来自寄存器。