UndefinedBehaviorSanitizer (UBSan) 会执行编译时插桩,以检查各种类型的未定义行为。UBSan 能够检测许多未定义行为 bug,而 Android 支持:
- alignment
- bool
- bounds
- 枚举
- float-cast-overflow
- float-divide-by-zero
- integer-divide-by-zero
- nonnull-attribute
- null
- 回攻
- returns-nonnull-attribute
- shift-base
- shift-exponent
- signed-integer-overflow
- unreachable
- unsigned-integer-overflow
- vla-bound
虽然 unsigned-integer-overflow 在技术上不是未定义行为,但其包含在排错程序中,并用在许多 Android 模块(包括 mediaserver 组件)中,以消除任何潜在的整数溢出漏洞。
实现
在 Android 构建系统中,您可以全局或局部地启用 UBSan。如需全局启用 UBSan,请在 Android.mk 中设置 SANITIZE_TARGET。要在每个模块级别启用 UBSan,请在 Android.mk 中设置 LOCAL_SANITIZE 并指定要查找的未定义行为。例如:
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)
等效的蓝图 (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 还有 integer
和 default-ub
这两种快捷方式,它们可同时启用一组排错程序。integer 会启用 integer-divide-by-zero
、signed-integer-overflow
和 unsigned-integer-overflow
。default-ub
会启用对编译器性能造成很小影响的检查:bool, integer-divide-by-zero, return,
returns-nonnull-attribute, shift-exponent, unreachable and vla-bound
。integer 排错程序类可以与 SANITIZE_TARGET 和 LOCAL_SANITIZE 一起使用,而 default-ub 只能与 SANITIZE_TARGET 一起使用。
更好的错误报告
Android 的默认 UBSan 实现在遇到未定义行为时会调用指定的函数。默认情况下,此函数被中止。但是,从 2016 年 10 月开始,Android 上的 UBSan 有一个可选的运行时库,可以提供更详细的错误报告,包括遇到的未定义行为的类型、文件和源代码行信息。如需对 integer 检查启用此错误报告,请将以下内容添加到 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
是一组预封装的选项,用于各个有符号和无符号整数溢出排错程序,且带有一个默认屏蔽名单。LOCAL_SANITIZE_DIAG
用于为排错程序启用诊断模式。请仅在测试期间使用诊断模式,因为该模式不会在溢出发生时中止,而这会完全抹杀缓解功能的安全优势。如需了解更多详情,请参阅问题排查。LOCAL_SANITIZE_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
属性是一组预封装的选项,用于各个有符号和无符号整数溢出排错程序,且带有一个默认屏蔽名单。
diag
属性集用于为排错程序启用诊断模式。只能在测试期间使用诊断模式。因为诊断模式不会在溢出发生时中止,而这会完全抹杀用户 build 中缓解功能的安全优势。如需了解详情,请参阅问题排查。
BLOCKLIST
属性用于指定屏蔽名单文件,以便开发者防止函数和源文件成为排错对象。如需了解详情,请参阅问题排查。
要逐个启用排错程序,请使用以下代码:
sanitize: { misc_undefined: ["signed-integer-overflow", "unsigned-integer-overflow"], diag: { misc_undefined: ["signed-integer-overflow", "unsigned-integer-overflow",], }, BLOCKLIST: "modulename_BLOCKLIST.txt", },
问题排查
如果您在新组件中启用整数溢出排错功能,或者依赖启用了整数溢出排错功能的平台库,则可能会遇到良性整数溢出导致进程中止的一些问题。您应测试启用了排错功能的组件,以确保可以发现良性溢出。
如需查找由用户 build 中的排错功能导致的进程中止,请搜索带 Abort 消息(指示 UBSan 捕获了溢出)的 SIGABRT
崩溃,例如:
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
属性停用函数中的排错功能(示例) - 通过屏蔽名单文件来禁止对函数或源文件进行排错(示例)
您应尽可能使用最精细的解决方案。例如,如果某大型函数具有多个算术运算和单个溢出运算,则应对单个溢出运算进行重构,而不是将整个函数列入屏蔽名单。
可能导致良性溢出的常见模式包括:
- 在转换为有符号类型之前发生无符号溢出的隐式类型转换(示例)
- 删除关联的列表,且删除时循环索引会递减(示例)
- 将无符号类型指定为 -1,而不是指定实际最大值(示例)
- 在条件中使无符号整数递减的循环(示例、示例)
在停用排错功能之前,开发者最好是确保排错程序检测到的溢出确实是良性的,并且没有意外的副作用或安全隐患。
停用 IntSan
您可以使用屏蔽名单或函数属性来停用 IntSan。请仅在重构代码不合理或者存在有问题的性能开销时,才谨慎停用 IntSan。
如需详细了解如何使用函数属性和屏蔽名单文件格式设置来停用 IntSan,请参阅上游 Clang 文档。应将屏蔽名单的作用范围限定为特定的排错程序,方法是使用区块名称指定目标排错程序,以免影响其他排错程序。
验证
目前没有专门针对整数溢出排错功能的 CTS 测试。 因此请确保 CTS 测试在启用或未启用 IntSan 的情况下均能通过,以证明其不会给设备带来影响。
边界排错功能
BoundsSanitizer (BoundSan) 将插桩添加到二进制文件,以插入对数组访问的边界检查。如果编译器在编译时无法证明访问将会是安全的,并且在运行时将会知道数组的大小,便会添加这些检查,以便对数组访问进行检查。Android 10 在蓝牙和编解码器中部署了 BoundSan。BoundSan 由编译器提供,在整个平台的各个组件中默认启用。
实现
BoundSan 使用 UBSan 的边界排错程序。此缓解功能在各个模块级别启用,有助于确保 Android 关键组件的安全性,因此不应停用。
我们强烈建议您为更多组件启用 BoundSan。理想的候选对象是特权原生代码或可解析不可信用户输入的复杂原生代码。与启用 BoundSan 相关的性能开销取决于无法证明安全的数组访问次数。预计开销所占的平均百分比很小,如果担心性能,您可以进行测试。
在 blueprint 文件中启用 BoundSan
您可以在 blueprint 文件中启用 BoundSan,方法是将 "bounds"
添加到二进制文件和库模块的 misc_undefined
sanitize 属性:
sanitize: { misc_undefined: ["bounds"], diag: { misc_undefined: ["bounds"], }, BLOCKLIST: "modulename_BLOCKLIST.txt",
diag
diag
属性用于为排错程序启用诊断模式。只能在测试期间使用诊断模式。诊断模式不会在溢出发生时中止,而这会抹杀该缓解功能的安全优势并导致更高的性能开销,因此不建议在正式版中使用。
屏蔽名单
BLOCKLIST
属性用于指定屏蔽名单文件,开发者可用来防止函数和源文件成为排错对象。仅当您担心性能且目标文件/函数有很大的影响时,才能使用此属性。应手动审核这些文件/函数以确保数组访问安全无虞。如需了解详情,请参阅问题排查。
在 makefile 中启用 BoundSan
您可以在 makefile 中启用 BoundSan,方法是将 "bounds"
添加到二进制文件和库模块的 LOCAL_SANITIZE
变量:
LOCAL_SANITIZE := bounds # Optional features LOCAL_SANITIZE_DIAG := bounds LOCAL_SANITIZE_BLOCKLIST := modulename_BLOCKLIST.txt
LOCAL_SANITIZE
接受以英文逗号分隔的排错程序列表。
LOCAL_SANITIZE_DIAG
用于开启诊断模式。只能在测试期间使用诊断模式。诊断模式不会在溢出发生时中止,而这会抹杀该缓解功能的安全优势并导致更高的性能开销,因此不建议在正式版中使用。
LOCAL_SANITIZE_BLOCKLIST
用于指定屏蔽名单文件,以便开发者防止函数和源文件成为排错对象。仅当您担心性能且目标文件/函数有很大的影响时,才能使用此属性。应手动审核这些文件/函数以确保数组访问安全无虞。如需了解详情,请参阅问题排查。
停用 BoundSan
您可以使用屏蔽名单或函数属性在函数和源文件中停用 BoundSan。最好让 BoundSan 保持启用状态,只有在函数或文件产生大量的性能开销并且已手动审核源文件时,才将其停用。
如需详细了解如何使用函数属性和屏蔽名单文件格式设置停用 BoundSan,请参阅 Clang LLVM 文档。应将屏蔽名单的作用范围限定为特定的排错程序,方法是使用区块名称指定目标排错程序,以免影响其他排错程序。
验证
没有专门针对 BoundSan 的 CTS 测试。因此,请确保无论是否启用 BoundSan,均可通过 CTS 测试,以证实它不会给设备带来影响。
问题排查
您应在启用 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]'