DataFlowSanitizer
介绍
DataFlowSanitizer 是动态数据流分析工具,和其他 Sanitizer 工具不同,该工具本身并非用于检测一些指定类型的bug。而是提供一个通用的动态数据流分析框架,用户可以使用它来检测代码中的应用相关的问题。
如何使用 DFSan build libc++
DFSan 要求你的所有代码进行插桩,或者在 ABI 列表中列出来的函数可以不进行插桩。
如果你想要得到插桩过的 libc++ 函数,你需要使用 DFSan 插桩,从源码来 build。下面展示了如何使用 DFS 插桩来 build libc++ 和 libc++ ABI。
cd libcxx-build
# An example using ninja
cmake -G Ninja 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来进行 build
使用
在没有程序更改的情况下,对程序应用 DFSan 不会改变程序的行为。为了使用 DFSan,程序使用 API 函数对数据添加标记以便对其进行跟踪,并且对特定的数据进行标签检查。DFSan 根据数据流向,来管理这些标签在程序中的传播。
API 定义在 sanitizer/dfsan_interface.h
中,要想获取每个函数的更进一步信息,请参考该头文件。
ABI 列表
DFSan 使用一个函数列表(即ABI列表),来决定一个指定函数的调用,应该使用操作系统的原生 ABI,还是应该使用通过函数参数和返回值来传播标记的 ABI 变体。在前一种情况下,ABI 列表文件也会控制标签的传播方式。
DFSan 有一个默认的 ABI 列表,意在最终覆盖 Linux 上的 glibc 库,但同时用户也可能在一些场景下对其ABI进行扩展:一个特定的库或函数无法使用编译器插桩(如使用汇编代码或者另外一种语言实现的,而DataFlowSanitizer并不支持该语言),或者从一个库中调用的函数无法使用编译器插桩。
DFSan 的 ABI 列表文件是 Sanitizer 的特例列表,pass 会将 ABI 列表文件中的未插桩类别的每个函数,当做是遵从原生ABI的。除非 ABI 列表包含有针对这些函数的额外类别,否则对这些函数的每一次调用都会生成一个警告信息,因为对函数的打标签操作是未知的。其他支持的类别是 discard
、functional
、custom
。
-
discard
在一定程度上,一个函数对用户可访问的内存进行了写操作,它也会更新 shadow memory 中的标签(这个条件不适用于未对可访问内存进行写操作的函数)。它的返回值是未标记的。
-
functional
和
discard
类似,除了其返回值的标签是其参数标签的并集。 -
custom
不会调用函数,而是调用自定义包装器
__dfsw_F
,其中F
是函数的名称。该函数可以包装原始函数或提供自己的实现。此类别通常用于无法插桩的函数,如对用户可访问内存进行写操作的函数,或具有更复杂标签传播行为的函数。__dfsw_F
的签名基于F
的签名,每个参数都有一个dfsan_label
类型的标签附加到参数列表。 如果F
是非空返回类型,则附加类型为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。它的参数会被认为是未标记的,但在影子内存中它会认为是有标记的。
# 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 = 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
// Or, equivalently:
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(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
// Or, equivalently:
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中有对应的检查逻辑。