关于android:字节Android-Native-Crash治理之Memory-Corruption工具原理与实践

41次阅读

共计 5145 个字符,预计需要花费 13 分钟才能阅读完成。

内容摘要

​ MemCorruption 工具是字节跳动 AppHealth (Client Infrastructure – AppHealth) 团队开发的一款用于定位野指针(UseAfterFree)、内存越界(HeapBufferOverflow)、反复开释(DoubleFree)类问题检测工具。宽泛用于字节跳动旗下各大 App 线上问题检测。本文将通过计划原理和实际案例来介绍此工具。

背景

​ 随着 Android App 开发的技术栈一直向 Native 层扩大,带来的线上 Native 稳定性问题日趋严重。Android 中有超过半数的破绽都来源于 Memory Corruption 问题。剖析定位线上此类问题的难点在于,首先线下难复现,其次问题产生时曾经不是第一案发现场,且此类问题调用栈体现类型多样化。这就导致了此类问题短期内难剖析、难定位、难解决的现状。

什么是 Memory Corruption 问题

UseAfterFree

UseAfterFree 上面简称 UAF,野指针类问题;

void HeapUseAfterFree() {int *ptr1 = (int*)malloc(4);
  if(ptr1 != NULL){
    *ptr1 = 0xcccc;
    free(ptr1);           //free ptr1           
    *ptr1 = 0xabcd;       //free 后 write ptr1 mem 这里不会解体
  }
}

​ 这里以 UAF 问题阐明 Native 解体后不是第一现场的场景。假如下面代码运行在线程 A,第 2 行申请 4byte 大小的一块堆内存,第 5 行开释这块堆内存,执行第 6 行火线程 A 工夫片执行完,切换到线程 B 执行,线程 B 此时申请 4byte 大小的内存块,内存管理器会概率性的调配之前曾经开释的 ptr1 指向的内存块调配给线程 B 应用,线程 B 给 ptr2 指向内存赋值 0xff,之后线程 B 工夫片执行完让出 CPU,切换线程 A 执行,ptr1 被赋值 0xabcd,之后切换回线程 B 进行条件判断,ptr2 内存值不为 0xff 触发异样逻辑。不是线程 B 预期的值。这样的场景在大型的 App 程序运行过程中时有发生。

DoubleFree

DoubleFree 上面简称 DF,堆内存二次开释类问题;

void DoubleFree() {int *ptr = (int*)malloc(4);
  free(ptr);
  free(ptr);
}

​ 同一块堆内存地址屡次开释问题,在理论开发中会有这样的场景,A 线程某个 C ++ 类 X 申请了一块堆内存,将内存地址传递给 Y 类办法应用,应用后通过析构函数开释,B 线程中申请同样大小的内存,申请到了这个曾经开释的地址,此时 A 线程的 X 类执行析构函数开释对应内存。

HeapBufferOverflow

HeapBufferOverflow 上面简称 HBO,堆内存越界类问题;

void HeapBufferOverflow() {char *ptr = (char*)malloc(sizeof(char)*100);
  *(ptr+101) = "aa";
  *(ptr+102) = "bb";
  *(ptr+103) = "cc";
  *(ptr+104) = "dd";
  free(ptr);
}

堆越界问题就更容易了解了,这里不再赘述。

工具现状

​ 业界有很多优良的工具用于 Memory Corruption 问题剖析,如 Asan(Address Sanitizer)、HWASAN、Valgrind 或 Coredump 等。但因为兼容性、性能功耗、接入老本过高、零碎限度等因素导致这些工具无奈在 Android App 客户端线上大规模应用。因而难以定位大规模用户场景下的简单问题。

工具比照:

字节计划

是否开发一个线上检测 Memory Corruption 类问题的工具?答案是必定的。

开发前首先要明确须要解决那些问题。

  • 须要解决的问题如下:

    • 兼容性强、性能开销低、内存耗费小、稳定性高;
    • 栈回溯高效且精确,须要记录线程信息、内存调配大小和内存地址信息;
  • 性能可配置化治理,不便线上线下应用,接入成本低;
  • 用户无感知检测,产生异样时不触发解体;

​ 宗旨思维是对 App 申请和开释的内存进行对立治理,达到对内存调配和开释的监管。因为内存申请开释十分频繁,如果监控所有内存并记录想要的信息,会对性能造成影响,所以工具通过 mmap 来申请一块内存,本人保护治理。内存申请策略依据随机采样调配,命中采样规定后,通过工具治理的内存池进行调配和开释,并对内存拜访权限进行管制,在调配的内存块前后增加隔离区,对开释后的内存设置为不可读写权限,并标记内存状态。通过一个数据结构来记录线程信息、线程栈帧、记录以后内存块状态达到检测的目标。同时通过线上动静下发配置形式实现可配置化治理。

Hook 工具选型

​ 定位 Memory Corruption 类问题,首先要 Hook 内存申请和开释的相干函数,达到对内存监控。这里波及到 Hook 计划的选型,线上首先须要思考的是高效稳固、兼容性好。

罕用的线上 Hook 工具类型如下:

​ 从工具比照看,通过大量试验,首选 dispatch table hook,因为 malloc/free 相干函数十分高频应用,hook dispatch table 形式高效稳固,性能影响小,线上能够大规模开启。hook 原理次要是找到 dispatch 表地址,替换表中 malloc 相干函数地址就能够达到 hook malloc 相干函数的需要。因为是 hook callee,所以不必思考 hook 增量库的问题。同时 Google/LLVM 在对 malloc 进行代理解决时就是应用这种形式。针对 Memory Corruption 类问题往往都是小内存申请开释(4k 以内)造成的问题,所以临时不须要 hook mmap 相干函数。兼容方面咱们适配了 Android5.x~11。

栈回溯计划选型

​ 安卓上的栈回溯规范品种繁多,通过调研比拟支流栈回溯计划。

​ 通过比拟和试验,在 Arm64 上咱们选用了 fp 的形式来进行栈回溯,Arm64 设施帧指针默认是开启状态,且通过试验察看线上 App 中 64 位的 so 没有敞开帧指针,且 fp 形式栈回溯简直不耗时,通过理论测试,15 层栈帧回溯均匀在 1~2μs,其余栈回溯根本都在 ms 级别。

​ 对于 Arm32 设施,帧指针默认是敞开状态。所以在 Arm32 设施无奈通过 fp 栈回溯形式记录 App 的内存调配和开释流程。咱们对 libunwind_stack 进行了优化,因而在 Arm32 下咱们抉择 libunwind_stack 来实现栈回溯,来达到记录调配和开释堆栈轨迹。

双采样内存配置策略

​ 对于现有 Memory Corruption 类问题监控,往往是通过注入、插桩形式监控所有内存申请和开释,而用户应用中一次滑动事件,App 程序都会申请开释数千到数万次。叠加栈回溯能力,会对被监控程序造成重大的性能影响,导致用户体验变差,呈现卡顿等问题。

​ 针对这类问题,这里采纳双采样机制来管制用户数与客户端内存调配数的形式。双采样是指服务端发送配置文件采样和客户端针对内存调配进行随机采样调配治理的办法,来对内存调配和开释进行监控 服务端配置文件采样是通过服务端设置用户采样比,依照不同问题类型、版本、机型等策略来进行按比例采样;客户端随机内存调配采样是通过端上随机调配采样算法来实现。这样对用户量和端上监控内存数就可进行随机内存调配配置化治理。

无感知检测

​ 当产生 Memory Corruption 类问题时,失常是会触发 SIGSEGV 类型解体。要做到用户无感知,就不能让程序产生解体退出。这里咱们的做法是通过注册 SIGSEGV 信号处理函数,当受控内存块被开释后会设置为不可读写权限,当产生异样时有代码拜访不可读写的这块内存就会触发 SIGSEGV,进入信号处理函数,在信号处理函数中,先确定以后产生异样的地址是否在咱们治理的内存池中,如果是咱们治理的内存段触发的异样,通过复原对应内存段的读写权限。来保障在信号处理流程中不触发程序退出流程。达到用户无感知检测 Memory Corruption 类问题。如果触发 SIGSEGV 的内存地址不在咱们治理的内存段中,就转发信号给原有的信号处理函数解决。

计划流程

计划优缺点

  • 长处

    • 线上线下可用,接入成本低,依赖 aar 组件初始化即可,无需额定操作;
    • 可配置化治理,通过云端下发配置,动静开关性能;
    • 内存调配采样治理,内存池线上管制在 100KB~8MB,总内存开销在 700KB~8.6MB;

  • 毛病

    • 监控内存块大小,最大 4kb;
    • 对非堆内存导致的解体问题无奈检测;
    • 临时不反对 ios/x86,前期可反对;
    • 不反对 Android4.4 及以下版本,前期可反对;

线上成果与案例剖析

​ MemCorruption 工具在字节多个 App 上线后,目前发现各类根底库 Memory Corruption 问题 200+。通过工具已定位解决问题 30+。

案例 1、UseAfterFree 问题

​ 日志记录信息,异样栈、Free 栈、Alloc 栈。Abort msg 会记录内存调配大小,Free 栈和 Alloc 栈记录调配和开释的线程信息。通过这些信息能够晓得一块内存的调配和开释状况。联合源码即可定位问题。

​ 上面是线上检测字节头部业务 SDK 有 UAF 问题,Abort msg 信息可知是 UAF 问题、申请内存大小 256byte,拜访内存 0x7a25a28b00 偏移 240byte 时触发 UAF 检测。这里也就是拜访了一个构造体变量的成员变量时产生了 UAF 问题。阐明对应构造体变量被开释后又应用。

  • 异样栈与 Abort msg

触发检测代码逻辑

  • Free 栈信息

​ 通过 Free 内存块栈信息,能够确定是 11170 线程开释了对应内存,联合代码可定位开释内存块变量 m_pDefaultFilter,而 m_filterType 是 m_pDefaultFilter 的成员变量。

Free 对象内存代码

  • Alloc 内存信息

​ 通过下面信息咱们很容易能判断出是因为 m_pDefaultFilter 实例对象曾经被开释,之后拜访其成员变量 m_filtertype 时内存曾经开释,就会触发 UAF 检测。MemCorruption 工具比传统的 Tombstone 只有一个异样栈的状况下,对剖析问题更清晰,且抓到的是问题第一现场。缩短研发同学对问题排查工夫,以晋升问题解决效率。

案例 2、DoubleFree 问题

  • 异样栈与 Abort msg

  • Free 栈信息

  • Alloc 栈信息

​ 从上述信息可知,libbinder 库中存在 double free 异样,free 有两条链路能够开释 Parcel 类的 mData 或 mObjects 对象。

链路一:在 java 层调用 recycle–>freebuffer–…–>freeData–>freeDataNoInit–>free, 在 freeDataNoInit 中会 free mData 和 mObjects 两个对象;

链路二:在 java 层调用 writeString–> nativeWritexxx–>writexxx16–>writexxx–>continueWrite–>realloc–>free;

continueWrite 和 freeDataNoInit 代码在 Parcel.cpp 且没有爱护,对于 mData 和 mObjects 对象的生命周期存在并发导致的屡次开释问题。联合异样栈信息,业务代码做爱护修复。

总结

​ Memory Corruption 问题是 C /C++ 开发人员避不开的问题。MemCorruption 工具原理并不简单,在大规模用户场景下,通过采样形式监控内存调配和开释,发现问题不触发程序解体,可能无效的发现线上低概率、边缘场景引发的 Memory Corruption 问题。缩小 App 程序破绽、晋升 App 稳定性。

​ MemCorruption 工具只是字节治理线上 Memory Corruption 类问题的一个点。还有很多的方面须要欠缺。请继续关注字节跳动终端技术团队,后续更加精彩。

后续打算

​ MemCorruption 工具为了不影响线上 App 性能,对内存监控范畴做了限度,后续咱们会扩大这部分的能力。同时 iOS 中也存在 Memory Corruption 类问题,iOS 版本敬请期待。

​ 此工具将来将在 APMPlus 中上线,APMPlus 是字节跳动利用开发套件 MARS 下的性能监控产品,通过先进的数据采集与监控技术,为企业提供全链路的利用性能监控服务,解决企业对各端监控的需要。具备非侵入式监控、丰盛的异样现场还原能力,助力企业晋升异样问题排查与解决的效率、优化利用品质,以降低成本进步支出。

​ 目前 APMPlus 面向新用户提供 试用 30 天的限时收费服务。其中蕴含 App 监控、Web 监控、Server 监控、小程序监控,App 监控和 Web 监控各 500 万条事件量,Server 与小程序监控限时不限量,欢送收费接入试用。

点击链接进入官网查看更多产品信息。

正文完
 0