AwCookieManager.nativeGetCookie crash 排查

背景

Android 平台上长期存在一类发生在 app 调用 CookieManager.getCookie(String url) 过程中的 native crash,困扰着很多研发,也严重影响了用户体验。此类问题 Android 4.1-9.0 均有覆盖,基本都发生在启动阶段。西瓜视频上此类问题长期占据 Top 3 榜单之一,存在时间已相当久远。在 Top 10 的 native crash 中占比超过 40%,Native crash 整体占比>30%,影响用户比例>1‰(此类 crash 的用户占比);主要集中在 Android 4.2.2、4.4.2、8.1、9.0 等版本上,其他 Android 版本上也均大量此类问题。最为严重的是此类 crash 基本都发生在启动 2s 以内,严重影响西瓜视频 app 的用户体验。其典型堆栈截图如下:

Native 堆栈

AwCookieManager.nativeGetCookie crash 排查_第1张图片

Java 堆栈(有>50%的 crash 没有 Java 堆栈)

排查思路

此类 crash 堆栈中只有 so 及偏移地址信息,没有相应的函数名,又不是必现问题,很难直接定位到问题原因。所以排查的关键是先找到有明确函数名的堆栈,有了详细的函数信息,才能进一步通过相关的函数名对照 AOSP 源码分析定位出原因。

AwCookieManager.nativeGetCookie crash 排查_第2张图片

AwCookieManager.nativeGetCookie crash 排查_第3张图片

初步调查

出现问题的 Android 版本和机型虽分布极广(Android 4.x - 9.0),但绝大部分堆栈几乎没有任何 Crash 相关的核心函数信息。幸运的是通过梳理所有相关的 crash,发现 Android 4.2.2 上有一类 crash 有一个函数信息_ZN4GURLC2ERKSs(GURL::GURL(std::string const&))。

AwCookieManager.nativeGetCookie crash 排查_第4张图片

这类 crash 的堆栈跟上述问题是一致的,都是在 Java 层调用到 nativeGetCookie 时 native 层出现了 crash,堆栈也基本相同,可以判定是一类问题。拉取并分析 Android 4.2.2 GURL 相关的源码,发现 GURL 涉及到的代码也是非常广的,具体哪个环节哪一层调用了 memmove 函数有点儿大海捞针。

AwCookieManager.nativeGetCookie crash 排查_第5张图片

既然能搜到 GURL 相关,猜测似乎跟 URL 相关。于是线上做了个简单的实验,看看是不是 getCookie 时传入的 URL 的问题。通过 hook 应用层所有 CookieManager.getCookie 的调用发现,发生 crash 时均存在多个线程同时调用 CookieManager.getCookie,怀疑可能是线程安全问题。

AwCookieManager.nativeGetCookie crash 排查_第6张图片

仅有这些信息是不够的,如果能拿到 crash 时的函数名,问题才能被确认。再次梳理这类包含有 GURL 堆栈的 crash 时发现,果然存在这类堆栈(ZN8url_util20LowerCaseEqualsASCIIEPKcS1_S1)。

AwCookieManager.nativeGetCookie crash 排查_第7张图片

同时梳理出的这种有明确上层 crash 函数名的堆栈还有以下两种,均是 GURL 的构造函数执行过程中出现的 crash,这其中有一类是 vector 相关的操作异常(vector 是非线程安全,这个本人印象很深刻,AOSP 源码里存在很多这类 vector 线程安全的问题:如 RenderNodeAnimator 等),这类异常也进一步加深了线程安全问题的怀疑。

AwCookieManager.nativeGetCookie crash 排查_第8张图片

深入分析

ZN8url_util20LowerCaseEqualsASCIIEPKcS1_S1 的原形是 url_util::LowerCaseEqualsASCII(char const*, char const*, char const*),这个堆栈跟前述问题是基本一致的,都是 crash 在 GURL::GURL(std::string const&)的构造函数调用链上,只是 crash 的原因不同。虽然不能简单判定为是同一类问题,但种种迹象表面就是同一类问题。这个堆栈有明确的 crash 时的函数名,通过这个问题或许可以发现问题的根本原因。

根据 PC=5cb1453e 发现,crash 是因为 R2 寄存器里为空(0x0)导致的,结合DoLowerCaseEqualsASCII 的源码可以判定 R2 寄存里存的正是函数的第三个参数 b,这说明 crash 是因 b 为 null 导致的。

AwCookieManager.nativeGetCookie crash 排查_第9张图片

AwCookieManager.nativeGetCookie crash 排查_第10张图片

AwCookieManager.nativeGetCookie crash 排查_第11张图片

确认了 crash 的原因,再结合源码发现调用 url_util::LowerCaseEqualsASCII(char const*, char const*, char const*) 的且堆栈在 GURL 构造函数的调用链上的有两处,一处是下图里的 CompareSchemeComponent 函数,另一处是 DoIsStandard 函数,相关源码截图如下:

AwCookieManager.nativeGetCookie crash 排查_第12张图片

第一处 CompareSchemeComponent 函数的第三个参数正是LowerCaseEqualsASCII的第三个参数,但这个参数 kFileScheme 是个常量,不可能为 null,所以首先排除嫌疑。

AwCookieManager.nativeGetCookie crash 排查_第13张图片

AwCookieManager.nativeGetCookie crash 排查_第14张图片

第二处 DoIsStandard 里的 LowerCaseEqualsASCII 的第三个参数是个全局变量,是在 InitStandardSchemes 里初始化的,仔细分析 InitStandardSchemes 的源码可以发现,standard_schemes 虽然是个全局变量,但采用的是懒加载的方式初始化的。那么问题来了,这个初始化过程/全局变量是线程安全的吗?

AwCookieManager.nativeGetCookie crash 排查_第15张图片

AwCookieManager.nativeGetCookie crash 排查_第16张图片

很遗憾这个函数并没有加锁,vector 也不是线程安全的,当然 std::vector* standard_schemes 也就不是线程安全的。多个线程同时调到这里的话就会出问题,当有线程正在初始化 standard_schemes 时,另一个线程可能也在执行初始化,这时会出 vector 操作的同步问题;同样的,当一个线程正在遍历 standard_schemes 时,另一个线程可能给 standard_schemes 重新设置了新的值,这时候就会有机率触发空指针问题。

查阅 chromium 源码发现 Android 4.0-9.0 里依赖的源码均存在 GURL 初始化的线程安全问题,该问题存在时间已经相当久远,好在是已在 2019.05.21 提交了相关修复(Make //url initialization thread-safe,https://source.chromium.org/chromium/chromium/src/+/0ef8191485b6327872bf7f644ee8c2fb4861bb4a?originalUrl=https://cs.chromium.org/)。但远水解不了近渴,市面上 Android 10 以内的老版本 chromium 仍存在此类问题,依赖系统升级最终解决此类问题是遥不可及的,为了不影响体验需要应用层主动修复或者采取措施规避。

修复方案

通过上述的分析可知,只需要保证 standard_schemes 在初始化完成前不会有第二个线程执行同样的逻辑即可。虽然没有系统层面的同步方案,但问题抛出的点都集中在应用层的同一处,在这个位置加个同步限制即可解决!不过为了保险起见还是在应用层做个全局防范(第一个执行完成之后放开限制)。西瓜视频 app 是通过自研的 AOP 工具 hook 应用层所有 CookieManager.getCookie(String url)的调用。

AwCookieManager.nativeGetCookie crash 排查_第17张图片

此方案在西瓜视频 app 432 版本灰度&全量上线后,再无此类问题,用很小的成本彻底解决了这类问题。

总结

调查 Native 问题时符号表信息是不可或缺的,大多数情况下可能缺少关键的符号信息,这给调查 Native 问题增加了很高的难度。但由于 Android 系统更新迭代的版本很多,加上厂商定制的差异,一些小众机型或 Android 版本的 crash 可能携带着关键的符号信息,这些往往就是突破点,排查问题时小众问题也应得到足够的重视。我们同时呼吁手机厂商尽量保留一些关键的符号表信息,为开发者保留一些可以方便定位问题的关键信息。

此外,虽然 http://androidxref.comhttps://cs.android.com 都可以在线查阅源码,但这两处的Android版本并不全。https://android.googlesource.com 这里可以下载到几乎所有版本的源码,本地通过 Sublime 分析源码也十分方便(可以直接显示和跳转到方法的定义&引用位置)。

欢迎关注「字节跳动技术团队」

你可能感兴趣的:(AwCookieManager.nativeGetCookie crash 排查)