以afl++
源码为例。分析slave在同步master的queue case的逻辑过程
在parallel_fuzzing.md
文档中提到,只有main节点才会和所有其他节点同步,secondary节点只会和main节点同步。
For performance reasons only -M main node syncs the queue with everyone, the -S secondary nodes will only sync from the main node.
如果是slave或者afl_import_first
定义为1,或者queue_cycle
为1,刚开始会进行一次同步。slave不会在每次fuzz_one
结束后都进行和master的同步,而是每5次(由SYNC_INTERVAL
宏定义控制)的fuzz_one
后进行一次同步。
// afl-fuzz.c
skipped_fuzz = fuzz_one(afl);
if (!skipped_fuzz && !afl->stop_soon && afl->sync_id) {
if (!(sync_interval_cnt++ % SYNC_INTERVAL)) { sync_fuzzers(afl); }
}
然后进入位于afl-fuzz-run.c
的sync_fuzzers
函数中。
首先扫描sync
下的所有除了自身的fuzz
实例文件夹。如果扫描者是slave的话,目标扫描的就是master fuzz
。而是否是master fuzz
是由在对应fuzz文件夹中是否有is_main_node
文件来判断的
// afl-fuzz-run.c
sd = opendir(afl->sync_dir);
if (!sd) { PFATAL("Unable to open '%s'", afl->sync_dir); }
afl->stage_max = afl->stage_cur = 0;
afl->cur_depth = 0;
/* Look at the entries created for every other fuzzer in the sync directory.
*/
while ((sd_ent = readdir(sd))) {
u8 qd_synced_path[PATH_MAX], qd_path[PATH_MAX];
u32 min_accept = 0, next_min_accept;
s32 id_fd;
/* Skip dot files and our own output directory. */
if (sd_ent->d_name[0] == '.' || !strcmp(afl->sync_id, sd_ent->d_name)) {
continue;
}
entries++;
...
然后在确保目标fuzz目录下有queue
文件夹后,扫描该queue
文件夹下的所有用例,获取queue
中的test case
数量
/* Skip anything that doesn't have a queue/ subdirectory. */
sprintf(qd_path, "%s/%s/queue", afl->sync_dir, sd_ent->d_name);
struct dirent **namelist = NULL;
int m = 0, n, o;
n = scandir(qd_path, &namelist, NULL, alphasort);
if (n < 1) {
if (namelist) free(namelist);
continue;
}
然后在slave的.synced
文件夹下面创建一个名为当前被扫描的fuzz示例名字的文件(文件内容为index。用于标注当前已经扫描存储进来的test case的index,即该index从0开始,逐一获取main queue里的id:index
测试用例,并递增更新对应的index
),
源码中n
变量表示queue中的test case总数,m
变量表示queue中未获取过的test case的最小index。此处m
和n
分别表述了我们要从目标queue中获取test case
的最小索引和最大索引界限。
之后o
变量从n-1
开始,不断递减直到m
,然后获取每个索引为o
的test case
o = n - 1;
while (o >= m) {
s32 fd;
struct stat st;
sprintf(path, "%s/%s", qd_path, namelist[o]->d_name);
afl->syncing_case = next_min_accept;
next_min_accept++;
o--;
/* Allow this to fail in case the other fuzzer is resuming or so... */
fd = open(path, O_RDONLY);
if (fd < 0) { continue; }
if (fstat(fd, &st)) { WARNF("fstat() failed"); }
/* Ignore zero-sized or oversized files. */
if (st.st_size && st.st_size <= MAX_FILE) {
... // 对每个o索引的test case的一系列操作
}
close(fd);
}
然后在每一轮对o索引的test case的一系列操作如下
u8 fault;
u8 *mem = mmap(0, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mem == MAP_FAILED) { PFATAL("Unable to mmap '%s'", path); }
/* See what happens. We rely on save_if_interesting() to catch major
errors and save the test case. */
write_to_testcase(afl, mem, st.st_size);
fault = fuzz_run_target(afl, &afl->fsrv, afl->fsrv.exec_tmout);
if (afl->stop_soon) { goto close_sync; }
afl->syncing_party = sd_ent->d_name;
afl->queued_imported +=
save_if_interesting(afl, mem, st.st_size, fault);
afl->syncing_party = 0;
munmap(mem, st.st_size);
主要过程为
- 读入test case
- 将读入的test case经过
write_to_testcase
函数,设置到afl状态中对应的buf中,并写出到testcase文件。此处write_to_testcase
不一定是将原始的test case直接写入buf中,如果有afl_custom_post_process
的话,会调用它对test case进行后续处理再写入buf中,再写出到testcase文件。 - 跑一次
fuzz_run_target
看看是否超时 - 然后经过一次
save_if_interesting
的测试来执行一次目标程序的运行,如果intestest的话,会把test case加入queue并返回1.即如果interest的话,queued_imported
会被设置为1.
最后就是各种清理工作了。
如果整个过程slave并没有找到master的话,它会把自己当作master并设置对应的is_main_node
文件。
此处注释中说,如果多个slave同时没有找到master,将自己设置为main的时候可能会有race condition
,但这个没有问题。因为在下一次运行的时候,这个slave如果在找到master的时候,他自己临时的is_main_node
会被删除。
// If we are a secondary and no main was found to sync then become the main
if (unlikely(synced == 0) && likely(entries) &&
likely(afl->is_secondary_node)) {
// there is a small race condition here that another secondary runs at the
// same time. If so, the first temporary main node running again will demote
// themselves so this is not an issue
u8 path[PATH_MAX];
afl->is_main_node = 1;
sprintf(path, "%s/is_main_node", afl->out_dir);
int fd = open(path, O_CREAT | O_RDWR, 0644);
if (fd >= 0) { close(fd); }
}