llvm学习(二十五):将libfuzzer移植为so的实践
libfuzzer属于llvm-project下的compiler-rt,通常情况下我们是将 libfuzzer 静态连接到可执行文件里的,本文介绍一种将 libfuzzer 移植为动态连接库的实践。
背景知识介绍
看一个官方的example代码,非常简单易懂
1 |
|
编译命令是:
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 | // 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 |
无论使用哪种方案,最终都逃不过静态连接这一步。但并不是所有的二进制都能把 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 | cd llvm-project |
阅读 https://github.com/llvm/llvm-project/tree/main/compiler-rt 后,cmake 也很好理解,我们从 CMakeLists.txt
和 lib/fuzzer/CMakeLists.txt
下手。
另外,由于 https://github.com/llvm/llvm-project/commit/50a1c697127749eec567d14819d549b63af1242f ,小于等于llvm8时,接受 trace-c
、trace-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 | make |
第二步:将STATIC替换为SHARED,重新编译
1 | diff --git a/compiler-rt/lib/fuzzer/CMakeLists.txt b/compiler-rt/lib/fuzzer/CMakeLists.txt |
测试一下效果,哦吼,符号没找到。
注意这里一定只进行编译、不要进行连接,需要添加 -c
,因为 -fsanitize=fuzzer
或者 -fsanitize=fuzzer-no-link
最终会连接到 /usr/lib/llvm-14/lib/clang/15.0.0/lib/linux/
下的 ubsan
系列静态库,好像会导致sancov失效,不是很懂,我们一定要避开它
1 | clang++ -c -fsanitize=fuzzer test_fuzzer.cc |
后来发现官方在 https://reviews.llvm.org/D84561 次讨论中添加了 Using libFuzzer as a library
功能,对应的 patch 为 https://github.com/llvm/llvm-project/commit/34ddf0b2b040918a6c946f589eeaf1d4fef95e7a ,我们主动把 patch 打上去。
第三步:导出符号,重新编译,成功
1 | clang++ -c -fsanitize=fuzzer test_fuzzer.cc |
compiler-rt 15.0.0
第一步:正常编译通过
不要在llvm-project下编译,不要在llvm-project下编译,不要在llvm-project下编译
在 compiler-rt/cmake/Modules/CompilerRTUtils.cmake
下有这么一句话:
1 | if (NOT EXISTS "${LLVM_MAIN_SRC_DIR_DEFAULT}") |
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 | clang++ -c -fsanitize=fuzzer test_fuzzer.cc |
结论
将 libfuzzer 移植为 so 是完全可行的。