drop check

drop check/may_dangle

Drop checker会检查一个类型是否能够安全地实现Drop Tarit。如果一个能够安全实现Drop的类型,那么它的泛型参数的生命周期必须严格长于它本身。一个违反Drop check的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#![allow(unused)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::fmt;
use std::mem;
use std::ptr;


fn main() {
let (y, x);
x = String::from("Hello World");
y = MyBox::new(&x);
}

struct MyBox<T> {
v: *mut T,
}

impl<T> MyBox<T> {
fn new(t: T) -> Self {
unsafe {
let p = System.alloc(Layout::array::<T>(1).unwrap());
let p = p as *mut T;
ptr::write(p, t);
MyBox {
v: p,
}
}
}
}

impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
ptr::drop_in_place(self.v);
let p = self.v as *mut _;
System.dealloc(p, Layout::array::<T>(1).unwrap());
}
}
}

编译将会发生报错:

1
2
3
4
5
6
7
8
9
10
11
12
error[E0597]: `x` does not live long enough
--> src/main.rs:13:20
|
13 | y = MyBox::new(&x);
| ^^ borrowed value does not live long enough
14 | }
| -
| |
| `x` dropped here while still borrowed
| borrow might be used here, when `y` is dropped and runs the `Drop` code for type `MyBox`
|
= note: values in a scope are dropped in the opposite order they are defined

从表面上看,xy的生命周期是一样长的。但是xy先定义,因此变量x会首先发生析构,因此x的生命周期并不严格长于y的生命周期,这显然不满足Drop checker的条件,因此发生报错:编译器认为在调用y的析构函数时,可能会使用x的引用,这会导致UB,因此编译器拒绝这段代码。

但是,实际上我们并没有在y的析构函数使用x的引用,不会出现UB行为。为了解决这个问题,可以开启特性#![feature(dropck_eyepatch)],并使用属性#[may_dangle]注解T,明确表示不会在y的析构函数中使用x的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#![allow(unused)]
#![feature(dropck_eyepatch)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::fmt;
use std::mem;
use std::ptr;


fn main() {
let (y, x);
x = String::from("Hello World");
y = MyBox::new(&x);
}

struct MyBox<T> {
v: *mut T,
}

impl<T> MyBox<T> {
fn new(t: T) -> Self {
unsafe {
let p = System.alloc(Layout::array::<T>(1).unwrap());
let p = p as *mut T;
ptr::write(p, t);
MyBox {
v: p,
}
}
}
}

unsafe impl<#[may_dangle] T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
ptr::drop_in_place(self.v);
let p = self.v as *mut _;
System.dealloc(p, Layout::array::<T>(1).unwrap());
}
}
}

编译将会顺利通过:

1
2
3
Compiling playground v0.0.1 (/playground)
Finished dev [unoptimized + debuginfo] target(s) in 7.20s
Running `target/debug/playground`

phantomData

使用may_dangle后编译器将不会进行Drop check检查,但是在下面的代码中将会出现UB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#![allow(unused)]
#![feature(dropck_eyepatch)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::fmt;
use std::mem;
use std::ptr;


fn main() {
let (y, x);
x = Hello::new("x", 13);
y = MyBox::new(Hello::new("y", &x));
}

#[derive(Copy, Clone, Debug)]
enum State {
Invalid,
Valid,
}

#[derive(Debug)]
struct Hello<T: fmt::Debug>(&'static str, T, State);

impl<T: fmt::Debug> Hello<T> {
fn new(name: &'static str, t: T) -> Self {
Hello(name, t, State::Valid)
}
}

impl<T: fmt::Debug> Drop for Hello<T> {
fn drop(&mut self) {
println!("Drop hello({}, {:?}, {:?})", self.0, self.1, self.2);
self.2 = State::Invalid;
}
}

struct MyBox<T> {
v: *mut T,
// _pd: PhantomData<T>
}

impl<T> MyBox<T> {
fn new(t: T) -> Self {
unsafe {
let p = System.alloc(Layout::array::<T>(1).unwrap());
let p = p as *mut T;
ptr::write(p, t);
MyBox {
v: p,
// _pd: PhantomData
}
}
}
}

unsafe impl<#[may_dangle] T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
ptr::drop_in_place(self.v);
let p = self.v as *mut _;
System.dealloc(p, Layout::array::<T>(1).unwrap());
}
}
}

编译结果:

1
2
Drop hello(x, 13, Valid)
Drop hello(y, Hello("x", 13, Invalid), Valid)

使用MIRI检查是否存在UB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
error: Undefined Behavior: pointer to alloc999 was dereferenced after this allocation got freed
--> /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:2116:1
|
2116 | fmt_refs! { Debug, Display, Octal, Binary, LowerHex, UpperHex, LowerExp, UpperExp }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ pointer to alloc999 was dereferenced after this allocation got freed
|
= 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

= note: inside `<&Hello<i32> as std::fmt::Debug>::fmt` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:2106:71
= note: inside `std::fmt::write` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:1168:17
= note: inside `<std::io::StdoutLock as std::io::Write>::write_fmt` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/mod.rs:1653:15
= note: inside `<&std::io::Stdout as std::io::Write>::write_fmt` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:844:9
= note: inside `<std::io::Stdout as std::io::Write>::write_fmt` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:818:9
= note: inside `std::io::stdio::print_to::<std::io::Stdout>` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:1186:21
= note: inside `std::io::_print` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:1199:5
note: inside `<Hello<&Hello<i32>> as std::ops::Drop>::drop` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/macros.rs:99:9
--> src/main.rs:32:9
|
32 | println!("Drop hello({}, {:?}, {:?})", self.0, self.1, self.2);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: inside `std::ptr::drop_in_place::<Hello<&Hello<i32>>> - shim(Some(Hello<&Hello<i32>>))` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:188:1
note: inside `<MyBox<Hello<&Hello<i32>>> as std::ops::Drop>::drop` at src/main.rs:59:13
--> src/main.rs:59:13
|
59 | ptr::drop_in_place(self.v);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: inside `std::ptr::drop_in_place::<MyBox<Hello<&Hello<i32>>>> - shim(Some(MyBox<Hello<&Hello<i32>>>))` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:188:1
note: inside `main` at src/main.rs:13:1

之所以会出现上述结果,是因为在y的析构函数中,我们调用了T: Hello::new("y", &x)的析构函数,在T的析构函数中会打印&x的值,而x在此时已经被析构了,它的值变成了Hello(x,13,Invalid)。因此y中的&x变成了悬垂引用,并且在T的析构函数中使用了&x,这显然是一种UB。

为了防止UB,我们可以在MyBox中添加PhantomData字段,表示MyBox拥有T,会在MyBox的析构函数中析构T(当然MyBox并不拥有T,也不一定会析构T),告诉编译器对MyBox进行Drop check检查。

修改上述代码,添加PhantomData字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#![allow(unused)]
#![feature(dropck_eyepatch)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::fmt;
use std::mem;
use std::ptr;
use std::marker::PhantomData;


fn main() {
let (y, x);
x = Hello::new("x", 13);
y = MyBox::new(Hello::new("y", &x));
}

#[derive(Copy, Clone, Debug)]
enum State {
Invalid,
Valid,
}

#[derive(Debug)]
struct Hello<T: fmt::Debug>(&'static str, T, State);

impl<T: fmt::Debug> Hello<T> {
fn new(name: &'static str, t: T) -> Self {
Hello(name, t, State::Valid)
}
}

impl<T: fmt::Debug> Drop for Hello<T> {
fn drop(&mut self) {
println!("Drop hello({}, {:?}, {:?})", self.0, self.1, self.2);
self.2 = State::Invalid;
}
}

struct MyBox<T> {
v: *mut T,
_pd: PhantomData<T>
}

impl<T> MyBox<T> {
fn new(t: T) -> Self {
unsafe {
let p = System.alloc(Layout::array::<T>(1).unwrap());
let p = p as *mut T;
ptr::write(p, t);
MyBox {
v: p,
_pd: PhantomData
}
}
}
}

unsafe impl<#[may_dangle] T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
ptr::drop_in_place(self.v);
let p = self.v as *mut _;
System.dealloc(p, Layout::array::<T>(1).unwrap());
}
}
}

编译将会报错:

1
2
3
4
5
6
7
8
9
10
11
12
error[E0597]: `x` does not live long enough
--> src/main.rs:13:36
|
13 | y = MyBox::new(Hello::new("y", &x));
| ^^ borrowed value does not live long enough
14 | }
| -
| |
| `x` dropped here while still borrowed
| borrow might be used here, when `y` is dropped and runs the `Drop` code for type `MyBox`
|
= note: values in a scope are dropped in the opposite order they are defined

这表明Drop checker起了作用,有效防止出现UB。

但是,这似乎与最初版的代码的编译是一样的,那要PhantomDatamay_dangle有什么用?答案当然是有用的,如果MyBox<T>中的T没有实现Drop trait,那么上述代码将会编译通过。

删除Hello的析构函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#![allow(unused)]
#![feature(dropck_eyepatch)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::fmt;
use std::mem;
use std::ptr;
use std::marker::PhantomData;


fn main() {
let (y, x);
x = Hello::new("x", 13);
y = MyBox::new(Hello::new("y", &x));
}

#[derive(Copy, Clone, Debug)]
enum State {
Invalid,
Valid,
}

#[derive(Debug)]
struct Hello<T: fmt::Debug>(&'static str, T, State);

impl<T: fmt::Debug> Hello<T> {
fn new(name: &'static str, t: T) -> Self {
Hello(name, t, State::Valid)
}
}

struct MyBox<T> {
v: *mut T,
_pd: PhantomData<T>
}

impl<T> MyBox<T> {
fn new(t: T) -> Self {
unsafe {
let p = System.alloc(Layout::array::<T>(1).unwrap());
let p = p as *mut T;
ptr::write(p, t);
MyBox {
v: p,
_pd: PhantomData
}
}
}
}

unsafe impl<#[may_dangle] T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
ptr::drop_in_place(self.v);
let p = self.v as *mut _;
System.dealloc(p, Layout::array::<T>(1).unwrap());
}
}
}

编译将会通过:

1
2
3
Compiling playground v0.0.1 (/playground)
Finished dev [unoptimized + debuginfo] target(s) in 1.53s
Running `target/debug/playground`

这是因为,如果MyBox<T>中的T没有实现Drop Trait,就算T中存在悬垂引用,也不可能在MyBox的析构函数中通过T的析构函数访问这个悬垂引用,因此不会出现UB,

如果不使用may_danglePhantomData,那么即使T没有实现Drop trait,代码也不会通过编译,即编译器会拒绝掉正确的代码,这显然不是我们所期望的。

总结

综上所述,may_danglePhantomData结合使用将会有如下效果:

  • 如果MyBox<T>中的T实现了Drop Trait,那么Drop Checker会要求T的生命周期严格长于MyBox
  • 如果MyBox<T>中的T没有实现Drop Trait,那么Drop Checker不会会要求T的生命周期严格长于MyBox

如果没有使用may_danglePhantomData,那么无论T有没有实现Drop Trait,Drop Checker都会要求T的生命周期严格长于MyBox

参考:


drop check
https://night-cruise.github.io/2022/05/22/drop-check/
作者
Night Cruise
发布于
2022年5月22日
许可协议