I'm running into a this weird issue where the program succeeds or fails based on if a put my unsafe
block in the constructor or not. Not sure if it is a bug or I am just getting lucky in the latter or I'm just missing something important.
我遇到了一个奇怪的问题,程序的成功或失败取决于是否在构造函数中放置不安全的块。我不确定这是不是一个错误,或者我只是在后者中走运,或者我只是错过了一些重要的东西。
Failing case:
失败案例:
fn main() {
let mut xc = Bob::default();
let mut xp = Bob::default();
let mut x = Bob::new(Some(&mut xc), Some(&mut xp));
unsafe {
(&mut *x.child.unwrap()).string = String::from("child");
(&mut *xp.child.unwrap()).string = String::from("middle");
}
println!("{:?}", xc);
println!("{:?}", x);
}
#[derive(Debug)]
struct Bob {
child: Option<*mut Bob>,
parent: Option<*mut Bob>,
string: String,
}
impl Bob {
fn new(child: Option<*mut Bob>, parent: Option<*mut Bob>) -> Bob {
let mut this = Bob {
child,
parent,
string: String::from("Hello")
};
if child.is_some(){
unsafe { child.unwrap().as_mut().unwrap().parent = Some(&mut this);}
}
if parent.is_some(){
unsafe { parent.unwrap().as_mut().unwrap().child = Some(&mut this);}
}
this
}
}
impl Default for Bob {
fn default() -> Bob {
Bob {
child: None,
parent: None,
string: String::from("Hello")
}
}
}
Bob { child: None, parent: Some(0x37b76ff7f8), string: "child" }
Bob { child: Some(0x37b76ff8b8), parent: Some(0x37b76ff8f0), string: "thread 'main' panicked at 'failed printing to stdout: Windows stdio in console mode does not support writing non-UTF-8 byte sequences', library\std\src\io\stdio.rs:1019:9
stack backtrace:
0: std::panicking::begin_panic_handler
at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library\std\src\panicking.rs:593
1: core::panicking::panic_fmt
at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library\core\src\panicking.rs:67
2: std::io::stdio::print_to
at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library\std\src\io\stdio.rs:1019
3: std::io::stdio::_print
at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library\std\src\io\stdio.rs:1096
4: rust_allocators::main
at .\src\main.rs:14
5: core::ops::function::FnOnce::call_once<void (*)(),tuple$<> >
at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225\library\core\src\ops\function.rs:250
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
error: process didn't exit successfully: `target\debug\rust-allocators.exe` (exit code: 0xc0000374, STATUS_HEAP_CORRUPTION)
Process finished with exit code -1073740940 (0xC0000374)
Working alternative:
工作替代品:
fn main() {
let mut xc = Bob::default();
let mut xp = Bob::default();
let mut x = Bob::new(Some(&mut xc), Some(&mut xp));
xp.child = Some(&mut x);
// unsafe { Some(&mut xp as *mut Bob).unwrap().as_mut().unwrap().child = Some(&mut x);} // ALSO WORKS
unsafe {
(&mut *x.child.unwrap()).string = String::from("child");
(&mut *xp.child.unwrap()).string = String::from("middle");
}
println!("{:?}", xc);
println!("{:?}", x);
}
#[derive(Debug)]
struct Bob {
child: Option<*mut Bob>,
parent: Option<*mut Bob>,
string: String,
}
impl Bob {
fn new(child: Option<*mut Bob>, parent: Option<*mut Bob>) -> Bob {
let mut this = Bob {
child,
parent,
string: String::from("Hello")
};
if child.is_some(){
unsafe { child.unwrap().as_mut().unwrap().parent = Some(&mut this);}
}
//if parent.is_some(){
// unsafe { parent.unwrap().as_mut().unwrap().child = Some(&mut this);}
//}
this
}
}
impl Default for Bob {
fn default() -> Bob {
Bob {
child: None,
parent: None,
string: String::from("Hello")
}
}
}
Bob { child: None, parent: Some(0x990ecff628), string: "child" }
Bob { child: Some(0x990ecff708), parent: Some(0x990ecff740), string: "middle" }
I expected both cases to work.
我原以为这两个案子都能奏效。
更多回答
Yes, you indeed are missing something important - the very purpose of Rust! Not only you're absolutely ignoring error handling, but also just returning a dangling pointer to the stack. You need to get to know both Rust and systems programming in general a little bit better before experimenting this way. In this case, as @cafce25 pointed out, you can use heap, but it would a mere crutch in terms of Rust. This entire logic needs to be rewritten. Without raw pointers when possible.
是的,你确实错过了一些重要的东西--铁锈的真正目的!您不仅完全忽略了错误处理,而且只返回了指向堆栈的悬空指针。在尝试这种方式之前,您需要更好地了解Rust和总体上的系统编程。在这种情况下,正如@afce25所指出的那样,您可以使用heap,但就锈蚀而言,它仅仅是一根拐杖。这整个逻辑需要重写。尽可能不使用原始指针。
@drewtato: I would note that Miri checks are not the end-all be-all, if anything, the existence of both the (original) Stacked Borrows model and the (new) Tree Borrows model shows that the exact semantics are still up for debate. As such, I would not advise treating Miri as an absolute. If it passes Miri, it's a good sign, if it doesn't, it's a bad sign, but it could pass Miri and still crash and it could not pass Miri and still not result in UB.
@drewatts:我要指出的是,Miri支票并不是最终的一切,如果说有什么不同的话,那就是(原始的)堆叠借入模型和(新的)树借入模型的存在表明,确切的语义仍然有待辩论。因此,我不建议将MIRI视为绝对的。如果它超过了米里,这是一个好兆头,如果它没有通过,这是一个坏迹象,但它可能会超过米里,但仍然会崩溃,但它不能超过米里,仍然不会导致UB。
As mentioned by drewtato, there's a fantastic tool called Miri which is very helpful -- though not absolute -- and points out a lot of likely flaws in unsafe code. You can install it with rustup +nightly component add miri
, and then run cargo miri test
to run your codes unit-tests under miri.
正如Drewatts提到的,有一个非常棒的工具,名为Miri,它非常有用--尽管不是绝对的--并指出了不安全代码中的许多可能的缺陷。您可以使用Rustup+夜间组件添加Miri来安装它,然后在Miri下运行Cargo Miri测试来运行您的代码单元测试。
The pointer to this
taken here
指向这个的指针在这里
unsafe { child.unwrap().as_mut().unwrap().parent = Some(&mut this);}
is pointing to a function local variable, it becomes dangling as soon as you return from that function! Dereferencing it afterwards is undefined behavior. Returning a value from a function moves it, so it's address might change.
指向一个函数局部变量,当您从该函数返回时,它就会变得悬空!事后取消引用它是未定义的行为。从函数返回值会移动它,因此它的地址可能会改变。
And while miri puts it in other words, it does say the same thing:
换句话说,虽然米莉说了这句话,但它确实表达了同样的意思:
error: Undefined Behavior: dereferencing pointer failed: alloc993 has been freed, so this pointer is dangling
--> src/main.rs:8:9
|
8 | (&mut *xp.child.unwrap()).string = String::from("middle");
| ^^^^^^^^^^^^^^^^^^^^^^^^^ dereferencing pointer failed: alloc993 has been freed, so this pointer is dangling
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
help: alloc993 was allocated here:
--> src/main.rs:22:13
|
22 | let mut this = Bob {
| ^^^^^^^^
help: alloc993 was deallocated here:
--> src/main.rs:34:5
|
34 | }
| ^
= note: BACKTRACE (of the first span):
= note: inside `main` at src/main.rs:8:9: 8:34
The second version works because you create the pointer and don't subsequently move the values so you don't access any dangling pointers, the parent
pointer of xc
is still dangling, but you don't access it so that's fine.
第二个版本之所以有效,是因为您创建了指针,并且不会随后移动值,因此您不会访问任何悬空指针,XC的父指针仍然悬空,但您不会访问它,所以这很好。
There is a way to pull this off, we have to ensure
有办法做到这一点,我们必须确保
- the actual value doesn't move in memory when we return — we can
Box::pin
it
- it can't be moved afterwards — add a
PhantomPinned
so it doesn't implement Unpin
So change to this:
因此,请更改为:
use core::marker::PhantomPinned;
use core::pin::Pin;
#[derive(Default, Debug)]
struct Bob {
child: Option<*mut Bob>,
parent: Option<*mut Bob>,
string: String,
_marker: PhantomPinned,
}
impl Bob {
fn new(child: Option<*mut Bob>, parent: Option<*mut Bob>) -> Pin<Box<Bob>> {
let mut this = Box::pin(Bob {
child,
parent,
string: String::from("Hello"),
_marker: PhantomPinned,
});
if child.is_some() {
let this = this.as_mut();
unsafe {
child.unwrap().as_mut().unwrap().parent = Some(this.get_unchecked_mut());
}
}
if parent.is_some() {
let this = this.as_mut();
unsafe {
parent.unwrap().as_mut().unwrap().child = Some(this.get_unchecked_mut());
}
}
this
}
}
更多回答
Amazing explanation and solution, thanks a lot! I didn't realize that "moves" may or may not (this is not a language constraint) result in a stack copy and de-initialization of old stack data, which may cause dangling references. Sounds like you should always use pinning if using raw pointers in these cases. Some additional references: users.rust-lang.org/t/why-do-values-need-to-be-moved/55971/27 reddit.com/r/rust/comments/qpaifu/…
精彩的解释和解决方案,非常感谢!我没有意识到“移动”可能会也可能不会(这不是语言限制)导致堆栈复制和旧堆栈数据的取消初始化,这可能会导致悬空引用。听起来,如果在这些情况下使用原始指针,您应该始终使用钉住。其他参考文献:users.rust-lang.org/t/why-do-values-need-to-be-moved/55971/27 reddit.com/r/rust/Comments/qpaifu/…
@mcmah309 the problem is not about pinning, it's about allocating on the stack or allocating on the heap. If a variable is allocated on the stack, then any pointer to it will become invalid as soon as the frame it lives in is destroyed (in Rust as in any other language, that's just how the stack works), which happens when the function returns.
@mcmah309问题不在于固定,而在于在堆栈上分配还是在堆上分配。如果在堆栈上分配了一个变量,那么指向它的任何指针一旦它所在的框架被销毁(在Rust中就像在任何其他语言中一样,堆栈就是这样工作的),那么它的任何指针就会变得无效,这在函数返回时发生。
@jthulhu learning here, so bear with me. But I don't believe this is about stack vs heap, but rather how ownership is transfered behind the scenes. Your answer suggests to me if I created a variable inside a function and returned it that would be an issue. If I don't use pinning and just use a Box
the code fails.
@jthulhu在这里学习,所以请容忍我。但我不认为这是关于堆栈和堆的问题,而是关于所有权如何在幕后转移的问题。你的回答告诉我,如果我在函数中创建了一个变量并返回它,那将是一个问题。如果我不使用锁定,而只是使用Box,代码就会失败。
@jthulhu: I wouldn't frame it on stack vs heap. While you are correct that returning the value moves it from the current frame to the parent frame (well, may move it), you can put a value on the heap and still move it...
@jthulhu:我不会把它放在堆栈和堆上。虽然返回值将它从当前帧移动到父帧(嗯,可能会移动它)是正确的,但您可以将值放在堆上并仍然移动它……
我是一名优秀的程序员,十分优秀!