未定义行为消毒器

介绍

未定义行为消毒器 (UBSan) 是一种快速未定义行为检测器。UBSan 在编译时修改程序,以在程序执行期间捕获各种类型的未定义行为,例如

  • 数组下标越界,其中边界可以静态确定

  • 超出其数据类型范围的按位移位

  • 取消对未对齐或空指针的引用

  • 有符号整数溢出

  • 转换为、从或在浮点类型之间转换,会导致溢出目标

请参阅下面列出的所有可用 检查

UBSan 具有可选的运行时库,可以提供更好的错误报告。这些检查的运行时成本很小,不会影响地址空间布局或 ABI。

如何构建

使用 CMake 构建 LLVM/Clang。

使用方法

使用 clang++ 使用 -fsanitize=undefined 选项编译和链接您的程序。确保使用 clang++(而不是 ld)作为链接器,以便您的可执行文件与正确的 UBSan 运行时库链接,除非所有启用的检查都使用陷阱模式。如果您正在编译/链接 C 代码,可以使用 clang 而不是 clang++

% cat test.cc
int main(int argc, char **argv) {
  int k = 0x7fffffff;
  k += argc;
  return 0;
}
% clang++ -fsanitize=undefined test.cc
% ./a.out
test.cc:3:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

您可以使用 -fsanitize=...-fno-sanitize= 来启用和禁用一个检查或一个检查组。对于单个检查,最后一个启用或禁用的选项将获胜。

# Enable all checks in the "undefined" group, but disable "alignment".
% clang -fsanitize=undefined -fno-sanitize=alignment a.c

# Enable just "alignment".
% clang -fsanitize=alignment a.c

# The same. -fno-sanitize=undefined nullifies the previous -fsanitize=undefined.
% clang -fsanitize=undefined -fno-sanitize=undefined -fsanitize=alignment a.c

对于大多数检查 (检查),检测到的程序会打印详细的错误报告,并在检查失败时继续执行。您可以使用以下选项更改错误报告行为

  • -fno-sanitize-recover=...: 打印详细的错误报告并退出程序;

  • -fsanitize-trap=...: 执行陷阱指令(不需要 UBSan 运行时支持)。如果未捕获信号,程序通常会因 SIGILLSIGTRAP 信号而终止。

例如

% clang++ -fsanitize=signed-integer-overflow,null,alignment -fno-sanitize-recover=null -fsanitize-trap=alignment a.cc

程序将在有符号整数溢出后继续执行,在第一次使用空指针后退出,并在第一次使用未对齐指针后发生陷阱。

% clang++ -fsanitize=undefined -fsanitize-trap=all a.cc

“undefined” 组中的所有检查都将进入陷阱模式。由于没有检查需要运行时支持,因此不会链接 UBSan 运行时库。请注意,某些其他消毒器也支持陷阱模式,并且 -fsanitize-trap=all 会为它们启用陷阱模式。

% clang -fsanitize-trap=undefined -fsanitize-recover=all a.c

-fsanitize-trap=-fsanitize-recover= 在没有 -fsanitize= 选项的情况下无效。没有未使用的命令行选项警告。

可用的检查

可用的检查是

  • -fsanitize=alignment: 使用未对齐指针或创建未对齐引用。还对 assume_aligned 类属性进行消毒。

  • -fsanitize=bool: 加载一个 bool 值,该值既不是 true 也不 false

  • -fsanitize=builtin: 向编译器内置函数传递无效值。

  • -fsanitize=bounds: 数组索引越界,在可以静态确定数组边界的情况下。该检查包括 -fsanitize=array-bounds-fsanitize=local-bounds。请注意,-fsanitize=local-bounds 不包含在 -fsanitize=undefined 中。

  • -fsanitize=enum: 加载枚举类型的值,该值不在该枚举类型可表示值的范围内。

  • -fsanitize=float-cast-overflow: 转换为、从或在浮点类型之间转换,会导致溢出目标。由于 Clang 支持的所有浮点类型的可表示值范围为 [-inf, +inf],因此检测到的唯一情况是从浮点类型到整型类型的转换。

  • -fsanitize=float-divide-by-zero: 浮点除以零。根据 C 和 C++ 标准,这未定义,但由 Clang(以及 ISO/IEC/IEEE 60559 / IEEE 754)定义为生成无穷大或 NaN 值,因此不包含在 -fsanitize=undefined 中。

  • -fsanitize=function: 通过错误类型的函数指针间接调用函数。

  • -fsanitize=implicit-unsigned-integer-truncation, -fsanitize=implicit-signed-integer-truncation: 从较宽位宽的整数到较窄位宽的整数的隐式转换,如果导致数据丢失。也就是说,如果降级后的值在转换回原始宽度后不等于降级之前的原始值。-fsanitize=implicit-unsigned-integer-truncation 处理两个 unsigned 类型之间的转换,而 -fsanitize=implicit-signed-integer-truncation 处理其余的转换 - 当其中一个或两个类型都有符号时。这些消毒器捕获的问题不是未定义行为,但通常是无意的。

  • -fsanitize=implicit-integer-sign-change: 整数类型之间的隐式转换,如果导致值的符号发生变化。也就是说,如果原始值为负而新值为正(或零),或者原始值为正而新值为负。这些消毒器捕获的问题不是未定义行为,但通常是无意的。

  • -fsanitize=integer-divide-by-zero: 整数除以零。

  • -fsanitize=implicit-bitfield-conversion: 从较宽位宽的整数到较窄位域的隐式转换,如果导致数据丢失。这包括无符号/有符号截断和符号更改,类似于 -fsanitize=implicit-integer-conversion 组的工作方式,但专门针对位域。

  • -fsanitize=nonnull-attribute: 将空指针作为声明为从不为空的函数参数传递。

  • -fsanitize=null: 使用空指针或创建空引用。

  • -fsanitize=nullability-arg: 将空指针作为标注有 _Nonnull 的函数参数传递。

  • -fsanitize=nullability-assign: 将空指针分配给标注有 _Nonnull 的左值。

  • -fsanitize=nullability-return: 从返回值类型标注有 _Nonnull 的函数中返回空指针。

  • -fsanitize=objc-cast: 将 ObjC 对象指针隐式转换到不兼容类型的无效操作。这通常是无意的,但不是未定义的行为,因此该检查不是 undefined 组的一部分。当前仅在 Darwin 上受支持。

  • -fsanitize=object-size: 尝试使用可能被优化器确定不是正在访问的对象一部分的字节。这还将检测某些类型的未定义行为,这些行为可能不会直接访问内存,但由于所涉及对象的尺寸而被证明是错误的,例如无效向下转换和对无效指针调用方法。这些检查是根据 __builtin_object_size 进行的,因此可能能够在更高的优化级别检测更多问题。

  • -fsanitize=pointer-overflow: 执行指针运算,该运算会溢出,或者旧指针值或新指针值为空指针(或在 C 中,当它们都是空指针时)。

  • -fsanitize=return: 在 C++ 中,到达返回值函数的末尾而不返回值。

  • -fsanitize=returns-nonnull-attribute: 从声明为从不返回空指针的函数中返回空指针。

  • -fsanitize=shift: 移位运算符,其中移位的位数大于或等于左操作数的提升位宽或小于零,或者其中左操作数为负。对于有符号左移,还会检查 C 中是否有有符号溢出,以及 C++ 中是否有无符号溢出。您可以使用 -fsanitize=shift-base-fsanitize=shift-exponent 分别仅检查移位运算的左操作数或右操作数。

  • -fsanitize=unsigned-shift-base: 检查左移运算的无符号左操作数是否不会溢出。这些消毒器捕获的问题不是未定义行为,但通常是无意的。

  • -fsanitize=signed-integer-overflow: 有符号整数溢出,即有符号整数计算结果无法在其类型中表示。这包括由 -ftrapv 涵盖的所有检查,以及对有符号除法溢出 (INT_MIN/-1) 的检查。请注意,即使启用 -fwrapv,也会添加检查。此消毒器不检查计算之前执行的丢失隐式转换(请参见 -fsanitize=implicit-integer-conversion)。这两个问题都由 -fsanitize=implicit-integer-conversion 组的检查处理。

  • -fsanitize=unreachable: 如果控制流到达一个不可到达的程序点。

  • -fsanitize=unsigned-integer-overflow: 无符号整数溢出,即无符号整数计算结果无法在其类型中表示。与有符号整数溢出不同,这不是未定义的行为,但通常是无意的。此消毒器不检查此类计算之前执行的丢失隐式转换(请参见 -fsanitize=implicit-integer-conversion)。

  • -fsanitize=vla-bound: 一个变量长度数组,其边界计算结果不是正值。

  • -fsanitize=vptr: 使用一个对象的 vptr,该 vptr 指示该对象是错误的动态类型,或者其生命周期尚未开始或已结束。与 -fno-rtti 不兼容。链接必须由 clang++ 执行,而不是 clang,以确保存在 C++ 特定的运行时库和 C++ 标准库。

您还可以使用以下检查组
  • -fsanitize=undefined: 除 float-divide-by-zerounsigned-integer-overflowimplicit-conversionlocal-boundsnullability-* 组检查之外的所有上述检查。

  • -fsanitize=undefined-trap: -fsanitize=undefined 的已弃用别名。

  • -fsanitize=implicit-integer-truncation: 捕获丢失的整数转换。启用 implicit-signed-integer-truncationimplicit-unsigned-integer-truncation

  • -fsanitize=implicit-integer-arithmetic-value-change: 捕获更改整数的算术值的隐式转换。启用 implicit-signed-integer-truncationimplicit-integer-sign-change

  • -fsanitize=implicit-integer-conversion: 检查隐式整数转换的可疑行为。启用 implicit-unsigned-integer-truncationimplicit-signed-integer-truncationimplicit-integer-sign-change

  • -fsanitize=implicit-conversion: 检查隐式转换的可疑行为。启用 implicit-integer-conversionimplicit-bitfield-conversion

  • -fsanitize=integer: 检查未定义或可疑的整数行为(例如无符号整数溢出)。启用 signed-integer-overflowunsigned-integer-overflowshiftinteger-divide-by-zeroimplicit-unsigned-integer-truncationimplicit-signed-integer-truncationimplicit-integer-sign-change

  • -fsanitize=nullability: 启用 nullability-argnullability-assignnullability-return。虽然违反可空性不具有未定义的行为,但它通常是无意的,因此 UBSan 提供了捕捉它的功能。

易变

对带有 volatile 限定符的类型的指针,nullalignmentobject-sizelocal-boundsvptr 检查不适用。

最小运行时

有一个适用于生产环境的最小 UBSan 运行时。此运行时的攻击面很小。它只提供非常基本的错误记录和重复数据删除,并且不支持 -fsanitize=vptr 检查。

要使用最小运行时,请将 -fsanitize-minimal-runtime 添加到 clang 命令行选项中。例如,如果您习惯使用 -fsanitize=undefined 编译,则可以使用 -fsanitize=undefined -fsanitize-minimal-runtime 启用最小运行时。

堆栈跟踪和报告符号化

如果您希望 UBSan 为每个错误报告打印符号化的堆栈跟踪,您需要

  1. 使用 -g-fno-omit-frame-pointer 编译以在您的二进制文件中获取正确的调试信息。

  2. 使用环境变量 UBSAN_OPTIONS=print_stacktrace=1 运行您的程序。

  3. 确保 llvm-symbolizer 二进制文件位于 PATH 中。

日志记录

诊断的默认日志文件是“stderr”。要将诊断记录到另一个文件,您可以设置 UBSAN_OPTIONS=log_path=...

静默无符号整数溢出

要静默来自无符号整数溢出的报告,您可以设置 UBSAN_OPTIONS=silence_unsigned_overflow=1。此功能与 -fsanitize-recover=unsigned-integer-overflow 结合使用,特别适用于在不炸毁日志的情况下提供模糊信号。

禁用对常见溢出模式的检测

存在某些依赖于溢出或容易发生溢出的代码模式,这些模式会为整数溢出/截断消毒器产生大量噪音。否定无符号常量、循环条件中的后递减以及简单的溢出检查是公认的且普遍存在的代码模式。但是,从检测这些代码模式的消毒器接收到的信号对于某些项目来说可能过于嘈杂。要禁用对这些常见模式的检测,应使用 -fsanitize-undefined-ignore-overflow-pattern=

目前,此选项支持三种依赖于溢出的代码习语

negated-unsigned-const

/// -fsanitize-undefined-ignore-overflow-pattern=negated-unsigned-const
unsigned long foo = -1UL; // No longer causes a negation overflow warning
unsigned long bar = -2UL; // and so on...

unsigned-post-decr-while

/// -fsanitize-undefined-ignore-overflow-pattern=unsigned-post-decr-while
unsigned char count = 16;
while (count--) { /* ... */ } // No longer causes unsigned-integer-overflow sanitizer to trip

add-signed-overflow-test,add-unsigned-overflow-test

/// -fsanitize-undefined-ignore-overflow-pattern=add-(signed|unsigned)-overflow-test
if (base + offset < base) { /* ... */ } // The pattern of `a + b < a`, and other re-orderings,
                                        // won't be instrumented (signed or unsigned types)
溢出模式类型

模式

消毒器

negated-unsigned-const

unsigned-integer-overflow

unsigned-post-decr-while

unsigned-integer-overflow

add-unsigned-overflow-test

unsigned-integer-overflow

add-signed-overflow-test

signed-integer-overflow

注意:add-signed-overflow-test 只会抑制对未定义行为的检查。积极的未定义行为优化仍然有可能。可以使用 -fwrapv-fno-strict-overflow 来解决此问题。

您可以使用 -fsanitize-undefined-ignore-overflow-pattern=all 启用所有排除,或使用 -fsanitize-undefined-ignore-overflow-pattern=none 禁用所有排除。如果未指定 -fsanitize-undefined-ignore-overflow-pattern,则隐式为 none。在其他值旁边指定 none 也隐式为 none,因为 none 的优先级高于其他值,包括 all

问题抑制

UndefinedBehaviorSanitizer 不应该产生误报。如果您看到一个,请再看一下;很可能是真阳性!

使用 __attribute__((no_sanitize("undefined"))) 禁用检测

您可以使用 __attribute__((no_sanitize("undefined"))) 禁用特定函数的 UBSan 检查。您可以在此属性中使用 -fsanitize= 标志的所有值,例如,如果您的函数故意包含可能的有符号整数溢出,您可以使用 __attribute__((no_sanitize("signed-integer-overflow")))

此属性可能不受其他编译器的支持,因此请考虑将其与 #if defined(__clang__) 结合使用。

抑制重新编译代码中的错误(忽略列表)

UndefinedBehaviorSanitizer 在 Sanitizer 特殊情况列表 中支持 srcfun 实体类型,可用于抑制指定源文件或函数中的错误报告。

运行时抑制

有时您可以在不重新编译代码的情况下,抑制特定文件、函数或库的 UBSan 错误报告。您需要在 UBSAN_OPTIONS 环境变量中传递一个抑制文件的路径。

UBSAN_OPTIONS=suppressions=MyUBSan.supp

您需要指定您要抑制的 检查 以及错误位置。例如

signed-integer-overflow:file-with-known-overflow.cpp
alignment:function_doing_unaligned_access
vptr:shared_object_with_vptr_failures.so

有一些限制

  • 有时您的二进制文件必须具有足够的调试信息和/或符号表,以便运行时可以找出源文件或函数名称以与抑制进行匹配。

  • 只能抑制可恢复的检查。对于上面的示例,您可以额外传递 -fsanitize-recover=signed-integer-overflow,alignment,vptr,尽管大多数 UBSan 检查默认情况下是可恢复的。

  • 在抑制文件里不能使用检查组(如 undefined),只支持细粒度检查。

安全注意事项

UndefinedBehaviorSanitizer 的运行时旨在用于测试目的,其在生产环境中的使用应从安全的角度谨慎考虑,因为它可能会损害生成的执行文件的安全性。对于安全敏感的应用程序,请考虑使用 最小运行时 或对所有检查使用陷阱模式。

支持的平台

UndefinedBehaviorSanitizer 在以下操作系统上受支持:

  • Android

  • Linux

  • NetBSD

  • FreeBSD

  • OpenBSD

  • macOS

  • Windows

运行时库具有相对的移植性和平台无关性。如果您需要的操作系统未在上面列出,UndefinedBehaviorSanitizer 可能已经可以使用,或者可以通过少量移植工作来实现。

当前状态

UndefinedBehaviorSanitizer 从 LLVM 3.3 开始在选定的平台上可用。测试套件集成到 CMake 构建中,可以使用 check-ubsan 命令运行。

其他配置

除非处于陷阱模式,否则 UndefinedBehaviorSanitizer 会为每个检查添加静态检查数据。此检查数据包含完整的文件名。选项 -fsanitize-undefined-strip-path-components=N 可用于修剪此信息。如果 N 为正数,则 UndefinedBehaviorSanitizer 发出的文件信息将从文件路径中删除前 N 个组件。如果 N 为负数,则将保留最后 N 个组件。

示例

对于名为 /code/library/file.cpp 的文件,以下是将发出的内容:

  • 默认(无标志,或 -fsanitize-undefined-strip-path-components=0): /code/library/file.cpp

  • -fsanitize-undefined-strip-path-components=1code/library/file.cpp

  • -fsanitize-undefined-strip-path-components=2library/file.cpp

  • -fsanitize-undefined-strip-path-components=-1file.cpp

  • -fsanitize-undefined-strip-path-components=-2library/file.cpp

更多信息