UndefinedBehaviorSanitizer

UndefinedBehaviorSanitizer (UBSan) 执行编译时检测,以检查各种类型的未定义行为。虽然 UBSan 能够检测许多未定义行为错误,但 Android 支持

  • 对齐
  • 布尔值
  • 边界
  • 枚举
  • 浮点转换溢出
  • 浮点除以零
  • 整数除以零
  • 非空属性
  • 空值
  • 返回
  • 返回非空属性
  • 移位基数
  • 移位指数
  • 有符号整数溢出
  • 无法访问
  • 无符号整数溢出
  • VLA 边界

无符号整数溢出虽然从技术上讲不是未定义行为,但也包含在清理器中,并在包括 mediaserver 组件在内的许多 Android 模块中使用,以消除任何潜在的整数溢出漏洞。

实施

在 Android 构建系统中,您可以全局或本地启用 UBSan。要全局启用 UBSan,请在 Android.mk 中设置 SANITIZE_TARGET。要在模块级别启用 UBSan,请设置 LOCAL_SANITIZE 并指定您要在 Android.mk 中查找的未定义行为。例如

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_CFLAGS := -std=c11 -Wall -Werror -O0

LOCAL_SRC_FILES:= sanitizer-status.c

LOCAL_MODULE:= sanitizer-status

LOCAL_SANITIZE := alignment bounds null unreachable integer
LOCAL_SANITIZE_DIAG := alignment bounds null unreachable integer

include $(BUILD_EXECUTABLE)

以及等效的 Blueprint (Android.bp) 配置

cc_binary {

    cflags: [
        "-std=c11",
        "-Wall",
        "-Werror",
        "-O0",
    ],

    srcs: ["sanitizer-status.c"],

    name: "sanitizer-status",

    sanitize: {
        misc_undefined: [
            "alignment",
            "bounds",
            "null",
            "unreachable",
            "integer",
        ],
        diag: {
            misc_undefined: [
                "alignment",
                "bounds",
                "null",
                "unreachable",
                "integer",
            ],
        },
    },

}

UBSan 快捷方式

Android 还有两个快捷方式 integerdefault-ub,用于同时启用一组清理器。integer 启用 integer-divide-by-zerosigned-integer-overflowunsigned-integer-overflowdefault-ub 启用对编译器性能问题影响最小的检查:bool、integer-divide-by-zero、return、returns-nonnull-attribute、shift-exponent、unreachable 和 vla-bound。整数清理器类可以与 SANITIZE_TARGET 和 LOCAL_SANITIZE 一起使用,而 default-ub 只能与 SANITIZE_TARGET 一起使用。

更好的错误报告

Android 的默认 UBSan 实现会在遇到未定义行为时调用指定的函数。默认情况下,此函数是 abort。但是,从 2016 年 10 月开始,Android 上的 UBSan 有一个可选的运行时库,可以提供更详细的错误报告,包括遇到的未定义行为类型、文件和源代码行信息。要使用整数检查启用此错误报告,请将以下内容添加到 Android.mk 文件

LOCAL_SANITIZE:=integer
LOCAL_SANITIZE_DIAG:=integer

LOCAL_SANITIZE 值在构建期间启用清理器。LOCAL_SANITIZE_DIAG 启用指定清理器的诊断模式。可以将 LOCAL_SANITIZE 和 LOCAL_SANITIZE_DIAG 设置为不同的值,但只有 LOCAL_SANITIZE 中的检查才会启用。如果某个检查未在 LOCAL_SANITIZE 中指定,但在 LOCAL_SANITIZE_DIAG 中指定,则该检查不会启用,也不会给出诊断消息。

以下是 UBSan 运行时库提供的信息示例

pixel-xl:/ # sanitizer-status ubsan
sanitizer-status/sanitizer-status.c:53:6: runtime error: unsigned integer overflow: 18446744073709551615 + 1 cannot be represented in type 'size_t' (aka 'unsigned long')

整数溢出清理

意外的整数溢出可能会导致与内存访问或内存分配相关的变量中出现内存损坏或信息泄露漏洞。为了解决这个问题,我们在 Android 7.0 中添加了 Clang 的 UndefinedBehaviorSanitizer (UBSan) 有符号和无符号整数溢出清理器,以强化媒体框架。在 Android 9 中,我们扩展了 UBSan 以覆盖更多组件,并改进了对它的构建系统支持。

此设计旨在在可能溢出的算术运算/指令周围添加检查,以便在发生溢出时安全地中止进程。这些清理器可以缓解一整类内存损坏和信息泄露漏洞,这些漏洞的根本原因是整数溢出,例如最初的 Stagefright 漏洞。

示例和源代码

整数溢出清理 (IntSan) 由编译器提供,并在编译时将检测添加到二进制文件中,以检测算术溢出。默认情况下,平台中的各种组件都启用了它,例如 /platform/external/libnl/Android.bp

实施

IntSan 使用 UBSan 的有符号和无符号整数溢出清理器。此缓解措施在模块级别启用。它有助于保持 Android 关键组件的安全性,不应禁用。

我们强烈建议您为其他组件启用整数溢出清理。理想的候选对象是特权本机代码或解析不受信任用户输入的本机代码。与清理器相关的性能开销很小,具体取决于代码的使用情况和算术运算的普及程度。预计性能开销百分比很小,如果担心性能,请进行测试。

在 Makefile 中支持 IntSan

要在 Makefile 中启用 IntSan,请添加

LOCAL_SANITIZE := integer_overflow
    # Optional features
    LOCAL_SANITIZE_DIAG := integer_overflow
    LOCAL_SANITIZE_BLOCKLIST := modulename_BLOCKLIST.txt
  • LOCAL_SANITIZE 接受逗号分隔的清理器列表,其中 integer_overflow 是针对各个有符号和无符号整数溢出清理器的预打包选项集,并带有默认 BLOCKLIST
  • LOCAL_SANITIZE_DIAG 启用清理器的诊断模式。仅在测试期间使用诊断模式,因为这不会在溢出时中止,从而完全否定了缓解措施的安全优势。有关更多详情,请参阅问题排查
  • LOCAL_SANITIZE_BLOCKLIST 允许您指定 BLOCKLIST 文件,以防止函数和源文件被清理。有关更多详情,请参阅问题排查

如果您想要更精细的控制,请使用以下一个或两个标志单独启用清理器

LOCAL_SANITIZE := signed-integer-overflow, unsigned-integer-overflow
    LOCAL_SANITIZE_DIAG := signed-integer-overflow, unsigned-integer-overflow

在 Blueprint 文件中支持 IntSan

要在 Blueprint 文件(例如 /platform/external/libnl/Android.bp)中启用整数溢出清理,请添加

   sanitize: {
          integer_overflow: true,
          diag: {
              integer_overflow: true,
          },
          BLOCKLIST: "modulename_BLOCKLIST.txt",
       },

与 Makefile 一样,integer_overflow 属性是针对各个有符号和无符号整数溢出清理器的预打包选项集,并带有默认 BLOCKLIST

diag 属性集启用清理器的诊断模式。仅在测试期间使用诊断模式。诊断模式不会在溢出时中止,这完全否定了用户构建中缓解措施的安全优势。有关更多详情,请参阅问题排查

BLOCKLIST 属性允许指定 BLOCKLIST 文件,开发人员可以使用该文件来防止函数和源文件被清理。有关更多详情,请参阅问题排查

要单独启用清理器,请使用

   sanitize: {
          misc_undefined: ["signed-integer-overflow", "unsigned-integer-overflow"],
          diag: {
              misc_undefined: ["signed-integer-overflow",
                               "unsigned-integer-overflow",],
          },
          BLOCKLIST: "modulename_BLOCKLIST.txt",
       },

问题排查

如果您在新组件中启用整数溢出清理,或者依赖于已进行整数溢出清理的平台库,您可能会遇到一些良性整数溢出导致中止的问题。您应该测试启用清理的组件,以确保可以浮现良性溢出。

要查找用户构建中由清理引起的中止,请搜索 SIGABRT 崩溃,其中中止消息指示 UBSan 捕获的溢出,例如

pid: ###, tid: ###, name: Binder:###  >>> /system/bin/surfaceflinger <<<
    signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
    Abort message: 'ubsan: sub-overflow'

堆栈跟踪应包含导致中止的函数,但是,内联函数中发生的溢出可能在堆栈跟踪中不明显。

为了更轻松地确定根本原因,请在触发中止的库中启用诊断,并尝试重现错误。启用诊断后,进程不会中止,而是会继续运行。不中止有助于最大限度地增加特定执行路径中良性溢出的数量,而无需在修复每个错误后重新编译。诊断会生成一条错误消息,其中包含导致中止的行号和源文件

frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp:2188:32: runtime error: unsigned integer overflow: 0 - 1 cannot be represented in type 'size_t' (aka 'unsigned long')

找到有问题的算术运算后,请确保溢出是良性的且是预期的(例如,没有安全隐患)。您可以通过以下方式解决清理器中止问题:

  • 重构代码以避免溢出 (示例)
  • 通过 Clang 的 __builtin_*_overflow 函数显式溢出 (示例)
  • 通过指定 no_sanitize 属性禁用函数中的清理 (示例)
  • 通过 BLOCKLIST 文件禁用函数或源文件的清理 (示例)

您应该使用最精细的解决方案。例如,一个包含许多算术运算和一个溢出运算的大型函数应重构单个运算,而不是 BLOCKLIST 整个函数。

可能导致良性溢出的常见模式包括

  • 隐式转换,其中在转换为有符号类型之前发生无符号溢出 (示例)
  • 链表删除,这会在删除时递减循环索引 (示例)
  • 将无符号类型赋值为 -1 而不是指定实际最大值 (示例)
  • 在条件中递减无符号整数的循环 (示例示例)

建议开发人员确保清理器检测到溢出的情况确实是良性的,并且在禁用清理之前没有意外的副作用或安全隐患。

禁用 IntSan

您可以使用 BLOCKLIST 或函数属性禁用 IntSan。请谨慎禁用,并且仅在重构代码不合理或存在有问题的性能开销时才禁用。

有关使用 函数属性BLOCKLIST 文件格式禁用 IntSan 的更多信息,请参阅上游 Clang 文档。BLOCKLIST 应限定于特定清理器,方法是使用指定目标清理器的节名称,以避免影响其他清理器。

验证

目前,没有专门针对整数溢出清理的 CTS 测试。相反,请确保 CTS 测试在启用或未启用 IntSan 的情况下均可通过,以验证它是否不会影响设备。

边界清理

BoundsSanitizer (BoundSan) 向二进制文件添加检测,以在数组访问周围插入边界检查。如果编译器无法在编译时证明访问是安全的,并且如果数组的大小在运行时已知,则会添加这些检查,以便可以针对该大小进行检查。Android 10 在蓝牙和编解码器中部署了 BoundSan。BoundSan 由编译器提供,默认情况下在平台中的各种组件中启用。

实施

BoundSan 使用 UBSan 的边界清理器。此缓解措施在模块级别启用。它有助于保持 Android 关键组件的安全性,不应禁用。

我们强烈建议您为其他组件启用 BoundSan。理想的候选对象是特权本机代码或解析不受信任用户输入的复杂本机代码。与启用 BoundSan 相关的性能开销取决于无法证明安全的数组访问次数。预计平均性能开销百分比很小,如果担心性能,请进行测试。

在 Blueprint 文件中启用 BoundSan

可以通过将 "bounds" 添加到二进制和库模块的 misc_undefined 清理属性中,在 Blueprint 文件中启用 BoundSan

    sanitize: {
       misc_undefined: ["bounds"],
       diag: {
          misc_undefined: ["bounds"],
       },
       BLOCKLIST: "modulename_BLOCKLIST.txt",
诊断

diag 属性启用清理器的诊断模式。仅在测试期间使用诊断模式。诊断模式不会在溢出时中止,这否定了缓解措施的安全优势,并且会带来更高的性能开销,因此不建议用于生产版本。

BLOCKLIST

BLOCKLIST 属性允许指定 BLOCKLIST 文件,开发人员可以使用该文件来防止函数和源文件被清理。仅当性能成为问题且目标文件/函数贡献很大时才使用此属性。手动审核这些文件/函数以确保数组访问是安全的。有关更多详情,请参阅问题排查

在 Makefile 中启用 BoundSan

可以通过将 "bounds" 添加到二进制和库模块的 LOCAL_SANITIZE 变量中,在 Makefile 中启用 BoundSan

    LOCAL_SANITIZE := bounds
    # Optional features
    LOCAL_SANITIZE_DIAG := bounds
    LOCAL_SANITIZE_BLOCKLIST := modulename_BLOCKLIST.txt

LOCAL_SANITIZE 接受以逗号分隔的清理器列表。

LOCAL_SANITIZE_DIAG 启用诊断模式。仅在测试期间使用诊断模式。诊断模式不会在溢出时中止,这否定了缓解措施的安全优势,并且会带来更高的性能开销,因此不建议用于生产版本。

LOCAL_SANITIZE_BLOCKLIST 允许指定 BLOCKLIST 文件,开发人员可以使用该文件来防止函数和源文件被清理。仅当性能成为问题且目标文件/函数贡献很大时才使用此属性。手动审核这些文件/函数以确保数组访问是安全的。有关更多详情,请参阅问题排查

禁用 BoundSan

您可以使用 BLOCKLIST 或函数属性禁用函数和源文件中的 BoundSan。最好保持启用 BoundSan,因此仅当函数或文件产生大量性能开销且已手动审核源文件时才禁用它。

有关使用 函数属性BLOCKLIST 文件格式禁用 BoundSan 的更多信息,请参阅 Clang LLVM 文档。BLOCKLIST 应限定于特定清理器,方法是使用指定目标清理器的节名称,以避免影响其他清理器。

验证

没有专门针对 BoundSan 的 CTS 测试。相反,请确保 CTS 测试在启用或未启用 BoundSan 的情况下均可通过,以验证它是否不会影响设备。

问题排查

在启用 BoundSan 后彻底测试组件,以确保解决任何以前未检测到的越界访问。

BoundSan 错误很容易识别,因为它们包含以下 tombstone 中止消息

    pid: ###, tid: ###, name: Binder:###  >>> /system/bin/foobar <<<
    signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
    Abort message: 'ubsan: out-of-bounds'

在诊断模式下运行时,源文件、行号和索引值会打印到 logcat。默认情况下,此模式不会抛出中止消息。查看 logcat 以检查是否有任何错误。

    external/foo/bar.c:293:13: runtime error: index -1 out of bounds for type 'int [24]'