如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

作者:davis

相信每一个研发人员,在日常的代码开发工作中,都会遇到同一个问题: "我合的这个代码,会不会有问题?"。 这是一个最为常见的场景。从最尖端的星球探测器,到每一个运营的小型 Web 站点,其上运行的软件,都会遇到相同的问题,"以前写的代码,现在写的代码,有没有问题?"。

背景

"一个功能要上线了,要合代码了",总会让我们研发人员感觉很忐忑。

进行代码静态分析、Check List 多个检查、各类检查规则、多个检测流水线、拉大佬做 Code Review……各类工具和办法都一起使用,目的都是相同的,都是为了在代码上线之前,尽早发现问题,找出 bug,发现代码漏洞,防止影响线上功能。

我们在这个方向的工作,也全部都围绕这个场景展开。

但同时,这又是一个非常难以处理的场景,它包含的范围面广,并且检测技术实现上也有很大的困难。美国某款著名付费的代码静态分析软件,深耕于此二十多年,可见一斑。

市场上也有多款检查工具,几乎每一款都对外称,"我们对 bug、对漏洞的检测有效率达到了 9X.XX%"。它们有的免费,有的付费,但没有任何一款可以宣称,"使用我们的软件检测代码漏洞和风险,有效率为 100%,您可以高枕无忧了,您可以放心合代码了"。

那么,无法"高枕无忧"的原因是什么?还有哪些痛点需要解决?

痛点解析

我们通过实际的调研,以及用户和业务方的反馈,发现了如下的原因和痛点:

特异性问题

甲之蜜糖,乙之砒霜。每个团队/业务都有一些自己的痛点,都有自己的关注问题。比如同一种开发语言 java,客户端团队和服务端团队,同样都是 java 语言,但关注的痛点是一定不同的。即使有非常庞大的 java 规则集,覆盖面全,但实际操作起来,检测规则只要有特异性,或者稍微一复杂,稍微一组装,那么通用规则就很难适配了。

需求与规则匹配问题

我们通过一段时间的观察和复盘发现,凡是业务方自己提出的问题、规则,往往都是最重要的痛点。是价值最高的,最被需要的,但是往往这种规则,在公司内外的工具里,不易找到,或者不存在。业务方往往消耗很多精力,来处理这件事情,下面的例子来做具体的解答。最痛的反而不好解决,这并不是个例。

实现成本问题

用一个我们遇到的真实的例子来表达。

假设某一个公共函数 f1 过期了,如果项目中再使用 f1 函数,将使您的项目面临风险,您负责要将项目中的 f1 全部找到并且删除。如果 f1 就是一个 static 的函数,那么您可以通过一行 linux grep,就全部找到了。【grep 方式】

此时,如果 f1 的名称恰好是 get(),而且 get 函数不是静态函数,而是对象实例函数,那么,f1 需要先初始化一个对象"N n = new N",再通过 n.get() 进行调用,此时,您 ctrl shift f 启动 IDE 的全局搜索,搜出来一片 get,因 get 是一个太常见的函数命名,redis,大量的 bean 等,都有 get 方法,甚至注释里面也有很多"get",所以此时无法确定是 N 的 get 方法。您变换了一个思路,此时搜对象类型 N,然后通过 N 慢慢找到 n.get()。【全局搜索方式】虽然麻烦点,不过也可以接受。

此时,如果有好几个 class 类型都叫做 N,只是它们的路径不一样,但是全局搜都是 N。好吧,已经有些麻烦了。您通过 IDE 中的搜索技巧,比如正则之类的,较为精确地搜索到了这个类型 N,或许也能搞定该问题。【搜索 正则方式】您花费了很多时间,找到了所有的 N n , n.get()的代码片段,进行了代码删除,处理了该风险。

此时,leader 说,新提交的代码,不能再有 N 和它的 get 了。【增量管控】好吧,即使您可以保证自己的 IDE 技巧可以支撑 leader 的要求,也无法保证其他同学也这样精通 IDE。这只是一个 N 和它的 get,如果以后再有 O , P , Q,其他的呢?

此时,您发现,或许可以使用 AST,公司内外工具找了很多,然后琳琅满目的 AST 工具,还有不同的 language,每一款都要先 clone 下来,试试效果,发现没有现成的,只有一个接近的。【调研】

然后,您需要大体看一下项目源代码,让这款工具可以适配 N 和它的 get(),找到需要改动的地方,改后再编译运行一下,发现能运行。【二次开发】然后要监听代码增量,需要做一个小型系统,处理 gitmerge 消息,在 merge 消息触达之后,进行分析,观察增量代码是不是有 N 和它的 get。【系统开发】

刚开始运行的还可以,结果后来有些大的代码文件,解析 AST 时,解析程序崩溃了,又要找 bug,这下麻烦了,解析 AST 那个函数的源代码,有一万多行,还要看怎么处理。【运维】

一套操作下来,差不多一个多月进去了。这个例子,是一个真实的例子,您在这里,可以看到做代码检测的成本,以及您要自己完整开发一个规则,来为自己团队的质量保驾护航,那么需要付出的成本,我们已经基本为您罗列了。当公司内外的工具、平台,没有 现成的功能、规则可以使用,那么自己上手的成本可能会很高昂,尤其是遇到不容易搞的、比较复杂的问题,现成的工具更不好找,甚至要做适配性的二次开发,那成本就更高了。

精准性问题

"好不好,看疗效"。如果使用了大量的规则集,或者使用了大量的扫描工具,尽管扫描的面积变大,但是相对应的,扫描出来的问题也变多了,真正的业务关注的问题,或者潜在的巨大隐患,都被掩藏了。比如扫出来几百个问题,但是真正会导致故障的可能就 1 个。

动态性问题

规则不动,研发行为易动。以上描述的,大多是静态扫描相关。静态扫描工具容易陷入一个误区,就是脱离实际的 代码仓库、人员、团队、经验这种水土,变得千篇一律。

换句话说,一段代码无论在任何地方,只要代码语法 match 了规则,都会报出问题。比如一个简单的例子,某检测软件对于一个变量 x 的赋值,x=True,会提示错误,因为该检测软件要求为常量都大写,也就是 x 必须写成 X,但是在下面的代码将 x 重新赋值为 x=False,x 即具有变量性质。但仍然报错。

不少研发同学都有这样的开发习惯,这不能说错,或者说这不能算作一个必须解决的问题。每次出现,都需要点击"忽略",将这些问题忽略掉。可见,规则是不动的,研发的行为是动态变化的。另外,团队成员的动态状况也会产生潜在的风险,而它是不能被 静态分析 所检测到的。

比如一个简单的例子,笔者是做大数据开发和后台开发,接触到的后端 language 比较多,但是如果项目需要,笔者要改一些网页,写 js 代码,那么必然对这个代码合入,信心不足,需要前端同学帮助 Code Review,来避免风险。

比如有位同学要合入的代码,解决的 conflict 比较多,那么这可能会产生风险,需要更多的 CR 投入,来确保排除风险。因此,这些风险和隐患都具有动态性,并不能通过单纯代码级别的静态检测来解决。

时效性问题

风险扫描的时效性,也是一个必须引起重视的方面。

如果一次扫描,启用了多款扫描工具,启用了上百个规则或者更多规则,我们实践过,这个耗时对一般的代码量微服务都是十分钟起步了,如果是更大的代码仓库,超时情况会加剧。业务合入代码时,是为了后续发布上线的,但是这边的检查一直未完成,不得不删减规则集,甚至跳过检查,那么检查的效果就缺失了。

还有一个重要的方面,就是有些扫描工具,是需要"COMPILE"的,依赖编译后的数据,比如依赖各语言的 bytecode,这种往往需要更多的分析时间,有的需要小时级,有的不支持增量,这就会使得 原本优秀的检测工具,业务方不去选择。

工具自有缺陷问题

代码有多种形态,在编辑文件里的,在 .py,.java,.kt 文件中的,是研发同学的源代码。

有些语言编译后形成的,是字节码。实际在操作系统中运行的,是机器码。但是它们从源头上,都是源代码的演化。一般编译器前端处理的,往往是 AST(抽象语法树),是一种有效而直观的代码语法组成的表达。

AST 一般由 代码原文件直接生成,市面上对应各种语言,都有一些 AST 解析工具,但是解析能力、速度、精确度各有不同。这里要注意的是,并不是所有的工具,都可以将一个代码原文件,翻译成为正确的 AST。

也就是 AST 有误解析的概率,并且概率不低,我们解析过百万个级别的代码文件,出错的、误解析的、解析不出来的,有很多。原因有以下几点:

1. AST 的维度是文件级别,也就是说,一般一个文件对应一个 AST,因此一般不具备 LINK 多文件的能力,这就使得一些 import / include 存在语义分歧,易引起误解析。

2. AST 的解析工具,有些偏重于解析精准度,这也意味着,输入必须是一个完整的代码文件,如果是缺失内容的代码文件,则会直接报错;有些则偏重于解析速度和容错率,即使输入是一小段不完整的代码,它们也会基于此,生成一部分 AST。比如解析至 static 关键字,代码片段就戛然而止,那么后面是一个 variable 类型,还是一个 class 类型?解析器无法做出判断。

3. 有些解析器通过 prediction 的方式来构造 ATN,反复尝试路径,直到找到唯一路径。这样的方式下,如果遇到大的代码文件,或者代码文件中有大的 列表、数组、字典变量的文本,解析器很容易因为暂存的路径太多,而导致程序内存耗尽,解析崩溃。

以上原因,会直接影响解析效果,因为 AST 都是有问题,那么 func = Rule(AST),对 AST 进行操作,必然无法得到正确的结论。

代码深度解析问题

由上文可知,AST 的解析存在置信度问题,由于代码文件、解析工具、解析策略、运行时机器资源 等多种因素的作用下,AST 对于源代码的 struct 还原,可能存在误差。所以对于代码的静态分析,对于以"检索、匹配"为主的,一般基于 AST 来进行。但是对于一些深度解析,比如指针、资源泄漏、线程问题,这种一般依赖 COMPILE IR 的能力,也就是源代码在编译期的表达。当然,也不是完全绝对的,比如 AST 在一些场景下,也可以用来做资源泄漏中的问题。虽然 COMPILE 的分析深度和准确度要好于 AST,但是代价比较大,分析时间和资源都要远高于 AST。首先编译的时间,无法省略,这是 IR 的原材料,然后 IR 的数据已经将调用栈 Link 到一起,类、函数、依赖库之间的调用链能够完整获取,因此在分析时,需要分析的内容更多,链路更长,范围也更大。

同时,在做分析时,一般使用符号化执行的方法,对路径进行约束问题求解,从而判定问题是否存在,当调用链变长、依赖关系更复杂、函数深度更长时,这样符号执行图也会更大,所需要暂存的路径也更多,约束条件也更复杂,所需要推断判定的 Variable 和 CallFunc 也更多。符号化执行所需要的资源,甚至比程序真正进入 Runtime 运行程序所需要的资源更多,原因就来自于此。由于带来更多的时间和资源的消耗,很多分析工具在资源少的服务器上,甚至无法运行,能够运行的,需要等待很长时间。这使得 COMPILE IR 的分析方式容易被弃用。但是,这也就失去了很多风险能够提前发现的可能性。

我们如何做

首先要说明的是,上述问题,市场上并没有一款产品能够完美解决,还是那句话,某著名软件深耕了 20 多年,如果好解决,也不用这么久了。

即使是市场上的一些付费软件,扫出来的效果真能有它们宣传的多么好,也不见得。

而且对扫描出来的问题,即使都改了,软件的质量能提高多少,这也是一个比较难以回答的问题。

这并不是市场上和社区的软件做的不好,相反,如果您 clone 下来代码,研究几天会发现,技术复杂度属于很高的一类。而且这些软件都在付出很多的努力,为了提高检测性能和准确度。比如,仅面对语言的 Version 升级,出了新的语法特性,它们去做适配,这就需要大量的工作。

对于上述痛点,是普遍存在的现象,这并不是一个或几个工具,部署上去,就能解决的了。

对于上述痛点,我们的工作也围绕它们,做了一些研究,有部分成果,附在下面的各个"典型案例"中,均为我们的工具能检查到,而其他工具忽略的。更多的是对其他工具的一种有效补充。

对于上述痛点,提供如下实践手段,供参考。

特异性问题

使用自研的、二次开发的规则。通用规则具有局限性,且互相联动、合并、拆解的能力有限,自研的规则则具备较强的针对性。

需求与规则匹配问题

与业务方进行深度沟通,挖掘痛点。暂时不追求规则的广泛性,更多强调定制化,强调解决方法,强调点的命中率,抛弃一些大而全。维护一个定制化的规则能力,若其他团队也遇到相似的问题,则小成本部署到其他团队。

实现成本问题

经过大量的踩坑、试验,在多个语言上,我们基本摸索出了比较合适的 AST 工具、AST 规则低成本映射能力、COMPILE 编译分析能力,在其上可以进行一定的源代码二次开发。因此,对于一般的需求,可以一个人力,一天开发,一天测试上线,基本就能完成。另外,我们通用化了 MR 增量判定、命中判定、风险通知等能力,这些不需要再进行开发,直接投入到规则本身即可。

精准性问题

采取的策略是,重点维护业务方认为是痛点、定制的规则,确保这种规则的命中率,只要出现,就尽可能捕捉到,若遗漏或者误报,则立刻投入进行修改。对于通用的规则,如果业务方认为不必要,则做下线处理。

动态性问题

借助数仓数据,能够获取到仓库、团队、同学的历史数据,因此可以构建出来一些动态数据,比如开发习惯、常用语言、经验度、熟悉度、擅长领域、源代码历史改动情况,这对推荐评审人、推荐关注人、提示老代码变更很有效。

时效性问题

我们已对风险分析的时效性做了较大提升。当代码 MR 合入的消息触达时,我们的检测流水线会触发,进行增量判定。对于 AST 类的检查,全流程大约只需要 40 秒。对于 COMPILE 类的检查,已经能够做增量检测,全流程大约只需要 编译时间(依赖编译,不可省略) 2 分钟的时间。两大类检查,时效性目前都满足了要求。

工具自有缺陷问题

使用的工具,都经过了源代码二次开发,有的动了底层的引擎源代码。这是应对工具自有缺陷的唯一有效方法。底层包有问题时,上层修复是徒劳,因此必须进入源码中进行解决。

代码深度解析问题

深度解析并不是效果不好,相反,效果可能会很好。只是受限于使用较为复杂,使用耗时较大,使用时内存 cpu 容易出问题、还要指定编译环境和编译器,但这些都不是不可解,我们目前在 COMPILE 分析中,不仅有了增量代码的分析能力,也跟业务方互相配合,解决了如上问题。

接下来将介绍,具体使用的流程架构,和分析的规则架构。

主要分类 AST 和 COMPILE 两大类,每一类都附录检测到的典型案例。

AST 静态分析

功能简介

以 AST 解析和 AST 规则为基础。整体功能为: 业务方的研发同学在 git 上新建或更新 MR -> 消息自动触发到我们平台和流水线上 -> 对增量代码进行 AST 构建、分析、规则加载执行 -> 若检查出问题,发送给相关研发同学或企业微信群。

技术实现

流程架构

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

  • 若为增量判定,则监听 git 仓库的 hook,使得 MR 在 init 或者 update 时,可以发出来消息。
  • 构建检测流水线,获取此次 MR 的元数据信息,在流水线上启动容器,进行代码拉取等。
  • 找到变更代码文件,变更行和片段。
  • 启动风险分析插件,执行规则判定。若有发现风险,则再次判定是否为增量风险,若为增量风险,则发出通知。
  • 若为全量判定,则省略增量判定步骤,直接录入历史数据。

规则判定

  • 对于不同语言的代码,抽象出不同的 AST 数据结构,驱动引擎使用同一个,但语法模板使用不同的解析模板。
  • 将不同语言的 AST,转化成为语言无关的 Intermediate Language(IL)结构。
  • 将 IL 结构形成 CFG,下面的规则判定等都依据 CFG,由于不区分语言,减少了后续规则加载的成本。
  • 若需要跟进变量的值,比如判断变量的条件语句等,则使用变量追踪,将变量的 Data Flow 暂存下来。
  • 如果变量在 Data Flow 中存在值的覆盖,则进行变量值覆盖。如果有多次的值覆盖,则记录多个 Data Flow。
  • 加载规则,将规则应用于变量的 Data Flow 中,如果有任意 Data Flow 不能到达尾节点,则命中该规则。反之,则规则未命中。
  • 反向查询 CFG Data Flow 的变量所在的代码行号,对于命中的节点,记录变量的信息并且报出风险问题。

性能调优

现在对于大部分仓库的 MR,性能平均在 40 秒,绝大多数在 1 分钟以内。优化之前一般在 5~10 分钟左右。所以如果加载我们的扫描能力,大约一次 MR 的风险评估结论,40 秒左右就可以制作出来。当然,如果变更的代码量较多,或者规则较复杂,使 CFG 的 path 过多,则可能减缓速度。我们在此前进行了多个性能调优的措施。

  • 统一了 AST 执行引擎,这使得我们并不需要使用多个工具。若使用多个工具,不仅 fork process 存在浪费,最重要的是多个工具做了大量重复性的工作,而且工具的性能难以保证,只要有慢的,就会拖慢整体的进度。另一方面,维护多个工具/引擎会增加很多成本,一个工具有问题,需要修改源代码,另外一个出问题,也需要修改源代码,但是无论修改的内容是什么,都无法确保修改后的工具,性能好于或者等于之前的情况。
  • 使用容器的文件缓存,优化 拉取代码,使之从分钟级优化至几秒钟。
  • 由于 AST 是代码级别粒度,因此直接对增量的代码文件进行 AST 化,并且对结果再进行变更行的判定,最大程度减少 AST 的开销。
  • 在资源可以接受的范围内,使用了并行分析。

典型案例

以下案例均为真实代码的检查案例。

数据库句柄未正确使用和关闭

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

数据库 Cursor 未关闭,涉及到资源,需要确保正确的使用和关闭。一般以下为正确的写法。在 try 块内使用,并且在 finally 中进行关闭,考虑到 close()也是一个抛异常的方法,因此也用 try catch 进行包裹。

Cursor cursor = null;try { cursor = db.query(...); // do something with cursor} catch (exception e) { // handle exception} finally { if (cursor != null) { try { cursor.close(); } catch (Exception e) { // handle exception } }}

该案例中,整个使用和关闭的操作在一个 for 循环内,在 c = cr.query(XXX) 时,或者 c.close()时,都有可能会抛出异常,并且中断循环。

那么这个程序片段执行的效果,可能会与既定的逻辑不符合。

危险的 sql 语句执行

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

该案例中,sql 语句的执行,会直接删除全表。

一般 sql 语句,包含 delete 等操作是没问题的,只要不出现 sql 注入等,并且在程序逻辑正确的前提下,不会对业务造成危险的影响。

或者,delete 表或者 drop 表的语句,在一些 sql 脚本中,而不在 java 程序中,只要项目同学可以确定这些语句仅是用于记录、备份等,也不会有问题。

但是,如果是在 java 代码语句中,并且使用了 database 的句柄去执行这类语句,那么就可能会有较大风险。

类似这种风险有 delete table , drop table 等。

控制条件疑似永真/永假

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

在该代码片段中,int rId 的赋值为 0 , 因此,if (rId != 0 ) 不可能成立。

因此下面所有的代码都不会执行。

这里看到,研发同学在该赋值上面,注释了一行代码,如果这行代码执行后,rId 确实可能不为 0,但是注释了这行代码,因此不排除误写或者 debug 之后忘记改回的可能性。这样的情况也是时有发生。

成对函数未同时修改

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

在特定场景中,有些函数往往成对出现。比如有一个 onX 函数,则对应就有一个 onY 函数。业务方对它们的要求是,如果修改了 onX,则需要确认 onY 是否也需要进行修改。

曾经有故障的发生,是因为修改了 onX,却没有修改 onY。这属于特定的需求,我们使用 AST 引擎来解决该问题。

COMPILE 编译期分析

功能介绍

以编译字节码解析和编译期规则为基础。如上文中描述,使用字节码 IR 做深度代码分析,比 AST 更能够识别出不容易发现的问题,下面将列举的案例中,其中有些,我们研发同学也不是一眼就能够看出问题,问题具有很强的隐蔽性。由于它的耗时、性能消耗较高,所以它的易用性并不如 AST,但这不代表它没有意义。

整体功能为: 业务方的研发同学在 git 上新建或更新 MR -> 消息自动触发到我们平台和流水线上 -> 加载编译的环境、脚本,并且记录增量的代码信息 -> 执行编译-> 抓取编译过程 -> 找到增量代码以及其所依赖链上所对应的子图 -> 进行 COMPILE 分析、规则加载执行 -> 若检查出问题,发送给相关研发同学或企业微信群。

技术实现

流程架构

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

  • 若为增量判定,则监听 git 仓库的 hook,使得 MR 在 init 或者 update 时,可以发出来消息。
  • 构建检测流水线,获取此次 MR 的元数据信息,在流水线上启动容器,进行代码拉取等。
  • 使用前置插件,找到变更代码文件,变更行和片段。并且加载编译所需要的环境、脚本以及编译前的准备事项。
  • 执行全量编译,执行构建脚本。
  • 抓取编译过程,提取字节码,构建增量代码所依赖的子图。
  • 执行分析过程,若有发现风险,则再次判定是否为增量风险,若为增量风险,则发出通知。
  • 若为全量判定,则省略增量判定步骤,直接录入历史数据。

规则判定

  • 由于 COMPILE 整体的分析过程,都依赖于编译的字节码,因此,前提条件是编译过程需要完成。
  • 对于编译过程中形成的 class,进行字节码的抓取。
  • extract 字节码的属性,得到所有涉及到的 class 的方法函数信息等。
  • 对 class 进行切分,一个 Method 作为一个 Node 节点。
  • 在 Method 中,拆分成为 Method Param、Body 和 Return 部分,其中 Method Body 由若干 Block 构成。
  • 对 Block 之间传递的变量,进行 Path 的寻址,构造出 Variable -> Block 的图关系。
  • 加载规则,执行符号化计算 Se(Varaible),模拟输入范围,根据不同的路径条件预设不同的值。
  • 根据符号计算的取值范围,对变量进行局部的取值求解。对于待解子图来说,某一个上层节点的取值将会传播至下游任一节点,该值会随着变量值覆盖的方法而发生变化。
  • 对变量的 Path 进行整体路径的求解。
  • 反向查询 CFG Data Flow 的变量所在的代码行号,对于命中的节点,记录变量的信息并且报出风险问题。

性能优化

现在该功能已经进行了多个优化措施,由于依赖编译过程,因此编译的时间无法省略,但是编译 分析,后面的分析过程优化至 2 分钟以内,所以全过程的时间为 编译时间 2 分钟。

  • 使用容器的文件缓存,优化 拉取代码,使之从分钟级优化至几秒钟。
  • 完成了增量分析,使之从全部字节码的分析,转换成增量文件所对应依赖链路的分析,使得以前 40~50 分钟的分析时长,缩短为几秒钟~几分钟。
  • 完成了并行分析,从串行改为并行,分析时间缩短为 50%。
  • 调配了容器配置、程序运行配置,适当增加子进程数量,并且通过观察容器的 cpu 负载、内存负载,反复调整程序运行时资源,最终达到一个相对合理的实践方案。
  • 修改了源代码中的分析器,缩减无效检查。

典型案例

以下案例均为真实代码的检查案例。

资源泄漏

这是一个非常典型的字节码案例,并且有区别于"代码语义,代码匹配,AST"等基于代码的方式,它发现了更加隐秘的资源泄漏问题。

比较了解 AST 的同学可以看出,这个案例是无法通过一般的 AST 等方式捕捉到的。它不同于上文中的数据库 cursor,它的逻辑性需要跟踪 CFG 来解决。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

代码中,这种方式看似是没有太大问题的。主干代码如下

source = null ;try{ source = xxx ;} finally { if source ! = null ; try { source.close() ; } catch { }}

但是要注意的是,"source"并不只有一个,而是有三个。

注意到 srcfis 是 类似是 FileInputStream 的变量,但是 srcfis.close()也会抛出异常。源代码截图如下:

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

因此,如果在 srcfis.close()时,就抛出了异常,在后面的 patchfis 和 fos 将不会执行 close()函数,将不会被关闭。

逻辑上有些绕,由于应用代码的 AST 并不能将 FileInputStream.close() 的 path 也加入到待 solve 的约束中来,因此这个案例只能是编译后的 IR 能够解决。

空指针

一般空指针问题是需要通过 IR 能够有些解决的。如下这个案例,具有较高的隐蔽性。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

在第 41 行,已经进行了判空处理,client 在 while 块中不会有空指针的问题。

但是却忽略了 client 下面的 42 行,rpcPort.first 这个取属性的方法。

rpcPort 的赋值,使用了另外一个文件中的 handShaking 函数,代码如下。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

可以看到,rpcPort 是 handShaking 函数的返回值,这个函数只要走到 catch 块中,则一定会 return null 。

所以 rpcPort.first 存在空指针问题。

仅靠 AST 的话,不能有效发现这个问题,因为 AST 需要连接在一起,才能还原这个调用关系。

字节码与之不同的是,在 handShaking 函数中,存在返回 null 路径的可能性,因此能够得出空指针的风险。

死锁

死锁出现的条件是: 锁 A 与锁 B,在不同线程中,执行加锁的顺序是相反的,则有可能导致死锁现象。如: 线程 t1 先锁 A,再锁 B ; 线程 t2 先锁 B,再锁 A。

这类风险问题不常见,但是很有代表性,而且出现问题时,往往比较难查,还需要借助一些线程的日志。且死锁的危害性是众所周知的。

需要指出的是,即使是字节码的方法,在死锁问题上也不能做到一个很高的命中率,它是在执行顺序 和 锁位置上做一种规则。

另外死锁规则,需要辅助的不是单一变量,而是一批锁变量,并且锁变量中也有锁的范围、锁的对象,形成一个 list,并且对 list 做路径求解。

以下代码片段比较多,也可以看出死锁的前提 CFG 比较复杂。

// 成员变量 mWaitingQueue,下文中会对它做加锁处理。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

// realPostRequest ,自身是一个类的普通 public 函数,非类的静态函数。在方法声明上,会对 当前类的实例对象 加锁。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

// addWaitingRequest ,自身是一个类的普通 private 函数,非类的静态函数。在方法中,会对 成员变量 mWaitingQueue 加锁。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

// postRequest (问题片段 1),自身是一个类的普通 public 函数,非类的静态函数。在方法声明上,会对 当前类的实例对象 加锁。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

// repostWaitingRequest (问题片段 2) ,自身是一个类的普通 public 函数,非类的静态函数。在方法中,先对 mWaitingQueue 加锁,再对 当前对象加锁。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

对以上代码片段,调用和加锁图如下:

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

首先,postRequest 函数,和 realPostRequest 函数,这两个函数,均为类的普通函数,非静态函数。

因此,这两个函数在 sync 的时候,sync 的是对象本身。

If count is an instance of SynchronizedCounter, then making these methods synchronized has two effects:First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.译文(主要): 当一个线程,正在对同一个对象,执行一个sync函数时,其他线程对该对象,执行sync函数时,会block,直到第一个sync函数完成。

因此,锁 L1,是当前对象,锁 L2,是当前对象的一个成员属性。

所以当线程 t1,执行 postRequest 时,当满足 if 条件时,就会先锁 L1 对象,再锁 L2 queue。

当线程 t2,执行 repostWaitingRequest 时,先锁 L2 queue,再锁 L1 对象。

因此这两段代码被判定为死锁隐患。

主线程阻塞

在一些 APP 客户端的场景中,主线程要求不能有阻塞式的操作,耗时较大或者阻塞式操作,需要在异步线程中执行。

如下是一个较为典型的主线程中出现了阻塞式调用的案例。

// getImageFilePath 报出了问题

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

glide 调用后,into 到一个 ImageView 中 , 该 ImageView 重写了 onClickListener ,这里是监听点击事件,为主线程。

在 onClick 事件中,调用了 getImageFilePath 函数,如下为 getImageFilePath 的内容。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

关注到第 209 行,调用了 loadShenPeituFile 函数,这在另外一个代码文件中。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

问题出在 241~245 行,这个片段中。glide 是一个开源代码库,因此 glide 不属于应用代码,不为研发人员自己写的代码。但是,它是程序的一部分,会被字节码识别到。

这里贴出了 glide 源代码中比较重要的代码片段,以及 issue 中该问题的相关回答。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

因此在主线程中使用 glide get(),是一个阻塞式调用,需要避免。

依托平台

目前的代码合并风险分析能力,集成在我们开发的一款平台中。

研发风险分析平台,专注 devops 各个环节的风险识别能力。

其中,代码合并环节作为 devops 环节的基础部分。我们在代码合并的时机,构建了上述文中介绍的功能。

分析功能主体

在接入平台之后,会配置一个 企业微信群的信息,以后如果识别出风险,将会推送至该群中。

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

然后点击"处理问题",可以查看详情

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

查看文件详情与风险详情提示

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

查看增量代码变更

如何进行代码合并时的风险分析(如何进行代码合并时的风险分析报告)

总结

我们在 代码合入时 进行触发,使用 AST 的检测能力,与 COMPILE 的检测能力,进行风险的检查。从静态和动态两个方面,进行风险的判定。依托于我们自研的平台,进行风险的通知和处理。因为篇幅原因,有些内容就不展开描述了。我们进行了较多的技术探索,由于水平有限,很多能力尚在探索中,也欢迎读者朋友一起来探索这个领域。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。