oss-fuzz-gen源码阅读

openai,你能便宜点吗?

libFuzzer

libfuzzer是一个进程内、覆盖引导、进化的Fuzz引擎。

LibFuzzer与被测库链接,并通过特定的Fuzzing入口点(又称为“目标函数”)将模糊测试输入送到被测库;然后,Fuzzer会跟踪代码的哪些区域被覆盖,并对输入数据语料库进行修改,以最大化代码覆盖率。libFuzzer的覆盖率信息由LLVMSanitizerCoverage提供代码插桩。关于LLVM的Sanitizer,笔者在Fuzzilli文章中有简要概述,主要介绍一些桩函数与回调函数啥的。

Fuzz Target

使用libFuzzer对库进行覆盖率引导模糊测试的第一步是实现一个Fuzz target,也就是一个函数。它接受一个字节数组,并使用要测试的API对这些字节执行一些”intrersting”的操作。例如:

1
2
3
4
5
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Values other than 0 and -1 are reserved for future use.
}

请注意,此模糊测试目标与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 # Builds the fuzz target w/o sanitizers
clang -g -O1 -fsanitize=fuzzer,address mytarget.c # Builds the fuzz target with ASAN
clang -g -O1 -fsanitize=fuzzer,signed-integer-overflow mytarget.c # Builds the fuzz target with a part of UBSAN
clang -g -O1 -fsanitize=fuzzer,memory mytarget.c # Builds the fuzz target with MSAN

这将执行必要的插桩,并与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) {
// 这里省略了真正的 fuzz 驱动代码,通常你会调用 libFuzzer 的接口
// 这里只是示范,通常不建议自己写main,除非很特殊

// 简单示范:用固定输入调用测试函数
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.git
cd 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
// Copyright 2016 Google Inc. All Rights Reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
#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.11gitDockerGoogle Cloud SDKC++ filt,(optional for project_src.py)clang-format

安装python相关依赖:

1
2
3
python -m venv.venv
source .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]
[...]
# E.g., generate fuzz targets for TinyXML-2 with default template and fuzz for 30 seconds.
# ./run_all_experiments.py -y ./benchmark-sets/all/tinyxml2.yaml

<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:

  1. comparison:一些OSS-Fuzz C/C++项目
  2. all:所有OSS-Fuzz C/C++项目
  3. 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.cpp
max 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>查看结果。

实验的工作流细节

配置和使用框架,请按照以下步骤进行:

  1. 配置Benchmark
  2. 设置Prompt模板
  3. 生成Fuzz目标
  4. 修复编译错误
  5. 评估Fuzz目标
  6. 使用本地Fuzz Introspector实例

配置Benchmark

准备一个benchmark YAML文件,指定需要测试的函数。关于这个YAML文件,可以使用intropector自动生成。但是请注意,待测项目需要集成到OSS-Fuzz中才能构建。

Benchmark YAML文件是Fuzz目标生成所必需的,它制定了functionsprojecttarget_path以及可选的target_name

  • functions列出了生成模糊测试目标的函数签名
  • projectOSS-Fuzz中包含functions的开源项目名称(例如,TinyXML-2)。
  • target_pathprojectOSS-Fuzz容器中现有fuzz 目标的路径。它将被替换为LLM生成的目标,并用于fuzzing评估。
  • target_name是一个可选字段,用于指定Fuzz目标的二进制名称。

可以使用introspector.pyOSS-Fuzz中生成c/c++项目的YAML文件:

1
2
3
# In virtual env under root directory.
python -m data_prep.introspector <project-name> -m <num_benchmark_per_project> -o <output_dir>
# E.g., python -m data_prep.introspector tinyxml2 -m 5 -o benchmark-sets/new

以这种方式生成的基准文件优先考虑OSS-Fuzz中覆盖范围较远但覆盖率较低的函数,因此更容易实现更高的max_line_coverage_diff

该框架将现有的人工编写的模糊测试目标(先前oss-fuzz项目中人工编写的harness)作为示例添加到Prompt中,以提高结果质量。我们的实验表明,使用来自同一项目的人工编写的模糊测试目标(即使针对不同的函数)可以为LLM提供更多项目特定的上下文,而使用来自不同项目的目标则可以减少过拟合。

每个示例都包含一个问题和一个解决方案。解决方案包含一个由OSS-Fuzz人工编写的模糊测试目标,该目标已被证明是有效的。其格式与我们预期的LLM响应相同。问题包含结果模糊测试目标中一个函数的签名。同样,其格式也与LLM的最终问题相同。

示例通过project_targets.py中的generate_data()自动添加到prompts中。

使用project_src.pyOSS-Fuzzc/c++项目的所有模糊测试目标文件检索到本地目录(默认情况下为example_targets/<projetc_name>):

1
2
3
4
5
6
# In virtual env under root directory.
python -m data_prep.project_src -p <project-name>
# E.g., retrieve all human-written fuzz targets for TinyXML-2:
# python -m data_prep.project_src -p tinyxml2
# E.g., retrieve all fuzz targets for all projects:
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
# In virtual env under root directory.
python -m data_prep.project_targets --project-name <project-name>
# E.g., generate data for TinyXML-2:
# python -m data_prep.project_targets --project-name tinyxml2
# E.g., generate data for all C/C++ projects:
# python -m data_prep.project_targets --project-name all

配置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/activate

cd $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.txt
cd 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          SIZE
gcr.io/oss-fuzz-base/base-runner latest 98a302497240 15 seconds ago 1.38GB
<none> <none> ee9986b8de3e 25 hours ago 1.38GB
gcr.io/oss-fuzz-base/base-builder-go latest 1e3e2bd1199c 25 hours ago 2.31GB
gcr.io/oss-fuzz-base/base-builder-rust latest 7a0091ac473f 26 hours ago 2.44GB
gcr.io/oss-fuzz-base/base-builder-jvm latest 05e48fb4e39a 26 hours ago 2.28GB
gcr.io/oss-fuzz-base/base-builder-python latest fc469b915f16 28 hours ago 2.01GB
gcr.io/oss-fuzz-base/base-builder latest 9b6900c549c3 28 hours ago 1.88GB
gcr.io/oss-fuzz-base/base-clang latest 05158c6b3ea6 34 hours ago 1.15GB

创建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>' # default is '2024-02-01'

具体请参考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')

# Capture time at start
start = time.time()
add_to_json_report(args.work_dir, 'start_time',
time.strftime(TIME_STAMP_FMT, time.gmtime(start)))
# Add num_samples to report.json
add_to_json_report(args.work_dir, 'num_samples', args.num_samples)

# Set introspector endpoint before performing any operations to ensure the
# right API endpoint is used throughout.
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工作区环境,会删除所有未跟踪的文件和目录,但保留venvbuild两个目录。

接下来回到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
# Set global variables that are updated throughout experiment runs.
WORK_DIR = args.work_dir

# Start parallel coverage aggregate analysis
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) # 5 minutes.
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.pyrun()方法。实际执行在_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."""
# Create a pool of worker processes
with pool.ThreadPool(processes=NUM_EVA) as p:
# Initialize thread-local storage in each worker before processing
task_args = [(benchmark, model, args, work_dirs, trial)
for trial in range(1, args.num_samples + 1)]
# trial_results = p.starmap(_fuzzing_pipeline, task_args)
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])
# Writing stage.
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

# Execution stage.
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

# Analysis stage.
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)
# Use keep to avoid deleting files, such as benchmark.yaml
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.txt

user:
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; // Initialize Unicode pointer to 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 for caching.
rewrite_project_to_cached_project(project, generated_oss_fuzz_project,'address')
# Prepare build
prepare_build(project, 'address', generated_oss_fuzz_project)
# Build the image
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-builder

RUN 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_IMAGE
#
# RUN 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/

然后来到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消息),可以注释掉stdoutstderr的重定向。

至此一大圈,我们完成了_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 fuzz target and build script in the container.
...
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.loadMem(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."""
# Replace fuzz target and build script in the container.
...
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."""
# Replace fuzz target and build script in the container.
...
# Recompile.
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_toolcompile()方法,也就是我们前面生成的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的支持。具体的职责包括:

  1. 基础配置:
    • 设置内核参数,如vm.mmap_rnd_bits=28
    • 设置编译标志,例如CFLAGS,CXXFLAGS,RUSTFLAGS
  2. 按语言处理特殊逻辑
    • Rust的introspector处理方式
    • JVM fuzzing的兼容性检查(仅支持libFuzzer或wycheproof)
    • Python fuzzing的sanitizer限制等
  3. 环境变量的传递
    • 将sanitizer类型映射到相应的编译标志
    • 设置LLVM工具链路径(如llvm-ar,llvm-nm
  4. 调用fuzz-introspector(如果启用Introspector sanitizer):
    • 执行fuzz-introspector分析
    • 生成HTML报告和YAML/JSON分析文件
  5. 构建项目
    • 调用$SRC/build.sh或回放脚本relay_build.sh进行构建
  6. 特殊工具和符号表处理:
    • 拷贝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."""
# Replace fuzz target and build script in the container.
...
# Double-check binary.
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

首先compilesFalsebinary_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)
# Use keep to avoid deleting files, such as benchmark.yaml
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:
'Given the following C++ fuzz harness and its build error message, fix the code to make it build for fuzzing.\n\nIf there is undeclared identifier or unknown type name error, fix it by finding and including the related libraries.\n\nMUST RETURN THE FULL CODE, INCLUDING UNCHANGED PARTS.\n'
user:
'Below is the code needs to be built:
<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's important to show the complete code, not only the fixed line.
<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|."""
# 当language为jvm,rust时的处理,暂时略过...
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?'

它会匹配形式如:

1
20 errors generated.

最后有一个关键词错误(由链接器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 xxxuse 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 xxxuse 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."""
# Refine this, e.g., check if the symbol is builtin or from a project file.
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" // Corrected path for 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) {
// Initialize parameters to call drawChar function

// Create FuzzedDataProvider object

// Initialize parameters with fuzzed data

// Call the function-under-test
}

最后一轮的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"

// Include the necessary headers for FuzzedDataProvider
// #include "FuzzedDataProvider.h"

void SplashOutputDev::drawChar(...) {
...
}

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// Initialize parameters to call drawChar function
...

// Create FuzzedDataProvider object
// FuzzedDataProvider stream(data, size);

// Initialize parameters with fuzzed data
...

// Call the function-under-test
...
}

其中,相同部分的代码我给略去了,也就是说,在这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.pyexecute()中返回build_result

然后会来到writing_stage.py中的execute(),记录fuzz_targetbuild_scriptchat_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)

# TODO(dongge): Save logs and more info into workdir.
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
# Writing stage.
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的编写。


oss-fuzz-gen源码阅读
https://loboq1ng.github.io/2025/05/28/oss-fuzz-gen源码阅读/
作者
Lobo Q1ng
发布于
2025年5月28日
许可协议