从零开始的 Rust 学习笔记(7)——Lifetime

这次的笔记只讲一个东西——Lifetime☆〜(ゝ。∂)

倒也不是说特别复杂,不过算是很与众不同的一个 feature~Rust的内存安全、无需 GC(垃圾回收) 则是因为有这个 feature(当然,真要在运行时搞事情的话,,编译器静态分析也未必能保证 100% 的安全)

然后我们说的内存安全的话,则是指需要禁止以下两种情况发生

  1. Use After Free
  2. 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();
}

再用 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       }

我们这里重点关注一下 retref 的 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);
}

编译时则会报错如下Σ(・□・;)

这个问题要怎么解决呢~?当然还是靠自己了,编译器也好,还是 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
}
声明: 本文为0xBBC原创, 转载注明出处喵~

Leave a Reply

Your email address will not be published. Required fields are marked *