这次的笔记只讲一个东西——Lifetime☆〜(ゝ。∂)
倒也不是说特别复杂,不过算是很与众不同的一个 feature~Rust的内存安全、无需 GC(垃圾回收) 则是因为有这个 feature(当然,真要在运行时搞事情的话,,编译器静态分析也未必能保证 100% 的安全)
然后我们说的内存安全的话,则是指需要禁止以下两种情况发生
- Use After Free
- Dangling Pointer
第一种情况的话,可能会导致 Segment Fault,也有可能会被 Hacker 利用,例如有些 iOS 版本上的越狱的一部分,则是基于 kernel 中包含了 UAF 的代码,UAF 的地址上的内容又可以被用户控制,随后通过一系列操作,在 kenrel 某些可以提权的代码里,再次 allocated 并用到这块被用户控制的内存时,就可以实现原本 需要 privileged 的操作了~
对于第二种情况的话,也就是「野指针」,比如某个函数返回了其栈上的内存的指针,而我们知道,当函数返回时,其栈上的内容是会被销毁的( ;´Д`)
举例如下~看看最后的输出是什么
#include <stdio.h> int * stack_ref() { // local variable `ret` is located on stack int ret = 233; // return address of stack memory associated with local variable `ret` return &ret; } // at this point, the stack memory is no longer valid int main(int argc, char *argv[]) { // get a ref, but on stack int * ref = stack_ref(); // inc 4 *ref += 4; // call that again stack_ref(); // and please guess the value printf("Guess: %d\n", *ref); }
为了方便后面解释,增加输出拿到的引用的地址~
#include <stdio.h> int * stack_ref() { // local variable `ret` is located on stack int ret = 233; // take ref int * ptr = &ret; // debug printf("ptr: %p\n", ptr); // return address of stack memory associated with local variable `ret` return ptr; } // at this point, the stack memory can be modified int main(int argc, char *argv[]) { // get a ref, but on stack int * ref = stack_ref(); // inc 4 // should be 237 *ref += 4; // just call that again stack_ref(); // and please guess the value printf("Guess: %d<%p>\n", *ref, ref); }
使用 clang 编译
Apple LLVM version 10.0.1 (clang-1001.0.46.4) Target: x86_64-apple-darwin18.6.0
在关闭优化或者开启 -O1
时,上面的代码会有类似如下输出
ptr: 0x7ffeed18a624 ptr: 0x7ffeed18a624 Guess: 233<0x7ffeed18a624>
可以看到两次都在 stack_ref()
中对同一 stack 上的地址赋值了,而第二次在 stack_ref()
中对该地址的覆写发生在 main()
中,我们修改 ref
之后——即当函数返回时,其栈上的内容就不保证是否会在其他地方 / 时候被修改了
也就是说,stack_ref()
里的 local variable 的有效期只在那个函数的 scope 之内,当 scope 结束时,它的有效性就不再得到保证,换句话说,也就是它的生命周期,理论上在 scope 结束之后,也结束了。
那么这个「生命周期」的话,就是 Rust 里的 Lifetime 了~也就是说其实 Lifetime 这个东西的话,C/C++ 里、其他很多语言里,实际上也都是有的,但是有些语言允许你这么写代码来搞事情,不得不说在某些情况下,可能还真需要搞事情来做一些 hack,但是 Rust 不让,不仅不让搞事情,Rust 还明确了 Lifetime 这一点~
同样的代码在 Rust 中则会报错~
fn stack_ref() -> &i32 { let ret = 233; &ret } fn main() { let r = stack_ref(); }
![](/wp-content/uploads/2019/10/stack-ref-lifetime.webp)
再用 C 举一个 UAF 的例子
#include <stdio.h> int * get_value() { // allocate memory on heap int * resource = (int *)malloc(sizeof(int)); // assign data *resource = 233; // return pointer on heap (safe, so far...) return resource; } int * cmp(int * a, int * b) { if (*a > *b) { return a; } else { return b; } } int main(int argc, char *argv[]) { srand(time(NULL)); // get a ref, on heap memory int * ref = get_value(); // get user input int p; scanf("%d", &p); // debug printf("p: %d, *ref: %d\n", p, *ref); // as a caller, you may not know // which pointer will be returned // because it totally depends on user input int * ret = cmp(ref, &p); // after a great amount of work // and you might accidentally free(ref) free(ref); // and later use of `ret` // will possibly be UAF printf("larger: %d\n", *ret); }
可以看到上面的代码有没有问题,全取决于用户输入的大小,如果用户输入小于等于了 233
的话,则 cmp
函数会返回的是 ref
,保存在 ret
之中,而 ref
在那之后还被 free()
了,因此最后使用 ret
时,由于 ref
已经被 free()
,所以 UAF 了
也就是说,在我们使用 ret
的时候,我们没有保证其可能引用的 ref
还未被 free()
,我们可以将 main()
函数中的各变量 Lifetime 的始末标记上~
int main(int argc, char *argv[]) { srand(time(NULL)); ----- ref: lifetime start int * ref = get_value(); | ----- p: lifetime start int p; | scanf("%d", &p); | | printf("p: %d, *ref: %d\n", p, *ref); | ----- ret: lifetime start int * ret = cmp(ref, &p); | ----- ref: lifetime end free(ref); | ----- use `ret` but might UAF printf("larger: %d\n", *ret); | ----- p: lifetime end, ret: lifetime end }
我们这里重点关注一下 ret
和 ref
的 Lifetime 的始末~
它们的 Lifetime 之间是有交集的,且 ret
的 Lifetime 并非 ref
的子集——即 ref
没活够那么长,坚持到 ret
使用它的时候,如果此时用户输入小于等于 233 的话,则会发生 UAF
那么 Rust 怎么处理这个 Lifetime 的坑呢?只需要更改一下 cmp()
函数
fn cmp<'a>(a: &'a i32, b: &'a i32) -> &'a i32 { if *a > *b { a } else { b } }
这里的 'a
就是增加的 Lifetime 标记~当然了,a
只是一个记号,你要想的话,写成 'lifetime
也行,或者写成 'yet_another_long_mark
也是没问题的~
那么这个 'a
到底是什么作用呢?可以这么去看(๑╹ω╹๑ )
如果返回值的 Lifetime 是 a
的话,那么它所可能依赖的两个参数的 Lifetime 至少是包含 a
的
也就是说,返回值的生命周期,需要是其所依赖的(即可能引用的)变量的生命周期的子集
因为也只有这样,才可以确定在使用返回值的时候,不会遇到 UAF
当然了,a
具体有多长,从哪儿到哪儿我们不需要关心,这个是写给编译器看的提示,是为了明确的告诉编译器,这个返回值的 Lifetime 应该是其所有可能引用的参数 Lifetime 交集的子集。
为什么要取其所有可能引用的参数 Lifetime 交集呢~?因为既然这个返回值可能会引用任一一个参数,所以需要保证在返回值的 Lifetime 范围内,其所有可能会引用的参数都是有效的
需要注意的是,'a
仅仅是一个记号,用于提示编译器的,而不是给变量 / 引用续命的!该改的代码还是要自己改的!
比如上面的 C 代码就可以改为
int main(int argc, char *argv[]) { srand(time(NULL)); ----- ref: lifetime start int * ref = get_value(); | ----- p: lifetime start int p; | scanf("%d", &p); | | printf("p: %d, *ref: %d\n", p, *ref); | ----- ret: lifetime start int * ret = cmp(ref, &p); | ----- use `ret` but might UAF printf("larger: %d\n", *ret); | ----- ref: lifetime end, free(ref); p: lifetime end, ret: lifetime end }
这样的话,Lifetime(ret)
$\in$ (Lifetime(ref)
$\cap$ Lifetime(p)
)了~那么 UAF 的问题就算解决了~
接下来则是 Dangling Pointer 的问题~比如说我们现在有一个结构体,然后它的其中一个 field 是去 hold 一个引用,显然,在 C 里的话,我们无法保证这个引用在每次通过结构体的实例使用时都是有效的
#include <stdio.h> struct Wallet { int * amount; }; int * get_money() { int * ret = (int *)malloc(sizeof(int)); *ret = 23333; return ret; } int main(int argc, char *argv[]) { int * money = get_money(); struct Wallet w = { money }; // start of a lot works // ... { int ouo = 233; w.amount = &ouo; } // variable `ouo` leaves its scope // w.amount is now a dangling pointer // ... // end of a lot works // here we can't promise that the amount pointer is still valid printf("%d\n", w.amount); }
C/C++ 里这段代码可以被编译,但是显然我们拿不到我们想要的结果~
而在 Rust 中的话,当结构体中有引用类型,那么在使用该结构体实例时也需要用 Lifetime 来保证引用内存的安全性~
那么上面的 Wallet
结构体就会先写成下面这样
struct Wallet<'a> { amount: &'a i32 }
这里给 Wallet
加上了 Lifetime 的标记,它的意思是:
如果结构体实例的 Lifetime 是 a
的话,那么该实例的 Lifetime 需要是其结构体中所有引用的 Lifetime 交集的子集
把上面有问题的代码用 Rust 写出来的话,,
struct Wallet<'a> { amount: &'a i32 } fn main() { let mut w = Wallet { amount: &23333 }; // start of a lot works // ... { let ouo = 233; w.amount = &ouo; } // variable `ouo` leaves its scope // w.amount is now a dangling pointer // ... // end of a lot works println!("{}", w.amount); }
编译时则会报错如下Σ(・□・;)
![](/wp-content/uploads/2019/10/ouo-dangling-pointer.webp)
这个问题要怎么解决呢~?当然还是靠自己了,编译器也好,还是 Lifetime 标记也好,都只是为了让开发者写出更安全的代码,而不是使用(当然也并无法使用) Lifetime 标记让编译器给那些被引用的变量续命,
struct Wallet<'a> { amount: &'a i32 } fn main() { let mut w = Wallet { amount: &23333 }; { let ouo = 233; w.amount = &ouo; println!("{}", w.amount); } // variable `ouo` leaves its scope }