llvm学习(二十五):将libfuzzer移植为so的实践

libfuzzer属于llvm-project下的compiler-rt,通常情况下我们是将 libfuzzer 静态连接到可执行文件里的,本文介绍一种将 libfuzzer 移植为动态连接库的实践。

背景知识介绍

看一个官方的example代码,非常简单易懂

1
2
3
4
5
6
7
8
9
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size > 0 && data[0] == 'H')
if (size > 1 && data[1] == 'I')
if (size > 2 && data[2] == '!')
__builtin_trap();
return 0;
}

编译命令是:

1
clang++ -fsanitize=address,fuzzer test_fuzzer.cc

这里 address 是用于检测内存破坏的,与 libfuzzer 本身无关,因此真正起作用的是 -fsanitize=fuzzer

长话短说,通过加上 -v 打印编译细节,如果添加了 -fsanitize=fuzzer ,就会在编译期进行插桩,在连接期将与 /usr/lib/llvm-14/lib/clang/15.0.0/lib/linux/libclang_rt.fuzzer-x86_64.a进行连接,而 main 函数是这个 libclang_rt.fuzzer-x86_64.a 提供的。

如果不愿意使用 libfuzzer 提供的 main 函数,需要使用 -fsanitize=fuzzer-no-link,表示只进行编译期插桩,需要由开发者与 /usr/lib/llvm-14/lib/clang/15.0.0/lib/linux/libclang_rt.fuzzer_no_main-x86_64.a 进行连接,需要由开发者提供 main 函数,需要由开发者在合适的时机调用约定好的 LLVMFuzzerRunDriver

1
extern "C" int LLVMFuzzerRunDriver(int *argc, char ***argv, int (*UserCb)(const uint8_t *Data, size_t Size));

简单demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// clang++ -fsanitize=fuzzer-no-link test_fuzzer.cc /usr/lib/llvm-14/lib/clang/15.0.0/lib/linux/libclang_rt.fuzzer_no_main-x86_64.a
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size > 0 && data[0] == 'H')
if (size > 1 && data[1] == 'I')
if (size > 2 && data[2] == '!')
__builtin_trap();
return 0;
}

extern "C" int LLVMFuzzerRunDriver(int *argc, char ***argv, int (*UserCb)(const uint8_t *Data, size_t Size));

int main(int argc, char** argv){
return LLVMFuzzerRunDriver(&argc, &argv, LLVMFuzzerTestOneInput);
}

无论使用哪种方案,最终都逃不过静态连接这一步。但并不是所有的二进制都能把 libfuzzer 静态连接进去的,加上出于好奇,于是有了本课题。

思路

目标:获得 libclang_rt.fuzzer_no_main-x86_64.a 对应的 libclang_rt.fuzzer_no_main-x86_64.so ,并且成功使用。

llvm本身是由 cmake 组织的,虽然 compiler-rt 作为它的子项目,但 compiler-rt 的功能是运行时候的一些实现,例如 asan 功能、libfuzzer 功能,而 cmake 是通过 STATIC 和 SHARED 来区分一个静态库和动态库的,初步想法是简单地将 STATIC 改为 SHARED 碰碰运气。

阅读官方文档 https://compiler-rt.llvm.org/ 后,编译命令是

1
2
3
4
5
cd llvm-project
mkdir build-compiler-rt
cd build-compiler-rt
cmake ../compiler-rt -DLLVM_CONFIG_PATH=/path/to/llvm-config
make

阅读 https://github.com/llvm/llvm-project/tree/main/compiler-rt 后,cmake 也很好理解,我们从 CMakeLists.txtlib/fuzzer/CMakeLists.txt 下手。

另外,由于 https://github.com/llvm/llvm-project/commit/50a1c697127749eec567d14819d549b63af1242f ,小于等于llvm8时,接受 trace-ctrace-pc-guard 插桩 和 inline-8bit-counters 插桩,而大于等于llvm9时,只支持 inline-8bit-counters 插桩。

很多情况下,被测目标不一定能在哪个clang下编译,因此需要验证两种情况下 libfuzzer 的表现。

compiler-rt 8.0

第一步:正常编译通过

不要编译无关的东西:

1
cmake /path/to/compiler-rt-8.0.0 -DCOMPILER_RT_BUILD_BUILTINS=OFF -DCOMPILER_RT_BUILD_SANITIZERS=OFF -DCOMPILER_RT_BUILD_XRAY=OFF -DCOMPILER_RT_BUILD_PROFILE=OFF

开始make,一次性成功(几秒钟的事)

1
2
3
4
5
6
7
8
9
make
[ 0%] Built target compiler-rt-headers
[ 40%] Built target compiler-rt
[ 55%] Built target RTfuzzer_main.x86_64
[ 92%] Built target RTfuzzer.x86_64
[ 96%] Linking CXX static library ../linux/libclang_rt.fuzzer-x86_64.a
[100%] Linking CXX static library ../linux/libclang_rt.fuzzer_no_main-x86_64.a
[100%] Built target clang_rt.fuzzer_no_main-x86_64
[100%] Built target clang_rt.fuzzer-x86_64

第二步:将STATIC替换为SHARED,重新编译

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/compiler-rt/lib/fuzzer/CMakeLists.txt b/compiler-rt/lib/fuzzer/CMakeLists.txt
index caea9734fe53..f71f5687db68 100644
--- a/compiler-rt/lib/fuzzer/CMakeLists.txt
+++ b/compiler-rt/lib/fuzzer/CMakeLists.txt
@@ -101,7 +101,7 @@ add_compiler_rt_runtime(clang_rt.fuzzer
PARENT_TARGET fuzzer)

add_compiler_rt_runtime(clang_rt.fuzzer_no_main
- STATIC
+ SHARED
OS ${FUZZER_SUPPORTED_OS}
ARCHS ${FUZZER_SUPPORTED_ARCH}
OBJECT_LIBS RTfuzzer

测试一下效果,哦吼,符号没找到。

注意这里一定只进行编译、不要进行连接,需要添加 -c,因为 -fsanitize=fuzzer 或者 -fsanitize=fuzzer-no-link 最终会连接到 /usr/lib/llvm-14/lib/clang/15.0.0/lib/linux/ 下的 ubsan 系列静态库,好像会导致sancov失效,不是很懂,我们一定要避开它

1
2
3
4
5
clang++ -c -fsanitize=fuzzer test_fuzzer.cc
clang++ test_fuzzer.o /home/leadroyal/b/lib/linux/libclang_rt.fuzzer_no_main-x86_64.so
/usr/bin/ld: test_fuzzer.o: in function `main':
test_fuzzer.cpp:(.text.main[main]+0x6c): undefined reference to `LLVMFuzzerRunDriver'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

后来发现官方在 https://reviews.llvm.org/D84561 次讨论中添加了 Using libFuzzer as a library 功能,对应的 patch 为 https://github.com/llvm/llvm-project/commit/34ddf0b2b040918a6c946f589eeaf1d4fef95e7a ,我们主动把 patch 打上去。

第三步:导出符号,重新编译,成功

1
2
3
4
clang++ -c -fsanitize=fuzzer test_fuzzer.cc
clang++ test_fuzzer.o /home/leadroyal/b/lib/linux/libclang_rt.fuzzer_no_main-x86_64.so
LD_LIBRARY_PATH=/home/leadroyal/b/lib/linux/ ./a.out
# CRASH!

compiler-rt 15.0.0

第一步:正常编译通过

不要在llvm-project下编译,不要在llvm-project下编译,不要在llvm-project下编译

compiler-rt/cmake/Modules/CompilerRTUtils.cmake 下有这么一句话:

1
2
3
4
5
6
if (NOT EXISTS "${LLVM_MAIN_SRC_DIR_DEFAULT}")
# TODO(dliew): Remove this legacy fallback path.
message(WARNING
"LLVM source tree not found at \"${LLVM_MAIN_SRC_DIR_DEFAULT}\". "
"You are not using the monorepo layout. This configuration is DEPRECATED.")
endif()

compiler-rt会根据是否位于 monorepo 下,走不同的逻辑。

如果在 monrepo 下,需要编译好 llvm 本体,而且还要 libcxx libcxxabi,还会涉及连接 gcc_s 的问题,坑很大。如果避开的话,会涉及 nostdinc++ 这个坑,因为 compiler-rt 的设计是不使用 std 下的东西,会导致很多 cpp 的头文件抽风。

如果在独立目录下,虽然这是 DEPRECATED 的方法,但我们就测一测无所谓。

不要编译无关的东西:

1
cmake /path/to/compiler-rt-15.0.0 -DCOMPILER_RT_BUILD_BUILTINS=OFF -DCOMPILER_RT_BUILD_CRT=OFF -DCOMPILER_RT_CRT_USE_EH_FRAME_REGISTRY=OFF -DCOMPILER_RT_BUILD_SANITIZERS=OFF -DCOMPILER_RT_BUILD_XRAY=OFF -DCOMPILER_RT_BUILD_PROFILE=OFF -DCOMPILER_RT_BUILD_MEMPROF=OFF -DCOMPILER_RT_BUILD_ORC=OFF -DCOMPILER_RT_BUILD_GWP_ASAN=OFF

然后还是失败了,见 https://github.com/llvm/llvm-project/issues/54183 ,原因是 llvm 开发者忘了把这文件 ./cmake/Modules/ExtendPath.cmake 打包进去了。

降级到 14.0.0,并从 github release 下载 compiler-rt-14.0.0.src.tar.xz

终于成功了。。。

第二步:将STATIC替换为SHARED,重新编译

成功

第三步:导出符号,重新编译,成功

1
2
3
4
clang++ -c -fsanitize=fuzzer test_fuzzer.cc
clang++ test_fuzzer.o /home/leadroyal/b/lib/linux/libclang_rt.fuzzer_no_main-x86_64.so
LD_LIBRARY_PATH=/home/leadroyal/b/lib/linux/ ./a.out
# CRASH!

结论

将 libfuzzer 移植为 so 是完全可行的。