clang之DataFlowSanitizer

2,868 阅读6分钟

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 DataFlowSanitizer 的翻译。仅供参考。

Introduction

DataFlowSanitizer是一个处于推广阶段的动态数据流分析器。

与其他的 Sanitizer 工具不同的是,该工具本身并非设计用于检测一些指定类型的bug。而是,提供一个通用的动态数据流分析框架,用户可以使用它来检测代码中的应用相关部分的问题。

如何使用DFSan来构建libc++

DFSan 要求你的全部代码进行编译器插桩,或者在ABI列表中列出的一些函数可以不进行插桩。

如果你想让插桩过的libc++函数,那就需要使用DFSan插桩,从源码来构建。这里是一个例子,演示了如何使用DataFlowSanitizer插桩来构建libc++及其ABI。

cd libcxx-build

# An example using ninja
cmake -GNinja path/to/llvm-project/llvm \
  -DCMAKE_C_COMPILER=clang \
  -DCMAKE_CXX_COMPILER=clang++ \
  -DLLVM_USE_SANITIZER="DataFlow" \
  -DLLVM_ENABLE_LIBCXX=ON \
  -DLLVM_ENABLE_PROJECTS="libcxx;libcxxabi"

ninja cxx cxxabi

注意:确保使用最新版本的Clang来进行构建。

用法

如果程序没有修改过,则对程序使用DataFlowSanitizer就不会修改其功能。为了使用DataFlowSanitizer,程序使用API函数对数据添加标记以便对其进行追踪,并且对特定的数据进行标记检查。DataFlowSanitizer根据数据流向,来管理这些标记在程序中的传递。

API函数定义在头文件 sanitizer/dfsan_interface.h 中。要想获取每个函数的更进一步信息,请参考该头文件。

ABI列表

DataFlowSanitizer使用一个函数列表(即ABI列表),来决定一个指定函数的调用,应该使用操作系统的原生ABI,还是应该使用通过函数参数和返回值来携带标记的变种ABI。ABI列表文件也会控制在之前的场景中如何携带标签。

DataFlowSanitizer有一个默认的ABI列表,意在最终覆盖Linux上的glibc库,但同时用户也可能在一些场景下对其ABI进行扩展:一个特定的库或函数无法使用编译器插桩(如使用汇编代码或者另外一种语言实现的,而DataFlowSanitizer并不支持该语言),或者一个库中调用的函数无法使用编译器插桩。

DataFlowSanitizer的ABI列表文件是一个用于Sanitizer的案例列表。编译器pass会将ABI列表文件中的未插桩类别的每一个函数,当做是遵从原生ABI的。除非ABI列表包含有针对这些函数的额外类别,否则对这些函数的一次调用都会生成一个警告信息,因为对函数的打标签操作是未知的。其他支持的类别是弃用类别、功能类别、自定义类别。

  • discard – 在一定程度上,一个函数对用户可访问的内存进行了写操作,它也会更新影子内存中的标记(这个条件不适用于未对可访问内存进行写操作的函数). 它的返回值不会被打上标记。
  • functional – 与弃用的类似,有一个例外就是其返还值的标签是其参数标签的集合。
  • custom – 这里F就是原函数名称,不会直接调用函数,而是使用一个自定义的装饰器函数 __dfsw_F 将其包装起来并调用。这个函数会将原始函数包装起来,或者提供它自己的实现。这个类别通常用于无法插桩的函数,如无法对用户可访问内存进行写操作的函数,或者有更复杂的标签传递行为的函数。__dfsw_F 的签名会基于原来函数F,而F的每个参数会有一个 dfsan_label 类型的标签接在参数列表的后边。如果F有一个非void类型的返回值,一个的 ***dfsan_label **** 类型的最终参数会接在参数列表的后边,其中自定义函数可以存储其返回值的标签。
void f(int x);
void __dfsw_f(int x, dfsan_label x_label);

void *memcpy(void *dest, const void *src, size_t n);
void *__dfsw_memcpy(void *dest, const void *src, size_t n,
                    dfsan_label dest_label, dfsan_label src_label,
                    dfsan_label n_label, dfsan_label *ret_label);

如果一个定义在正在编译的转换单元中的函数属于未插桩的类别,那该函数必须编译以符合原生的ABI。它的参数会被认为是未标记的,但在影子内存中它会认为是有标记的。(英文原文:If a function defined in the translation unit being compiled belongs to the uninstrumented category, it will be compiled so as to conform to the native ABI. Its arguments will be assumed to be unlabelled, but it will propagate labels in shadow memory.)

例如:

# main is called by the C runtime using the native ABI.
fun:main=uninstrumented
fun:main=discard

# malloc only writes to its internal data structures, not user-accessible memory.
fun:malloc=uninstrumented
fun:malloc=discard

# tolower is a pure function.
fun:tolower=uninstrumented
fun:tolower=functional

# memcpy needs to copy the shadow from the source to the destination region.
# This is done in a custom function.
fun:memcpy=uninstrumented
fun:memcpy=custom

例子

下边的程序通过检查正确标签的传递,来演示标签的传递过程。

#include <sanitizer/dfsan_interface.h>
#include <assert.h>

int main(void) {
  int i = 1;
  dfsan_label i_label = dfsan_create_label("i", 0);
  dfsan_set_label(i_label, &i, sizeof(i));

  int j = 2;
  dfsan_label j_label = dfsan_create_label("j", 0);
  dfsan_set_label(j_label, &j, sizeof(j));

  int k = 3;
  dfsan_label k_label = dfsan_create_label("k", 0);
  dfsan_set_label(k_label, &k, sizeof(k));

  dfsan_label ij_label = dfsan_get_label(i + j);
  assert(dfsan_has_label(ij_label, i_label));
  assert(dfsan_has_label(ij_label, j_label));
  assert(!dfsan_has_label(ij_label, k_label));

  dfsan_label ijk_label = dfsan_get_label(i + j + k);
  assert(dfsan_has_label(ijk_label, i_label));
  assert(dfsan_has_label(ijk_label, j_label));
  assert(dfsan_has_label(ijk_label, k_label));

  return 0;
}

如何理解:

dfsan_label ij_label = dfsan_get_label(i + j); 可简单理解为,ij_label会同时追踪i和j两个int值对应的标签,所以在后边的assert中有对应的检查逻辑。

fast16labels模式

如果需要16个或更少的标签,可以使用 fast16labels 插桩操作,来节约CPU和代码尺寸的消耗。为了使用该插桩,需要在编译和链接命令中指定参数 -fsanitize=dataflow -mllvm -dfsan-fast-16-labels ,使用修改过的API来生成和管理标签。

fast16labels 模式下,基本的标签都是简单的16位无符号整数,都是2的幂次方数(如1,2,4,8,...,32768),且联合标签是对基本标签使用 ORing 操作来创建的。在这种模式下,DFSan并不管理任何标签的元数据,所以这些函数(dfsan_create_label, dfsan_union, dfsan_get_label_info, dfsan_has_label, dfsan_has_label_with_desc, dfsan_get_label_count, and dfsan_dump_labels)都不支持了。(既然无法使用这些函数了,)用户就应该自己来维护基本标签的任意必需的元数据。

例如:

#include <sanitizer/dfsan_interface.h>
#include <assert.h>

int main(void) {
  int i = 100;
  int j = 200;
  int k = 300;
  dfsan_label i_label = 1;
  dfsan_label j_label = 2;
  dfsan_label k_label = 4;
  dfsan_set_label(i_label, &i, sizeof(i));
  dfsan_set_label(j_label, &j, sizeof(j));
  dfsan_set_label(k_label, &k, sizeof(k));

  dfsan_label ij_label = dfsan_get_label(i + j);

  assert(ij_label & i_label);  // ij_label has i_label
  assert(ij_label & j_label);  // ij_label has j_label
  assert(!(ij_label & k_label));  // ij_label doesn't have k_label
  assert(ij_label == 3);  // Verifies all of the above

  dfsan_label ijk_label = dfsan_get_label(i + j + k);

  assert(ijk_label & i_label);  // ijk_label has i_label
  assert(ijk_label & j_label);  // ijk_label has j_label
  assert(ijk_label & k_label);  // ijk_label has k_label
  assert(ijk_label == 7);  // Verifies all of the above

  return 0;
}

当前状态

DataFlowSanitizer的工作正在进行中,目前正在开发用于x86_64 Linux中的版本。

设计

请参考 设计文档