Skip to main content

Android ASan 内存错误检测

一、功能背景

Native开发时,无法保证内存的安全性,开发时的不规范写法或无心之过可能导致内存相关的崩溃问题,但崩溃堆栈往往又不是第一案发现场,线上采集的minidump信息也十分有限,这使得Native代码中的内存错误很难排查。 为了解决这些影响程序稳定性的内存问题,Bugly提供线上ASan内存错误检测能力,支持检测在堆内存中分配、释放和使用时导致的基本错误。

Bugly提供的内存错误检测工具原理基于GWP-ASan,是一种基于采样内存分配的轻量级内存错误检测工具,性能开销小且不需要指令插桩和重新编译,适用于线上场景。

GWP-ASan

Google官方提供GWP-ASan原生内存分配器功能,用于调试和减少内存错误,支持在tombstone中提供ASan内存错误崩溃信息,仅支持Android 11(API 级别30)以上的系统,且不支持采样及参数控制。 详情可参考 Google Android GWP-ASan

在使用前,需要先简单了解Bugly ASan内存错误检测工具的原理。

通过初始化接口和配置开启功能后,Bugly ASan会随机拦截堆内存分配操作,并将从指定的ASan Memory Pool中返回可用的内存地址给分配操作。ASan Memory Pool中的内存通常以页为单位连续排列,主要由两种类型的页交替间隔组成。红色 部分的内存页Guard Page为不可读写的页,绿色 部分的内存页Slot Page为可读写的页,用于内存的分配,如下图所示。

ASan原理

在内存分配事件采样命中时,会从指定的ASan Memory Pool中寻找可用的Slot Page进行内存分配,实际分配空间通常都小于一页,因此分配的内存会随机进行左右对齐。当分配内存左对齐时,可用于检测 Underflow 内存问题,访问Underflow地址时是不可访问区域,会触发SIGSEGV错误。同理,当分配内存右对齐时,可用于检测 Overflow 内存问题。

当对ASan分配内存进行释放后,会将所在Slot Page区域一段时间内标记为不可读写(标记时间取决于空余可读写Slot Page数量,一定时间后空余Slot Page数量不多则会重新将不可读写区域标记回可读写),如果再对相同的区域进行内存释放操作时,会触发 Double free 问题。如果对已经释放内存的区域进行写操作时,会触发 Use after free 问题。

由于内存的分配和对齐方式都是随机采样的,因此需要足够多的测试机器来检测对应的问题。

二、功能开启

Bugly ASan能力需要使用 最新版本SDK (版本待发布,详询Bugly小助手),接入方式与Bugly其他性能模块无差异,可以采用SDK端和portal端共同控制的方式。在接口初始化后,并且配置了 sample_ratio 采样率命中了当前设备,Bugly ASan能力才会开启,仅初始化接口或仅修改采样配置,不会开启功能。

1. 初始化接口

开启Bugly ASan能力需在接口中开启对应功能项。

// 开启Bugly ASan内存错误检测能力
RMonitor.startMonitors(MonitorName.ASAN);

通过接口初始化后,需参考配置指南,修改功能的采样率 sample_ratio 来配置功能开启。

2. 开启成功日志

当前设备被采样命中后,可以看到以下初始化日志。

06-11 21:03:29.182 12351 12397 I Bugly_Asan_Monitor: prepare to start addressSanitizer, local sample success!
06-11 21:03:30.162 12351 12397 D Bugly_Asan_Monitor: start addressSanitizer success!
重要提醒

由于ASan功能会对系统内存分配操作进行hook,与 FD触顶 监控和 Native内存触顶 监控不建议同时开启,因此当同时开启这几项功能时,会在本地进行二次的开启采样,如未采样到ASan功能时,上面的日志会打印开启失败。如需保证ASan的开启率,建议关闭或降低 FD触顶 监控和 Native内存触顶 监控的采样率。

三、配置指南

在portal设置 -> SDK配置中,开启ASan内存检测能力。其中对应的配置项说明如下。

监控项配置项配置参数取值说明
ASan内存错误检测"asan""sample_ratio"设备采样率,取值[0,1],1表示全部打开,0表示全部关闭,其他表示按指定概率抽样打开
ASan内存错误检测"asan""slot_size"可分配内存Slot Page大小,单位KB,越大所需内存开销越大,建议使用默认值
ASan内存错误检测"asan""slot_count"可分配内存Slot Page数量,数量越多会有更多的内存用于内存错误检测,内存开销越大,建议使用默认值
ASan内存错误检测"asan""max_sample_gap_count"循环采样随机数的最大值,值越大内存分配时采样命中的概率越低
ASan内存错误检测"asan""left_side_align_percentage"在ASan Memory Pool中内存分配时左对齐的概率,取值[0,100],值越小越易于检测Overflow问题,反之越容易检测Underflow问题
ASan内存错误检测"asan""right_side_perfect_align"在ASan Memory Pool中内存分配右对齐时是否不按页对齐,若不按页对齐更容易检测出Overflow问题,但也可能带来未知的crash风险
ASan内存错误检测"asan""ignore_overlapped_reading"是否忽略重读类型的ASan错误
ASan内存错误检测"asan""target_so_patterns"需要检测的SO文件,若为空默认检测全部SO,支持正则表达式设置,当内存问题能定位具体SO时,建议设置该值,可以减少稳定性问题
ASan内存错误检测"asan""ignore_so_patterns"需要忽略的SO文件,若不为空默认不忽略任何SO,支持正则表达式设置

ASan配置

四、功能使用

1. ASan 错误异常触发

在开启Bugly ASan内存错误检测能力后,当堆内存错误发生且被检测到时,会 触发SIGSEGV异常,直接Crash退出程序,因此内存错误的表现就是一个Native Crash异常。

提醒

如堆内存错误错误发生,但未开启Bugly ASan内存错误检测,或未被检测到时,可能并不会触发SIGSEGV异常,但可能后续会导致其他不可预估的异常,且难以定位解析。

在触发的Native Crash异常中,Crash Error Message信息和个例详情中会指明异常来自ASan内存错误检测,并标识出错误的类型,ASan错误的类型支持在portal上展示及筛选,可以通过筛选 ASan类型 快速定位出ASan模块触发的异常问题。

ASan异常

Crash异常的堆栈即为发生内存错误的第一现场。对于不同类型的问题,栈顶操作分别表示以下含义:

  • Overflow: 栈顶表示首次访问内存上溢地址时执行的操作。
  • Underflow: 栈顶表示首次访问内存下溢地址时执行的操作。
  • Use after free: 栈顶表示首次访问被释放内存地址时执行的操作。
  • Double free: 栈顶表示首次重复释放已释放内存地址时执行的操作。

2. ASan 附件信息

在ASan内存错误触发的SIGSEGV异常中,会携带错误内存的分配及释放信息,可以在个例详情中 附件 -> asan_info.zip 中找到对应的附件,如下图所示。

ASan附件

提醒
  1. ASan附件信息在进程二次启动时进行上报,因此当没有看到对应附件时,可能需要重启应用触发上报。
  2. 附件上报存在5min左右的处理延迟,重启后,需等待5min左右再才能看到 asan_info.zip 附件。

ASan附件主要包含以下信息,错误类型、触发的地址、触发的进程及线程、错误内存的释放线程及堆栈(如未释放则没有释放堆栈)、错误内存的分配线程及堆栈等。释放及分配堆栈支持附件直接翻译,点击附件后即可查看翻译后的堆栈(需上传对应SO的符号表)。

ASan附件内容

五、注意事项

开启Bugly ASan内存检测功能需关注以下事项。

重要提醒
  1. 开启ASan内存错误检测后,当触发错误被检测到会直接触发Crash异常,不管是否对当前App运行已经造成影响。
  2. 如在标记为ASan内存错误的异常问题中出现bugly相关堆栈,如 TriggerIssue 等关键字时,并不是bugly导致异常,而是由bugly主动进行异常触发上报内存错误问题。
  3. ASan内存错误检测功能会对内存分配操作进行hook拦截,如命中采样会直接从ASan内存池中返回可用内存地址,并不会回调原有内存分配操作。因此,如业务有自行对内存分配操作执行hook操作时,需评估hook冲突问题,或通过配置避免在有业务hook内存分配的SO中开启ASan内存错误检测
  4. 开启ASan内存错误检测能力,会通过PLT hook方式修改对应SO中的内存分配实现,不同业务之间环境复杂,建议优先测试环境使用或线上小规模灰度使用。同时,也建议在配置中指定 target_so_patterns,减少对SO的hook操作,提高稳定性。

附、内存错误简单示例

Overflow:

ptr = (char*) malloc(16);
ptr[20] = '\0';

Underflow:

ptr = (char*) malloc(16);
ptr[-5] = '\0';

Use after free:

ptr = (char*) malloc(16);
snprintf(ptr, 16, "test");
free(ptr);
snprintf(ptr, 16, "test");

Double free:

ptr = (char*) malloc(16);
snprintf(ptr, 16, "test");
free(ptr);
free(ptr);