[论文笔记] Reducing Static Analysis Unsoundness with Approximate Interpretation
Introduction
之前的一些 JS 的 callgraph 相关的工作忽略了动态属性访问(比如 Field-based Analysis)。最近的 ECOOP'22 的工作显示动态属性访问是最主要的 unsoundness 的来源。这篇文章主要关注动态属性访问,使用 dynamic pre-analysis 的信息减少 unsoundness。
这个技术的灵感来源于之前 OOPSLA'14 的工作:当使用动态对象操作来初始化库 API 时,这些操作往往发生在与程序输入无关的执行上下文中。确定性 determinacy 指在某个程序位置 \(l\) 和上下文 \(c\) 中,变量 \(x\) 的值始终相同。以往的确定性技术还没有在大规模的程序上做验证。这里的 approximate interpretation 技术是一种轻量化的动态确定性技术,还会收集到之前的方法收集不到的关系型信息 relational information。
简单来说,approximate interpretation 会通过强制运行代码来收集一系列提示 hints 给主要的静态分析过程使用。强制执行可能到达一些在实际情况下不能达到的程序状态,但是这种不精确是可以接受的。
Motivating Example

在这个例子中,调用关系包括:
-
express()调用createApplication() -
createApplication()调用mixin()两次,把EventEmitter.prototype和proto的descriptor复制到app上 -
(a) 中的
app.get实际上可以看作app["get"],这个函数在 (d) 中初始化的时候通过app[method] = ...进行赋值,最终返回的对象为proto,(b) 中调用mixin方法再把proto上对应地属性函数复制到 (a) 中的app上,实际上的调用关系非常复杂 -
app.listen同样是在初始化的时候被动态加入到对象属性当中的
在这种复杂的例子下,如果做 over-approximate,精度就会特别差;如果忽略动态属性访问(比如 Field-based 的 WALA),那么就会丢失掉这些调用边
Express 的 application 对象在每次调用 createApplication 时都以相同方式初始化,那么只需要一次运行,就能得到确定性的信息,补齐静态分析丢失的信息:

随意运行任意的函数并不容易,采用一种称为近似解释 approximate interpretation 的强制执行策略避免构造能够覆盖所有相关函数的输入。
Approximate Interpretation

有几点特殊的语言性质需要注意:
-
作为模块的源代码可以通过函数
require动态的引入 -
使用 function value 来指代作为运行时值存在的函数,因为函数可能包含那些不在自身定义域里面定义的变量(闭包),模块函数例外
Approximate interpretation 使用 worklist 算法:
-
\(Worklist\) 维护一个待处理的模块和 function values 的列表
-
\(Visited\) 是已处理过的模块和 function definitions 的集合
-
\(H_R : Loc \rightarrow P(Loc)\) 是从某个代码位置到一组代码位置的映射,\(l' \in H(l)\) 意味着在位置 \(l'\) 创建的对象在 \(l\) 被动态属性读取了
-
\(H_W : \{ Loc \times String \times Loc \}\) 是三元组 \((l, p, l')\) 的集合,意味着位置 \(l'\) 创建的对象通过动态属性访问被写入了位置 \(l\) 创建的对象的属性 \(p\) 上
-
\(loc : Object \rightarrow Loc\);\(this : Object \rightarrow Object\)
运行一个函数 \(f\) 会有些麻烦:\(f\) 需要参数,也可能访问 arguments 和 this。使用 JS 的 proxy object:创建一个全局对象 \(p \star\),通过 f.apply(w, p★) 来运行,若 this(f) 已定义,则 w = this(f),否则 w = p★。然后根据 JS 代码选择不同的操作:
-
创建对象:把对象 \(v\) 和位置 \(l\) 加入到 \(loc\) 映射中
-
函数定义:把 function value \(v\) 和位置 \(l\) 加入到 \(loc\) 映射中,如果 \(loc(v) \notin Visited\) 那么就加入到 \(Worklist\) 中去
-
函数调用:1. 如果
v = p★那么直接返回p★;2. 如果函数是一个 Node.js 标准库函数,那么使用 mock 替换可能与外部环境交互的函数,模拟函数会立即调用所有作为参数传入的回调,并以p★作为返回值;3. 如果是 JS 标准库函数,对于某些构造 object 等特殊的函数需要特殊处理(Object.create被看作创建对象,Object.defineProperty被看作动态写属性);4. 普通函数调用,function value 被移进 \(Visited\),从 \(Worklist\) 里面移出。 -
静态属性读:若
v = p★,则通过代理机制将p★作为结果值 -
静态属性写:若
v = p★,则本次写入被忽略 -
动态属性读:通过将 \(loc(v)\) 加入 \(H_R(l)\) 收集一次读提示,然后按常规方式完成读取
-
动态属性写:将三元组 \((loc(v), p, loc(v′′))\) 加入 \(H_W\) 收集一次写提示

Reducing unsoundness of static call graph and points-to analysis
静态分析是一个传统的,基于子集规则的,流不敏感和上下文不敏感的方法。

前面几个规则比较简单,我们更关注最后两条使用动态分析信息辅助的静态分析规则。回顾动态分析过程,若在位置 \(l\) 的动态属性读取操作 \(E[E']\) 的近似执行过程中,观察到一个源自位置 \(l'\) 的对象作为结果值,则会生成读提示 \(l' \in H_R(l)\)。对于每一个这样的提示,我们将对应的抽象值 \(t_{l'}\) 加入该表达式的抽象结果中,即 \(t_{l'} \in \llbracket E[E'] \rrbracket\)。
写提示 \(H_W\) 的处理方式类似。若在动态属性写入操作 \(E[E'] = E''\) 的近似执行过程中,一个源自 \(l''\) 的对象被写入到源自 \(l\) 的对象的属性 \(p\) 上,则会生成写提示 \((l, p, l'') \in H_W\)。因此,我们只需将建模自 \(l''\) 的抽象值 \(t_{l''}\) 加入建模自 \(l\) 的抽象值 \(t_l\) 的属性 (p) 中,即 \(t_{l''} \in \llbracket t_l.p \rrbracket\)。一个重要特性是:它们能够更精确地捕获“被写入的抽象对象—属性名—被写入的抽象值”三者之间的关联信息。

浙公网安备 33010602011771号