跳至主要內容

Rust与算法基础(4++):你以为的安全真的安全吗?

QuenTine...大约 5 分钟编程Rust算法

我们用了一个我们认为安全的复制方法,绕过Copy Trait实现了向量内元素的移动。然而它真的安全吗?

在插入排序的单元测试里加入这么一个用例:

#[test]
fn it_struct_sort_ascending_equal_box() {
    #[derive(Debug, PartialEq)]
    struct Foo {
        id: u32,
        name: &'static str,
    }

    let mut v = vec![
        Box::new(Foo {
            id: 22,
            name: "ZS",
        }),
        Box::new(Foo {
            id: 43,
            name: "LS",
        }),
        Box::new(Foo {
            id: 145,
            name: "WW",
        }),
        Box::new(Foo {
            id: 1,
            name: "ZL",
        }),
        Box::new(Foo {
            id: 9,
            name: "SQ",
        }),
        Box::new(Foo {
            id: 43,
            name: "LS2",
        })
    ];

    InsertionSorter(&mut v).sort_by(|prev, next| prev.id <= next.id);
    assert_eq!(
        v,
        vec![
            Box::new(Foo {
                id: 1,
                name: "ZL",
            }),
            Box::new(Foo {
                id: 9,
                name: "SQ",
            }),
            Box::new(Foo {
                id: 22,
                name: "ZS",
            }),
            Box::new(Foo {
                id: 43,
                name: "LS",
            }),
            Box::new(Foo {
                id: 43,
                name: "LS2",
            }),
            Box::new(Foo {
                id: 145,
                name: "WW",
            })
        ]
    );
}

执行发现报错了,它甚至无法正常输出错误信息,断点调试直接跳到汇编去了。我也不是很懂汇编,那就在报错前看看调试器监测的数据吧。

可以看到在函数执行到

vec[j] = e;

这一句之后,evec[1]都变成了错误的数据(一个很大的id),此时e仍然能被监测到,到下一个循环时e按理说就被释放掉了。 而到下一次vec[j] = e时崩溃了。

我们现在把vec[j] = e换成下面的,再调试一下:

ptr::copy(&e, &mut vec[j], 1);

这次调试的运行情况比之前有一些细微的变化。

这一句之后,evec[1]还是正确的数,而到结束这个循环到下一个循环时,e不再被监测得到,同时vec[1]的值才在这时候变成了错误的值。 而且看e的类型,是个Foo *,也就是Box<Foo>,可以猜测,是在e脱离作用域后,被释放掉了,这不是浅层地释放掉Box指针而已, 而是层层调用释放函数,将资源释放掉。

e是被拷贝出来的,所有权也在本函数内,即使再放回(以copy的方式)原位,数组没有变化,也会因为evec[1]两个指针指向同一个数据,e被释放掉导致vec[1]悬垂了。

再对比之前的运行,执行vec[j] = e之后,e的所有权被移交出去了,而在所有权移交的同时,数据发生了改变。

第一个循环,vec[1] --> e vec[0] < e,e维持原位,重新赋值回vec[1] vec[1]数据出错(被释放了,并强行读取了数据),id是一个很大的数

注意e是拷贝出来的,也就是vec[1]也是一个有所有权的数据。e移动到vec[1],就意味着vec[1]原本存放的数据没人要了(显然也没有任何办法可以索引到了),没人要的数据会立即结束声明周期,就地释放。

而又因为被释放的数据和e以及vec[1]是同一个指针,导致evec[1]都悬垂了。到了下一个循环时,当被复制出来的数据要再次替换vec某个位置的数据时, 这个数据再次被替换了出去,触发了已释放的空间重复释放,导致程序崩溃。

第二个循环,vec[2] --> e vec[1] > e,vec[1] --> vec[2] vec[0] < e, 尝试e --> vec[1] vec[1]已经被释放,再次释放,程序崩溃

这里还有一个需要注意的点,就是移动的意义。可以看出移动也是复制,evec[1]同时悬垂也意味着两者存的是同一个指针。 那么这种移动在一般的安全实现中,移出方结束生命周期,为什么接受方并没有悬垂呢?这正是因为移出的数据已经标记为不拥有所有权, 所以这个变量在结束生命周期时,不会执行释放,仅销毁变量本身所保存的值(比如指针)。

那么e是被强制复制出来的,现在同时存在两个指向同一个资源的指针,我们要做的就是防止其中一个指针在声明周期结束时释放资源。 也就是说,手动把一个变量标记为“已移出”。正好,Rust的mem::forget()就是干这个的。

所以在

ptr::copy(&e, &mut vec[j], 1);

的后面,加上

mem::forget(e);

这个问题就解决了。

应当注意的是,mem::forget()并不是一个unsafe函数。因为Rust的安全机制并不保证资源的释放, 所以即使程序只写安全代码也会造成内存泄漏。Rust的安全机制是保证只要不显式地使用forgetdrop等等手动选择释放或者不释放资源的操作, 一般的安全行为不会隐式地制造内存泄漏、指针悬垂、重复释放等内存安全问题。


copyforget的写法还有一个更优雅的解法。注意这个e是用ptr::read()拷贝出来的, 这个函数是不安全的,因为拷贝出来之后原来保存的指针还在。 而在一系列移位之后,原来保存的指针被覆盖掉了,这时候的e又回到正常的单指针状态了,是可以自然释放的。

对应前面的read,这里就把刚才的copyforget写法改成ptr::write()

ptr::write(&mut vec[j], e);

writecopy不同的是,copy的src参数是一个raw pointer,不涉及所有权。 而write函数对src是夺取所有权的,将e代入src,e会被吃掉。 这个Box指针会被重新写入数组,只要算法本身没实现错,这个Box指针就仍是唯一的, e也因为被夺取所有权,不会被重复释放。

上次编辑于:
贡献者: qt911025
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3