[论文笔记] 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

image

在这个例子中,调用关系包括:

  • express() 调用 createApplication()

  • createApplication() 调用 mixin() 两次,把 EventEmitter.prototypeprotodescriptor 复制到 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 时都以相同方式初始化,那么只需要一次运行,就能得到确定性的信息,补齐静态分析丢失的信息:

image

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

Approximate Interpretation

image

有几点特殊的语言性质需要注意:

  • 作为模块的源代码可以通过函数 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\) 需要参数,也可能访问 argumentsthis。使用 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\) 收集一次写提示

image

Reducing unsoundness of static call graph and points-to analysis

静态分析是一个传统的,基于子集规则的,流不敏感和上下文不敏感的方法。

image

前面几个规则比较简单,我们更关注最后两条使用动态分析信息辅助的静态分析规则。回顾动态分析过程,若在位置 \(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\)。一个重要特性是:它们能够更精确地捕获“被写入的抽象对象—属性名—被写入的抽象值”三者之间的关联信息。

posted @ 2025-08-03 22:53  sysss  阅读(23)  评论(0)    收藏  举报