PATA
PATA (path-sensitive alias-aware typestate analysis)发表于ASPLOS 22,随后作者24年在TOCS对其进行扩展为SPATA (summary-based, alias-aware, and path-sensitive typestate analysis)。关于typestate analysis这篇blog里有简单介绍。
1.PATA
参考文献 [ 1 ] ^{[1]} [1],关于题目中的词语path-sensitive应该是在每个分支处都会fork一下分析结果(不过type state分析本身就是path-sensitive的),alias-aware应该是说原始ESP算法没有考虑变量间的别名情况,PATA考虑了。
1.1.Introduction
分析OS有以下难点:
(1).难以基于指针分析进行别名分析(SVF分析很多项目都会有这个问题),这是由于OS是多模块以及application-driven的。其中许多函数并没有caller,因此参数的指向集为空,丢失了许多别名信息。下面示例中
dev->plat_dev
和pdev
存在别名关系并且1282行如果pdev == NULL
那么存在空指针异常。不过,s5p_mfc_probe
函数是在另一个OS模块通过platform_driver
的.probe
字段进行调用的,因此在分析当前模块时, pts ( pdev ) = ∅ \text{pts}(\text{pdev}) = \emptyset pts(pdev)=∅,dev->plat_dev
和pdev
的别名关系并没有建立。虽然可以在分析COPY
语句时记录别名关系,但是难以应用到OS等大型code base上。(2).OS Codebase非常大,进行typestate分析的开销也随之增大。虽然可以遍历value-flow graph减少遍历CFG的开销,但value-flow graph基于指针分析构造,而OS下指针分析的结果也不sound。
// FILE: linux-5.6/drivers/media/platform/s5p-mfc/s5p_mfc.c
1266. static int s5p_mfc_probe(struct platform_device *pdev) {
......
1280. dev->plat_dev = pdev; // create alias relationship
1281. if (!dev->plat_dev) { // pdev can be NULL
1282. dev_err(&pdev->dev, ...); // Null-pointer dereference!
1283. return -ENODEV;
1284. }
......
1415. }
// These interface functions have no explicit caller functions // in the OS code
1664. static struct platform_driver s5p_mfc_driver = {
1665. .probe = s5p_mfc_probe,
1666. .remove = s5p_mfc_remove,
......
1672. }
下面示例中展示了PATA找出的Zephyr蓝牙子系统的一个空指针异常,2709行的 model->user_data
和 cfg
存在别名关系,2720行进行了判空。表明了这两个值可能为 NULL
,在 NULL
分支后面会调用 send_friend_status
,里面 cfg->frnd
存在空指针解引用。其中存在横跨多个函数的别名关系。
// FILE: zephyr-2.1.0/subsys/bluetooth/cfg_srv.c
2680. static void send_friend_status(type *model, ...) {
......
2684. struct bt_mesh_cfg_srv *cfg = model->user_data; // Alias
......
2687. net_buf_simple_add_u8(&msg, cfg->frnd); // Unsafe dereference!
......
2692. }
2705. static void friend_set(...) {
......
2709. struct bt_mesh_cfg_srv *cfg = model->user_data; // Alias
......
2720. if (!cfg) { // Pointer cfg can be NULL
2721. BT_WARN(...);
2722. goto send_status;
2723. }
......
2747. send_status:
2748. send_friend_status(model, ctx);
2749. }
Challenge:C1.如何在不进行指针分析的前提下基于CFG路径和access path分析别名关系。C2.OS中的变量非常多,没法对每个变量维护SMT symbol以及typestate,得使用别名关系减少typestate追踪和路径验证的开销。
Solution:C1.提出了path-based alias analysis,对于control-flow path在每一个程序点维护一个alias graph表示这个path下的别名关系,同时基于被分析的指令以及相关变量的access paths更新alias graph。C2.提出了alias-aware typestate tracking method分析不同类型的bug以及alias-aware path-validation method来验证bug路径的可行性。
之前的typestate分析方法对于每个变量维护一个typestate以及一个SMT symbol,而PATA对于一个alias graph下的所有变量维护一个typestate以及一个SMT symbol,并且只要一个变量被写入了就修改typestate。此外ESP [ 4 ] ^{[4]} [4] 似乎只分析scalar数据没有考虑指针别名。
1.2.Approach
Workflow如下,主要有3部分:1.Path-Based Alias Analysis:进行别名分析;2.Alias-Aware Typestate-Tracking Method:进行IFDS分析找bug;3.Alias-Aware Path-Validation Method:进行路径条件分析。
在分析过程中,PATA会从OS代码中没有caller function的函数开始沿着CFG path分析,每个path对应1个alias graph,当遇到多个后继结点时alias graph会clone一份。每个程序点分析完时会调用typestate tracking分析是否潜在存在bug。注意:跟Pinpoint等方法一样,为了避免分析循环和递归调用带来的开销,PATA将循环和递归调用展开1次,因此理论上CFG中不存在环路,同时PATA会跳过间接调用分析。
1.2.1.别名分析
作者基于alias graph [ 3 ] ^{[3]} [3] 进行别名分析。alias graph的每个结点 n n n 表示一个别名集合 Vars ( n ) \text{Vars}(n) Vars(n),其中集合中所有变量互为别名;每条边标有一个field或者解引用 *
。下面alias graph有4个结点 n 1 n_1 n1, n 2 n_2 n2, n 3 n_3 n3, n 4 n_4 n4,推测代表的代码片段如下。这4个结点的alias set分别为:{ x }
, { y }
, { p, q, &x->f, &y->g }
, { s, *p, *q, *(&x->f), *(&y->g) }
。可以看出一个结点的别名集合基于alias graph反向遍历获得。
p = &x->f;
q = &y->g;
p = q;
s = *p;
alias graph表示为 ⟨ N , E ⟩ \langle N, E \rangle ⟨N,E⟩,用 Node ( v ) \text{Node}(v) Node(v) 表示变量 v v v 对应的结点,更新规则如下,其中对于所有变量 v 1 , v 2 v_1, v_2 v1,v2 有: n 1 = Node ( v 1 ) , n 2 = Node ( v 2 ) n_1 = \text{Node}(v_1), n_2 = \text{Node}(v_2) n1=Node(v1),n2=Node(v2)。 n 2 → ∗ n 1 n_2 \stackrel{*}{\rightarrow} n_1 n2→∗n1 表示 ∗ v 2 *v_2 ∗v2 和 v 1 v_1 v1 存在别名关系, n 2 → f n 1 n_2 \stackrel{f}{\rightarrow} n_1 n2→fn1 表示 & v 2 -> f \&v_2\text{->}f &v2->f 和 v 1 v_1 v1 存在别名关系 。更新过程会涉及到结点的合并。
语句类型 | 示例 | 规则 | 说明 |
---|---|---|---|
Copy |
v 1 = v 2 v_1 = v_2 v1=v2 | v 1 ∈ Vars ( n 2 ) , Vars ( n 1 ) \ v 1 v_1 \in \text{Vars}(n_2), \text{Vars}(n_1) \;\backslash \; v_1 v1∈Vars(n2),Vars(n1)\v1 | v 1 v_1 v1 被重新赋值,之前的别名关系被kill |
Store |
∗ v 2 = v 1 *v_2 = v_1 ∗v2=v1 | E \ n 2 → ∗ n x , n 2 → ∗ n 1 ∈ E E \;\backslash \; n_2 \stackrel{*}{\rightarrow} n_x, n_2 \stackrel{*}{\rightarrow} n_1 \in E E\n2→∗nx,n2→∗n1∈E | kill其它 n x n_x nx 对 v 2 v_2 v2 的存储操作,添加 n 1 n_1 n1 对 v 2 v_2 v2 的存储 |
Load |
v 1 = ∗ v 2 v_1 = *v_2 v1=∗v2 | 1. Vars ( n 1 ) \ v 1 , v 1 ∈ Vars ( n x ) ∣ ∀ n 2 → ∗ n x ∈ E \text{Vars}(n_1) \;\backslash\; v_1, v_1 \in \text{Vars}(n_x) \mid \forall n_2 \stackrel{*}{\rightarrow} n_x \in E Vars(n1)\v1,v1∈Vars(nx)∣∀n2→∗nx∈E. 2. n 2 → ∗ n 1 ∈ E ∣ ∄ n 2 → ∗ n x ∈ E n_2 \stackrel{*}{\rightarrow} n_1 \in E \mid \nexists n_2 \stackrel{*}{\rightarrow} n_x \in E n2→∗n1∈E∣∄n2→∗nx∈E | 如果存在 store 操作 ∗ v 2 = v x *v_2 = v_x ∗v2=vx,那么 v 1 v_1 v1 和 v x v_x vx 存在别名关系。否则暂时标记 v 1 v_1 v1 和 ∗ v 2 *v_2 ∗v2 别名。 |
Gep |
v 1 = & v 2 -> f v_1 = \&v_2\text{->}f v1=&v2->f | 1. Vars ( n 1 ) \ v 1 , v 1 ∈ Vars ( n x ) ∣ ∀ n 2 → f n x ∈ E \text{Vars}(n_1) \;\backslash\; v_1, v_1 \in \text{Vars}(n_x) \mid \forall n_2 \stackrel{f}{\rightarrow} n_x \in E Vars(n1)\v1,v1∈Vars(nx)∣∀n2→fnx∈E. 2. n 2 → f n 1 ∈ E ∣ ∄ n 2 → f n x ∈ E n_2 \stackrel{f}{\rightarrow} n_1 \in E \mid \nexists n_2 \stackrel{f}{\rightarrow} n_x \in E n2→fn1∈E∣∄n2→fnx∈E | 如果存在gep关系 v x = & v 2 -> f v_x = \&v_2\text{->}f vx=&v2->f,那么 v 1 v_1 v1 和 v x v_x vx 存在别名关系。否则暂时标记 v 1 v_1 v1 和 & v 2 -> f \&v_2\text{->}f &v2->f 别名。 |
Call |
r = f ( a 1 , . . . , a n ) r = f(a_1, ..., a_n) r=f(a1,...,an) | 添加 copy 关系 f p i = a i f_{p_i} = a_i fpi=ai 以及 r = f ret r = f_{\text{ret}} r=fret 并处理 |
添加caller实参到callee形参以己callee返回值到caller receiver的 copy 关系。 |
下面代码的alias graph如下
1. foo(p) {
2. r = &(p->s);
3. t = *r;
4. if (!t)
5. bar(p);
6. else
7. a = *t;
8. }
9. bar(p) {
10. r = &(p->s);
11. t = *r;
12. a = *t;
13. }
分析完line 3有
&p->s
和r
的别名关系以及*r
和t
的别名。line 4有多个后继,会clone一份当前的alias graph。
执行
else
分支分析完line 7后,多出别名关系*t
和a
。
1.2.2.Typestate Tracking
这里每个程序点分析完更新alias graph后就会进行typestate分析。Typestate分析的原理这篇blog里介绍过,这里作者定义了3个FSM来分别进行null-pointer dereferences (NPD), uninitialized-variable accesses (UVA) memory leaks (ML)的检测。这里每个alias graph上的每个结点(alias set)都会有一个type state。
Bug类型 | 状态 | 说明 |
---|---|---|
所有 | S 0 S_0 S0 | 初始状态 |
NPD | S NON S_{\text{NON}} SNON | 当前alias set非空 |
NPD | S N S_{N} SN | 当前alias set为空 |
NPD | S N P D S_{NPD} SNPD | 空指针异常 |
UVA | S UI S_{\text{UI}} SUI | 局部变量或者heap object未初始化 |
UVA | S I S_{I} SI | 局部变量或者heap object已初始化 |
UVA | S UVA S_{\text{UVA}} SUVA | 访问未初始化变量 |
ML | S NF S_{\text{NF}} SNF | heap object未被 free |
ML | S F S_{F} SF | heap object已被 free |
ML | S ML S_{\text{ML}} SML | 内存泄漏 |
Bug类型 | 操作 | 说明 |
---|---|---|
NPD | ass_null |
分配空值 |
NPD | br_null |
指针空的时候跳转,比如 if (!t) 时为 true 的时候 |
NPD | br_nonnull |
指针非空的时候跳转 |
NPD | deref |
解引用指针 |
UVA | ass_const |
给变量分配常量值 |
UVA | load |
从未初始化的heap object或者struct field加载值 |
UVA | alloc |
加载局部变量的值 |
UVA | use |
访问局部变量或者heap object |
ML | malloc |
分配heap object |
ML | free |
释放heap object |
ML | ret |
执行 ret 指令 |
3个checker的FSM分别如下
NPD
UVA
ML
下面示例的NPD分析过程如下
1. foo(p) {
2. r = &(p->s);
3. t = *r;
4. if (!t)
5. bar(p);
6. else
7. a = *t;
8. }
9. bar(p) {
10. r = &(p->s);
11. t = *r;
12. a = *t;
13. }
line 4后在 if (!t)
的情况下跳转后到line 12后检测到NPD。PATA通过别名关系将别名变量合并到一个结点简化了typestate分析过程。
1.2.3.路径验证
作者观察到:1.潜在有bug的path只占所有path的一小部分、2.在给定的path中, V a r s ( n ) Vars(n) Vars(n) 中所有变量应该满足相同的约束,因此这些变量可以在SMT求解器中由同一个符号表示。作者基于这两个观察提出了一个路径验证方法,推测该方法每当触发bug时会回溯当前alias graph对应的路径生成路径约束。
对于一个path作者定义了3个函数 T vars T_{\text{vars}} Tvars, T exp T_{\text{exp}} Texp, T stm T_{\text{stm}} Tstm 来生成SMT约束,这3个函数分别处理左值、表达式、语句。用 R ( v ) R(v) R(v) 表示 v v v 的右值。
函数 | 处理对象 | 结果 |
---|---|---|
T vars T_{\text{vars}} Tvars | 变量 v v v | R ( v ) R(v) R(v) |
T exp T_{\text{exp}} Texp | 常量 c c c | c c c |
T exp T_{\text{exp}} Texp | 变量 v v v | T vars ( v ) T_{\text{vars}}(v) Tvars(v) |
T exp T_{\text{exp}} Texp | 双目运算 e 1 ⊕ e 2 e_1 \oplus e_2 e1⊕e2 | T exp ( e 1 ) ⊕ T exp ( e 2 ) T_{\text{exp}}(e_1) \oplus T_{\text{exp}}(e_2) Texp(e1)⊕Texp(e2) |
T exp T_{\text{exp}} Texp | 单目运算 ⊕ e \oplus e ⊕e | ⊕ T exp ( e ) \oplus T_{\text{exp}}(e) ⊕Texp(e) |
T stm T_{\text{stm}} Tstm | 恒等 v = = e v == e v==e | T vars ( v ) = = T exp ( e ) T_{\text{vars}}(v) == T_{\text{exp}}(e) Tvars(v)==Texp(e) |
T stm T_{\text{stm}} Tstm | 条件 true 跳转 br t ( e ) \text{br}_t(e) brt(e) |
T exp ( e ) = = 1 T_{\text{exp}}(e) == 1 Texp(e)==1 |
T stm T_{\text{stm}} Tstm | 条件 false 跳转 br f ( e ) \text{br}_f(e) brf(e) |
T exp ( e ) = = 0 T_{\text{exp}}(e) == 0 Texp(e)==0 |
下面代码存在潜在的空指针解引用路径 2 → 3 → 4 → 6 → 7 2 \rightarrow 3 \rightarrow 4 \rightarrow 6 \rightarrow 7 2→3→4→6→7,line 7解引用 *q
。在line 7处存在alias set: {t, p}
, {t->f, p->f}
, {q}
;路径约束生成时,处理完line 2有 R ( q ) = = NULL R(q) == \text{NULL} R(q)==NULL,处理完line 3有 R ( p -> f ) = = 0 R(p\text{->}f) == 0 R(p->f)==0,处理完line 6有 R ( t -> f ) ! = 0 R(t\text{->}f) != 0 R(t->f)!=0,p, t
别名,因此line 3和line 6生成的约束冲突,该bug不成立。
1. void func(p, q) {
2. if (q == NULL)
3. p->f = 0;
4. t = p;
5. ......
6. if (t->f != 0)
7. *p = *q;
8. }
1.3.Evaluation
作者选择了4个OS来进行分析
OS | Version | Source files (*.c) | LOC |
---|---|---|---|
Linux kernel | 5.6 | 28,260 | 14.2M |
Zephyr | 2.1.0 | 1,669 | 383K |
RIOT | 2020.04 | 4,402 | 1,575K |
TencentOS-tiny | Commit 23313e | 1,497 | 572K |
PATA找到了797个bug,作者花了12小时人工验证,其中574为true positive,包括463个NPD、90个UVA、21个ML,总体误报率28%。作者随机挑选了200个linux kernel bug发送到linux community,120个IOT OS的bug全部发送给了开发者, 206个被确认(138个linux bug、23个Zephyr、23个RIOT以及22个TencentOS-tiny)。
作者对bug进行了进一步分析,发现在linux下设备驱动bug占比达到75%,而在IOT OS下,第三方库bug占比达到68%。这是由于这部分代码由第三方组织维护质量比OS社区维护的差。
至于为什么PATA还有误报,作者总结了3点原因:
1.当array索引非常量时,PATA采用array-insensitive方法,
array[i+1]
,array[i]
会被视为别名。2.当涉及到复杂的算数运算以及跨多个函数的数据依赖时,PATA难以处理。循环条件也是影响PATA性能的一大因素。
3.忽略并发内存访问。
为了验证别名关系的作者,作者实现了一个alias-unaware的PATA-NA,对比后如下,可以发现PATA-NA的误报率远高于PATA。
2.SPATA
与PATA相比,SPATA面临的挑战还包括:如何在不同的calling context下区分别名关系。为SPATA针对每个function都会构造一个summary,相比PATA加速了6.7倍。这里提到了pinpoint,尽管提出的SEG加速了分析,但是在每个callsite遍历SEG依旧非常耗时。相比已有的summary-based方法,SPATA考虑了跨函数的别名情况。和PATA一样的地方是对循环、递归、数组访问、间接调用等处理。
2.1.Summary-based方法
2.1.1.Alias Summary
summary尤其需要考虑到callee对caller的side-effect,主要来自以下几个方面:
S1.哪些局部变量与来自caller的参数别名(
Ref/Load
):下面示例中&p->f
和u
别名,因此对u
的store
操作会影响caller。S2.callee会修改caller中的哪些别名信息(
Mod/Store
):*u = r
会将r
写入到&p->f
指向的区域,造成side-effect。S3.哪些指令与typestate tracking相关:
*u = r
同时会修改caller中别名变量的状态。S4.哪些SMT约束能改变代码路径可行性:约束
r == 0
会影响跨函数路径验证。
1. typedef struct {
2. int f;
3. } T;
4. void func(T* p, int r) {
5. u = &(p->f);
6. if (r == 0)
7. *u = r;
8. ....
9. }
考虑到以上4点作者对PATA中的alias graph进行了改进,目前alias graph可以表示为 ⟨ N , E , E T , I , C ⟩ \langle N, E, \mathbb{ET}, \mathbb{I}, \mathbb{C} \rangle ⟨N,E,ET,I,C⟩,在分析规则方面函数内分析和PATA基本一致,主要是更新 N N N 和 E E E,剩下3个集合暂时没更新。
E T \mathbb{ET} ET 为一个边标签集合, E E E 中的边包括
load/gep
标签为Ref
,store
为Mod
。 E E E 中的Ref
边会在后续用来进行top-down bug检测,而Mod
边会在bottom-up更新alias graph中用到。I \mathbb{I} I 是一个typestate-transition-related指令序列,每个指令可以更改alias graph表示的变量集合的状态。
C \mathbb{C} C 是一个SMT约束集合。
function summary可以表示为一个由 ⟨ AS , path ⟩ \langle \text{AS}, \text{path} \rangle ⟨AS,path⟩ 组成的列表。 AS \text{AS} AS 可是精简后的alias graph。精简的方式是只保留与形参和返回值可达的 Ref/Mod
边对应的边和结点, path \text{path} path 即alias graph对应的路径。下图(b)为初始alias graph,( c )为summary(不过似乎也没修剪)。
在alias graph分析规则方面,SPATA相比PATA主要差别在 call
和 ret
的处理上。在bug检测方面,SPATA包括函数内bug检测以及跨函数bug检测。跨函数检测在分析callsite触发,检测callee中的bug。
分析工具 | 语句类型 | 示例 | 规则 | 说明 |
---|---|---|---|---|
PATA | Call/Ret |
r = f ( a 1 , . . . , a n ) r = f(a_1, ..., a_n) r=f(a1,...,an) | 添加 copy 关系 f p i = a i f_{p_i} = a_i fpi=ai 以及 r = f ret r = f_{\text{ret}} r=fret 并处理 |
添加caller实参到callee形参以己callee返回值到caller receiver的 copy 关系。 |
SPATA | Call |
r = f ( a 1 , . . . , a n ) r = f(a_1, ..., a_n) r=f(a1,...,an) | TopDownAnalysis ( f , G ) \text{TopDownAnalysis}(f, G) TopDownAnalysis(f,G), BottomUpAnalysis ( f , G ) \text{BottomUpAnalysis}(f, G) BottomUpAnalysis(f,G) | 通过caller -> callee 的top-down分析bug以及验证对应路径条件;通过 callee -> caller 的bottom-up分析更新caller的summary。 |
SPATA | Ret |
ret v \text{ret} \; v retv | AS = GenSummary ( G ) \text{AS} = \text{GenSummary}(G) AS=GenSummary(G), ⟨ AS , path ⟩ ∈ Summary ( f ) \langle \text{AS}, \text{path} \rangle \in \text{Summary}(f) ⟨AS,path⟩∈Summary(f) | 生成当前 ret 指令在当前函数 f f f 中的summary graph AS \text{AS} AS,并添加到 f f f 的summary中,对应路径 path \text{path} path |
2.1.2.在summary中添加typestate信息
这部分就是更新某个function的summary中的 I \mathbb{I} I 集合,对于summary对应的path中的每个指令,首先找到其操作数对应的node,然后将该指令存储在找到的节点中以进行类型状态追踪,用于跨函数bug检测。
在跨函数typestate分析过程中,在callee的summary中,首先找到指向与caller alias graph中节点相同object的结点,然后利用该找到的节点中存储的指令来更新alias graph中对应结点的状态。如果该状态变化触发了某种bug,则检测到一个潜在的bug。同时,将找到的结点中存储的指令追加到alias graph的相应结点上,作为被分析函数summary的一部分。在下面示例中,17行和20行分别对 fg
和 dp
进行了 load
,21行对 d
进行了 store
,因此被添加到对应结点中。
2.1.3.在summary中添加路径约束信息
在给定path下,同一个alias set中所有变量的约束相同。当分析到callsite需要merge callee summary时,在callee的summary中,首先找到与caller存在别名关系的结点,然后结合各自的summary中存储的路径约束进行检查,并添加 calleeCond == callerCond
的约束条件。下面示例中分别对 f = *fg
、*d = 0
添加路径约束 X 3 > 0 X_3 > 0 X3>0、 X 7 = = 0 X_7 == 0 X7==0。
2.1.4.跨函数分析
这部分主要对应分析callsite中如何同步callee的summary,分别通过top-down分析找出callee中潜在的bug并验证路径条件可行性以及bottom-up分析更新caller的summary。分析规则分别如下,其中 ⟨ N , E , E T , I , C ⟩ \langle N, E, \mathbb{ET}, \mathbb{I}, \mathbb{C} \rangle ⟨N,E,ET,I,C⟩ 是caller的alias graph, ⟨ N ′ , E ′ , E T ′ , I ′ , C ′ ⟩ \langle N^{'}, E^{'}, \mathbb{ET}^{'}, \mathbb{I}^{'}, \mathbb{C}^{'} \rangle ⟨N′,E′,ET′,I′,C′⟩ 是callee的alias graph, stk \text{stk} stk 和 stk ′ \text{stk}^{'} stk′ 分别表示caller实参和callee形参变量。
2.2.Evaluation
用的benchmark和PATA一样。检测效果如下
消融实验方面作者实现了non-alias、non-summary的SPATA-NAS以及non-path-validation的SPATA-NPV,对比效果如下。
和其它工具检测工具的对比效果如下: