【问题标题】:Is it safe to temporarily give away ownership of the contents of a mutable borrow in Rust? [duplicate]暂时放弃 Rust 中可变借用内容的所有权是否安全? [复制]
【发布时间】:2021-12-21 20:36:18
【问题描述】:

通过函数FnOnce(T) -> T 修改&mut T 的函数是否可以安全地生锈,还是会导致未定义的行为?它是包含在某个地方的标准库中,还是包含在某个知名的 crate 中?

如果你还假设T: Default,那看起来像

fn modify<T, F: FnOnce(T) -> T>(x: &mut T, f: F) -> ()
where
    T: Default
{
    let val = std::mem::take(x);
    let val = f(val);
    *x = val;
}

(另请参阅 https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f015812bac6f527fe663fe4e0b7a3188)

我的问题是做同样的事情,但删除where T: Default 子句(也没有T: Clone)。这需要不同的实现,因为您不能使用std::mem::take。 我不确定如何实现不受约束的版本,但使用 unsafe Rust 应该是可能的。

我正在从线性类型和子结构逻辑的背景中学习 Rust。 Rust 的可变借用似乎与将资源移入然后移出函数非常相似,但我不知道像这样获取可变借用内容的临时所有权是否真的安全。

【问题讨论】:

  • 您几乎肯定想查看replace_with crate,它在解决紧急安全问题的同时提供了该功能。

标签: rust borrow-checker ownership


【解决方案1】:

好吧,您正在用默认值替换借用内存位置的内容。这意味着记忆确实在每一点都是正确的。所以不应该有任何未定义的行为。

基本上从可变引用x 的角度来看,您将其更改为默认值,然后再次将其更改为不同的新值。

一般来说,如果有可能出现未定义的行为,您将需要使用unsafe 关键字。或者有人在使用unsafe 关键字时犯了错误。这些事情在标准库中比较少见。

如果必须,请继续查看代码中的安全说明:https://doc.rust-lang.org/src/core/mem/mod.rs.html#756

【讨论】:

  • 我知道如果T: Default,这个函数是安全的(正如编译器通过不需要unsafe 关键字来证明的那样。我的问题是关于你的版本notT: Default
【解决方案2】:

它很安全,甚至还有箱子(现在找不到)。

但是。

在编写不安全的代码时,你必须非常小心。如果你不知道自己在做什么,很容易导致 UB。

例如,这里有一些你可能没有想到的东西:恐慌安全。

假设我们简单地实现它:

pub fn modify<T, F: FnOnce(T) -> T>(v: &mut T, f: F) {
    let prev = unsafe { std::ptr::read(v) };
    let new = f(prev);
    unsafe { std::ptr::write(v, new) };
}

非常正确。

是吗?

fn main() {
    struct MyStruct(pub i32);
    impl Drop for MyStruct {
        fn drop(&mut self) {
            println!("MyStruct({}) dropped", self.0);
        }
    }

    let mut v = MyStruct(123);
    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        modify(&mut v, |_prev| {
            // `prev` is dropped here.
            panic!("Haha, evil panic!");
        })
    }))
    .unwrap_err();
    v.0 = 456; // Writing to an uninitialized memory!
               // `v` is dropped here, double drop!
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6f7312a8be70cd43cf5cf7a9816be56a

我使用了一个自定义类型,它的析构函数除了打印之外什么都不做,但想象一下如果这是一个释放内存的Vec 并且我们正在写入释放的内存(然后,作为奖励,得到一个双-免费)。

正如@Kendas 所说,当没有中断点时,在 Rust 中让内存处于未初始化状态是有效的。问题是,实际上中断点比您希望的要多得多。事实上,在编写不安全的代码时,您必须考虑对外部代码的任何调用(即不是您的代码,也不是您相信不会做坏事的代码,例如std)是一个中断点。

不安全的代码很难。最好留在安全的地方。

编辑:您可能想知道AssertUnwindSafe 是什么。也许您甚至尝试删除它并注意到它没有编译。好吧,UnwindSafe 是针对这种情况的保护,AssertUnwindSafe 是一种绕过保护的方法。

你可能会问,这有什么意义?关键是,这种保护确实不准确。如此不准确,绕过它甚至不需要不安全。但它仍然存在,所以我们意外UB的可能性较低。

作为 API 的编写者,这对你来说并不重要 - 你应该表现得好像这种保护不存在,因为绕过它是安全的,而且很容易被错误地绕过。 Rust 标准库本身在过去也有类似的错误 (#86443, #81740, ... - 它们都在同一个代码中并非偶然 - 这些问题往往以块的形式出现。但是有'更多)。

【讨论】:

    猜你喜欢
    • 2022-07-16
    • 1970-01-01
    • 1970-01-01
    • 2021-12-21
    • 2021-06-10
    • 1970-01-01
    • 2018-12-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多