被测程序在启用libFuzzer
并编译链接后,即成为了一个可接受用户参数的命令行程序,直接执行程序便是启动测试。
1. 命令统一格式
一般格式:
./fuzzer -flag1=val1 -flag2=val2 ... dir1 dir2 ... |
flags代表各个控制测试过程的选项参数,可以提供零到任意个,但必须是严格的-flag=value
形式,并不是很符合unix命令行习惯:
- 选项前导用单横线,即使选项是一个词而非单个字符
- 选项必须要提供对应的值,即使只是一个开关选项如
-help
,必须要写作-help=1
,且选项与值中间只能用等号,不能用空格。
dirs表示语料库目录,它们的内容都会被读取作为初始语料库,但测试过程中生成的新输入只会被保存到第一个目录下。
常用有以下参数:
对于开关选项(如help
),效用一列表示当参数启用时(-help=1
)的效果
选项 | 默认 | 效用 |
---|---|---|
verbosity | 1 | 运行时输出详细日志 |
seed | 0 | 随机种子。如果为0,则自动生成 |
runs | -1 | 单个测试运行的次数(-1表示无限) |
max_len | 0 | 测试输入的最大长度。若为0,libFuzzer会自行猜测 |
cross_over | 1 | 交叉输入 |
mutate_depth | 5 | 每个输入连续突变的数量 |
shuffle | 1 | 启动时打乱初始语料库 |
prefer_small | 1 | 打乱语料库时将小输入置于优先位置 |
timeout | 1200 | 若为正,表示单元运行最大秒数。超时会被提前中止 |
error_exitcode | 77 | libFuzzer本身出错时的退出码 |
timeout_exitcode | 77 | libFuzzer超时退出码 |
max_total_time | 0 | 若为正,表示整个模糊测试运行最大秒数 |
help | 0 | 打印帮助并退出 |
merge | 0 | 不损失覆盖率前提下,将第2/3/4/…个语料库合并到第一个中去 |
merge_control_file | 0 | 指定合并进程的控制文件,用于恢复合并状态 |
jobs | 0 | 运行的作业数量。所有作业的输出会被重定向到fuzz-JOB.log。 |
workers | 0 | 运行作业的并发进程数。若为0,实验CPU核心数一半 |
reload | 1 | 每N秒载主语料库,以知悉其他进程发现的单元 |
only_ascii | 0 | 仅生成ASCII(isprint + isspace)输入 |
print_final_stats | 0 | 退出时打印统计信息 |
print_corpus_stats | 0 | 退出时打印语料库元素统计信息 |
print_coverage | 0 | 退出时打印覆盖率信息 |
close_fd_mask | 0 | 为1则在关闭stdout,为2则关闭stderr,为3则关闭二者 |
重运行模式:
./fuzzer -flag1=val1 -flag2=val2 ... file1 file2 ... |
与上面一样,但是选项后面接的是文件列表而非文件夹列表,这些输入样例将会重新读取并输入运行,不会产生新样例,在回归测试时十分有用。
2. 进程、作业、并发
测试代码:
// process.cpp |
编译,
clang -fsanitize=fuzzer process.cpp -o fuzzer |
当使用默认选项启动模糊测试程序时,
./fuzzer |
只会有一个作业,当然也只有一个测试进程。
此时,libFuzzer的日志信息会输出到终端的stderr,程序本身的stdout会被输出到终端的stdout。
而其中所有测试样例其实都是运行在同一个进程内,也就是说函数LLVMFuzzerTestOneInput被循环调用了,因此全局变量将会在每个样例运行时共享。如上述示例程序的stdout为:
global count: 1
global count: 2
global count: 3
global count: 4
global count: 5
这也就是libFuzzer为什么建议不要去修改全局变量,因为如果此做,同一个测试样例在两次被修改得不同的全局变量环境下运行,可能会产生不一致的结果,这违反了输入-行为确定性的规则。
然后如果使用jobs
选项指定作业数量,
./fuzzer -jobs=2 |
此时日志和程序输出不会输出到终端,而是重定向到fuzz-N.log文件,查看其内容,发现二中中打印的global_count
都是1、2、3、4、5,这说明二者运行在不同的进程之中,全局变量也是独立的。
但是多任务也会随之引发协调问题,每个任务并不知道其他任务发现了哪些样例,这时候需要指定reload
等选项应对这些问题。
然后是进程的并发数量问题,这个数量由workers
参数指定。
当指定了多个任务时,程序启动时会先产生一个master
进程,同时并发启动相应数量个worker
进程,master会把jobs分配到workers上去执行,当某个任务结束后,相应进程终止,同时master会启动一个新的任务进程分配到对应的worker上,平均每个worker上会分配jobs/workers个任务。
./fuzzer -jobs=16 |
对于8核电脑,其进程树如下:
fuzzer -jobs=16
├─sh -c ./fuzzer >fuzz-0.log 2>&1
│ └─fuzzer
│ └─{fuzzer}
├─sh -c ./fuzzer >fuzz-1.log 2>&1
│ └─fuzzer
│ └─{fuzzer}
├─sh -c ./fuzzer >fuzz-2.log 2>&1
│ └─fuzzer
│ └─{fuzzer}
├─sh -c ./fuzzer >fuzz-3.log 2>&1
│ └─fuzzer
│ └─{fuzzer}
└─5*[{fuzzer}]
如果电脑不准备干其他活,可以把workers提升到CPU核心数量,满载并行。但是再往上加worker数量大多就没什么用了,反正也达不到并行效果,总有些进程会被挂起。但是也是有例外的,比如如果程序里有一步需要网络IO,这时候CPU不能真正并行也不是什么问题。
额外说一下线程吧,libFuzzer不管理线程,但你可以在自己的逻辑代码中自己开线程,自己记得关上就行。