Scudo

Scudo 是一个动态的用户模式内存分配器(也称为堆分配器),旨在抵御与堆相关的漏洞(如基于堆的缓冲区溢出释放后再使用重复释放),同时保持性能良好。它提供了标准 C 分配和取消分配基元(如 malloc 和 free),以及 C++ 基元(如 new 和 delete)。

AddressSanitizer (ASan) 等成熟的内存错误检测器相比,Scudo 更像是一个缓解工具。

从 Android 11 版本开始,Scudo 会用于所有原生代码(低内存设备除外,其仍使用 jemalloc)。在运行时,所有可执行文件及其库依赖项的所有原生堆分配和取消分配操作均由 Scudo 完成;如果在堆中检测到损坏情况或可疑行为,该进程会中止。

在 Android 10 中,必须通过在 .mk 文件中设置 LOCAL_SANITIZE := scudo 选项或在 .bp 文件中设置 sanitize: { scudo: true, } 选项,以按二进制文件启用 Scudo。

Scudo 是开放源代码的,并且是 LLVM 的 compiler-rt 项目的一部分。如需获取相关文档,请访问 https://llvm.org/docs/ScudoHardenedAllocator.html。Scudo 运行时是 Android 工具链的一部分,且已针对 Soong 和 Make 添加了支持,以允许在二进制文件中轻松启用分配器。

您可以使用下述选项在分配器中启用或停用其他缓解功能。

自定义

可以通过以下几种方式针对各进程定义分配器的一些参数:

  • 静态:在程序中定义 __scudo_default_options 函数(返回要解析的选项字符串)。该函数必须具有以下原型:extern "C" const char *__scudo_default_options()
  • 动态:使用环境变量 SCUDO_OPTIONS(包含要解析的选项字符串)。以这种方式定义的选项会替换通过 __scudo_default_options 进行的任何定义。

您可以使用以下选项。

选项 64 位默认值 32 位默认值 说明
QuarantineSizeKb 256 64 用于延迟区块的实际取消分配的隔离区大小(以 KB 为单位)。如果指定较低的值,可能会减少内存使用量,但会降低缓解功能的效果;如果指定负值,会还原为默认值。将此选项和 ThreadLocalQuarantineSizeKb 均设置为零会完全停用隔离区。
QuarantineChunksUpToSize 2048 512 可以隔离的最大区块大小(以字节为单位)。
ThreadLocalQuarantineSizeKb 64 16 用于分流全局隔离区的每个线程中的缓存大小(以 KB 为单位)。 如果指定较低的值,可能会减少内存使用量,但也可能会增加对全局隔离区的争用。将此选项和 QuarantineSizeKb 均设置为零会完全停用隔离区。
DeallocationTypeMismatch false false 启用针对 malloc/delete、new/free、new/delete[] 的错误报告。
DeleteSizeMismatch true true 启用针对 new 和 delete 大小不匹配的错误报告。
ZeroContents false false 在分配和取消分配时启用零区块内容。
allocator_may_return_null false false 指定在出现可恢复的错误时分配器可以返回 null,而不是终止进程。
hard_rss_limit_mb 0 0 当进程的 RSS 达到此限制时,进程会终止。
soft_rss_limit_mb 0 0 当进程的 RSS 达到此限制时,进一步分配会失败或返回 null(具体取决于 allocator_may_return_null 的值),直到 RSS 回落以允许新的分配为止。
allocator_release_to_os_interval_ms 5000 仅影响 64 位分配器。如果设置了该选项,会尝试将未使用的内存释放给操作系统,但释放频率不会超过此间隔(以毫秒为单位)。 如果值为负,不会将内存释放给操作系统。
abort_on_error true true 如果设置了该选项,该工具会在报告错误消息后调用 abort()(而不是 _exit())。

验证

目前,没有专门针对 Scudo 的 CTS 测试。因此请确保 CTS 测试在针对指定二进制文件启用或未启用 Scudo 的情况下均能通过,以确认它不会影响到设备。

问题排查

如果检测到不可恢复的问题,分配器会向标准错误描述符显示一条错误消息,然后终止进程。导致进程终止的堆栈轨迹会添加到系统日志中。输出通常以 Scudo ERROR: 开头,后跟问题的简短摘要以及任何指针。

下面列出了当前的错误消息及其潜在原因:

  • corrupted chunk header:区块头的校验和验证失败。可能原因有二:区块头被部分或全部覆盖,也可能是传递给函数的指针不是区块。
  • race on chunk header:两个不同的线程会同时尝试操控同一区块头。这种症状通常是在对该区块执行操作时出现争用情况或通常未进行锁定造成的。
  • invalid chunk state:对于指定操作,区块未处于预期状态,例如,在尝试释放区块时其处于未分配状态,或者在尝试回收区块时其未处于隔离状态。双重释放是造成此错误的典型原因。
  • misaligned pointer:强制执行基本对齐要求:32 位平台上为 8 个字节,64 位平台上为 16 个字节。如果传递给函数的指针不适合这些函数,传递给其中一个函数的指针就不会对齐。
  • allocation type mismatch:启用此选项后,在区块上调用的取消分配函数必须与用于分配区块而调用的函数类型一致。类型不一致会引发安全问题。
  • invalid sized delete:如果使用的是符合 C++14 标准的删除运算符,在启用可选检查之后,取消分配区块时传递的大小与分配区块时请求的大小会出现不一致的情况。这通常是由于编译器出现问题或是对要取消分配的对象产生了类型混淆
  • RSS limit exhausted:已超出选择性指定的 RSS 大小上限。

如果您要调试操作系统本身出现的崩溃问题,可以使用 HWASan 操作系统 build。如果调试应用中的崩溃,也可以使用 HWASan 应用 build