如何在Linux内核中可靠地触发竞争

2020-05-05 20:56:21

可以肯定的是,大多数读者都知道什么是竞争条件。然而,为了完整性,让我包含简短的描述。就计算机编程而言,争用条件是两段代码如果同时执行会导致错误的错误。

作为内核,QA我最感兴趣的是编写测试用例,这些测试用例可以在内核代码中重现曾经固定的竞争,以避免回归,并确保所有代码流(如稳定内核)都没有bug。

这些测试的主要问题是它们出了名的不可靠。特别是当竞争窗口非常小,只有几条指令长度时,几乎不可能编写能够合理可靠地触发问题的测试。

竞争复制器测试通常是这样实现的,这样两个线程运行两个不同的循环,每个循环都有不同的代码段,希望竞争能够触发。对于内核,这通常意味着两个线程,每个线程在一个循环中调用不同的syscall。这种方法的问题是,我们依赖系统抖动才能达到比赛窗口。正如你可能知道的那样,计算机大多是确定性的,因此我们在这种情况下很大程度上依赖于运气。

当我们将概念的CVE证明转换为LTP测试用例时,我们开始思考是否可以做得更好。经过三次重新设计和重写,我们最终得到了一个很好的库,它可以极大地提高成功引发竞赛的可能性。作为一个副作用,我们需要的迭代次数要少得多,并且我们的测试用例在无竞争系统上完成的速度要快得多。

总体思路相当简单。首先,我们采样导致比赛运行的两段代码需要多长时间。一旦解决了这个问题,我们需要做的就是相应地同步代码段。因为我们不知道两个部分的哪些部分需要对齐才能触发竞争,所以我们同步代码部分,以便在每次迭代中对齐是随机的。但我们也要确保两个赛段重叠,否则就没有机会触发比赛。

不过,实现有点复杂。在抽样阶段,我们使用移动平均,我们等待偏差稳定下来,以便我们对赛段持续时间有合理的近似值。区段的同步依赖于原子增量和自旋锁,用于排列随机化的延迟由校准的繁忙环路引入。

不,不完全是。现实世界的问题要比这复杂一点。这是真的,模糊同步库适用于许多种族复制者很好,但有些情况下根本不起作用。我们发现的问题之一是,系统调用的持续时间可能会根据竞速代码的第二部分的对齐而有很大的不同。

例如,考虑使用Close()与recvmsg()竞争。如果我们将这两个syscall对齐,使文件描述符在recvmsg()开始时关闭,则syscall将快速返回EBADF,这是零成功的机会。CVE-2016-7117就是这种情况,因此必须引入一个函数来偏置代码段的偏移量,以便在这种情况下成功完成采样阶段。

下面是一段试图重现d90a10e2444b“fstification:fix fstify_mark_connector race”的代码。

静态结构tst_fzsync_air fzsync_air;static int fd;static void*write_Seek(void*unuse){char buf[64];while(tst_fzsync_run_b(&;fzsync_air)){tst_fzsync_start_race_b(&;fzsync_air);safe_write(0,fd,buf,sizeof(Buf));Safe_Write(0,fd,buf,sizeof(Buf));Safe_。}静态无效设置(VOID){FD=SAFE_OPEN(FNAME,O_CREAT|O_RDWR0600);tst_fzsync_pair_init(&;fzsync_pair);}static无效清理(VALID){IF(FD&>;0)SAFE_CLOSE(FD);tst_fzsync_pair_cleanup(&;fzsync_pair);}static VALID VERIFY_INNOTIFY(VALID){INT INOTIFY_FD;INT wd;INOTIFY_FD=SAFE_MYINOTIFY_INIT1(0);TST_FZSYNC_Pair_RESET(&;fzsync_Pair,Write_Seek);While(TST_fzsync_run_a(&;fzsync_Pair)){wd=SAFE_MYINOTIFY_ADD_WATCH(inotify_fd,FNAME,IN_Modify);tst_fzsync_start_race_a(&;fzsync_air);wd=myinotify_RM_watch(inotify_FD,wd);TST。inotify_rm_watch()失败。";);}SAFE_CLOSE(Inotify_Fd);/*我们在给定时间内存活-测试成功*/tst_res(tpass,";内核在inotify测试中存活";);}静态结构tst_test={.nesids_tmpdir=1,.setup=setup,.leanup=Cleanup,.test_all=Verify_inotify,};

如您所见,初始化和退出是在setup()和leanup()函数中处理的。

函数的作用是:清除计数器,并启动与主线程竞争的线程。