clang之UndefinedBehaviorSanitizer

5,218 阅读11分钟

Clang 12 documentation

Clang 12 documentation包含了一系列工具,如 AddressSanitizerThreadSanitizerLeakSanitizerLibTooling等。

  1. clang之AddressSanitizer
  2. clang之MemorySanitizer
  3. clang之LeakSanitizer
  4. clang之UndefinedBehaviorSanitizer
  5. clang之Hardware-assisted-AddressSanitizer
  6. clang之SafeStack
  7. clang之ShadowCallStack
  8. clang之ThreadSanitizer
  9. clang之Thread-Safety-Analysis
  10. clang之DataFlowSanitizer

这部分是对clang文档 Clang 12 documentation UndefinedBehaviorSanitizer 的翻译。仅供参考。

介绍

UndefinedBehavior 到底是啥?

So, what is undefined behavior? Undefined behavior happens when your code has correct syntax but its behavior is not within the bounds of what the language allows.

简单来说:代码语法正确,但是行为确实错误的。如整数除零、有符号整数溢出等类似操作。

C和C++标准中关于UndefinedBehavior的定义类似:

Undefined behavior: Behavior for which this international standard imposes no requirements.

UndefinedBehaviorSanitizer (UBSan)是一个快速检测程序中未定义行为的工具。UBSan会在编译时期对程序代码做一些修改,从而在运行时期捕获到程序中的各种未定义行为,如:

  • 使用未对齐的或者null的指针
  • 有符号的整数的溢出
  • 浮点类型的转换可能导致的溢出

这里可以查看完整的列表

UBSan有一个额外的运行时库,提供了更好的错误报告。检测操作的运行时消耗比较小,且对地址空间布局和ABI没有影响。

如何构建

使用CMake来构建LLVM/Clang。

用法

设置标记 -fsanitize=undefined 后,可以使用 clang++ 来编译和链接程序。确保使用 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'

你可以仅仅开启 UBSan 的一部分检查功能,对每一项检查定义对应的选项即可:

  • -fsanitize=...: 输出冗长的错误报告,并且继续执行(默认情况);
  • -fno-sanitize-recover=...: 输出冗长的错误报告,并且退出程序;
  • -fsanitize-trap=...: 执行一个(操作系统的)陷阱指令(不需要 UBSan 的运行时支持).

注意:trap / recover 选项并没有开启对应的 sanitizer,通常需要添加合适的 -fsanitize= 标记。

例如,可以编译连接程序:

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

在遇到有符号整数溢出(signed integer overflows)后,程序会继续执行;在第一次遇到空指针(null pointer)的非法使用的时候会退出;在第一次遇到未对齐指针(misaligned pointer)的使用时触发操作系统的陷阱指令。

额外的检查

有如下额外的检查:

  • -fsanitize=alignment: 未对齐指针的使用,或者未对齐引用的创建。另外会检查其他类似对齐的属性。
  • -fsanitize=bool: 加载一个非true且非false的bool值。
  • -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: 通过一个错误类型的函数指针,来间接调用一个函数。(仅支持Darwin/Linux, C++ and x86/x86_64)
  • -fsanitize=implicit-unsigned-integer-truncation, -fsanitize=implicit-signed-integer-truncation: 将更大的bit宽度的整数,隐式转换为更小的bit宽度的整数,在结果导致了数据丢失的情况下。即:精度被降级的值,再回退转换为原始值,是基于与降级转换之前的原始值并不相等。当其中至少有一个类型是有符号的情况下,-fsanitize=implicit-unsigned-integer-truncation 处理两个无符号类型之间的转换,-fsanitize=implicit-signed-integer-truncation 处理剩下的转换(即有符号类型)。这一类错误并非未定义行为,但却常常不经意就发生了。
  • -fsanitize=implicit-integer-sign-change: 因为整数类型之间的隐式转换,而导致值的符号被改变了。即,若最初值是负数而新值是正数(或者零),或者最初值是正数而新值是负数。这一类捕获的问题并非是未定义行为,但却常常不经意就发生了。
  • -fsanitize=integer-divide-by-zero: 整数除以零。
  • -fsanitize=nonnull-attribute: 将null指针作为函数参数传入,而该函数参数声明了不能为null。
  • -fsanitize=null: 使用一个null指针,或者创建一个null的引用。
  • -fsanitize=nullability-arg: 将null传递给一个标注为 _Nonnull 的函数参数。
  • -fsanitize=nullability-assign: 将null传给一个标注为 _Nonnull 的值。
  • -fsanitize=nullability-return: 从标注返回 _Nonnull 值的函数中返回一个null。
  • -fsanitize=objc-cast: 将ObjC对象指针隐式转换为不匹配的类型。这常常不经意就发生了,但却不是未定义行为,所以该检查项并非未定义的一部分。目前仅在Darwin上支持。
  • -fsanitize=object-size: 可能尝试去使用一些编译器能够决定的字节,然而这些字节却并非正在访问的对象的一部分(英文原文:An attempt to potentially use bytes which the optimizer can determine are not part of the object being accessed.)。这也可以检查一些类型的未定义行为:可能没有直接访问内存,而从对象大小来看的话确实是非法的,如非法的降级(downcasts)和使用非法指针来调用方法。 这些检查是根据 __builtin_object_size 来实施的,因此可能用于在更高的优化层级上来检查更多问题。
  • -fsanitize=pointer-overflow: 执行指针运算,却导致了溢出,或者旧的或新的指针值是一个null指针(或者在C中,它们都是null指针)。
  • -fsanitize=return: 在C++代码中,在函数将要结束时,却没有返回一个值。
  • -fsanitize=returns-nonnull-attribute: 在声明不会返回null的函数中,返回了null指针。
  • -fsanitize=shift: Shift operators where the amount shifted is greater or equal to the promoted bit-width of the left hand side or less than zero, or where the left hand side is negative. 对于一个有符号的左边shift操作,也会检查C中的有符号溢出,和C++中的无符号溢出。可以使用 -fsanitize=shift-base 或者 -fsanitize=shift-exponent 来仅仅各自检查shift操作的左边或右边。
  • -fsanitize=signed-integer-overflow: 有符号整数的运算结果无法使用其类型来表示的,即有符号整数溢出。包含了所有使用 -ftrapv 的检查,以及对于有符号除法的溢出( INT_MIN/-1 ),但不包含在运算之前就已存在的会导致精度缺失的隐式转换(见 -fsanitize=implicit-conversion )。这两类问题都可以使用 -fsanitize=implicit-conversion 问题组来处理。
  • -fsanitize=unreachable: 控制流进入一个不可达的程序代码。
  • -fsanitize=unsigned-integer-overflow: 无符号整数溢出,且无符号整数计算的结果不能用其类型类表示了。与有符号整数溢出不同的是,无符号整数溢出不是未定义行为,但却常常不经意就发生了。在这一类计算之前,sanitizer不会对有损的隐式转换进行检查,(见 -fsanitize=implicit-conversion)。
  • -fsanitize=vla-bound: 一个变量长度的数组,其边界不是一个正数。
  • -fsanitize=vptr: 使用一个对象,但是其虚指针表(vptr)表明其是一个错误的动态类型,或者其生命周期未开始或已结束。不能与 -fno-rtti 兼容。必须使用 clang++ (而非clang)进行重新链接,以确保运行时库和 C++ 标准库中的 C++ 特定部分能正常使用。

也可以使用如下的检查组:

  • -fsanitize=undefined: 包括以上所有的检查项,除了浮点数除零、无符号整数溢出、隐式转换、本地边界和 nullability- 的检查。
  • -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-conversion: 检查隐式转换中的可疑行为。开启 implicit-unsigned-integer-truncationimplicit-signed-integer-truncationimplicit-integer-sign-change
  • -fsanitize=integer: 未定义或可疑的整数行为(如未定义整数溢出)。开启这些选项:signed-integer-overflow, unsigned-integer-overflow, shift, integer-divide-by-zero, implicit-unsigned-integer-truncation, implicit-signed-integer-truncation, and implicit-integer-sign-change
  • -fsanitize=nullability: 开启 nullability-argnullability-assign、和 nullability-return。因 violating nullability 并没有未定义行为,但却常常不经意就发生了,所以UBSan会将其捕获到。

Volatile

volatile关键字的作用是禁止编译器指令重排。

volatile 作用下,null、对齐、对象大小、本地边界、vptr 检查,并不适用于指向类型的指针。(原文:The null, alignment, object-size, local-bounds, and vptr checks do not apply to pointers to types with the volatile qualifier.)。

最小的运行时

这是一个可用于生产环境的最小的 UBSan 运行时可用。This runtime has a small attack surface. 仅仅提供了最基本的问题记录和去重,不支持 -fsanitize=function-fsanitize=vptr 检查。在clang命令行参数中添加 -fsanitize-minimal-runtime 可以使用该最小运行时。例如,如果你习惯于使用 -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***一起使用,对于 ***providing fuzzing signal without blowing up logs ***尤其有用。

问题剔除

UndefinedBehaviorSanitizer 不会产生误报。如果发现了误报,请仔细看;通常都是正确的报告。

禁止使用 attribute((no_sanitize("undefined"))) 进行插桩

可以使用 attribute((no_sanitize("undefined"))) 对特定函数禁用 UBSan 检查。可以在该编译器属性设置中使用 -fsanitize= 标记的所有值。例如,如果函数故意想要包含可能的有符号整数溢出,可以使用编译器属性 attribute((no_sanitize("signed-integer-overflow")))

该编译器属性可能在其他编译器中不支持,所以将其与 #if defined(clang) 一起使用。

剔除重新编译代码中的错误

在Sanitizer的特定case列表中,UndefinedBehaviorSanitizer 支持 src and fun entity types ,这一点可用于剔除指定文件或函数中的错误报告。

运行时剔除报告(Runtime suppressions)

有些时候,可以对于特定文件、函数、库进行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 ),仅支持 fine-grained 的检查。

支持的平台

UndefinedBehaviorSanitizer 支持如下操作系统:

  • Android
  • Linux
  • NetBSD
  • FreeBSD
  • OpenBSD
  • macOS
  • Windows

运行时库是可移植的,且与平台无关。如果操作系统没有在上边列出来,UndefinedBehaviorSanitizer依然可能适用,或者仅需一点点移植操作即可。

当前状态

从LLVM 3.3开始,UndefinedBehaviorSanitizer就已经支持特定平台了。测试套件集成在CMake构建中,可以使用check-ubsan命令来运行。

额外的配置

UndefinedBehaviorSanitizer对每一项检查都会添加静态检查数据,陷阱模式除外。检查数据包含了完整的文件名。使用选项***-fsanitize-undefined-strip-path-components=N可以去掉该信息。如果N是正数,则UndefinedBehaviorSanitizer***生成的文件信息会丢掉文件路径中的前N个目录名。如果N是负数,则会保留后N个目录名。

案例

对于源码文件 */code/library/file.cpp8,生成的文件信息会如下:

  • Default (No flag, or -fsanitize-undefined-strip-path-components=0): /code/library/file.cpp
  • -fsanitize-undefined-strip-path-components=1: code/library/file.cpp
  • -fsanitize-undefined-strip-path-components=2: library/file.cpp
  • -fsanitize-undefined-strip-path-components=-1: file.cpp
  • -fsanitize-undefined-strip-path-components=-2: library/file.cpp

更多信息