openai,你能便宜点吗?
libFuzzer libfuzzer是一个进程内、覆盖引导、进化的Fuzz引擎。
LibFuzzer与被测库链接,并通过特定的Fuzzing入口点(又称为“目标函数”)将模糊测试输入送到被测库;然后,Fuzzer会跟踪代码的哪些区域被覆盖,并对输入数据语料库进行修改,以最大化代码覆盖率。libFuzzer的覆盖率信息由LLVM
的SanitizerCoverage
提供代码插桩。关于LLVM的Sanitizer,笔者在Fuzzilli
文章中有简要概述,主要介绍一些桩函数与回调函数啥的。
Fuzz Target 使用libFuzzer对库进行覆盖率引导模糊测试的第一步是实现一个Fuzz target ,也就是一个函数。它接受一个字节数组,并使用要测试的API对这些字节执行一些”intrersting”的操作。例如:
1 2 3 4 5 extern "C" int LLVMFuzzerTestOneInput (const uint8_t *Data, size_t Size) { DoSomethingInterestingWithMyAPI(Data, Size); return 0 ; }
请注意,此模糊测试目标与libFuzzer无关,因此使用其他Fuzzing引擎(例如AFL或Radamsa)是可能的,甚至更可取。
关于fuzz target:
libfuzzer将在同一进程中使用不同的输入多次执行fuzz target
它必须能够忍受任何类型的输入(空、huge size、格式错误等)
它不得在任何输入上exit()
它可以使用线程,但理想情况下,所有线程应在函数结束时加入
它必须尽可能确定。非确定性(例如基于输入字节的不随机决策)会使Fuzzing效率低下
它必须很快。尽量避免三次方或更高复杂度、日志记录或过度内存消耗
理想情况下,它不应该修改任何全局状态(尽管这不是强制性的)。
通常,目标越窄越好。例如,如果你的目标可以解析多种数据格式,将其拆分为多个目标,每个格式一个。
Fuzzer usage 较新版本的Clang(从6.0开始)已经包含libFuzzer,无需额外安装。
在build Fuzz target源码时,在编译和链接时使用-fsanitizer=fuzzer
标志。在大多数情况下,将libFuzzer和AddressSanitizer(ASAN)、UndefinedBehaviorSanitizer(UBSAN)结合使用,也可以使用MemorySanitizer(MSAN)进行构建。
1 2 3 4 clang -g -O1 -fsanitize=fuzzer mytarget.c clang -g -O1 -fsanitize=fuzzer,address mytarget.c clang -g -O1 -fsanitize=fuzzer,signed-integer-overflow mytarget.c clang -g -O1 -fsanitize=fuzzer,memory mytarget.c
这将执行必要的插桩,并与libFuzzer库进行链接。请注意,-fsanitize=fuzzer
时,会给目标代码插桩,还会自动把libFuzzer库链接进来,其中包括libFuzzer自己定义的main()
函数。这代表目标代码中不能再定义另一个main()
函数,否则会冲突。
libFuzzer是一个模糊测试引擎和框架,所以它的main
是为了启动Fuzzing loop。那么在实际使用的过程中,使用libFuzzer测试函数时是不带main()
的,也就是说我们写在LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
内的是测试目标,它不带main()函数。libFuzzer会不断生成*Data
调用LLVMFuzzerTestOneInput
以充分测试。
那么如果我们写入的目标中存在一个main()函数的话,也就是说想自定义测试前的行为。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 extern int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size) ;int main (int argc, char **argv) { const char *test_input = "TEST FUZZ" ; LLVMFuzzerTestOneInput((const uint8_t *)test_input, strlen (test_input)); return 0 ; }
那么就需要编译链接分开,先编译:
1 2 clang -fsanitize=fuzzer-no-link -c fuzzer_test.c -o fuzzer_test.o clang -c my_main.c -o my_main.o
再链接:
1 clang fuzzer_test.o my_main.o -fsanitize=fuzzer -o my_fuzzer
通过一个示例(CVE-2016-5180)来理解libFuzzer的工作流程。首先将c-ares
项目克隆下来:
1 2 3 git clone https://github.com/c-ares/c-ares.gitcd c-ares/ git reset --hard 51fbb479f7948fca2ace3ff34a15ff27e796afdd
再编译c-ares
:
1 2 3 ./buildconf ./configure make CC="clang -O2 -fno-omit-frame-pointer -g -fsanitize=address -fsanitize-coverage=trace-cmp,trace-gep,trace-div"
编译成功后,写LLVMFuzzerTestOneInput()
函数,将其输入的字节流进行转换,再调用ares_create_query()
,将代码保存为.cc
文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdint.h> #include <stdlib.h> #include <string> #include <arpa/nameser.h> #include <ares.h> extern "C" int LLVMFuzzerTestOneInput (const uint8_t *Data, size_t Size) { unsigned char *buf; int buflen; std ::string s (reinterpret_cast<const char *>(Data), Size) ; ares_create_query(s.c_str(), ns_c_in, ns_t_a, 0x1234 , 0 , &buf, &buflen, 0 ); free (buf); return 0 ; }
编译该文件。
1 clang++ -g fuzzer_test.cc -fsanitize=address,fuzzer -I c-ares c-ares/.libs/libcares.a -o fuzzer_test
然后执行即可出现crash。那么这里测试的API就是ares_create_query()
,当然这只是顶层API,其中依然会调用其它API,也会一起测试。
oss-fuzz-gen usage 准备工作 安装python 3.11
,git
,Docker
,Google Cloud SDK
,C++ filt
,(optional for project_src.py)clang-format
安装python相关依赖:
1 2 3 python -m venv.venvsource .venv/bin/activate pip install -r requirements.txt
LLM 接入 配置OpenAI的API Key:
1 export OPENAI_API_KEY='<your-api-key>'
运行实验 通过本地实验生成和评估 benchmark set中的目标
1 2 3 4 5 6 7 8 9 ./run_all_experiments.py \ --model=<model-name> \ --benchmarks-directory='./benchmark-sets/comparison' \ [--ai-binary=<llm-access-binary>] \ [--template-directory=prompts/custom_template] \ [--work-dir=results-dir] [...]
<model-name>
必须是支持模型之一的名称。OSS-Fuzz-gen支持的模型列表会定期更新,可以使用run_all_experitments.py --help
列出所有可支持的模型。
实验也可以在google Cloud上使用Google Cloud Build运行。您可以通过传递--cloud <experiment-name> --cloud-experiment-bucket <bucket>
来完成此操作,其中<bucket>
是Google Cloud Storage bucket的名称。
目前提供的各种benchmark sets:
comparison
:一些OSS-Fuzz C/C++项目
all
:所有OSS-Fuzz C/C++项目
c-specific
:一个专注于C项目的基准测试集
…
可视化的结果 一旦完成,框架将输出类似以下的实验结果:
1 2 3 4 5 == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == * <project-name> , <function-name> * build success rate: <build-rate> , crash rate: <crash-rate> , max coverage: <max-coverage> , max line coverage diff: <max-coverage-diff> max coverage sample: <results-dir> / <benchmark-dir> / fixed_targets/ <LLM-generated-fuzz-target> max coverage diff sample: <results-dir> / <benchmark-dir> / fixed_targets/ <LLM-generated-fuzz-target>
其中<build-rate>
是可编译的模糊测试目标数量与LLM生成的总模糊测试数量的比值(例如,如果8个模糊目标中有4个可以构建,则为0.5),<crash-rate>
是运行时崩溃率,<max-coverage>
衡量所有目标的最高行覆盖率,而<max-coverage-diff>
显示LLM生成的目标相对于OSS-Fuzz中现有人类编写的目标的最高新行覆盖率。
注意<max-coverage>
和<max-coverage-diff>
是基于与模糊测试目标链接的代码计算的,而不是整个项目。例如:
1 2 3 4 5 ================================================================================ *tinyxml2, tinyxml2::XMLDocument::Print * build success rate: 1.0 , crash rate: 0.125 , max coverage: 0.29099427381572096 , max line coverage diff: 0.11301753077209996 max coverage sample: <result-dir> /output-tinyxml2-tinyxml2-xmldocument-print /fixed_targets/08 .cppmax coverage diff sample: <result-dir> /output-tinyxml2-tinyxml2-xmldocument-print /fixed_targets/08 .cpp
结果报告 要通过Web UI可视化这些结果,并查看有关所使用的确切提示、生成的样本和其他日志的更多详细信息,可运行:
1 2 python -m report.web -r <results-dir> -o <output-dir> python -m http.server <port> -d <output-dir>
其中<results-dir>
是实验中传递给--work-dir
的目录(默认值 ./results
),然后导航到http://localhost:<port>
查看结果。
实验的工作流细节 配置和使用框架,请按照以下步骤进行:
配置Benchmark
设置Prompt模板
生成Fuzz目标
修复编译错误
评估Fuzz目标
使用本地Fuzz Introspector实例
配置Benchmark 准备一个benchmark YAML文件,指定需要测试的函数。关于这个YAML文件,可以使用intropector自动生成。但是请注意,待测项目需要集成到OSS-Fuzz
中才能构建。
Benchmark YAML文件是Fuzz目标生成所必需的,它制定了functions
,project
,target_path
以及可选的target_name
:
functions
列出了生成模糊测试目标的函数签名
project
是OSS-Fuzz
中包含functions
的开源项目名称(例如,TinyXML-2
)。
target_path
是project
的OSS-Fuzz
容器中现有fuzz 目标的路径。它将被替换为LLM生成的目标 ,并用于fuzzing评估。
target_name
是一个可选 字段,用于指定Fuzz目标的二进制名称。
可以使用introspector.py
在OSS-Fuzz
中生成c
/c++
项目的YAML文件:
1 2 3 python -m data_prep.introspector <project-name> -m <num_benchmark_per_project> -o <output_dir>
以这种方式生成的基准文件优先考虑OSS-Fuzz
中覆盖范围较远但覆盖率较低的函数,因此更容易实现更高的max_line_coverage_diff
该框架将现有的人工编写的模糊测试目标(先前oss-fuzz
项目中人工编写的harness)作为示例添加到Prompt中,以提高结果质量。我们的实验表明,使用来自同一项目的人工编写的模糊测试目标(即使针对不同的函数)可以为LLM提供更多项目特定的上下文,而使用来自不同项目的目标则可以减少过拟合。
每个示例都包含一个问题和一个解决方案。解决方案包含一个由OSS-Fuzz
人工编写的模糊测试目标,该目标已被证明是有效的。其格式与我们预期的LLM响应相同。问题包含结果模糊测试目标中一个函数的签名。同样,其格式也与LLM的最终问题相同。
示例通过project_targets.py
中的generate_data()
自动添加到prompts中。
使用project_src.py
将OSS-Fuzz
中c
/c++
项目的所有模糊测试目标文件检索到本地目录(默认情况下为example_targets/<projetc_name>
):
1 2 3 4 5 6 python -m data_prep.project_src -p <project-name> python -m data_prep.project_src -p all
我们提供了一些生成用于模型微调或参数高效调优(PET)的训练数据的方法。训练数据是一个包含两个项目的子列表,其中函数签名和对应的fuzz目标分别为子列表的第一项和第二项。由于一个Fuzz目标可能会测试多个函数,因此多个function_signature
可能共享同一个fuzz_target
,即:
1 2 3 4 5 [ [<function_signature_1 > , <fuzz_target_1 > ], [<function_signature_2 > , <fuzz_target_2 > ], [<function_signature_3 > , <fuzz_target_2 > ], ]
具体生成训练数据的命令:
1 2 3 4 5 6 python -m data_prep.project_targets --project-name <project-name>
配置Prompt模板 准备Prompt模板。LLM的提示词将基于oss-fuzz-gen/prompts/
下的文件构建。它首先会定义主要目标和重要注意事项,然后是一些示例问题和解决方案。每个示例问题的格式和最终问题相同(即模糊测试的函数签名),解决方案是针对同一项目或其他项目中不同函数的人工编写的harness
。提示还可以包含更多关于函数的信息(例如,函数的用法、源代码或参数类型定义)以及特定于模型的注释(例如,需要避免的常见陷阱)。
可以通过--template-directory
传递备用模板目录。新的模板目录不必包含所有文件:如果缺少template_xml/
中的文件,框架将默认使用这些文件。默认Prompt的结果如下:
1 2 3 4 <Priming > <Model-specific notes > <Examples > <Final question + Function information >
生成Fuzz目标 脚本run_all_experiments.py
将使用上面构建的Prmopt模板通过LLM生成Fuzz目标,并测量其代码覆盖率。所有实验数据都将保存到--work-dir
中。
修复编译错误 当Fuzz目标构建失败时,框架会在终止前自动尝试修复五次。每次尝试都会请求LLM根据OSS-Fuzz
的构建失败信息修复模糊测试目标,解析响应中的源代码,然后重新编译。
评估Fuzz目标 如果模糊测试目标编译成功,框架会使用libFuzzer
对其进行模糊测试,并测量其行覆盖率。模糊测试超时由--run-timeout
标志指定。此外,还会将其行覆盖率与生产环境中现有的人工编写的OSS-Fuzz
目标进行比较。
使用本地Fuzz Introspector实例 运行本地版本的Fuzz Introspector Web应用程序可能比直接查询introspector分析过的数据更合适。这在测试OSS-Fuzz-gen扩展程序时非常有用,因为该扩展程序Fuzzing查询时需要新的程序分析数据,也可能面临查询网站的网络带宽受限制,或者网站可能关闭等问题。可以通过-e
标志传递给run_all_experiments.py
来将OSS-Fuzz-gen设置为使用本地的fuzz-introspector
。但是,要做到这一点,首先需要在本地初始化fuzz-introspector
端点的本地实例。
搭建 OSS-Fuzz-gen实验 我这里用的pyenv装的python 3.11.12
,装了Docker
等。关于Google Cloud SDK
,因为不打算用Google AI Platform就本地测试一下,因此没有装。
本地fuzz-introspector 由于用的pyenv管理的python版本,所以改了一下/fuzz-introspector/scripts/oss-fuzz-gen-e2e/build_all.sh
,将python3 -m virtualenv .venv
改成了python -m venv .venv
,只是创建虚拟环境的方式不同而已。
对于docker容器内的代理配置说明
特别要注意,由于需要用到docker,需要配置代理来拉镜像以及构建镜像。在给clash中记得设置监听地址bind-address: 0.0.0.0:7890
,而不是只监听127.0.0.1:7890
。否则,你的docker容器是无法访问到代理的,因为对于docker容器来说127.0.0.1
是它本身,而不是宿主机。可以使用netstat -tunlp | grep 7890
来查看你7890
端口所监听的地址。
配置好docker的守护进程(Daemon.json
)代理和容器(config.json
)代理后,改原脚本build_all.sh
为:
1 2 3 4 5 6 7 8 9 10 11 12 13 ROOT_FI=$PWD /../../ python3 -m venv .venv . .venv/bin/activatecd $ROOT_FI python3 -m venv .venv . .venv/bin/activate python3 -m pip install -r ./requirements.txt python3 -m pip install -r ./tools/web-fuzzing-introspection/requirements.txtcd oss_fuzz_integration ./build_post_processing.sh
因为不太想把oss-fuzz-gen
放在fuzz-introspector
下,这样集成度太高,封装太好会导致理解不了到底introspector做了啥。运行完build_all.sh
脚本后,下载的镜像如下:
1 2 3 4 5 6 7 8 9 REPOSITORY TAG IMAGE ID CREATED SIZEgcr .io/oss-fuzz-base/base-runner latest 98 a302497240 15 seconds ago 1 .38 GB<none> <none> ee9986b8de3e 25 hours ago 1 .38 GBgcr .io/oss-fuzz-base/base-builder-go latest 1 e3e2bd1199c 25 hours ago 2 .31 GBgcr .io/oss-fuzz-base/base-builder-rust latest 7 a0091ac473f 26 hours ago 2 .44 GBgcr .io/oss-fuzz-base/base-builder-jvm latest 05 e48fb4e39a 26 hours ago 2 .28 GBgcr .io/oss-fuzz-base/base-builder-python latest fc469b915f16 28 hours ago 2 .01 GBgcr .io/oss-fuzz-base/base-builder latest 9 b6900c549c3 28 hours ago 1 .88 GBgcr .io/oss-fuzz-base/base-clang latest 05158 c6b3ea6 34 hours ago 1 .15 GB
创建webserver DB 首先去项目目录下source .venv/bin/activate
。随后,去/fuzz-introspector/tools/web-fuzzing-introspection/app/static/assets/db
目录下,执行以下指令为introspector构建指定项目的db:
1 ./launch_specific_targets.sh xpdf
DB是以.json文件创建的可供webapp
理解使用的数据库。所使用的原始数据是OSS-Fuzz每日生成的Fuzz Introspector
报告。所有的OSS-Fuzz项目的报告都会被整理并精简为更小的数据单元,然后合并到代表OSS-Fuzz宏观状态的数据结构中。例如,为了统计OSS-Fuzz覆盖的行数,google会合并所有OSS-Fuzz项目的数据。
DB由web_db_creator_from_summary.py
创建。此文件名中的summary
是对Fuzz Introspector为每个报告输出的summary.json
文件的引用。简单来说就是,通过使用google storage的project状态数据来构建一个本地的db(实际是json文件)存储项目的状态信息。
运行webserver 然后去/web-fuzzing-introspector
下跑webserver
,并让它在后台运行。
1 python3 ./main.py > /tmp/fi-weblog.txt 2>&1 &
检查服务是否启动成功:
1 curl http://127.0.0.1:8080/api/far-reach-but-low-coverage?project=xpdf
如果有数据,那么说明webserver启动成功。也就是本地搭建fuzz-introspector成功。
启动OSS-Fuzz-gen 把项目克隆下来,进入项目根目录,创建虚拟环境然后配置环境即可,不赘述了。
LLM Access 配置 需要准备相关大模型的api,需要通过设置相应的环境变量,例如openai需要设置OPENAI_API_KEY='<your-api-key>'
。如果是Azure上的openai模型,那么相应的设置:
1 2 3 export AZURE_OPENAI_API_KEY='<your-azure-api-key>' export AZURE_OPENAI_ENDPOINT='<your-azure-endpoint>' export AZURE_OPENAI_API_VERSION='<your-azure-api-version>'
具体请参考oss-fuzz-gen/USAGE.md at main · google/oss-fuzz-gen
Benchmark YAML 构建 在项目根目录下,使用如下指令构建xpdf的YAML文件:
1 python -m data_prep.introspector xpdf -m <target数量> -o benchmark-sets/xpdf-test
Fuzz Target 构建 在项目根目录下,使用如下指令构建xpdf,将其自动添加到prompts中。
1 python -m data_prep.project_src -p xpdf
Start 启动测试xpdf的某些API(可以在/oss-fuzz-data
中看到相关数据):
1 ./run_all_experiments.py --model=gpt-3 .5-turbo --benchmarks-directory= './benchmark-sets/xpdf-test ' -e http://127.0.0.1 :8080 /api
对的,你一定会经常构建失败。(除非你不在国内)
为了使你的服务器不爆炸的话,建议每次失败的实验都清空一下docker images和contianer:(以下为删除所有容器(请谨慎,我这是只有我自己在用的服务器,且没有其他docker container在跑),以及删除所有包含’xpdf-‘的images)
1 2 3 4 docker rm -f $ (docker ps -aq ) docker images --format "{{.Repository}}:{{.Tag}} {{.ID}}" | grep 'xpdf-' | awk '{print $2}' | xargs -r docker rmi -f docker builder prune -a -f docker volume prune -f
1 ./run_all_experiments.py --model=gpt-3 .5-turbo --benchmarks-directory= './benchmark-sets/xpdf-test ' -e http://127.0.0.1 :8080 /api --context -lo info -of ../oss-fuzz -w ./results
oss-fuzz-gen源码阅读 此环节的debug时的参数和上述一致:run_all_experiments.py --model gpt-3.5-turbo --benchmarks-directory ./benchmark-sets/xpdf-test -e http://127.0.0.1:8080/api --context -lo info -of ../oss-fuzz -w ./results
。
如果你在运行的时候也报各种代理错误的话,也看看源码吧。万变不离其宗。
run_all_experiments.py 从run_all_experiments.py
开始入手:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def main (): global WORK_DIR args = parse_args() _setup_logging(args.log_level, is_cloud=args.cloud_experiment_name != '' ) logger.info('Starting experiments on PR branch' ) start = time.time() add_to_json_report(args.work_dir, 'start_time' , time.strftime(TIME_STAMP_FMT, time.gmtime(start))) add_to_json_report(args.work_dir, 'num_samples' , args.num_samples) introspector.set_introspector_endpoints(args.introspector_endpoint) run_one_experiment.prepare(args.oss_fuzz_dir)
parse_args()
拿到参数,一个列表形式放入args
变量中。紧接着设置log_level
。然后记录时间,并将时间与num_samples
记录于结果目录中。其中num_samples
是每次大模型返回的样本数量,默认为2。
然后设置fuzz-introspector
,这里使用本地搭建的introspector
,因此将-e
参数设定的webapp设置为introspector
。随后进入到run_one_experiment.py
中,执行prepare()
方法。
1 2 3 4 def prepare (oss_fuzz_dir: str ) -> None : """Prepares the experiment environment.""" oss_fuzz_checkout.clone_oss_fuzz(oss_fuzz_dir) oss_fuzz_checkout.postprocess_oss_fuzz()
这里的oss_fuzz_dir是我们通过参数传递的../oss-fuzz
。通过clone_oss_fuzz()
方法将全局变量global OSS_FUZZ_DIR
设置为了这里的参数oss_fuzz_dir
。设置后会调用git clean -fxd -e venv -e build
,清理OSS_FUZZ_DIR
工作区环境,会删除所有未跟踪的文件和目录,但保留venv
和build
两个目录。
接下来回到main()
中的剩余部分,首先会执行prepare_experiment_targets
部分。它会遍历我们给予的benchmark-sets/xpdf-test/
中所有的yaml文件(此前已经生成好的),获取目标的相关配置,例如函数,变量等信息。
1 2 3 4 5 6 experiment_targets = prepare_experiment_targets(args) if oss_fuzz_checkout.ENABLE_CACHING: oss_fuzz_checkout.prepare_cached_images(experiment_targets) logger.info('Running %s experiment(s) in parallels of %s.' , len (experiment_targets), str (NUM_EXP))
然后会来到prepare_cached_images(experiments_targets)
,参数就是读取的xpdf.yaml
内容。
它会检查fuzz_build_script/
目录下是否存在xpdf
这么一个build
脚本。发现是没有的。因此会输出 INFO oss_fuzz_checkout - _prepare_image_cache: No cached script for xpdf 。那么prepare_cached_images()
就是加载一个build
脚本,然后使用docker pull镜像。由于本次没有走这个路径,因此返回到到main()
中继续:
1 2 3 4 5 6 7 WORK_DIR = args.work_dir coverage_gains_process = Process( target=extend_report_with_coverage_gains_process) coverage_gains_process.start()
设置-w
参数,然后去启动一个新进程,新进程执行的函数为extend_report_with_coverage_gains_process()
,它会每间隔5min执行一次extend_report_with_coverage_gains()
,其会更新当前实验的状态到report.json
。
1 2 3 4 5 6 7 8 9 10 def extend_report_with_coverage_gains_process (): """A process that continuously runs to update coverage gains in the background.""" while True : time.sleep(300 ) try : extend_report_with_coverage_gains() except Exception: logger.error('Failed to extend report with coverage gains' ) traceback.print_exc()
紧接着运行实验,并输出实验结果。分析其中run_experiments(target_benchmark)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def run_experiments (benchmark: benchmarklib.Benchmark, args ) -> Result: """Runs an experiment based on the |benchmark| config.""" try : work_dirs = WorkDirs(os.path.join(args.work_dir, f'output-{benchmark.id } ' )) args.work_dirs = work_dirs model = models.LLM.setup( ai_binary=args.ai_binary, name=args.model, max_tokens=MAX_TOKENS, num_samples=args.num_samples, temperature=args.temperature, temperature_list=args.temperature_list, ) result = run_one_experiment.run(benchmark=benchmark, model=model, args=args, work_dirs=work_dirs) return Result(benchmark, result) except Exception as e: logger.error('Exception while running experiment: %s' , str (e)) traceback.print_exc() return Result(benchmark, f'Exception while running experiment: {e} ' )
首先会初始化并建立与大模型的连接,然后调用run_one_experiment.py
的run()
方法。实际执行在_fuzzing_pipeline()
中,为了调试方便,将NUM_EVA = int(os.getenv('LLM_NUM_EVA', '3'))
改成NUM_EVA = int(os.getenv('LLM_NUM_EVA', '1'))
,并且源码_fuzzing_pipes()
中多线程调用_fuzzing_pipe()
改成单线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def _fuzzing_pipelines (benchmark: Benchmark, model: models.LLM, args: argparse.Namespace, work_dirs: WorkDirs ) -> BenchmarkResult: """Runs all trial experiments in their pipelines.""" with pool.ThreadPool(processes=NUM_EVA) as p: task_args = [(benchmark, model, args, work_dirs, trial) for trial in range (1 , args.num_samples + 1 )] trial_results = [ _fuzzing_pipeline(*args) for args in task_args] return BenchmarkResult(benchmark=benchmark, work_dirs=work_dirs, trial_results=trial_results)
这种情况下,我们能够来到_fuzzing_pipe()
中,初始化LLM相关的变量后,来到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def execute (self, result_history: list [Result] ) -> list [Result]: """ Runs the fuzzing pipeline iteratively to assess and refine the fuzz target. 1. Writing Stage refines the fuzz target and its build script using insights from the previous cycle. 2. Evaluation Stage measures the performance of the revised fuzz target. 3. Analysis Stage examines the evaluation results to guide the next cycle's improvements. The process repeats until the termination conditions are met. """ self .logger.debug('Pipeline starts' ) cycle_count = 0 self ._update_status(result_history=result_history) while not self ._terminate(result_history=result_history, cycle_count=cycle_count): cycle_count += 1 self ._execute_one_cycle(result_history=result_history, cycle_count=cycle_count) return result_history
execute()
函数返回值为一个周期的实验结果。进入到_execute_one_cycle()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 def _execute_one_cycle (self, result_history: list [Result], cycle_count: int ) -> None : """Executes the stages once.""" self .logger.info('[Cycle %d] Initial result is %s' , cycle_count, result_history[-1 ]) result_history.append( self .writing_stage.execute(result_history=result_history)) self ._update_status(result_history=result_history) if (not isinstance (result_history[-1 ], BuildResult) or not result_history[-1 ].success): self .logger.warning('[Cycle %d] Build failure, skipping the rest steps' , cycle_count) return result_history.append( self .execution_stage.execute(result_history=result_history)) self ._update_status(result_history=result_history) if (not isinstance (result_history[-1 ], RunResult) or not result_history[-1 ].log_path): self .logger.warning('[Cycle %d] Run failure, skipping the rest steps' , cycle_count) return result_history.append( self .analysis_stage.execute(result_history=result_history)) self ._update_status(result_history=result_history) self .logger.info('[Cycle %d] Analysis result %s: %s' , cycle_count, result_history[-1 ].success, result_history[-1 ])
进入到writing stage中的execute()
会调用project_targets.generate_data()
来生成fuzz targets。期间会访问
1 2 3 4 5 6 7 8 9 10 def generate_data (project_name: str , language: str , sig_per_target: int = 1 , max_samples: int = 1 , cloud_experiment_bucket: str = '' ): """Generates project-specific fuzz targets examples.""" target_funcs = introspector.get_project_funcs(project_name) project_fuzz_target_dir = _get_fuzz_target_dir(project_name) target_content_signature_dict = _bucket_match_target_content_signatures( target_funcs, project_fuzz_target_dir, project_name)
它会请求”https://storage.googleapis.com/oss-fuzz-introspector/ “ 获得xpdf/
下的所有summary.json
文件列表,然后读取最新的summary.json
文件。随后,根据这个summary文件,访问本地建立的xpdf数据库,获取func的源码,覆盖率等信息。随后会和LLM交互生成fuzz target:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def execute (self, result_history: list [Result] ) -> BuildResult: """Executes the agent based on previous result.""" last_result = result_history[-1 ] logger.info('Executing %s' , self .name, trial=last_result.trial) WorkDirs(self .args.work_dirs.base, keep=True ) prompt = self ._initial_prompt(result_history) cur_round = 1 build_result = BuildResult(benchmark=last_result.benchmark, trial=last_result.trial, work_dirs=last_result.work_dirs, author=self ,· chat_history={self .name: prompt.gettext()}) while prompt and cur_round <= self .max_round: self ._generate_fuzz_target(prompt, result_history, build_result, cur_round) self ._validate_fuzz_target(cur_round, build_result) prompt = self ._advice_fuzz_target(build_result, cur_round) cur_round += 1 return build_result
上面的源码可以看到,先初始化proompt
,这里的prompt在默认情况下,且没有参数ag
时,构造为如下文件。默认的Prompt模板会由以下文件构成:
1 2 3 4 5 system : priming.txt + cpp- specific - priming- filter.txtuser : problme.txt + func_source_code
调用model.py
中的API初始化LLM Client,通过上面源码中的_generate_fuzz_target()
生成了一个如下样式的libfuzzer target:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include "/src/xpdf-4.05/xpdf/OutputDev.h" void SplashOutputDev::drawChar (GfxState *state, double x, double y, double dx, double dy, double originX, double originY, CharCode code, int nBytes, Unicode *u, int uLen, GBool fill, GBool stroke, GBool makePath) ;int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size) { FuzzedDataProvider stream (data, size) ; GfxState state; double x = stream.ConsumeFloatingPoint <double >(); double y = stream.ConsumeFloatingPoint <double >(); double dx = stream.ConsumeFloatingPoint <double >(); double dy = stream.ConsumeFloatingPoint <double >(); double originX = stream.ConsumeFloatingPoint <double >(); double originY = stream.ConsumeFloatingPoint <double >(); CharCode code = stream.ConsumeIntegral <CharCode>(); int nBytes = stream.ConsumeIntegral <int >(); Unicode *u = nullptr ; int uLen = stream.ConsumeIntegral <int >(); GBool fill = stream.ConsumeBool (); GBool stroke = stream.ConsumeBool (); GBool makePath = stream.ConsumeBool (); SplashOutputDev splashOutputDev; splashOutputDev.drawChar (&state, x, y, dx, dy, originX, originY, code, nBytes, u, uLen, fill, stroke, makePath); return 0 ; }
随后来到_validate_fuzz_target()
方法中,它会编译AI生成的fuzz_targets,也就是harness
。先看看镜像是如何创建的,以及为什么我正常运行过程中一直在创建镜像,而不执行。看看one_prompt_prototyper.py
中的_validate_fuzz_taget()
方法。
1 2 3 4 5 6 7 8 def _validate_fuzz_target (self, cur_round: int , build_result: BuildResult ) -> None : """Validates the new fuzz target by recompiling it.""" benchmark = build_result.benchmark compilation_tool = ProjectContainerTool(benchmark=benchmark) ....
构建镜像是在ProjectContianerTool()
中,它会使用__init__()
进行初始化。初始化会准备以下内容:
1 2 3 4 5 6 7 def __init__ (self, benchmark: Benchmark, name: str = '' ) -> None : super ().__init__(benchmark, name) self .image_name = self ._prepare_project_image() self .container_id = self ._start_docker_container() self .build_script_path = '/src/build.sh' self ._backup_default_build_script() self .project_dir = self ._get_project_dir()
其中_prepare_project_image()
就是image_name = oss_fuzz_checkout.prepare_project_image(self.benchmark)
。定位到该函数中。
镜像名称为gcr.io/oss-fuzz/xpdf
,并且通过uuid
构建一个临时id方便区分:
1 2 3 4 5 6 7 8 def prepare_project_image (benchmark: benchmarklib.Benchmark ) -> str : """Prepares original image of the |project|'s fuzz target build container.""" project = benchmark.project image_name = f'gcr.io/oss-fuzz/{project} ' generated_oss_fuzz_project = f'{benchmark.id } -{uuid.uuid4().hex } ' generated_oss_fuzz_project = rectify_docker_tag(generated_oss_fuzz_project) create_ossfuzz_project(benchmark, generated_oss_fuzz_project) ...
首先,这个benchmark.id
就是project名字加上benchmark-sets/
下生成的xpdf.yaml
中的name
字段。例如,这里为"name": "_ZN15SplashOutputDev8drawCharEP8GfxStateddddddjiPjiiii"
。这里的generated_oss_fuzz_project
组成字段为xpdf-name-uuid
。随后调用rectify_docker_tag
修正该docker名称,修改一些Docker无法处理的名称,例如-_
等。
紧接着执行create_oss_fuzz_project()
,它会做啥呢?创建'../oss-fuzz/projects/xpdf-zn15splashoutputdev8drawcharep8gfxstateddddddjipjiiii-cc7d7bd89e0545e982e828bc537e1012'
目录,并复制oss-fuzz/projects/xpdf
中的内容于新目录中。
然后会判断是否存在缓存,是否使用缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def prepare_project_image (benchmark: benchmarklib.Benchmark ) -> str : """Prepares original image of the |project|'s fuzz target build container.""" ... if not ENABLE_CACHING: logger.warning('Disabled caching when building image for %s' , project) elif is_image_cached(project, 'address' ): logger.info('Will use cached instance.' ) rewrite_project_to_cached_project(project, generated_oss_fuzz_project,'address' ) prepare_build(project, 'address' , generated_oss_fuzz_project) logger.info('Using cached project image for %s: %s' ,generated_oss_fuzz_project, image_name) ...
接下来进入到is_image_cached()
中,它要访问远程是否有xpdf的构建 缓存。因此会执行docker manifest inspect 'us-central1-docker.pkg.dev/oss-fuzz/oss-fuzz-gen/xpdf-ofg-cached-address'
来判断是否存在元数据。我们使用的是OSS-Fuzz
中的项目,因此是存在的。
所以会看到 INFO oss_fuzz_checkout - prepare_project_image: Will use cached instance.
那么会来到rewrite_project_to_cached_project()
,这个函数将已有的 Dockerfile 加以修改,使其支持从缓存镜像$CACHE_IMAGE
从而加速构建过程。将原始Dockerfile
复制,并保存为original_dockerfile
。读取原来的Dockerfile并添加ARG指令,替换原来的FROM行,使其基于缓存镜像构建。
随后会对Dockerfile做“精简处理”,也就是注释掉一些内容,只保留关键的几行。因为大部分内容在缓存镜像中已经存在了,所以不需要重复执行。例如,原Dockerfile如下:
1 2 3 4 5 6 7 8 9 10 11 FROM gcr.io/oss-fuzz-base/base-builderRUN git clone --depth 1 https://gitlab.freedesktop.org/freetype/freetype RUN apt-get update RUN apt-get install --no-install-recommends -y make wget cmake qtbase5-dev libcups2-dev autoconf automake autotools-dev libtool RUN wget https://dl.xpdfreader.com/xpdf-latest.tar.gz WORKDIR $SRC COPY fuzz_*.cc $SRC / COPY build.sh $SRC / COPY fuzz_*.options $SRC /
而通过精简以及缓存加载修改后的Dockerfile如下:
1 2 3 4 5 6 7 8 9 10 11 FROM $CACHE_IMAGECOPY build.sh $SRC / COPY fuzz_*.options $SRC /
然后来到prepare_build()
中,这里会判断选择哪个Dockerfile
,由于使用缓存镜像,因此会使用刚创建的Dockerfile_address_cached
,所以这里会看到 Using cached dockerfile 。
以及Build前的一个消息:INFO oss_fuzz_checkout - prepare_project_image: Using cached project image for xpdf-zn15splashoutputdev8drawcharep8gfxstateddddddjipjiiii-cc7d7bd89e0545e982e828bc537e1012: gcr.io/oss-fuzz/xpdf
最后build镜像,return _build_image(generated_oss_fuzz_project)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def _build_image (project_name: str ) -> str : """Builds project image in OSS-Fuzz""" adjusted_env = os.environ | { 'FUZZING_LANGUAGE' : get_project_language(project_name) } command = [ 'python3' , 'infra/helper.py' , 'build_image' , '--pull' , project_name ] try : sp.run(command, cwd=OSS_FUZZ_DIR, env=adjusted_env, stdout=sp.PIPE, stderr=sp.PIPE, check=True ) logger.info('Successfully build project image for %s' , project_name) return f'gcr.io/oss-fuzz/{project_name} ' except sp.CalledProcessError as e: logger.error('Failed to build project image for %s: %s' , project_name, e.stderr.decode('utf-8' )) return ''
会使用oss-fuzz/infra/helper.py
来pull以及build镜像。关于helper.py
的整个调用链为:main(), build_image(), build_image_impl(), pull_images() + docker_build()
。有兴趣可以看看源码。记录一下这里执行的命令为:
1 python3 infra/helper.py build_image --pull xpdf-zn15splashoutputdev8drawcharep8gfxstateddddddjipjiiii-cc7d7bd89e0545e982e828bc537e1012
然后会build出一个镜像文件:
终端也会出现消息:
INFO oss_fuzz_checkout - _build_image: Successfully build project image for xpdf-zn15splashoutputdev8drawcharep8gfxstateddddddjipjiiii-cc7d7bd89e0545e982e828bc537e1012
由于子进程运行时重定向了stdout
所以,执行helper.py期间的消息就不会输出到终端。若是想知道这期间执行的相关命令(oss-fuzz中的logger.info消息),可以注释掉stdout
和stderr
的重定向。
至此一大圈,我们完成了_validate_fuzz_target()
中关于ProjectContainerTool()
的构造函数__init__()
中的self._prepare_project_image()
。也就是为xpdf项目创建了一个镜像。回到__init__()
:
1 2 3 4 5 6 7 def __init__ (self, benchmark: Benchmark, name: str = '' ) -> None : super ().__init__(benchmark, name) self .image_name = self ._prepare_project_image() self .container_id = self ._start_docker_container() self .build_script_path = '/src/build.sh' self ._backup_default_build_script() self .project_dir = self ._get_project_dir()
接下来执行_start_docker_container()
,首先会构造一个docker run命令,在后台运行一个基于OSS-Fuzz
项目镜像的容器,并返回容器ID。那么这里执行的:
1 docker run -d -t --entrypoint=/bin/sh -e FUZZINGLANGUAGE=c++ gcr.io/oss-fuzz/xpdf-zn15splashoutputdev8drawcharep8gfxstateddddddjipjiiii-cc7d7bd89e0545e982e828bc537e1012
在后台运行一个容器,并将容器返回给self.container_id
。紧接着设置使用的build_scrpit
脚本路径。然后调用_backup_default_build_script()
,在执行前,我们进入容器尝尝咸淡:
1 docker exec -it <contianer_id> /bin/bash
这就是拉取的google缓存好的镜像。里头有源码以及一些fuzzer。那么执行_backup_default_build_script()
:
1 2 3 4 5 6 7 def _backup_default_build_script (self ) -> None : """Creates a copy of the human-written /src/build.sh for LLM to use.""" backup_command = f'cp {self.build_script_path} /src/build.bk.sh' process = self .execute(backup_command) if process.returncode: logger.error('Failed to create a backup of %s: %s' , self .build_script_path, self .image_name)
这个self.execute()
是在容器内执行command。因此会在容器内执行指令cp /src/build.sh /src/build.bk.sh
,而这个build.sh
就是oss-fuzz/projects/xpdf/build.sh
。
最后的self._get_project_dir()
就是容器内的project目录,也就是/src
至此,初始化出一个ProjectContainerTool
对象,拉取google库中的项目对应的镜像,并根据该镜像创建容器,执行必要的初始化操作。接下来回到_validate_fuzz_target()
中:
1 2 3 4 5 6 7 8 9 10 11 12 def _validate_fuzz_target (self, cur_round: int , build_result: BuildResult ) -> None : """Validates the new fuzz target by recompiling it.""" ... replace_file_content_command = ( 'cat << "OFG_EOF" > {file_path}\n{file_content}\nOFG_EOF' ) compilation_tool.execute( replace_file_content_command.format ( file_path=benchmark.target_path, file_content=build_result.fuzz_target_source)) ...
这构建一个指令,并且在会在容器内执行该指令。在容器内执行的命令如下:
1 cat << "OFG_EOF" > /src/fuzz_zxdoc.cc\n#include "/src/xpdf-4.05/xpdf/OutputDev.h" \n\nvoid SplashOutputDev::drawChar(GfxState *state, double x, double y,\n\t\t\t double dx, double dy,\n\t\t\t double originX, double originY,\n\t\t\t CharCode code, int nBytes,\n\t\t\t Unicode *u, int uLen,\n\t\t\t GBool fill, GBool stroke, GBool makePath);\n\nint LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {\n FuzzedDataProvider stream(data, size);\n\n GfxState state;\n double x = stream.ConsumeFloatingPoint<double>();\n double y = stream.ConsumeFloatingPoint<double>();\n double dx = stream.ConsumeFloatingPoint<double>();\n double dy = stream.ConsumeFloatingPoint<double>();\n double originX = stream.ConsumeFloatingPoint<double>();\n double originY = stream.ConsumeFloatingPoint<double>();\n CharCode code = stream.ConsumeIntegral<CharCode>();\n int nBytes = stream.ConsumeIntegral<int>();\n Unicode *u = nullptr; // Initialize Unicode pointer to nullptr\n int uLen = stream.ConsumeIntegral<int>();\n GBool fill = stream.ConsumeBool();\n GBool stroke = stream.ConsumeBool();\n GBool makePath = stream.ConsumeBool();\n\n SplashOutputDev splashOutputDev;\n splashOutputDev.drawChar(&state, x, y, dx, dy, originX, originY, code, nBytes, u, uLen, fill, stroke, makePath);\n\n return 0;\n}\nOFG_EOF
也就是将LLM生成的harness覆盖容器内,执行指令前的fuzz_zxdoc.cc
内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdlib.h> #include <string.h> #include <stdint.h> #include "Zoox.h" extern "C" int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size) { char *ss = (char *)malloc (size+1 ); memcpy (ss, data, size); ss[size] = '\0' ; ZxDoc Z1; ZxDoc *new_doc = Z1.l oadMem(ss, size); if (new_doc != NULL ) delete new_doc; free (ss); return 0 ; }
执行命令后,就会将其覆盖为LLM生成的harness。随后继续回到_validate_fuzz_target()
:
1 2 3 4 5 6 7 8 9 10 11 def _validate_fuzz_target (self, cur_round: int , build_result: BuildResult ) -> None : """Validates the new fuzz target by recompiling it.""" ... if build_result.build_script_source: compilation_tool.execute( replace_file_content_command.format ( file_path='/src/build.sh' , file_content=build_result.build_script_source)) ...
这个部分主要是替换容器内的/src/build.sh
,但是这里为空。那么就不会覆盖重写该build脚本。紧接着_validate_fuzz_target()
就开始编译目标了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def _validate_fuzz_target (self, cur_round: int , build_result: BuildResult ) -> None : """Validates the new fuzz target by recompiling it.""" ... logger.info('===== ROUND %02d Recompile =====' , cur_round, trial=build_result.trial) start_time = time.time() compile_process = compilation_tool.compile () end_time = time.time() logger.debug('ROUND %02d compilation time: %s' , cur_round, timedelta(seconds=end_time - start_time), trial=build_result.trial) compile_succeed = compile_process.returncode == 0 logger.debug('ROUND %02d Fuzz target compiles: %s' , cur_round, compile_succeed, trial=build_result.trial) ...
调用compilation_tool
的compile()
方法,也就是我们前面生成的ProjectComtainerTool
对象。在重写完相关文件与harness后进行编译。进入到compile()
中,执行的指令为:
1 docker exec 042e63a1bc152b17073ce094eb3ed185e6b2f69f01ea1b627b8bac1de5e39e6d /bin/bash -c compile > /dev/null
看看compile是个啥:
1 2 3 4 root@042e63a1bc15:/src# which compile /usr/local/bin/compile root@042e63a1bc15:/src# file /usr/local/bin/compile /usr/local/bin/compile: Bourne-Again shell script, ASCII text executable
是个脚本,源码太长了就不贴了。丢给AI解释解释:OSS-Fuzz项目的主构建脚本,主要作用是在构建fuzz target之前,配置和处理环境变量、编译标志、构建工具、平台兼容性、特定语言支持(如Rust、Python、JVM等),以及对不同fuzzing引擎和sanitizers(如ASan、UBSan等)以及Introspector的支持。具体的职责包括:
基础配置:
设置内核参数,如vm.mmap_rnd_bits=28
设置编译标志,例如CFLAGS
,CXXFLAGS
,RUSTFLAGS
按语言处理特殊逻辑
Rust的introspector处理方式
JVM fuzzing的兼容性检查(仅支持libFuzzer或wycheproof)
Python fuzzing的sanitizer限制等
环境变量的传递
将sanitizer类型映射到相应的编译标志
设置LLVM工具链路径(如llvm-ar
,llvm-nm
)
调用fuzz-introspector(如果启用Introspector sanitizer):
执行fuzz-introspector
分析
生成HTML报告和YAML/JSON分析文件
构建项目
调用$SRC/build.sh
或回放脚本relay_build.sh
进行构建
特殊工具和符号表处理:
拷贝llvm-symbolizer
到$OUT/
,方便后续的栈信息符号化
为JVM fuzzing准备jazzer_driver_with_sanitizer
也就是说,它会调用/src/build.sh
进行构建,关于本次harness的build编译会出现错误:
1 2 3 4 5 6 'sysctl: setting key "vm.mmap_rnd_bits", ignoring: Read-only file system\n+ tar -zxf xpdf-latest.tar.gz\n++ tar -tzf xpdf-latest.tar.gz\n++ head -1\n++ cut -f1 -d/\n+ dir_name=xpdf-4.05\n+ cd xpdf-4.05\n+ PREFIX=/work/prefix\n+ mkdir -p -p /work/prefix\n++ which pkg-config\n+ export \' PKG_CONFIG= --static\'\n+ PKG_CONFIG=\' --static\'\n+ export PKG_CONFIG_PATH=/work/prefix/lib/pkgconfig\n+ PKG_CONFIG_PATH=/work/prefix/lib/pkgconfig\n+ export PATH=/work/prefix/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/out\n+ PATH=/work/prefix/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/out\n+ pushd /src/freetype\n+ CFLAGS=\'-O1 -fno-omit-frame-pointer -gline-tables-only -Wno-error=enum-constexpr-conversion -Wno-error=incompatible-function-pointer-types -Wno-error=int-conversion -Wno-error=deprecated-declarations -Wno-error=implicit-function-declaration -Wno-error=implicit-int -Wno-error=vla-cxx-extension -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -fsanitize=address -fsanitize-address-use-after-scope -fsanitize=fuzzer-no-link -D_GNU_SOURCE\'\n+ ./autogen.sh\n+ CFLAGS=\'-O1 -fno-omit-frame-pointer -gline-tables-only -Wno-error=enum-constexpr-conversion -Wno-error=incompatible-function-pointer-types -Wno-error=int-conversion -Wno-error=deprecated-declarations -Wno-error=implicit-function-declaration -Wno-error=implicit-int -Wno-error=vla-cxx-extension -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -fsanitize=address -fsanitize-address-use-after-scope -fsanitize=fuzzer-no-link -D_GNU_SOURCE\'\n+ ./configure --prefix=/work/prefix --disable-shared PKG_CONFIG_PATH=/work/prefix/lib/pkgconfig --with-png=no --with-zlib=no\n./configure: line 3492: --static: command not found\nconfigure: WARNING:\n `make refdoc\' will fail since pip package `docwriter\' is not installed.\n To install, run `python3 -m pip install docwriter\', or to use a Python\n virtual environment, run `make refdoc-venv\' (requires pip package\n `virtualenv\'). These operations require Python >= 3.5.\n \n++ nproc \n+ CFLAGS=\'-O1 -fno-omit-frame-pointer -gline-tables-only -Wno-error=enum-constexpr-conversion -Wno-error=incompatible-function-pointer-types -Wno-error=int-conversion -Wno-error=deprecated-declarations -Wno-error=implicit-function-declaration -Wno-error=implicit-int -Wno-error=vla-cxx-extension -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -fsanitize=address -fsanitize-address-use-after-scope -fsanitize=fuzzer-no-link -D_GNU_SOURCE\'\n+ make -j112\n+ CFLAGS=\'-O1 -fno-omit-frame-pointer -gline-tables-only -Wno-error=enum-constexpr-conversion -Wno-error=incompatible-function-pointer-types -Wno-error=int-conversion -Wno-error=deprecated-declarations -Wno-error=implicit-function-declaration -Wno-error=implicit-int -Wno-error=vla-cxx-extension -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -fsanitize=address -fsanitize-address-use-after-scope -fsanitize=fuzzer-no-link -D_GNU_SOURCE\'\n+ make install\n+ popd \n+ sed -i \'s/#--- object files needed by XpdfWidget/add_library(testXpdfStatic STATIC $<TARGET_OBJECTS:xpdf_objs>)\\n#--- object files needed by XpdfWidget/\' ./xpdf/CMakeLists.txt\n+ sed -i \'s/#--- pdftops/add_library(testXpdfWidgetStatic STATIC $<TARGET_OBJECTS:xpdf_widget_objs>\\n $<TARGET_OBJECTS:splash_objs>\\n $<TARGET_OBJECTS:xpdf_objs>\\n ${FREETYPE_LIBRARY} \\n ${FREETYPE_OTHER_LIBS} )\\n#--- pdftops/\' ./xpdf/CMakeLists.txt\n+ mkdir -p build\n+ cd build\n+ export LD=clang++\n+ LD=clang++\n+ make\nCMake Deprecation Warning at CMakeLists.txt:11 (cmake_minimum_required):\n Compatibility with CMake < 3.5 will be removed from a future version of\n CMake.\n\n Update the VERSION argument <min> value or use a ...<max> suffix to tell\n CMake that the project does not need compatibility with older versions.\n\n\n+ for fuzzer in zxdoc pdfload JBIG2\n+ cp ../../fuzz_zxdoc.cc .\n+ clang++ fuzz_zxdoc.cc -o /out/fuzz_zxdoc -O1 -fno-omit-frame-pointer -gline-tables-only -Wno-error=enum-constexpr-conversion -Wno-error=incompatible-function-pointer-types -Wno-error=int-conversion -Wno-error=deprecated-declarations -Wno-error=implicit-function-declaration -Wno-error=implicit-int -Wno-error=vla-cxx-extension -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -fsanitize=address -fsanitize-address-use-after-scope -fsanitize=fuzzer-no-link -stdlib=libc++ -fsanitize=fuzzer ./xpdf/libtestXpdfStatic.a ./fofi/libfofi.a ./goo/libgoo.a ./splash/libsplash.a ./xpdf/libtestXpdfWidgetStatic.a /work/prefix/lib/libfreetype.a -I../ -I../goo -I../fofi -I. -I../xpdf -I../splash\nIn file included from fuzz_zxdoc.cc:1:\n/src/xpdf-4.05/xpdf/OutputDev.h:99:28: error: unknown type name \'Ref\'\n 99 | virtual void startStream(Ref streamRef, GfxState *state) {}\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:100:26: error: unknown type name \'Ref\'\n 100 | virtual void endStream(Ref streamRef) {}\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:156:61: error: unknown type name \'Object\'\n 156 | virtual void tilingPatternFill(GfxState *state, Gfx *gfx, Object *strRef,\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:157:37: error: unknown type name \'Dict\'\n 157 | int paintType, int tilingType, Dict *resDict,\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:198:47: error: unknown type name \'Object\'\n 198 | virtual void drawImageMask(GfxState *state, Object *ref, Stream *str,\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:202:6: error: unknown type name \'Object\'\n 202 | Object *ref, Stream *str,\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:205:43: error: unknown type name \'Object\'\n 205 | virtual void drawImage(GfxState *state, Object *ref, Stream *str,\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:208:49: error: unknown type name \'Object\'\n 208 | virtual void drawMaskedImage(GfxState *state, Object *ref, Stream *str,\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:211:11: error: unknown type name \'Object\'\n 211 | Object *maskRef, Stream *maskStr,\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:214:53: error: unknown type name \'Object\'\n 214 | virtual void drawSoftMaskedImage(GfxState *state, Object *ref, Stream *str,\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:217:8: error: unknown type name \'Object\'\n 217 | Object *maskRef, Stream *maskStr,\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:224:42: error: unknown type name \'Dict\'\n 224 | virtual void opiBegin(GfxState *state, Dict *opiDict);\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:225:40: error: unknown type name \'Dict\'\n 225 | virtual void opiEnd(GfxState *state, Dict *opiDict);\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:234:25: error: unknown type name \'Ref\'\n 234 | virtual void drawForm(Ref id ) {}\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:254:62: error: unknown type name \'Dict\'\n 254 | virtual void beginStructureItem(const char *tag, int mcid, Dict *dict) {}\n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:88:48: error: use of undeclared identifier \'NULL\'\n 88 | GBool (*abortCheckCbk)(void *data) = NULL, \n | ^\n/src/xpdf-4.05/xpdf/OutputDev.h:89:37: error: use of undeclared identifier \'NULL\'\n 89 | void *abortCheckCbkData = NULL)\n | ^\nfuzz_zxdoc.cc:3:6: error: use of undeclared identifier \'SplashOutputDev\'\n 3 | void SplashOutputDev::drawChar(GfxState *state, double x, double y,\n | ^\nfuzz_zxdoc.cc:10:34: error: unknown type name \'uint8_t\'\n 10 | int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {\n | ^\nfatal error: too many errors emitted, stopping now [-ferror-limit=]\n20 errors generated.\n')
也就是编译失败,紧接着_validate_fuzz_target()
会进行二次check:
1 2 3 4 5 6 7 8 9 10 11 12 13 def _validate_fuzz_target (self, cur_round: int , build_result: BuildResult ) -> None : """Validates the new fuzz target by recompiling it.""" ... ls_result = compilation_tool.execute(f'ls /out/{benchmark.target_name} ' ) binary_exists = ls_result.returncode == 0 logger.debug('ROUND %02d Final fuzz target binary exists: %s' , cur_round, binary_exists, trial=build_result.trial) ...
也就是去容器中检查编译出来的可执行文件是否存在,在容器内执行指令ls /out/fuzz_zxdoc
。由于编译失败,故也不存在。接下来_validate_fuzz_target()
会查看待测API是否被生成的harness引用。
1 2 function_referenced = self ._validate_fuzz_target_references_function( compilation_tool, benchmark, cur_round, build_result.trial)
调用_validate_fuzz_target_references_function()
方法,用来验证由LLM生成的fuzz target是否在其汇编代码中实际引用了目标函数。这个检查方法会跳过'jvm','python', 'rust'
的校验,因为这些语言的编译产物通常不是ELF可执行文件,objdump
无法正常反汇编,所以略过检查。它的核心是在容器内执行:
1 objdump --disassemble=LLVMFuzzerTestOneInput -d /out/fuzz_zxdoc
验证指令返回内容中是否有_ZN15SplashOutputDev8drawCharEP8GfxStateddddddjiPjiiii
。那显然没有,因为编译失败了。那么这里会输出:
[Trial ID: 01] DEBUG [logger.debug]: ROUND 01 Final fuzz target function referenced: False
[Trial ID: 01] DEBUG [logger.debug]: ROUND 01 Final fuzz target function not referenced
紧接着最后部分的_validate_fuzz_target()
,会先停止这个运行的容器,然后更新build result.
1 2 3 4 5 6 7 8 9 10 11 def _validate_fuzz_target (self, cur_round: int , build_result: BuildResult ) -> None :"""Validates the new fuzz target by recompiling it.""" ... compilation_tool.terminate() self ._update_build_result(build_result, compile_process=compile_process, compiles=compile_succeed, binary_exists=binary_exists, referenced=function_referenced)
更新的参数如下:
1 2 3 4 5 6 7 8 9 10 def _update_build_result (self, build_result: BuildResult, compile_process: sp.CompletedProcess, compiles: bool , binary_exists: bool , referenced: bool ) -> None : """Updates the build result with the latest info.""" build_result.compiles = compiles build_result.binary_exists = binary_exists build_result.compile_error = compile_process.stderr build_result.compile_log = self ._format_bash_execution_result( compile_process) build_result.is_function_referenced = referenced
首先compiles
为False
,binary_exists
也为Flase
。然后将编译错误的输出保存,并将编译过程也保存。其实保存的内容是一致的,只不过删了些\n
。
随后我们来到execute()
循环中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def execute (self, result_history: list [Result] ) -> BuildResult: """Executes the agent based on previous result.""" last_result = result_history[-1 ] logger.info('Executing %s' , self .name, trial=last_result.trial) WorkDirs(self .args.work_dirs.base, keep=True ) prompt = self ._initial_prompt(result_history) cur_round = 1 build_result = BuildResult(benchmark=last_result.benchmark, trial=last_result.trial, work_dirs=last_result.work_dirs, author=self , chat_history={self .name: prompt.gettext()}) while prompt and cur_round <= self .max_round: self ._generate_fuzz_target(prompt, result_history, build_result, cur_round) self ._validate_fuzz_target(cur_round, build_result) prompt = self ._advice_fuzz_target(build_result, cur_round) cur_round += 1 return build_result
紧接着就来到了_advice_fuzz_target()
,要用LLM修复此前生成的harness。首先会初始化一个新的agent作为fixer,然后从编译过程的log中,摘取出报错部分,发现了一个bug,在_advice_fuzz_target()
中,执行collect_context()
后,context
为空。在执行完_advice_fuzz_target
后,重新生成了新的prompt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 system: user: <code> source_code, 也就是上一次LLM生成的源码。 </code> Below is the error to fix: The code has the following build issues: <error > </error > Below are instructions to assist you in fixing the error . <instruction>IMPORTANT: ALWAYS INCLUDE STANDARD LIBRARIES BEFORE PROJECT-SPECIFIC (xpdf) LIBRARIES. This order prevents errors like "unknown type name" for basic types. Additionally, include project-specific libraries that contain declarations before those thatuse these declared symbols. </instruction> Fix code:1 . Consider possible solutions for the issues listed above.2 . Choose a solution that can maximize fuzzing result, which is utilizing the function under test and feeding it not null input.3 . Apply the solutions to the original code. It <solution>
error
字段为空,所以才导致我此前一直在创建docker却没有什么结果,因为它压根没有修复编译错误,所以一直是编译失败。重新执行,在_advice_fuzz_target()
中打断点,查看为何提取的errors没了,因为编译失败的log是已经保存了的。首先会经过extract_error_from_lines()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def extract_error_from_lines (log_lines: list [str ], project_target_basename: str , language: str ) -> list [str ]: """Extracts error message and its context from the file in |log_path|.""" target_name, _ = os.path.splitext(project_target_basename) error_lines_range: list [Optional [int ]] = [None , None ] temp_range: list [Optional [int ]] = [None , None ] error_start_pattern = r'\S*' + target_name + r'(\.\S*)?:\d+:\d+: .+: .+\n?' error_include_pattern = (r'In file included from \S*' + target_name + r'(\.\S*)?:\d+:\n?' ) error_end_pattern = r'.*\d+ errors? generated.\n?' error_keywords = [ 'multiple definition of' , 'undefined reference to' , ] errors = [] unique_symbol = set ()
这里定义了错误的匹配模式,首先定义Clang错误的起止位置:
1 error_start_pattern = r'\S*' + target_name + r'(\.\S*)?:\d+:\d+: .+: .+\n?'
它会匹配形式如:
1 /src/xpdf/target .cc:123:45: error : ... # 标准 Clang error 起始
其次第二个匹配模式,抓取include模式:
1 error_include_pattern = r'In file included from \S*' + target_name + r'(\.\S*)?:\d+:\n?'
它会匹配形式如:
1 In file included from /src/ xpdf/target.cc:5 :
最后一个匹配模式会抓取错误结束行,也就是Clang最后一句。
1 error_end_pattern = r'.*\d+ errors? generated.\n?'
它会匹配形式如:
最后有一个关键词错误(由链接器ld提供):
1 error_keywords = ['multiple definition of' , 'undefined reference to' ]
具体的提取匹配逻辑就不贴了,errors包含的内容为从 In file included from fuzz_zxdoc.cc:1: 到 20 errors generated. 但不包含最后一个汇总错误数量语句。随后执行:
1 return group_error_messages(errors)
由于匹配是以行进行构建的,而有些错误应该是多行的形式,因此通过该函数对error行处理形成error块。
随后来到collect_context()
中,参数是完成分块后的errors。
1 2 3 4 5 6 7 8 9 10 11 def collect_context (benchmark: benchmarklib.Benchmark, errors: list [str ] ) -> str : """Collects the useful context to fix the errors.""" if not errors: return '' context = '' for error in errors: context += _collect_context_no_member(benchmark, error) return context
这里返回的context
为空,因为错误主要为以下两类(unknown type name xxx
和 use of undeclared identifier xxxx
):
1 2 3 4 5 6 /src/ xpdf-4.05 /xpdf/ OutputDev.h:99 :28 : error: unknown type name 'Ref' 99 | virtual void startStream(Ref streamRef, GfxState *state) {} | ^/src/ xpdf-4.05 /xpdf/ OutputDev.h:88 :48 : error: use of undeclared identifier 'NULL' 88 | GBool (*abortCheckCbk)(void *data) = NULL , | ^
而当前只支持处理no member named
错误,它的正则匹配模式为NO_MEMBER_ERROR_REGEX = r"error: no member named '.*' in '([^':]*):?.*'"
,因此此处是不会匹配上任何error的,返回为空。
紧接着来到collect_instructions()
中,根据构建/编译 错误信息errors和harness源码收集修复这些错误的 说明性指令 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def collect_instructions (benchmark: benchmarklib.Benchmark, errors: list [str ], fuzz_target_source_code: str ) -> str : """Collects the useful instructions to fix the errors.""" if not errors: return '' instruction = '' for error in errors: instruction += _collect_instruction_file_not_found(benchmark, error, fuzz_target_source_code) instruction += _collect_instruction_undefined_reference( benchmark, error, fuzz_target_source_code) instruction += _collect_instruction_fdp_in_c_target(benchmark, errors, fuzz_target_source_code) instruction += _collect_instruction_no_goto(fuzz_target_source_code) instruction += _collect_instruction_builtin_libs_first(benchmark, errors) instruction += _collect_instruction_extern(benchmark) instruction += _collect_consume_buffers(fuzz_target_source_code) return instruction
当前遇到的错误为unknown type name xxx
和use of undeclared identifier
其实质都是由于有些库没有加载进来,我们进一步看它的处理逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 def _collect_instruction_builtin_libs_first (benchmark: benchmarklib.Benchmark, errors: list [str ] ) -> str : """Collects the instructions to include builtin libraries first to fix unknown type name error.""" if any (UNKNOWN_TYPE_ERROR in error for error in errors): return ( 'IMPORTANT: ALWAYS INCLUDE STANDARD LIBRARIES BEFORE PROJECT-SPECIFIC ' f'({benchmark.project} ) LIBRARIES. This order prevents errors like ' '"unknown type name" for basic types. Additionally, include ' 'project-specific libraries that contain declarations before those that' 'use these declared symbols.' ) return ''
这里增加的提示是,当遇到“unknown type name”错误时,提示用户检查并调整投文件包含顺序,特别是要先包含标准库,再包含项目特定头文件。OK,接下来就是正常的第二轮了,将对应的prompt + 源码给LLM后,它继续生成harness。现在少了use of undeclared identifier
错误,多了一个file not found
:
1 2 3 4 fuzz_zxdoc.cc:5 :10 : fatal error: '/src/xpdf-4.05 /xpdf/Splash.h' file not found 5 | #include "/src /xpdf- 4.05/xpdf /Splash.h " | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~16 errors generated .
随后下一轮生成的harness便修复了这个错误:
1 #include "/src/xpdf-4.05/splash/Splash.h"
到第42轮的时候,出现了问题。也就是经过了42轮的重新编译,修复问题,却连最开始的unknown type name
的问题都没有修复,每次给予LLM的修复中,只有instruction来解决unknown type name
,而error
中却为空。
可以尝试改一下逻辑,将正则匹配提取的errors
信息放进error
中,而不是只把no member
类的错误给LLM。
还有一个问题,每次重新执行并不会用到已经创建的容器,而是会重新pull。而其实每次更改的部分不多,仅是harness文件而已,那为什么不可以把harness文件写入到同一个contianer,只为了保留每次生成的harness而直接创建镜像和容器未免有点太奢侈了,完全将每次的编译过程以及源文件可以写入到文件中。
此次执行,第一次生成的harness如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include "/src/xpdf-4.05/xpdf/OutputDev.h" void SplashOutputDev::drawChar (...) { ... }extern "C" int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size) { }
最后一轮的harness如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <cstdint> #include "/src/xpdf-4.05/xpdf/OutputDev.h" #include "/src/xpdf-4.05/xpdf/GfxState.h" #include "/src/xpdf-4.05/splash/Splash.h" #include "/src/xpdf-4.05/splash/SplashPath.h" #include "/src/xpdf-4.05/splash/SplashFont.h" #include "/src/xpdf-4.05/fofi/FoFiBase.h" #include "/src/xpdf-4.05/xpdf/Stream.h" #include "/src/xpdf-4.05/xpdf/SplashOutputDev.h" #include "/src/xpdf-4.05/fofi/FoFiType1.h" #include "/src/xpdf-4.05/fofi/FoFiType1C.h" void SplashOutputDev::drawChar (...) { ... }extern "C" int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size) { ... ... ... }
其中,相同部分的代码我给略去了,也就是说,在这42轮的编译错误修复中,只是在增加头文件以解决unknown type name
问题。但却一直解决不掉。
那也就是说,单纯靠当前的Prompt+Fuzz-introspector提供的函数签名及摘要生成harness时,针对于大部分OSS-Fuzz项目是很有可能生成一个需要修复N轮的harness
后半部分就没有继续调试了,因为本次源码阅读仅为了解决我使用oss-fuzz-gen过程中遇到的问题。当然,后续会继续补充完善。
2025-5-29更:
当我们加上参数--max-round 20
时,它会在20轮重新编译后结束,那么看看后面的流程,首先毋庸置疑的就是编译失败:
1 2 3 4 5 6 2025-05-29 13:25:48 [Trial ID: 01] INFO [logger.info]: ===== ROUND 20 Recompile ===== 2025-05-29 13:26:13 [Trial ID: 01] DEBUG [logger.debug]: ROUND 20 compilation time : 0:00:24.273684 2025-05-29 13:26:13 [Trial ID: 01] DEBUG [logger.debug]: ROUND 20 Fuzz target compiles: False 2025-05-29 13:26:13 [Trial ID: 01] DEBUG [logger.debug]: ROUND 20 Final fuzz target binary exists: False 2025-05-29 13:26:13 [Trial ID: 01] DEBUG [logger.debug]: ROUND 20 Final fuzz target function referenced: False 2025-05-29 13:26:13 [Trial ID: 01] DEBUG [logger.debug]: ROUND 20 Final fuzz target function not referenced
随后会在one_prompt_prototyper.py
的execute()
中返回build_result
。
然后会来到writing_stage.py
中的execute()
,记录fuzz_target
,build_script
和chat_history
等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def execute (self, result_history: list [Result] ) -> Result: """Executes the writing stage.""" if result_history and result_history[-1 ].fuzz_target_source: agent = self .get_agent(index=1 ) else : agent = self .get_agent() agent_result = self ._execute_agent(agent, result_history) build_result = cast(BuildResult, agent_result) self .logger.write_fuzz_target(build_result) self .logger.write_build_script(build_result) self .logger.write_chat_history(build_result) self .logger.debug('Writing stage completed with with result:\n%s' , build_result) return build_result
至此writing stage就彻底结束了。由于writing stage中并没有成功编译目标,所以会结束fuzz。
1 2 3 4 5 6 7 8 9 result_history.append( self .writing_stage.execute(result_history=result_history))self ._update_status(result_history=result_history)if (not isinstance (result_history[-1 ], BuildResult) or not result_history[-1 ].success): self .logger.warning('[Cycle %d] Build failure, skipping the rest steps' , cycle_count) return
当然,仔细看OSS-Fuzz-Gen的文档,会发现它发现的漏洞大模型都是在Vertex AI,那么显然,它用手工编写的harness进行了训练,模型微调等等。因为它提供了一个获得训练数据的方法:oss-fuzz-gen/data_prep/README.md at main · google/oss-fuzz-gen
1 python -m data_prep.project_targets --project -name <project -name>
变相说明,通用大模型其实是没戏的。例如,笔者用gpt-3.5 turpo以及4o是做不到的,起码做不到复现oss-fuzz-gen发现的漏洞。
为什么harness这么难写呢?后续打算继续研究研究一个高质量harness的编写。