数据流消毒器

简介

数据流消毒器是一种通用的动态数据流分析。

与其他消毒器工具不同,此工具本身并非旨在检测特定类别的错误。相反,它提供了一个通用的动态数据流分析框架,供客户端使用,以帮助他们在自己的代码中检测特定于应用程序的问题。

如何使用 DFSan 构建 libc++

DFSan 需要您的所有代码都经过检测,或者将未检测的函数列为 uninstrumentedABI 列表 中。

如果您想拥有检测过的 libc++ 函数,那么您需要使用 DFSan 检测从源代码构建它。以下是如何使用数据流消毒器检测构建 libc++ 和 libc++ ABI 的示例。

mkdir libcxx-build
cd libcxx-build

# An example using ninja
cmake -GNinja -S <monorepo-root>/runtimes \
  -DCMAKE_C_COMPILER=clang \
  -DCMAKE_CXX_COMPILER=clang++ \
  -DLLVM_USE_SANITIZER="DataFlow" \
  -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi"

ninja cxx cxxabi

注意:请确保您使用的是足够新的 Clang 版本。

使用

在没有任何程序更改的情况下,将数据流消毒器应用于程序不会改变其行为。要使用数据流消毒器,程序使用 API 函数将标签应用于数据,以使其被跟踪,并检查特定数据项的标签。数据流消毒器根据其数据流管理标签在程序中的传播。

这些 API 在头文件 sanitizer/dfsan_interface.h 中定义。有关每个函数的更多信息,请参阅头文件。

ABI 列表

数据流消毒器使用一个函数列表(称为 ABI 列表)来决定对特定函数的调用应该使用操作系统的本机 ABI,还是应该使用该 ABI 的变体,该变体还通过函数参数和返回值传播标签。ABI 列表文件还控制标签在前一种情况下的传播方式。数据流消毒器附带一个默认的 ABI 列表,该列表最终旨在涵盖 Linux 上的 glibc 库,但用户可能需要在某些情况下扩展 ABI 列表,例如某个特定库或函数无法检测(例如,因为它是在程序集中实现的或数据流消毒器不支持的其他语言中实现的)或从无法检测的库或函数中调用函数。

数据流消毒器的 ABI 列表文件是一个 消毒器特殊情况列表。此通道将 ABI 列表文件中 uninstrumented 类别中的每个函数视为符合本机 ABI。除非 ABI 列表包含这些函数的其他类别,否则调用这些函数之一将产生警告消息,因为该函数的标签行为未知。其他支持的类别是 discardfunctionalcustom

  • discard – 在此函数写入(用户可访问的)内存的范围内,它也会更新影子内存中的标签(对于不写入用户可访问内存的函数,此条件很容易满足)。它的返回值没有标签。

  • 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);

如果编译中翻译单元中定义的函数属于 uninstrumented 类别,它将被编译以符合本机 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

对于检测过的函数,ABI 列表支持一个 force_zero_labels 类别,它将使所有存储和返回值设置零标签。函数永远不应该用 force_zero_labelsuninstrumented 或任何未检测的包装器类型进行标记。

例如

# e.g. void writes_data(char* out_buf, int out_buf_len) {...}
# Applying force_zero_labels will force out_buf shadow to zero.
fun:writes_data=force_zero_labels

编译标志

  • -dfsan-abilist – 控制如何传递影子参数的附加 ABI 列表文件。文件名用逗号隔开。

  • -dfsan-combine-pointer-labels-on-load – 控制是否在加载指令中包含或忽略指针的标签。它的默认值为 true。例如

v = *p;

如果该标志为 true,则 v 的标签是 p 的标签和 *p 的标签的并集。如果该标志为 false,则 v 的标签只是 *p 的标签。

  • -dfsan-combine-pointer-labels-on-store – 控制是否在存储指令中包含或忽略指针的标签。它的默认值为 false。例如

*p = v;

如果该标志为 true,则 *p 的标签是 p 的标签和 v 的标签的并集。如果该标志为 false,则 *p 的标签只是 v 的标签。

  • -dfsan-combine-offset-labels-on-gep – 控制是否在 GEP 指令中传播偏移量的标签。它的默认值为 true。例如

p += i;

如果该标志为 true,则 p 的标签是 p 的标签和 i 的标签的并集。如果该标志为 false,则 p 的标签保持不变。

  • -dfsan-track-select-control-flow – 控制是否跟踪选择指令的控制流。它的默认值为 true。例如

v = b? v1: v2;

如果该标志为 true,则 v 的标签是 bv1v2 的标签的并集。如果该标志为 false,则 v 的标签只是 v1v2 的标签的并集。

  • -dfsan-event-callbacks – 一项实验性功能,用于插入某些数据事件的回调。目前,回调仅针对加载、存储、内存传输(即 memcpy 和 memmove)和比较插入。它的默认值为 false。如果此标志设置为 true,用户必须提供以下回调函数的定义

void __dfsan_load_callback(dfsan_label Label, void* Addr);
void __dfsan_store_callback(dfsan_label Label, void* Addr);
void __dfsan_mem_transfer_callback(dfsan_label *Start, size_t Len);
void __dfsan_cmp_callback(dfsan_label CombinedLabel);
  • -dfsan-conditional-callbacks – 一项实验性功能,用于插入控制流条件表达式的回调。这可以用来查找污染值可以控制执行的位置。

    除了此编译标志外,还必须使用 dfsan_set_conditional_callback(my_callback); 注册一个回调处理程序,其中 my_callback 是一个函数,其签名与 void my_callback(dfsan_label l, dfsan_origin o); 相匹配。当禁用来源追踪时,此签名是相同的 - 在这种情况下,传递的 dfsan_origin 将始终为 0。

    只有当污染值到达控制流的条件表达式(如 if 的条件)时,才会调用回调。对于信号处理程序内的条件表达式,回调将被跳过,因为这容易导致死锁。信号处理程序内的条件表达式中使用的污染值将通过按位或进行聚合,可以使用 dfsan_label dfsan_get_labels_in_signal_conditional(); 访问它们。

  • -dfsan-reaches-function-callbacks – 一项实验性功能,用于插入数据进入函数的回调。

    除了此编译标志外,还必须使用 dfsan_set_reaches_function_callback(my_callback); 注册一个回调处理程序,其中 my_callback 是一个函数,其签名与 void my_callback(dfsan_label label, dfsan_origin origin, const char *file, unsigned int line, const char *function); 相匹配。当禁用来源追踪时,此签名是相同的 - 在这种情况下,传递的 dfsan_origin 将始终为 0。

    当污染值到达函数的堆栈/寄存器上下文时,将调用回调。污染值可以到达函数:* 通过函数的参数 * 通过函数中发生的调用的返回值 * 通过函数中发生的加载的加载值

    对于信号处理程序内的条件表达式,回调将被跳过,因为这容易导致死锁。到达信号处理程序内函数的污染值将通过按位或进行聚合,可以使用 dfsan_label dfsan_get_labels_in_signal_reaches_function() 访问它们。

  • -dfsan-track-origins – 控制如何跟踪来源。当其值为 0 时,运行时不跟踪来源。当其值为 1 时,运行时在内存存储操作时跟踪来源。当其值为 2 时,运行时在内存加载和存储操作时跟踪来源。其默认值为 0。

  • -dfsan-instrument-with-call-threshold – 如果要进行插桩的函数需要超过此数量的来源存储,则使用回调而不是内联检查(-1 表示从不使用回调)。其默认值为 3500。

环境变量

  • warn_unimplemented – 是否在未实现的函数上发出警告。其默认值为 false。

  • strict_data_dependencies – 是否仅在存在显式明显的依赖关系时传播标签(例如,在比较字符串时,忽略比较结果可能隐式依赖于字符串内容的事实)。这仅适用于 ABI 列表中类别为 custom 的函数。其默认值为 true。

  • origin_history_size – 来源链长度的限制。非正数表示无限制。其默认值为 16。

  • origin_history_per_stack_limit – 来源节点的引用计数限制。非正数表示无限制。其默认值为 20000。

  • store_context_size – 来源跟踪堆栈跟踪的深度限制。其默认值为 20。

  • zero_in_malloc – 是否将新分配内存的阴影空间清零。其默认值为 true。

  • zero_in_free — 是否将释放内存的阴影空间清零。其默认值为 true。

示例

DataFlowSanitizer 支持最多 8 个标签,以实现低 CPU 和代码大小开销。基本标签只是 8 位无符号整数,它们是 2 的幂(即 1、2、4、8、…、128),联合标签是通过对基本标签进行 OR 操作创建的。

以下程序演示了通过检查是否传播了正确的标签来传播标签。

#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;
}

来源跟踪

DataFlowSanitizer 可以跟踪带标签值的来源。此功能通过 -mllvm -dfsan-track-origins=1 启用。例如,

% cat test.cc
#include <sanitizer/dfsan_interface.h>
#include <stdio.h>

int main(int argc, char** argv) {
  int i = 0;
  dfsan_set_label(i_label, &i, sizeof(i));
  int j = i + 1;
  dfsan_print_origin_trace(&j, "A flow from i to j");
  return 0;
}

% clang++ -fsanitize=dataflow -mllvm -dfsan-track-origins=1 -fno-omit-frame-pointer -g -O2 test.cc
% ./a.out
Taint value 0x1 (at 0x7ffd42bf415c) origin tracking (A flow from i to j)
Origin value: 0x13900001, Taint value was stored to memory at
  #0 0x55676db85a62 in main test.cc:7:7
  #1 0x7f0083611bbc in __libc_start_main libc-start.c:285

Origin value: 0x9e00001, Taint value was created at
  #0 0x55676db85a08 in main test.cc:6:3
  #1 0x7f0083611bbc in __libc_start_main libc-start.c:285

通过 -mllvm -dfsan-track-origins=1,DataFlowSanitizer 仅收集带标签值经过的中间存储。来源跟踪使程序执行速度降低 2 倍,这是在通常的 DataFlowSanitizer 速度降低的基础上增加的,并且内存开销增加了 1 倍。通过 -mllvm -dfsan-track-origins=2,DataFlowSanitizer 还收集带标签值经过的中间加载。此模式使程序执行速度降低 4 倍。

当前状态

DataFlowSanitizer 正在开发中,目前正在为 x86_64 Linux 开发。

设计

请参阅 设计文档