学习真实变异:神经漏洞检测器的漏洞创建

互联不一般哥 2024-06-14 13:03:42

引用

C. Richter and H. Wehrheim, "Learning Realistic Mutations: Bug Creation for Neural Bug Detectors," 2022 IEEE Conference on Software Testing, Verification and Validation (ICST), Valencia, Spain, 2022, pp. 162-173, doi: 10.1109/ICST53961.2022.00027.

摘要

突变是对程序代码进行的小规模、通常是令牌级别的更改,通常在突变测试中进行,用于评估测试套件的质量。最近,代码突变已经被用于创建有缺陷代码的基准。这种 bug 基准为评估测试、调试或 bug 修复工具提供了有价值的帮助。此外,它们还可以作为基于学习的(神经网络)bug 检测器的训练数据。以上这些应用的关键是创建与软件开发人员所犯错误密切相似的真实 bug。 在本文中,我们提出了一种基于学习的突变方法。我们提出了一种全新的上下文突变操作符,它融合了关于突变上下文的知识,以将更自然、更真实的 bug 注入代码中。我们的方法采用了一个掩蔽语言模型来生成一个上下文相关的可行标记替换分布。因此,生成真实突变的策略是被学习的。我们对 Java、JavaScript 和 Python 程序的实验评估表明,从语言模型中采样不仅产生更准确地代表真实错误的突变(与测试中使用的突变相比,其再现得分几乎提高了 70%),而且在这样的bug基准上进行训练效果更好。

1 引言

在过去的软件测试中,代码变异已成为一项标准技术,并在变异测试中得到了广泛应用。这种方法涉及对代码进行小范围的修改,通常仅涉及到令牌级别的更改,例如算术、条件或文字的替换,以评估测试套件的质量。近年来,变异算子在基准创建中的应用越来越多,即从无缺陷的现有代码开始,通过应用不同的变异算子来引入错误。这些错误基准有助于评估各种与错误相关的技术,如调试、测试或自动修复。特别是,对于基于学习的错误检测,错误基准是必不可少的。然而,最复杂的错误学习方法只能像其学习的错误基准一样好:如果基准中的错误不能与软件开发人员所犯的编程错误密切相似,那么神经错误检测器将无法在真实错误上表现良好。因此,另一种创建错误基准的方法是从公共存储库中挖掘错误,以获取开发人员所犯的错误。然而,错误数量有限,而且挖掘只能找到进入最终提交的错误。因此,由此产生的基准通常只包含一些数百个代码片段,这对于基于学习的方法的适当泛化来说太少了。因此,我们提出了一种新方法,用于为神经错误检测器生成训练数据,以克服当前的这些缺陷。我们的方法针对单个令牌错误,即程序员错误的一种类型,可以通过替换单个令牌来修复。我们的方法包括一种新颖的与错误类型无关的上下文变异算子,它可以从数据中学习产生逼真的变异体。更具体地说,我们使用了一个在数百万个程序上进行训练的遮罩语言模型,其目标是预测被遮罩位置的替换令牌。然后,通过遮罩一个随机的错误位置并替换该位置的令牌,来注入错误,替换的概率根据适合该位置的概率进行抽样——忽略原始令牌。因此,生成的变异体更有可能适应其变异上下文。与最近提出的语义错误播种方法相反,我们可以在任意程序位置注入错误,而不仅仅是在与先前看到的错误模式匹配的位置。因此,上下文变异独立于错误类型。我们的算子受到自然语言处理的最新进展的启发,该领域表明了预训练生成器以及与之一起训练的检测引擎产生了卓越的语言表示。然而,在这项工作中,我们并不感兴趣于产生语言表示,而是生成逼真的程序员错误。因此,我们将训练方法调整到我们的领域,包括实质性的更改,例如:(a)设计一个固定的生成器以更好地适应变异情景,(b)将程序员错误的分布限制在语法上可行的错误上,以及(c)设计一种有效的遮罩过程,以将变异过程限制到特定的错误类型。我们已经实现了我们的方法,并在Java、JavaScript和Python软件中进行了测试。为了衡量我们注入的错误的逼真程度,我们建议使用逆Brier分数作为重现现有错误的概率度量。我们的实验表明,我们提出的变异体的确比现有的变异算子产生更逼真的变异体,通过实现逆Brier分数达到0.71,而使用变异测试中使用的经典变异算子仅达到0.42。更重要的是,使用因此生成的错误基准来训练神经错误检测器可以显著提高其性能,甚至对于像函数误用这样的新错误类型也是如此。总之,我们做出了以下贡献:• 我们开发了一种新颖的与错误类型无关的学习上下文变异器• 通过利用语言模型,我们提出了一种新的训练变异器的策略,无需真实错误,• 我们实验证明了上下文变异可以生成逼真的错误。• 我们实验证明了上下文变异体和变异学习可以显著提高神经错误检测的性能。尽管我们目前的实验局限于错误生成和神经错误检测,但我们看到成功地在经典变异测试中采用上下文变异的潜力。

2 方法

2.1 概述

本论文的主要贡献在于提出了一种新形式的变异,称为上下文变异,它可以生成逼真的变异体。这些变异体用于创建错误基准,从而训练神经错误检测器。我们通过实验证明,这最终提高了错误检测器在真实世界代码上的性能。我们首先通过直观的解释介绍了上下文变异的概念。图1展示了从 Defects4J 基准中提取的代码片段中的一行代码和三种变体,这些变体是通过不同形式的变异获得的。图1a 展示了原始源代码,其中进行了与 null 的相等测试,以确定是否退出函数(返回)或继续执行。从这个源代码开始,图1b 中的第一个变异简单地检查了 == 令牌(二元运算符)的类型,并将其替换为任意其他二元运算符,例如 / 。由于这种任意性,我们将这种类型的变异称为松散变异。已经证明,松散变异只能达到次优的结果用于错误学习。在松散变异上训练的神经错误检测器在合成基准上表现出令人印象深刻的性能,但在真实世界测试中表现不佳。图1c 中的下一个变异发现更适合使用比较运算符,并选择了 <= 。我们称这样的替换为严格变异。尽管严格变异器可以提供更逼真的变异体,但它们通常受限于仅包含非上下文信息在其决策中。此外,这些操作符对于标识符并不是很好定义,而标识符对于播种标识符相关的错误(如变量误用)至关重要。最后,我们的上下文变异器考虑了源代码的上下文。通过学习到与 null 比较更有可能是 == 或 !=,它将 != 注入作为 == 的替换。这种上下文变异比松散或严格的变异更逼真,对于这个例子实际上是代码片段中存在的原始错误。我们的方法提出了学习上下文变异的概念。学习变异的能力基于公共软件存储库中自然出现的源代码显示出与自然语言类似的统计规律。这些规律可以被利用来构建代码的统计模型,从而预测某些代码位置的最佳拟合。我们的基于学习的方法包括一系列步骤,如图2所示。所有实线部分都属于我们的方法;虚线部分描述了随后操作的错误检测和修复过程如何使用变异体。整个过程从将现有可能正确的源代码输入变异器开始。首先,我们需要识别代码中可以变异的位置。这是句法标签器的任务,它将代码分割成标记并为标记分配句法角色(例如,将“binary operator”分配给 ==,将“variable usage”分配给 dataset)。

图1:从Defects4J/Chart#1获得的bug修复

图2:上下文突变整体流程

2.1 上下文突变

A. 用于识别突变位置的句法标注 首先,我们开发了一个句法标注器来识别潜在的突变位置。该标注器分析程序的句法结构,以抽象语法树(AST)的形式进行,同时使用唯一的句法角色标记每个单独的程序标记。我们相信标记每个标记使得我们的方法可以在各种应用场景中使用,甚至超出了本文考虑的范围。我们的句法标注器是在常见的可用于AST解析的解析器上实现的。将程序转换为其AST后,标注器从分析直接节点邻域开始为每个叶节点分配一个句法角色。然后,注释的AST转换回一个标记序列,将每个叶注释分配给相应的标记。形式上,这一步生成一个标记序列 T = [t1,t2,...,tn],以及一个句法角色注释函数 roleT:{t1,t2,...,tn} → S。所有的句法角色都取自一个固定的句法角色集合S。总的来说,我们区分了14种句法角色(见表I)。现在,将我们的初始示例作为输入,句法标注器产生了以下注释的标记化(颜色见表I):if ( dataset == null ) 有了这个注释,现在确定 dataset 是一个变量使用,并且因此被分类为变量误用错误的潜在突变位置。

B. 用于生成突变候选项的通用掩码突变器 几乎所有单个标记突变都包括将一个标记替换为另一个相同类型的标记。接下来,我们将这一步分为首先掩码化要替换的标记,然后由后续语言模型选择替换的步骤。总的突变过程在我们的案例中由一个通用掩码突变器执行。掩码突变器是通用的,因为它可以应用于程序中的每个位置。然而,在我们的实验中,我们配置掩码突变器只考虑单个突变目标,这样突变器就只选择与我们指定目标兼容的程序位置。在确定了潜在的突变位置后,突变器通过随机选择一个位置并掩码 ([M]) 掉选择的位置处的程序标记来进行。除了改变给定的源代码外,掩码突变器还有一个目标,即生成可行突变候选集。虽然对于例如二进制运算符替换这样的过程很简单,但对于变量使用这样的突变目标,需要更多的工作(因为可能的替换首先需要从程序中的标识符中计算出)。因此,我们将突变候选集定义为所有在相同程序内或从定义的词汇表中语法上可能且与我们的突变目标兼容的标记替换。最终的突变体随后通过选择一个候选项来替换掩码标记而生成。因此,要替换的标记从未包含在候选集中。为了给出我们的通用掩码突变器如何工作的具体示例,我们再次考虑图1中的运行示例,目标是注入二进制运算符错误。作为第一步,突变器扫描源代码以寻找表示二进制运算符的程序位置 ti(roleT(ti) = BOP)。然后,突变器随机选择一个位置并用特殊的掩码标记替换标记:if (dataset [M] null) return result; 由于我们替换了一个等号操作符 ==,我们用 {!=,<=,>=,...} 初始化了突变候选集 MT,即所有二进制运算符,不包括等号运算符。

C. 使用语言模型进行上下文突变体选择 在我们描述如何将语言模型整合到我们的框架中之前,我们再次想要通过一个例子说明为什么语言模型是识别真实突变体的可行选择:for(int i = 0; i [M] length; i++) 尽管我们的目标是找到一个二进制运算符的替换,但这次我们搜索的是在该程序片段的上下文中最可能或正确的替换。即使只给出一个循环头,经验丰富的开发人员仍然可以确定使用一个小于 (<) 或等于 (<=) 运算符更有可能。其他运算符{==,!=,>,>=}不太可能,因为它们的使用要么不常见,要么会从一开始就终止循环,要么会产生无限循环。训练用于此任务的语言模型会得出相同的结论,仅仅是因为它已经学会了在给定上下文中更频繁地使用较小的运算符。尽管我们搜索了一个正确的替换,但也可以利用这种判断进行突变。例如,假设原始操作符实际上是一个小于操作符,那么选择下一个可能的操作符 (<=) 将导致一个经典的偏移错误。因为我们观察到语言模型经常将真实突变体排在最高位置,所以我们建议利用语言模型作为生成上下文突变体的自动方式。

表1:由句法标注器区分的句法角色。颜色被用来可视化标记的角色。

突变作为采样。单令牌突变的过程可以分解为两个步骤的采样过程:第一步是从可行位置集合中随机抽样一个突变位置,可行位置是通过我们的句法标记器确定的,并由我们的通用掩码突变器完成抽样。在第二步中,从替换分布中抽样一个替换令牌,使得一个令牌以概率P被抽样。这个分布可以根据突变应该应用的程序上下文进行条件化。为了将抽样过程限制在可用的突变候选集中,我们要求:

掩码语言模型(MLM)的目标是在给定其余程序作为上下文时预测掩码位置的原始令牌。受到我们最初的观察的启发,我们滥用掩码语言模型产生的概率分布来识别突变位置的上下文相关替换。形式上,给定一系列令牌 ( T = [t_1, t_2, ..., t_n] ),一个掩码语言模型模拟概率:

其中 ( Tmasked) 在所有位置 ( i != m ) 处等于 ( T ),并使用特殊的掩码令牌 ( [M] ) 表示 ( tm )。在我们的情况下,掩码令牌序列是由我们的通用掩码突变器生成的。幸运的是,语言模型的概率分布是针对大范围的程序令牌定义的。因此,可以将语言模型应用于突变二进制运算符,也可以应用于标识符,如变量使用和函数调用,这些标识符不受经典突变运算符支持。

突变重新加权。语言模型的概率分布通常定义在一个包含 ( MT ) 的大词汇表 ( V ) 上。因此,语言模型不直接适用于定义采样分布,因为概率可能对于不在 ( MT ) 中的元素高于零。因此,我们对于所有 ( t in MT ) 使用以下重新加权方法限制分布:

对于 ( V ) 中的所有其他令牌的概率设置为零。假设 ( PLM ) 是一个 softmax 分布,重新加权等效于将词汇表 ( V ) 减少到 ( MT )。我们的重新加权方法应用示例如图3所示。

图3:使用掩码语言模型的上下文突变举例

3 实验

3.1 研究问题

我们的实验旨在探究一下研究问题:

RQ1:上下文突变有多现实?

RQ2:在Java中训练实际突变是否可以提高神经bug检测和修复的效果?

RQ3:实际突变对神经bug检测和修复的影响是否能够转移到其他编程语言?

在RQ1和RQ2中,我们评估了在公共Java存储库中发现的错误。为了探索我们的突变器对其他语言中神经bug检测器的影响,在RQ3中我们评估了来自真实项目的Python和JavaScript错误。

3.2 RQ1:上下文突变有多现实?

为了评估我们的上下文突变器在引入真实错误方面的有效性,我们对实际世界基准测试中的复制任务使用评估的突变器进行了测试。

我们的实验结果可在图4中可视化。所有比较的突变器类型列在X轴上,而蓝线表示获得的逆Brier分数。我们还报告了虚线灰线的定位和修复分数,目前可以忽略不计。如果我们现在比较不同突变器的得分,我们可以得出以下观察结果。

上下文突变器更有可能复制真实错误。对于我们测试的所有bug类型,上下文突变器更有可能复制基准测试中发现的原始bug。改进的幅度取决于测试的bug类型,并与宽松突变器基线相比,范围在变量误用方面为1.5到函数误用方面的191以上。

上下文对注入标识符错误非常重要。虽然对于所有测试的bug类型来说上下文都很重要,但我们观察到在标识符bug上,严格和上下文突变器之间的性能差距较大。基于这一观察结果,我们手动调查了标识符bug的基准测试。表III的第二行中可以找到典型示例。bug和bug修复的标识符名称无关,这使得我们的严格突变器很难处理。相比之下,上下文突变器则排名较高,因为两种类型的String和List都支持contains操作。

虽然上下文突变器更有可能复制真实错误,但存在这样一种情况,即突变器更倾向于生成与真实bug不同的其他变异体。因此,我们手动分析了由我们的上下文突变器在我们的基准测试中最有可能生成的突变体。一些典型的上下文突变器示例如表2所示。总的来说,我们发现上下文突变器在识别适合其上下文的令牌方面是有效的。

基于这些观察结果,我们得出以下结论:上下文突变体平均比非上下文突变体更现实。

图4:复现真实bug的突变效果

表2

3.3 RQ2:在Java中训练实际突变是否可以提高神经bug检测和修复的效果?

我们在所有缺陷类型和变异器类型的真实世界基准测试中的结果如表3所示。我们针对每种方法报告了关键指标的百分比,并以粗体突出显示每种指标的最佳结果。表中还包括了Python和JavaScript缺陷的结果,我们将在RQ3中进一步探讨。总体而言,我们得出以下观察结果。

在上下文突变的训练优于替代突变策略。与严格突变的训练相比,当训练在上下文突变时,性能在所有指标和缺陷类型上提高了最多达6%,除了BOR缺陷的定位。对于BOR缺陷,我们在定位性能与修复性能之间进行了权衡(修复性能增加了2.5%)。权衡也体现在联合准确性上,即在上下文突变上训练的模型能够正确检测和修复更多的BOR缺陷,比如正确检测和修复了超过30个新的BOR缺陷,而这些以前被认为是不可能的。

复制与检测和修复相关。如果我们比较在变异体上训练的错误检测器的性能和产生相应训练样本的变异器的逆Brier分数,我们可以观察到这些测量值是相关的。平均而言,每次从松散突变切换到严格突变,以及从严格突变切换到上下文突变,都会改善性能(在松散和严格突变之间的联合定位准确性上最多提高6%,在使用上下文突变时最多再提高5%)。这一整体趋势也可以在图4中观察到。

总的来说,我们发现训练更逼真的变异体可以改善缺陷定位和修复性能。由于上下文突变代表了我们研究的最逼真的突变类型,因此它也实现了对其他基准变异器的性能改进的最高水平。因此,我们得出结论:

平均而言,训练在逼真的变异体上可以提高神经缺陷检测和修复的效果。使用上下文突变可以获得最高的性能改进。

表3:检测和修复在真实世界基准测试中的结果(最佳结果以粗体标出)

3.4 RQ3:逼真的变异体对神经缺陷检测和修复的影响是否能够转移到其他编程语言?

我们针对检测Python和JavaScript错误的主要结果报告在表 IV 中。我们再次以百分比形式报告关键指标,同时用粗体突出显示最佳结果。对于JavaScript,我们另外在图 5 中比较了不同版本的 DeepBugs 在用于识别错误操作数的不同决策阈值下的精确度和召回率曲线。总体而言,我们观察到以下情况。

在Python和JavaScript上进行上下文变异的训练可以改善性能。对上下文突变进行训练可以改善所有指标,除了 VarMisuse 错误的定位(下降了 0.38%)。对于JavaScript,使用上下文变异显然提高了精确度和召回率。使用默认阈值 0.5,检测到的错误数量从 7.83% 增加到 38.46%。

明确学习模仿错误可以进一步提高错误检测率。在有足够多用于 SemSeed 训练的错误的前提下,我们发现在由 SemSeed 播种的错误上进行训练的 DeepBugs 进一步提高了召回率(在阈值为 0.5 时高达 15%)。相比之下,上下文变异能够在不访问真实错误的情况下提高错误检测,同时仍然提高错误检测的总体精确度。

总结我们的观察,我们发现上下文变异可以提高各种语言的性能,而无需额外的真实错误作为训练示例。然而,正如 SemSeed 所示,将真实错误集成到我们的上下文变异框架中可能会进一步改善变异体的生成。我们将这一点留给未来的研究。

总之,基于学习的系统在更真实的变异体上训练的观察到的性能增益可以转移到其他编程语言。

图5:DeepBugs在随机错误(灰色虚线)、由SemSeed引入的错误(红色虚线)或上下文变异体(蓝色)上训练的精确度和召回率。

0 阅读:0

互联不一般哥

简介:感谢大家的关注