From 14685b8a123fbe4197a9f798ebfdb0899fabd668 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Mon, 13 Feb 2023 22:49:34 -0300 Subject: [PATCH 01/10] Pre-RFC: dropck-implication --- text/0000-dropck-implication.md | 397 ++++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 text/0000-dropck-implication.md diff --git a/text/0000-dropck-implication.md b/text/0000-dropck-implication.md new file mode 100644 index 00000000000..3171bd4b294 --- /dev/null +++ b/text/0000-dropck-implication.md @@ -0,0 +1,397 @@ +- Feature Name: `dropck_implication` +- Start Date: 2023-02-13 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +Provide a flexible framework for understanding and resolving dropck obligations, +built upon implied bounds, simplify dropck elaboration, and effectively +stabilize (a refinement of) `may_dangle`. + +# Motivation +[motivation]: #motivation + +TODO + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +## Liveness obligations + +Liveness obligations are effectively bounds related to the liveness of a +type/lifetime. Lifetime bounds are themselves a form of liveness obligations: +a lifetime bound defines an outlives relationship between a type/lifetime and +another lifetime. However, there are also 3 kinds of liveness obligations which +are specially relevant to dropck. These can also be called dropck obligations, +and they are named as such: + +1. You must not touch the type/value in question. +2. You may only drop the type/value in question. +3. You may freely access the type/value in question. + +For a type which does not itself implement `Drop`, these are implied by what's +in the type. These implications are akin to variance or traits like `Send` and +`Sync`. Unlike `Send`/`Sync` these cannot be overriden by non-`Drop` types. + +For a type which implements `Drop`, these obligations can be added to the type +parameters, in which case they must be explicitly specified on both the type +and the `Drop` impl. + +### You may only drop the type/value + +The most interesting of these obligations is probably obligation type 2: you may +only drop the type/value. This obligation is particularly interesting for +collection types. + +For example, consider a custom `Box`: + +```rust +struct MyBox { + inner: *const T, +} + +impl MyBox { + fn new(t: T) -> Self { ... } +} + +impl Drop for MyBox { + fn drop(&mut self) { ... } +} +``` + +This is... fine. However, actual `Box`es have the following property: + +```rust +let x = String::new(); +let y = Box::new(&x); +drop(x); +// y implicitly dropped here +``` + +Meanwhile, using `MyBox` produces the following error: + +```text +error[E0505]: cannot move out of `x` because it is borrowed + --> src/main.rs:16:10 + | +15 | let y = MyBox::new(&x); + | -- borrow of `x` occurs here +16 | drop(x); + | ^ move out of `x` occurs here +17 | // y implicitly dropped here +18 | } + | - borrow might be used here, when `y` is dropped and runs the `Drop` code for type `MyBox` +``` + +As the error says, "borrow might be used here, when `y` is dropped and runs the +`Drop` code for type `MyBox`". To allow `MyBox` to have this property, simply +put a "You may only drop the type/value in question." bound on `T`, like so: + +```rust +struct MyBox { + inner: *const T, +} + +impl MyBox { + fn new(t: T) -> Self { ... } +} + +impl Drop for MyBox { + fn drop(&mut self) { ... } +} +``` + +This "You may only drop the type/value in question." bound, represented by +`T: '!`, is only (directly) compatible with exactly one function: +`core::ptr::drop_in_place`, which has a `T: '!` bound. (N.B. it's also perfectly +fine to wrap `drop_in_place` and call it indirectly, as long as the dropck +bound is carried around.) Trying to use `T` in any other way causes an error. +For our example, this bound prevents the `Drop` impl from using the borrow, +tho it can still be dropped. (Obviously, dropping a borrow is a no-op.) + +### You may freely access the type/value in question + +This obligation is the default obligation (requires no additional syntax), for +both backwards compatibility reasons and because it puts no restrictions on the +`Drop` impl: the `Drop` impl can do whatever it wants with the given type/value. + +For example, it could print something out on `Drop`: + +```rust +use core::fmt::Display; + +struct PrintOnDrop(T); + +impl Drop for PrintOnDrop { + fn drop(&mut self) { + println!("{}", self.0); + } +} +``` + +This is only possible while the type parameter is still live. As such, the +compiler rejects the following code: + +```rust +let x = String::new(); +let y = PrintOnDrop(&x); +drop(x); +// y dropped here +``` + +```text +error[E0505]: cannot move out of `x` because it is borrowed + --> src/main.rs:14:10 + | +13 | let y = PrintOnDrop(&x); + | -- borrow of `x` occurs here +14 | drop(x); + | ^ move out of `x` occurs here +15 | // y implicitly dropped here +16 | } + | - borrow might be used here, when `y` is dropped and runs the `Drop` code for type `PrintOnDrop` +``` + +And it also rejects an `T: '!` bound: + +```rust +struct PrintOnDrop(T); + +impl Drop for PrintOnDrop { + fn drop(&mut self) { + println!("{}", self.0); + } +} +``` + +```text +TODO +``` + +### You must not touch the type/value in question + +This obligation prevents you from touching the type/value in your `Drop` impl +entirely. This is also the most flexible for the *user* of your type. + +So far, the other 2 dropck obligations have applied to type parameters. This +dropck obligation instead applies to lifetimes. + +Actually, there are 2 forms of it: As applied to one or more lifetimes, it +prevents accessing those lifetimes. As applied to *all possible* lifetimes, it +prevents accessing the *type* entirely. A bound can be applied to all possible +lifetimes with the use of HRTB syntax, i.e. `for<'a>`. + +#### As applied to one (or more) lifetimes + +[modified from tests/ui/dropck/issue-28498-ugeh-with-lifetime-param.rs] + +[TODO explain] + +```rust +// run-pass + +// Demonstrate the use of the unguarded escape hatch with a lifetime param +// to assert that destructor will not access any dead data. +// +// Compare with ui/span/issue28498-reject-lifetime-param.rs + +#![feature(dropck_implication)] + +#[derive(Debug)] +struct ScribbleOnDrop(String); + +impl Drop for ScribbleOnDrop { + fn drop(&mut self) { + self.0 = format!("DROPPED"); + } +} + +struct Foo<'a: '!>(u32, &'a ScribbleOnDrop); + +unsafe impl<'a: '!> Drop for Foo<'a> { + fn drop(&mut self) { + // Use of `'a: '!` is sound, because destructor never accesses `self.1`. + println!("Dropping Foo({}, _)", self.0); + } +} + +fn main() { + let (last_dropped, foo0); + let (foo1, first_dropped); + + last_dropped = ScribbleOnDrop(format!("last")); + first_dropped = ScribbleOnDrop(format!("first")); + foo0 = Foo(0, &last_dropped); + foo1 = Foo(1, &first_dropped); + + println!("foo0.1: {:?} foo1.1: {:?}", foo0.1, foo1.1); +} +``` + +#### As applied to all possible lifetimes + +[TODO explain, this is more of a construct to enable the semantics of &T etc] + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +TODO: nesting rules + +The following types have the given dropck implications (based on existing usage +of `#[may_dangle]`): + +```text +ManuallyDrop where for<'a> &'a T: '! +PhantomData where for<'a> &'a T: '! +*const T where for<'a> &'a T: '! +*mut T where for<'a> &'a T: '! +&'_ T where for<'a> &'a T: '! +&'_ mut T where for<'a> &'a T: '! + +OnceLock +RawVec +Rc +rc::Weak +VecDeque +BTreeMap +LinkedList +Box +Vec +vec::IntoIter +Arc +sync::Weak +``` + +# Drawbacks +[drawbacks]: #drawbacks + +Why should we *not* do this? + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +- Why is this design the best in the space of possible designs? +- What other designs have been considered and what is the rationale for not choosing them? +- What is the impact of not doing this? +- If this is a language proposal, could this be done in a library or macro instead? Does the proposed change make Rust code easier or harder to read, understand, and maintain? + +# Prior art +[prior-art]: #prior-art + +Discuss prior art, both the good and the bad, in relation to this proposal. +A few examples of what this can include are: + +- For language, library, cargo, tools, and compiler proposals: Does this feature exist in other programming languages and what experience have their community had? +- For community proposals: Is this done by some other community and what were their experiences with it? +- For other teams: What lessons can we learn from what other communities have done here? +- Papers: Are there any published papers or great posts that discuss this? If you have some relevant papers to refer to, this can serve as a more detailed theoretical background. + +This section is intended to encourage you as an author to think about the lessons from other languages, provide readers of your RFC with a fuller picture. +If there is no prior art, that is fine - your ideas are interesting to us whether they are brand new or if it is an adaptation from other languages. + +Note that while precedent set by other languages is some motivation, it does not on its own motivate an RFC. +Please also take into consideration that rust sometimes intentionally diverges from common language features. + +- Compiler MCP 563: This RFC was supposed to come after the implementation of MCP 563 but that didn't happen. This RFC is basically a refinement of the ideas in the MCP. +- Unsound dropck elaboration for `BTreeMap`: + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +- What parts of the design do you expect to resolve through the RFC process before this gets merged? +- What parts of the design do you expect to resolve through the implementation of this feature before stabilization? +- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? + +## Spooky-dropck-at-a-distance + +Currently, the following code is not accepted by the compiler: + +```rust +use core::cell::Cell; +use core::marker::PhantomData; + +struct Dropper(T); + +impl Drop for Dropper { + fn drop(&mut self) {} +} + +struct Foo<'a, T>(PhantomData>>>, T); + +fn main() { + fn make_selfref<'a, T>(x: &'a Foo<'a, T>){} + let x = Foo(PhantomData, String::new()); + make_selfref(&x); +} +``` + +At the same time, replacing `String::new()` with `()` makes it compile, which +is really surprising: after all, the error is seemingly unrelated to the +`String`. This is an interaction between at least 4 factors: `T` having drop +glue, the presence of `Dropper`, the invariance of `'a`, and the use of +`make_selfref`. + +This RFC recommends removing this spooky-dropck-at-a-distance behaviour +entirely, to make the language less surprising. Since this RFC ties +user-defined dropck obligations with a `Drop` impl, it automatically prevents +users from defining similar spooky-dropck behaviour. If removed, the above +example would be accepted by the compiler. + +However, this spooky-dropck behaviour can also be used in no-alloc crates to +detect potentially-unsound `Drop` impls in current stable. For example, the +`selfref` crate *could* do something like this: + +```rust +type UBCheck...; + +// paste selfref::opaque! ub_check here. +``` + +But this RFC provides an alternative way which does not require spooky-dropck, +and `selfref` explicitly does not rely on spooky-dropck as the behaviour of +spooky-dropck has changed in the past: namely, between Rust 1.35 and Rust 1.36, +in the 2015 edition: . +(Arguably, this is when it *became* "spooky".) + +# Future possibilities +[future-possibilities]: #future-possibilities + +## `dyn Trait` dropck obligations + +Currently, if you attempt to make a self-referential (e.g.) `dyn Iterator`, you +get an error: + +```rust +use core::cell::RefCell; + +#[derive(Default)] +struct Foo<'a> { + vec: RefCell>>>, + iter: RefCell>>>>, +} + +fn main() { + let x = Foo::default(); + x.vec.borrow_mut().insert(vec![]).push(&x); +} +``` + +```text +error[E0597]: `x` does not live long enough + --> src/main.rs:11:44 + | +11 | x.vec.borrow_mut().insert(vec![]).push(&x); + | ^^ borrowed value does not live long enough +12 | } + | - + | | + | `x` dropped here while still borrowed + | borrow might be used here, when `x` is dropped and runs the destructor for type `Foo<'_>` +``` + +A future RFC could build on this RFC to allow traits to demand dropck +obligations from their implementers. Adding these bounds to existing traits +would be semver-breaking, so it can't be done with `Iterator`, but it could +be useful for other traits. From 21900ffd560b2dd71289bd3ef2ae80da6f7176e1 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Mon, 13 Feb 2023 22:51:41 -0300 Subject: [PATCH 02/10] Clean up slightly --- text/0000-dropck-implication.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/text/0000-dropck-implication.md b/text/0000-dropck-implication.md index 3171bd4b294..af5e4438272 100644 --- a/text/0000-dropck-implication.md +++ b/text/0000-dropck-implication.md @@ -280,30 +280,12 @@ Why should we *not* do this? # Prior art [prior-art]: #prior-art -Discuss prior art, both the good and the bad, in relation to this proposal. -A few examples of what this can include are: - -- For language, library, cargo, tools, and compiler proposals: Does this feature exist in other programming languages and what experience have their community had? -- For community proposals: Is this done by some other community and what were their experiences with it? -- For other teams: What lessons can we learn from what other communities have done here? -- Papers: Are there any published papers or great posts that discuss this? If you have some relevant papers to refer to, this can serve as a more detailed theoretical background. - -This section is intended to encourage you as an author to think about the lessons from other languages, provide readers of your RFC with a fuller picture. -If there is no prior art, that is fine - your ideas are interesting to us whether they are brand new or if it is an adaptation from other languages. - -Note that while precedent set by other languages is some motivation, it does not on its own motivate an RFC. -Please also take into consideration that rust sometimes intentionally diverges from common language features. - - Compiler MCP 563: This RFC was supposed to come after the implementation of MCP 563 but that didn't happen. This RFC is basically a refinement of the ideas in the MCP. - Unsound dropck elaboration for `BTreeMap`: # Unresolved questions [unresolved-questions]: #unresolved-questions -- What parts of the design do you expect to resolve through the RFC process before this gets merged? -- What parts of the design do you expect to resolve through the implementation of this feature before stabilization? -- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? - ## Spooky-dropck-at-a-distance Currently, the following code is not accepted by the compiler: From d8abf981c60e43286287704b37a95792e1b51bf1 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Mon, 13 Feb 2023 23:33:00 -0300 Subject: [PATCH 03/10] Clean up unsafe impl --- text/0000-dropck-implication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-dropck-implication.md b/text/0000-dropck-implication.md index af5e4438272..ba236fc2dd1 100644 --- a/text/0000-dropck-implication.md +++ b/text/0000-dropck-implication.md @@ -210,9 +210,9 @@ impl Drop for ScribbleOnDrop { struct Foo<'a: '!>(u32, &'a ScribbleOnDrop); -unsafe impl<'a: '!> Drop for Foo<'a> { +impl<'a: '!> Drop for Foo<'a> { fn drop(&mut self) { - // Use of `'a: '!` is sound, because destructor never accesses `self.1`. + // Use of `'a: '!` means destructor cannot access `self.1`. println!("Dropping Foo({}, _)", self.0); } } From d5d6f010b9f8daed2794052920f921bfa1cc6c2b Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Wed, 15 Feb 2023 08:33:10 -0300 Subject: [PATCH 04/10] Update --- text/0000-dropck-implication.md | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/text/0000-dropck-implication.md b/text/0000-dropck-implication.md index ba236fc2dd1..539da2a63a5 100644 --- a/text/0000-dropck-implication.md +++ b/text/0000-dropck-implication.md @@ -244,10 +244,11 @@ of `#[may_dangle]`): ```text ManuallyDrop where for<'a> &'a T: '! -PhantomData where for<'a> &'a T: '! +PhantomData where for<'a> &'a T: '! // see below for unresolved questions +[T; 0] where for<'a> &'a T: '! // see below for unresolved questions *const T where for<'a> &'a T: '! *mut T where for<'a> &'a T: '! -&'_ T where for<'a> &'a T: '! +&'_ T where for<'a> &'a T: '! // N.B. this is special &'_ mut T where for<'a> &'a T: '! OnceLock @@ -262,6 +263,8 @@ Vec vec::IntoIter Arc sync::Weak +HashMap +// FIXME: other types which currently use may_dangle but were not found by grep ``` # Drawbacks @@ -272,16 +275,21 @@ Why should we *not* do this? # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives -- Why is this design the best in the space of possible designs? -- What other designs have been considered and what is the rationale for not choosing them? -- What is the impact of not doing this? -- If this is a language proposal, could this be done in a library or macro instead? Does the proposed change make Rust code easier or harder to read, understand, and maintain? +This design is a further refinement of RFC 1327 with intent to stabilize. It +tries to avoid introducing too many new concepts, but does attempt to integrate +the lessons learned from RFC 1327 into the type system - specifically, allowing +them to be checked by the compiler. + +In particular, this design explicitly prevents the user from doing unsound +operations in safe code, while also allowing `impl Drop` to be *safe* even in +the presence of liveness obligations/dropck bounds. # Prior art [prior-art]: #prior-art - Compiler MCP 563: This RFC was supposed to come after the implementation of MCP 563 but that didn't happen. This RFC is basically a refinement of the ideas in the MCP. - Unsound dropck elaboration for `BTreeMap`: +- `may_dangle`: RFC 1238, RFC 1327 # Unresolved questions [unresolved-questions]: #unresolved-questions @@ -337,6 +345,12 @@ spooky-dropck has changed in the past: namely, between Rust 1.35 and Rust 1.36, in the 2015 edition: . (Arguably, this is when it *became* "spooky".) +## Behaviour of `[T; 0]` + +Should `[T; 0]` have dropck obligations? As above, this also basically falls +under spooky-dropck-at-a-distance, since `[T; 0]` lacks drop glue. This RFC +proposes treating `[T; 0]` the same as `PhantomData`, as above. + # Future possibilities [future-possibilities]: #future-possibilities @@ -377,3 +391,9 @@ A future RFC could build on this RFC to allow traits to demand dropck obligations from their implementers. Adding these bounds to existing traits would be semver-breaking, so it can't be done with `Iterator`, but it could be useful for other traits. + +## Generalize to "bounds generics" or "associated bounds" + +This proposal limits itself to dropck semantics, but a future proposal could +generalize these kinds of bounds to some sort of "bounds generics" or +"associated bounds" kind of system. From 1f26a3f3939d88d9f15f44a6d3b4abefdd1c2809 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Thu, 16 Feb 2023 10:18:34 -0300 Subject: [PATCH 05/10] Update motivation --- text/0000-dropck-implication.md | 98 +++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/text/0000-dropck-implication.md b/text/0000-dropck-implication.md index 539da2a63a5..a4ddbe4cffb 100644 --- a/text/0000-dropck-implication.md +++ b/text/0000-dropck-implication.md @@ -13,6 +13,104 @@ stabilize (a refinement of) `may_dangle`. # Motivation [motivation]: #motivation +Rust's drop checker (dropck) is an invaluable tool for making sure `impl Drop` +is sound. However, it can be too strict sometimes. Consider a custom `Box`: + +```rust +struct MyBox { + inner: *const T, +} + +impl MyBox { + fn new(t: T) -> Self { ... } +} + +impl Drop for MyBox { + fn drop(&mut self) { ... } +} +``` + +This is... fine. However, actual `Box`es have the following property: + +```rust +let x = String::new(); +let y = Box::new(&x); +drop(x); +// y implicitly dropped here +``` + +Meanwhile, using `MyBox` produces the following error: + +```text +error[E0505]: cannot move out of `x` because it is borrowed + --> src/main.rs:16:10 + | +15 | let y = MyBox::new(&x); + | -- borrow of `x` occurs here +16 | drop(x); + | ^ move out of `x` occurs here +17 | // y implicitly dropped here +18 | } + | - borrow might be used here, when `y` is dropped and runs the `Drop` code for type `MyBox` +``` + +This is where `may_dangle` comes in: it allows the impl to say "I don't touch +this parameter in a way that may cause unsoundness", and in fact the real `Box` +does use it. However, `may_dangle` was introduced as a hack specifically to make +`Box` (and collections like `Vec`) work like this, and it was never intended to +be the final form. + +`may_dangle` is itself a refinement of "unguarded escape hatch" (or UGEH for +short), because UGEH was found to be too much of a footgun even for an internal +compiler feature. UGEH effectively applied `may_dangle` to *all* parameters. But +even `may_dangle` sometimes comes back to bite, for example when dropck was +simplified, causing a broken BTreeMap implementation to become unsound. (see +rust-lang/rust#99413) + +This RFC proposes a *safe* refinement of `may_dangle`, while also making it +resistant to the pitfalls observed with the existing `may_dangle` mechanism. +This refined mechanism can be called "Liveness obligations" or "Dropck bounds". +In particular, it tries to encode the soundness obligations of `may_dangle` in +the type system, directly, so that they can be checked by the compiler. + +## Custom Box and Custom Collections + +The perhaps main use-case for a stable `may_dangle` is custom collections. With +the `MyBox` above, we can have a `Drop` impl as below: + +```rust +impl Drop for MyBox { + fn drop(&mut self) { + unsafe { + drop_in_place(self.inner); + free(self.inner); + } + } +} +``` + +(N.B. this still uses `unsafe`! however, the unsafety is about upholding the +contract of `drop_in_place` and `free`, *not* the `: '!` mechanism.) + +## Self-referential types + +The second use-case for a stable `may_dangle` is the ability to have `Drop` for +self-referential types. This doesn't come up too often, but: + +```rust +struct Foo<'a> { + this: Cell>> +} + +impl<'a: '!> Drop for Foo<'a> { + fn drop(&mut self) { + ... + } +} +``` + +## Checking dropck in a macro, without alloc + TODO # Guide-level explanation From fd2b361aacde189ef45cd26d090f05603430b445 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Fri, 17 Feb 2023 23:38:53 -0300 Subject: [PATCH 06/10] Update --- text/0000-dropck-implication.md | 220 +++++++++++++++++++++++++++++++- 1 file changed, 216 insertions(+), 4 deletions(-) diff --git a/text/0000-dropck-implication.md b/text/0000-dropck-implication.md index a4ddbe4cffb..db035982baa 100644 --- a/text/0000-dropck-implication.md +++ b/text/0000-dropck-implication.md @@ -109,9 +109,8 @@ impl<'a: '!> Drop for Foo<'a> { } ``` -## Checking dropck in a macro, without alloc - -TODO +Without this proposal, such a struct cannot be dropped. (It is, in fact, +possible to write such a `Drop` impl. It just errors at site-of-use.) # Guide-level explanation [guide-level-explanation]: #guide-level-explanation @@ -335,7 +334,204 @@ fn main() { # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -TODO: nesting rules +## Syntax + +The core syntax for dropck obligations is `'!` in bound position. This is +further restricted to only being allowed in functions (including trait +functions), data types (structs, enums), and impls. + +## Extent of bound-ness + +Dropck obligations aren't true bounds. If anything, they're syntactic sugar for +an implicit impl. They also work backwards from how most bounds work. This was +chosen for both ergonomics and backwards compatibility reasons. + +A type is considered "safe to drop" if all of its fields are considered "safe to +drop". + +For example, given a struct like: + +```rust +struct Foo<'a>(&'a str); +``` + +The struct `Foo` is considered safe to drop if `&'a str` is safe to drop. Since +`&'a str` is always safe to drop, then `Foo` is always safe to drop. + +For another example, given a struct like: + +```rust +struct Bar(T); + +impl Drop for Bar { + ... +} +``` + +Then `Bar` is safe to drop if `T` is safe to drop. (Note that `Bar` implicitly +drops `T`.) + +Effectively, it's as if these were automatically generated by the compiler: + +```rust +impl<'a> SafeToDrop for Foo<'a> where &'a str: SafeToDrop {} +impl SafeToDrop for Bar where T: SafeToDrop {} +``` + +But they are only evaluated where `Drop` needs to be called. + +Meanwhile, `ManuallyDrop` is always safe to drop. As a lang item, it behaves as +if: + +```rust +impl SafeToDrop for ManuallyDrop {} +``` + +Additionally, where a parameter is used in the `Drop` impl, the type is only +safe to drop while the parameter is alive: + +```rust +struct Baz(T); + +impl Drop for Baz { + ... +} + +// effectively: +impl SafeToDrop for Baz where T: Alive {} +``` + +(A type which is `Alive` is also `SafeToDrop`.) + +These examples also show that this kind of bound is primarily about types, not +lifetimes. But `may_dangle` also works on lifetimes, so they must be supported +too. Lifetimes are "easier" to support, since we just need to forbid using their +scope entirely. + +## Parametricity (or lack thereof) + +Dropck bounds are non-parametric. They effectively assert properties about the +concrete type `T`: either that it is safe to use, or that it is safe to drop. + +If it is safe to use, then the `Drop` impl is allowed to use it. If it's safe to +drop, then the `Drop` impl is only allowed to drop it. + +Given the classic example of parametric dropck unsoundness: (rust-lang/rust#26656) + +```rust +// Using this instead of Fn etc. to take HRTB out of the equation. +trait Trigger { fn fire(&self, b: &mut B); } +impl Trigger for () { + fn fire(&self, b: &mut B) { + b.push(); + } +} + +// Still unsound Zook +trait Button { fn push(&self); } +struct Zook { button: B, trigger: Box+'static> } + +impl Drop for Zook { + fn drop(&mut self) { + self.trigger.fire(&mut self.button); + } +} + +// AND +struct Bomb { usable: bool } +impl Drop for Bomb { fn drop(&mut self) { self.usable = false; } } +impl Bomb { fn activate(&self) { assert!(self.usable) } } + +enum B<'a> { HarmlessButton, BigRedButton(&'a Bomb) } +impl<'a> Button for B<'a> { + fn push(&self) { + if let B::BigRedButton(borrowed) = *self { + borrowed.activate(); + } + } +} + +fn main() { + let (mut zook, ticking); + zook = Zook { button: B::HarmlessButton, + trigger: Box::new(()) }; + ticking = Bomb { usable: true }; + zook.button = B::BigRedButton(&ticking); +} +``` + +This errors directly in `Zook::drop`, since it attempts to call an +inappropriate function. + +## Interactions with `ManuallyDrop` + +`may_dangle` interacts badly with `ManuallyDrop`. This is unsound: + +```rust +struct Foo(ManuallyDrop); + +unsafe impl<#[may_dangle] T> Drop for Foo { + fn drop(&mut self) { + unsafe { ManuallyDrop::drop(&mut self.0) } + } +} +``` + +Because `may_dangle` just defers to the drop obligations of the fields, and +`ManuallyDrop` does not have drop obligations regardless of its contents, you +can give it a type that *does* have (invalid) drop obligations and cause UB: + +```rust +struct Bomb<'a>(&'a str); +impl<'a> Drop for Bomb<'a> { + fn drop(&mut self) { println!("{}", self.0); } +} + +let s = String::from("hello"); +let bomb = Foo(Bomb(&s)); +drop(s); +``` + +Meanwhile, this RFC requires the `Drop for Foo` to be annotated with "safe to +drop" obligations on `T` (i.e. `T: '!`), preventing this mistake. With +`may_dangle`, one needs to remember to add a `PhantomData` for the dropck +obligations instead. Likewise for pointer types. + +While the compiler can't prevent double-frees here without real typestate, it +can at least prevent regressions like rust-lang/rust#99413. + +## Dropck elaboration + +After processing all implied bounds for the types, dropck becomes a matter of +checking those bounds when dropping. This is a weakened form of typestate, +which only applies to dropck, but is not too dissimilar to existing dropck. The +main difference is that the type is already fully annotated by the time we get +here, so no recursing into the type's fields is necessary. + +In other words, we treat dropck obligations as both bounds (when writing code +and running typeck) and annotations (when running dropck). + +Given a type `T`: + +- If `T` has no (type or lifetime) parameters, then it can be dropped. +- If `T` has lifetime parameters: + - For each lifetime parameter that is not annotated with a `'!` bound, check + that it's still live. +- If `T` has type parameters: + - For each type parameter that is not annotated with a `'!` bound, check + that it's still live. + - For each type parameter that is annotated with a `'!` bound, check that it + can be dropped. + +## Tweaks to Typeck + +Lifetimes tagged as `'!` cannot be used. Types tagged as `'!` cannot be used +except where `drop_in_place` is concerned. `unsafe` does not allow sidestepping +these restrictions. If all possible lifetimes for a type are tagged as `'!` +(i.e. `for<'a> &'a T: '!`), then said type cannot be dropped either (since that +would require creating a reference to it, for `fn drop(&mut self)`). + +## Dropck obligations for built-in types The following types have the given dropck implications (based on existing usage of `#[may_dangle]`): @@ -382,6 +578,15 @@ In particular, this design explicitly prevents the user from doing unsound operations in safe code, while also allowing `impl Drop` to be *safe* even in the presence of liveness obligations/dropck bounds. +The explicit goals are: + +- Safe `impl Drop` for self-referential structs. +- Preventing dropck mistakes, both with first-party and third-party collections. + +Explicit **non**-goals include: + +- Preventing double frees, use-after-free, etc in unsafe code. + # Prior art [prior-art]: #prior-art @@ -392,6 +597,13 @@ the presence of liveness obligations/dropck bounds. # Unresolved questions [unresolved-questions]: #unresolved-questions +## Syntax for "no dropck obligations, cannot use or drop type" + +This RFC proposes the syntax `for<'a> &'a T: '!` to discharge all dropck +obligations and restrict `impl Drop` the most. However, something feels off +about this syntax, but we can't quite put a finger on what. Meanwhile, using +`T: '!` and `'a: '!` for the rest of the dropck bounds feels fine. + ## Spooky-dropck-at-a-distance Currently, the following code is not accepted by the compiler: From 2495ec28c7f2b092174f408e8ec7cbef7550f541 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Sat, 18 Feb 2023 08:35:28 -0300 Subject: [PATCH 07/10] Update --- text/0000-dropck-implication.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/text/0000-dropck-implication.md b/text/0000-dropck-implication.md index db035982baa..1388de6db16 100644 --- a/text/0000-dropck-implication.md +++ b/text/0000-dropck-implication.md @@ -329,7 +329,23 @@ fn main() { #### As applied to all possible lifetimes -[TODO explain, this is more of a construct to enable the semantics of &T etc] +When given a type and a lifetime: + +```rust +struct Bar<'a: '!, T: '!>(&'a T); + +impl<'a: '!, T: '!> Drop for Bar<'a, T> { + ... +} +``` + +This treats `T` like it needs to be safe to drop, which is overly restrictive. +(After all, a type with `'a` and `T: 'a` could have both `&'a T` and an owned +`T` in it.) So we need a way to convey that we don't want that. + +[FIXME: `for<'a> &'a T: '!` doesn't make sense, what we want is more akin to +`for<'a> 'a: T + '!`, or "for all `'a`, `'a` outlives `T` and may dangle", which +is just cursed.] # Reference-level explanation [reference-level-explanation]: #reference-level-explanation @@ -587,6 +603,16 @@ Explicit **non**-goals include: - Preventing double frees, use-after-free, etc in unsafe code. +Leveraging typeck is good. The fact that `: '!` acts akin to `?Sized` can be a +bit confusing, but is necessary for backwards compatibility, and further, if we +treat `may_dangle` as a bound, it's basically in the name: *may* dangle. + +As a consequence of `'!` being akin to `?Sized`, something like +`<'a: '!, 'b: 'a>` does not imply `'b: '!`, while `<'a: 'b + '!, 'b>` does imply +`'b: '!`. (In the first, `'b` outlives `'a`, which may dangle. if `'a` dangles, +`'b` does not also need to dangle. In the second, `'a` outlives `'b`, and so if +`'a` dangles, then so must `'b`.) + # Prior art [prior-art]: #prior-art From 791c3ba1a7fc62ae1cc6898cd22005aaa8641297 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Mon, 20 Feb 2023 18:58:24 -0300 Subject: [PATCH 08/10] Hopefully finalize reference-level explanation Syntax is still bound to be bikeshedded but we think this looks pretty good. --- ...lication.md => 3390-dropck-obligations.md} | 329 ++++++++++-------- 1 file changed, 192 insertions(+), 137 deletions(-) rename text/{0000-dropck-implication.md => 3390-dropck-obligations.md} (69%) diff --git a/text/0000-dropck-implication.md b/text/3390-dropck-obligations.md similarity index 69% rename from text/0000-dropck-implication.md rename to text/3390-dropck-obligations.md index 1388de6db16..18db60c90ee 100644 --- a/text/0000-dropck-implication.md +++ b/text/3390-dropck-obligations.md @@ -1,4 +1,4 @@ -- Feature Name: `dropck_implication` +- Feature Name: `dropck_obligations` - Start Date: 2023-02-13 - RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) @@ -79,7 +79,7 @@ The perhaps main use-case for a stable `may_dangle` is custom collections. With the `MyBox` above, we can have a `Drop` impl as below: ```rust -impl Drop for MyBox { +impl Drop for MyBox { fn drop(&mut self) { unsafe { drop_in_place(self.inner); @@ -129,7 +129,7 @@ and they are named as such: 3. You may freely access the type/value in question. For a type which does not itself implement `Drop`, these are implied by what's -in the type. These implications are akin to variance or traits like `Send` and +in the type. These obligations are akin to variance or traits like `Send` and `Sync`. Unlike `Send`/`Sync` these cannot be overriden by non-`Drop` types. For a type which implements `Drop`, these obligations can be added to the type @@ -187,22 +187,24 @@ As the error says, "borrow might be used here, when `y` is dropped and runs the put a "You may only drop the type/value in question." bound on `T`, like so: ```rust -struct MyBox { +struct MyBox { inner: *const T, } +// N.B: `'!` provides a *relaxation* of bounds, similarly to `?Sized`, hence +// neither `Droppable` nor `'!` appear here. impl MyBox { fn new(t: T) -> Self { ... } } -impl Drop for MyBox { +impl Drop for MyBox { fn drop(&mut self) { ... } } ``` This "You may only drop the type/value in question." bound, represented by -`T: '!`, is only (directly) compatible with exactly one function: -`core::ptr::drop_in_place`, which has a `T: '!` bound. (N.B. it's also perfectly +`T: Droppable + '!`, is only (directly) compatible with exactly one function: +`core::ptr::drop_in_place`, which has the same bound. (N.B. it's also perfectly fine to wrap `drop_in_place` and call it indirectly, as long as the dropck bound is carried around.) Trying to use `T` in any other way causes an error. For our example, this bound prevents the `Drop` impl from using the borrow, @@ -228,6 +230,18 @@ impl Drop for PrintOnDrop { } ``` +or even create new instances of `T`: + +```rust +struct Foo(T); + +impl Drop for Foo { + fn drop(&mut self) { + T::default(); + } +} +``` + This is only possible while the type parameter is still live. As such, the compiler rejects the following code: @@ -251,12 +265,12 @@ error[E0505]: cannot move out of `x` because it is borrowed | - borrow might be used here, when `y` is dropped and runs the `Drop` code for type `PrintOnDrop` ``` -And it also rejects an `T: '!` bound: +And it also rejects an `T: Droppable + '!` bound: ```rust -struct PrintOnDrop(T); +struct PrintOnDrop(T); -impl Drop for PrintOnDrop { +impl Drop for PrintOnDrop { fn drop(&mut self) { println!("{}", self.0); } @@ -264,7 +278,7 @@ impl Drop for PrintOnDrop { ``` ```text -TODO +`self.0` does not live long enough, [...] ``` ### You must not touch the type/value in question @@ -276,9 +290,8 @@ So far, the other 2 dropck obligations have applied to type parameters. This dropck obligation instead applies to lifetimes. Actually, there are 2 forms of it: As applied to one or more lifetimes, it -prevents accessing those lifetimes. As applied to *all possible* lifetimes, it -prevents accessing the *type* entirely. A bound can be applied to all possible -lifetimes with the use of HRTB syntax, i.e. `for<'a>`. +prevents accessing those lifetimes. As applied to a type, it +prevents accessing the *type* entirely. #### As applied to one (or more) lifetimes @@ -294,7 +307,7 @@ lifetimes with the use of HRTB syntax, i.e. `for<'a>`. // // Compare with ui/span/issue28498-reject-lifetime-param.rs -#![feature(dropck_implication)] +#![feature(dropck_obligations)] #[derive(Debug)] struct ScribbleOnDrop(String); @@ -327,9 +340,9 @@ fn main() { } ``` -#### As applied to all possible lifetimes +#### As applied to a type -When given a type and a lifetime: +When applied to a type: ```rust struct Bar<'a: '!, T: '!>(&'a T); @@ -339,13 +352,7 @@ impl<'a: '!, T: '!> Drop for Bar<'a, T> { } ``` -This treats `T` like it needs to be safe to drop, which is overly restrictive. -(After all, a type with `'a` and `T: 'a` could have both `&'a T` and an owned -`T` in it.) So we need a way to convey that we don't want that. - -[FIXME: `for<'a> &'a T: '!` doesn't make sense, what we want is more akin to -`for<'a> 'a: T + '!`, or "for all `'a`, `'a` outlives `T` and may dangle", which -is just cursed.] +This makes `T` completely unusable. # Reference-level explanation [reference-level-explanation]: #reference-level-explanation @@ -356,73 +363,108 @@ The core syntax for dropck obligations is `'!` in bound position. This is further restricted to only being allowed in functions (including trait functions), data types (structs, enums), and impls. -## Extent of bound-ness - -Dropck obligations aren't true bounds. If anything, they're syntactic sugar for -an implicit impl. They also work backwards from how most bounds work. This was -chosen for both ergonomics and backwards compatibility reasons. +## Extent of bound-ness and interactions with borrowck -A type is considered "safe to drop" if all of its fields are considered "safe to -drop". +May dangle bounds aren't true bounds - they instead opt-out of a bound, similar +to `?Sized`. -For example, given a struct like: +Specifically, given a function like ```rust -struct Foo<'a>(&'a str); +fn foo<'a, T>(a: &'a (), t: T) ``` -The struct `Foo` is considered safe to drop if `&'a str` is safe to drop. Since -`&'a str` is always safe to drop, then `Foo` is always safe to drop. - -For another example, given a struct like: +The borrowck implicitly introduces an fn-scope lifetime that basically exists +for the duration of the function call, and defines everything as needing to +outlive it. Effectively desugared as: ```rust -struct Bar(T); +fn foo<'a: 'fn, T: 'fn>(a: &'a (), t: T) +``` -impl Drop for Bar { - ... +The may dangle bound, `'!`, simply opts-out of this mechanism. This requires +borrowck to treat the type/lifetime as if it were already dropped/moved, +just as how it would be if one tried to write: + +```rust +fn main() { + let y; + { + let x = String::new(); + y = &x; + } + y; } ``` -Then `Bar` is safe to drop if `T` is safe to drop. (Note that `Bar` implicitly -drops `T`.) +But this would still forbid our `MyBox` impl. We need a mechanism to identify +if a type is safe to drop in a given lifetime, without introducing that lifetime +as a bound. -Effectively, it's as if these were automatically generated by the compiler: +Given a hypothetical `T: SafeToDrop in 'a` syntax which is distinct from +`T: SafeToDrop<'a>`, and desugaring implicit borrowck lifetimes, we can discuss +whether a type is safe to drop: + +For a type `T` with no (type or lifetime) parameters, regardless of `Drop` impl, +we have: ```rust -impl<'a> SafeToDrop for Foo<'a> where &'a str: SafeToDrop {} -impl SafeToDrop for Bar where T: SafeToDrop {} +struct Foo { + field: Type, + ... +} + +impl SafeToDrop for Foo where Type: SafeToDrop in 'self {} ``` -But they are only evaluated where `Drop` needs to be called. +(This is effectively always true - Rust doesn't have "undroppable types", tho +it does have undroppable *values*.) -Meanwhile, `ManuallyDrop` is always safe to drop. As a lang item, it behaves as -if: +For a type with lifetime parameters, but no type parameters, and no `Drop`: ```rust -impl SafeToDrop for ManuallyDrop {} +struct Foo<'a> { + field: &'a Type, + ... +} + +impl<'a> SafeToDrop for Foo<'a> where Type, 'a {} ``` -Additionally, where a parameter is used in the `Drop` impl, the type is only -safe to drop while the parameter is alive: +(Note the lack of bounds on `Type`! This is because of `&'a T`, more on this +later.) -```rust -struct Baz(T); +For a type with type parameters and no `Drop`, we just extend the field rule to +type parameters, as such: -impl Drop for Baz { - ... +```rust +struct Foo { + field: T, + ... } -// effectively: -impl SafeToDrop for Baz where T: Alive {} +impl SafeToDrop for Foo where T: SafeToDrop in 'self {} ``` -(A type which is `Alive` is also `SafeToDrop`.) +When `Drop` is involved, we then add additional requirements, unless they've +been opted out: -These examples also show that this kind of bound is primarily about types, not -lifetimes. But `may_dangle` also works on lifetimes, so they must be supported -too. Lifetimes are "easier" to support, since we just need to forbid using their -scope entirely. +```rust +struct Foo<'live, 'opt_out: '!, Accessible, Dropped: Droppable + '!, Dead: '!> { + ... +} + +impl<...> Drop for Foo<...> { ... } + +impl<'live, 'opt_out, Accessible, Dropped, Dead> SafeToDrop for Foo<...> where + 'live: 'self, + 'opt_out, + Accessible: 'self, + Dropped: SafeToDrop in 'self, + Dead, + ... // + bounds generated by fields in Foo +{} +``` ## Parametricity (or lack thereof) @@ -432,7 +474,8 @@ concrete type `T`: either that it is safe to use, or that it is safe to drop. If it is safe to use, then the `Drop` impl is allowed to use it. If it's safe to drop, then the `Drop` impl is only allowed to drop it. -Given the classic example of parametric dropck unsoundness: (rust-lang/rust#26656) +Given the classic example of parametric dropck unsoundness: +(rust-lang/rust#26656, adapted) ```rust // Using this instead of Fn etc. to take HRTB out of the equation. @@ -445,9 +488,9 @@ impl Trigger for () { // Still unsound Zook trait Button { fn push(&self); } -struct Zook { button: B, trigger: Box+'static> } +struct Zook { button: B, trigger: Box+'static> } -impl Drop for Zook { +impl Drop for Zook { fn drop(&mut self) { self.trigger.fire(&mut self.button); } @@ -477,11 +520,14 @@ fn main() { ``` This errors directly in `Zook::drop`, since it attempts to call an -inappropriate function. +inappropriate function. (Using the desugaring mentioned in the previous section, +it attempts to use an `B: 'fn` function, but it only has an +`B: SafeToDrop in 'fn`.) -## Interactions with `ManuallyDrop` +## Interactions with `ManuallyDrop` and pointer types -`may_dangle` interacts badly with `ManuallyDrop`. This is unsound: +For some historical background, `may_dangle` interacts badly with +`ManuallyDrop`. This is unsound: ```rust struct Foo(ManuallyDrop); @@ -509,74 +555,96 @@ drop(s); ``` Meanwhile, this RFC requires the `Drop for Foo` to be annotated with "safe to -drop" obligations on `T` (i.e. `T: '!`), preventing this mistake. With -`may_dangle`, one needs to remember to add a `PhantomData` for the dropck -obligations instead. Likewise for pointer types. +drop" obligations on `T` (i.e. `T: Droppable + '!`), preventing this mistake. +With `may_dangle`, one needs to remember to add a `PhantomData` for the +dropck obligations instead. Likewise for pointer types. While the compiler can't prevent double-frees here without real typestate, it can at least prevent regressions like rust-lang/rust#99413. -## Dropck elaboration - -After processing all implied bounds for the types, dropck becomes a matter of -checking those bounds when dropping. This is a weakened form of typestate, -which only applies to dropck, but is not too dissimilar to existing dropck. The -main difference is that the type is already fully annotated by the time we get -here, so no recursing into the type's fields is necessary. - -In other words, we treat dropck obligations as both bounds (when writing code -and running typeck) and annotations (when running dropck). +Using the previously mentioned desugaring syntax (see: interactions with +borrowck), we can describe the behaviour of `ManuallyDrop` and pointer types: -Given a type `T`: +```rust +// there is some bikeshedding about the existence of dangling references, but +// #[may_dangle] does allow it, and this is observable on stable through various +// means. +impl<'a, T> SafeToDrop for &'a T where 'a, T {} +impl<'a, T> SafeToDrop for &'a mut T where 'a, T {} + +impl SafeToDrop for ManuallyDrop where T {} +impl SafeToDrop for *const T where T {} +impl SafeToDrop for *mut T where T {} + +// this is arguably not a pointer type, but for internal purposes it is treated +// as one. +impl SafeToDrop for Box where T: SafeToDrop in 'self {} +``` -- If `T` has no (type or lifetime) parameters, then it can be dropped. -- If `T` has lifetime parameters: - - For each lifetime parameter that is not annotated with a `'!` bound, check - that it's still live. -- If `T` has type parameters: - - For each type parameter that is not annotated with a `'!` bound, check - that it's still live. - - For each type parameter that is annotated with a `'!` bound, check that it - can be dropped. +## Dropck elaboration -## Tweaks to Typeck +After processing all of the above, we now have our `SafeToDrop` impls. We then +evaluate them at the drop lifetime. As an exaple, the desugared `drop` fn is: -Lifetimes tagged as `'!` cannot be used. Types tagged as `'!` cannot be used -except where `drop_in_place` is concerned. `unsafe` does not allow sidestepping -these restrictions. If all possible lifetimes for a type are tagged as `'!` -(i.e. `for<'a> &'a T: '!`), then said type cannot be dropped either (since that -would require creating a reference to it, for `fn drop(&mut self)`). +```rust +fn drop(t: T) { +} +``` ## Dropck obligations for built-in types -The following types have the given dropck implications (based on existing usage -of `#[may_dangle]`): +The following types have the given dropck obligations (based on existing usage +of `#[may_dangle]`), using the "fully-desugared" syntax (since these - aside +from `ManuallyDrop` - aren't representable using `struct` syntax): ```text -ManuallyDrop where for<'a> &'a T: '! -PhantomData where for<'a> &'a T: '! // see below for unresolved questions -[T; 0] where for<'a> &'a T: '! // see below for unresolved questions -*const T where for<'a> &'a T: '! -*mut T where for<'a> &'a T: '! -&'_ T where for<'a> &'a T: '! // N.B. this is special -&'_ mut T where for<'a> &'a T: '! - -OnceLock -RawVec -Rc -rc::Weak -VecDeque -BTreeMap -LinkedList -Box -Vec -vec::IntoIter -Arc -sync::Weak -HashMap +// The previously mentioned pointer types (and `ManuallyDrop`): +impl<'a, T> SafeToDrop for &'a T where 'a, T {} +impl<'a, T> SafeToDrop for &'a mut T where 'a, T {} +impl SafeToDrop for ManuallyDrop where T {} +impl SafeToDrop for *const T where T {} +impl SafeToDrop for *mut T where T {} +``` + +And the next types have the given dropck obligations, using the surface syntax: + +``` +// Various container types: +struct OnceLock +struct RawVec +struct Rc +struct rc::Weak +struct VecDeque +struct BTreeMap +struct LinkedList +struct Box +struct Vec +struct vec::IntoIter +struct Arc +struct sync::Weak +struct HashMap // FIXME: other types which currently use may_dangle but were not found by grep ``` +We can also explain the behaviour of a few oddities. `PhantomData` and arrays +currently behave as such: + +```rust +impl SafeToDrop for PhantomData where T: SafeToDrop in 'self {} +impl SafeToDrop for [T; N] where T: SafeToDrop in 'self {} +``` + +But these are only checked if they are actually drop-elaborated! Since they +don't have drop glue, they're not usually checked. For more details, see +rust-lang/rust#103413. However, this RFC proposes making them: + +```rust +impl SafeToDrop for PhantomData where T {} +impl SafeToDrop for [T; N] where T: SafeToDrop in 'self {} +// not actually possible (at this time). see also `Copy`. +impl SafeToDrop for [T; 0] where T {} +``` + # Drawbacks [drawbacks]: #drawbacks @@ -603,16 +671,10 @@ Explicit **non**-goals include: - Preventing double frees, use-after-free, etc in unsafe code. -Leveraging typeck is good. The fact that `: '!` acts akin to `?Sized` can be a +Leveraging borrowck is good. The fact that `: '!` acts akin to `?Sized` can be a bit confusing, but is necessary for backwards compatibility, and further, if we treat `may_dangle` as a bound, it's basically in the name: *may* dangle. -As a consequence of `'!` being akin to `?Sized`, something like -`<'a: '!, 'b: 'a>` does not imply `'b: '!`, while `<'a: 'b + '!, 'b>` does imply -`'b: '!`. (In the first, `'b` outlives `'a`, which may dangle. if `'a` dangles, -`'b` does not also need to dangle. In the second, `'a` outlives `'b`, and so if -`'a` dangles, then so must `'b`.) - # Prior art [prior-art]: #prior-art @@ -623,13 +685,6 @@ As a consequence of `'!` being akin to `?Sized`, something like # Unresolved questions [unresolved-questions]: #unresolved-questions -## Syntax for "no dropck obligations, cannot use or drop type" - -This RFC proposes the syntax `for<'a> &'a T: '!` to discharge all dropck -obligations and restrict `impl Drop` the most. However, something feels off -about this syntax, but we can't quite put a finger on what. Meanwhile, using -`T: '!` and `'a: '!` for the rest of the dropck bounds feels fine. - ## Spooky-dropck-at-a-distance Currently, the following code is not accepted by the compiler: @@ -666,7 +721,7 @@ users from defining similar spooky-dropck behaviour. If removed, the above example would be accepted by the compiler. However, this spooky-dropck behaviour can also be used in no-alloc crates to -detect potentially-unsound `Drop` impls in current stable. For example, the +detect potentially-unsound `Drop` impls in *current stable*. For example, the `selfref` crate *could* do something like this: ```rust @@ -728,8 +783,8 @@ obligations from their implementers. Adding these bounds to existing traits would be semver-breaking, so it can't be done with `Iterator`, but it could be useful for other traits. -## Generalize to "bounds generics" or "associated bounds" +## Generalize to "bounds generics" or "associated bounds" (or typestate?) This proposal limits itself to dropck semantics, but a future proposal could generalize these kinds of bounds to some sort of "bounds generics" or -"associated bounds" kind of system. +"associated bounds" or "typestate" kind of system. From 00c2150a543a3e665c7023cf467c8d302aebf188 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Thu, 23 Feb 2023 23:38:50 -0300 Subject: [PATCH 09/10] Narrow scope further --- text/3390-daddy-borrowck-little-dropck.md | 470 +++++++++++++ text/3390-dropck-obligations.md | 790 ---------------------- 2 files changed, 470 insertions(+), 790 deletions(-) create mode 100644 text/3390-daddy-borrowck-little-dropck.md delete mode 100644 text/3390-dropck-obligations.md diff --git a/text/3390-daddy-borrowck-little-dropck.md b/text/3390-daddy-borrowck-little-dropck.md new file mode 100644 index 00000000000..aacf9babefd --- /dev/null +++ b/text/3390-daddy-borrowck-little-dropck.md @@ -0,0 +1,470 @@ +- Feature Name: `daddy_borrowck_little_dropck` +- Start Date: 2023-02-13 +- RFC PR: [rust-lang/rfcs#3390](https://github.com/rust-lang/rfcs/pull/3390) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +Provide a flexible framework for understanding and resolving dropck obligations, +built upon the borrow checker; simplify dropck elaboration, and effectively +stabilize (a refinement of) `may_dangle`. + +# Motivation +[motivation]: #motivation + +Rust's drop checker (dropck) is an invaluable tool for making sure `impl Drop` +is sound. However, it can be too strict sometimes. Consider a custom `Box`: + +```rust +struct MyBox { + inner: *const T, +} + +impl MyBox { + fn new(t: T) -> Self { ... } +} + +impl Drop for MyBox { + fn drop(&mut self) { ... } +} +``` + +This is... fine. However, actual `Box`es have the following property: + +```rust +let x = String::new(); +let y = Box::new(&x); +drop(x); +// y implicitly dropped here +``` + +Meanwhile, using `MyBox` produces the following error: + +```text +error[E0505]: cannot move out of `x` because it is borrowed + --> src/main.rs:16:10 + | +15 | let y = MyBox::new(&x); + | -- borrow of `x` occurs here +16 | drop(x); + | ^ move out of `x` occurs here +17 | // y implicitly dropped here +18 | } + | - borrow might be used here, when `y` is dropped and runs the `Drop` code for type `MyBox` +``` + +This is where `may_dangle` comes in: it allows the impl to say "I don't touch +this parameter in a way that may cause unsoundness", and in fact the real `Box` +does use it. However, `may_dangle` was introduced as a hack specifically to make +`Box` (and collections like `Vec`) work like this, and it was never intended to +be the final form. + +`may_dangle` is itself a refinement of "unguarded escape hatch" (or UGEH for +short), because UGEH was found to be too much of a footgun even for an internal +compiler feature. UGEH effectively applied `may_dangle` to *all* parameters. But +even `may_dangle` sometimes comes back to bite, for example when dropck was +simplified, causing a broken BTreeMap implementation to become unsound. (see +rust-lang/rust#99413) + +This RFC proposes a *safe* refinement of `may_dangle`, while also making it +resistant to the pitfalls observed with the existing `may_dangle` mechanism. +This refined mechanism can be called "Liveness obligations" or "Dropck bounds". +In particular, it tries to encode the soundness obligations of `may_dangle` and +dropck itself in the type/borrow system, directly. + +## Custom Box and Custom Collections + +The perhaps main use-case for a stable `may_dangle` is custom collections. With +the `MyBox` above, we can have a `Drop` impl as below: + +```rust +impl Drop for MyBox { + fn drop(&mut self) { + unsafe { + drop_in_place(self.inner); + free(self.inner); + } + } +} +``` + +(N.B. this still uses `unsafe`! however, the unsafety is about upholding the +contract of `drop_in_place` and `free`, *not* the `: '!` mechanism.) + +## Self-referential types + +The second use-case for a stable `may_dangle` is the ability to have `Drop` for +self-referential types. This doesn't come up too often, but: + +```rust +struct Foo<'a> { + this: Cell>> +} + +impl<'a: '!> Drop for Foo<'a> { + fn drop(&mut self) { + ... + } +} +``` + +Without this proposal, such a struct cannot be dropped. (It is, in fact, +possible to write such a `Drop` impl. It just errors at site-of-use.) + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +Rust functions generally have an implicit requirement that all lifetimes must +outlive the function. This can be desugared by treating: + +```rust +fn foo<'a>(x: &'a ()) {} +``` + +as: + +```rust +fn foo<'a: 'fn>(x: &'a ()) {} +``` + +Likewise, types also come with an implicit `'self` lifetime: + +```rust +struct Foo<'a>(&'a ()); +``` + +is really: + +```rust +struct Foo<'a: 'self>(&'a ()); +``` + +The ability to opt-out of this bound is called "non-local lifetimes". They're +lifetimes external or out-of-scope to a type or function. + +This out-of-scope-ness can be represented using blocks: + +```rust +fn foo<'a: '!, 'b: '!, 'c>(arg1: &'a (), arg2: &'b (), arg3: &'c ()) { + function_body(); +} +``` + +becomes: + +```rust +let arg1; +let arg2; +let arg3; +{ // block of non-local lifetimes + // need to make sure these are non-'static + let temporary_arg1 = (); + let temporary_arg2 = (); + arg1 = &temporary_arg1; + arg2 = &temporary_arg2; +} +let temporary_arg3 = (); +arg3 = &temporary_arg3; +function_body(); +``` + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +## Syntax + +The core syntax for opting out of the implicit lifetime bounds is `'!` in bound +position. This is further restricted to only being allowed in functions +(including trait functions), data types (structs, enums), and impls. + +This is an opt-out bound: it is similar to `?Sized`. + +## Interaction with borrowck + +The non-local lifetime interacts with borrowck the exact same way as the block +representation above shows, replicated below for contextualization: + +```rust +fn foo<'a: '!, 'b: '!, 'c>(arg1: &'a (), arg2: &'b (), arg3: &'c ()) { + function_body(); +} +``` + +becomes: + +```rust +let arg1; +let arg2; +let arg3; +{ // block of non-local lifetimes + // need to make sure these are non-'static + let temporary_arg1 = (); + let temporary_arg2 = (); + arg1 = &temporary_arg1; + arg2 = &temporary_arg2; +} +let temporary_arg3 = (); +arg3 = &temporary_arg3; +function_body(); +``` + +This effectively makes arg1 and arg2 unusable in the function body. + +## Interactions with references + +References have a few special properties: + +1. They participate in implicit lifetime bounds. +2. They're generally required to be alive. + +This RFC relaxes those properties. For the first one, we have that `&'a &'b T` +does no longer imply `'b: 'a` if `'b: '!`, and thus `&'b T: 'a` is also no +longer true. + +The second one is a bit more complicated. However, there is an existing +mechanism which also breaks this assumption: `#[may_dangle]`. Putting +`#[may_dangle]` on a lifetime allows that lifetime to no longer be alive when +the code runs (but `#[may_dangle]` has its own set of issues, like allowing +the relevant lifetimes to be interacted with). + +## Interactions with dropck + +This opt-out would enable implementing `Drop` for self-referential structs: + +```rust +struct Foo<'a: '!> { + inner: Cell>>, +} + +impl<'a: '!> Drop for Foo<'a> { + fn drop(&mut self) { + // can't actually use `self` here, as per the previously defined rules. + } +} + +let foo = Foo { inner: Default::default() }; +foo.inner.set(Some(&foo)); +// foo dropped here, compiles successfully. +``` + +## Interactions with Box/Vec, adapting for `may_dangle` + +Traits can be implemented for lifetime relations which are stricter than the +lifetime relations of the parent types, today: + +```rust +struct Foo<'a, 'b, T>(&'a T, &'b T); + +impl<'a, 'b, T> Default for Foo<'a, 'b, T> where 'a: 'b { + ... +} +``` + +However, the lifetime relations are unique to the trait impl - they cannot be +specialized (it's not possible to impl the same trait for two different sets of +lifetime relations for the same type). + +Thus we simply have the compiler implicitly build a `SafeToDrop` trait impl for +every type, as such: + +```rust +struct Foo<'a, T> { + field1: &'a (), + field2: T, + ... +} + +impl<'a: '!, T: '!> SafeToDrop for Foo<'a, T> where &'a (): SafeToDrop, T: SafeToDrop {} +``` + +In other words, a type impls `SafeToDrop` when all of its fields impl +`SafeToDrop`, but the `SafeToDrop` impl doesn't apply any additional lifetime +bounds except those required by such `SafeToDrop` impls. + +When the type has a `Drop` impl, the `SafeToDrop` impl follows the `Drop` impl: + +```rust +struct Foo<'a, T> { + field1: &'a (), + field2: T, + ... +} +impl<'a, T> Drop for Foo<'a, T> { ... } +impl<'a, T> SafeToDrop for Foo<'a, T> where &'a (): SafeToDrop, T: SafeToDrop {} +``` + +(Note how the `SafeToDrop` impl now requires `'a: 'self` and `T: 'self`, due to +the requirements of the `Drop` impl.) + +The lang items that are special for `SafeToDrop` are: + +```rust +impl<'a: '!, T: '!> SafeToDrop for &'a T {} +impl<'a: '!, T: '!> SafeToDrop for &'a mut T {} +impl SafeToDrop for *const T {} +impl SafeToDrop for *mut T {} +impl SafeToDrop for ManuallyDrop {} +impl SafeToDrop for PhantomData {} // but see unresolved questions below +impl SafeToDrop for [T; N] where T: SafeToDrop {} // but see unresolved questions below +``` + +The following lang item gains a `SafeToDrop` bound, and loses its implied/`'fn` +bounds: + +```rust +pub unsafe fn drop_in_place(to_drop: *mut T) {...} +``` + +# Drawbacks +[drawbacks]: #drawbacks + +The Rust compiler pretty extensively assumes `&'a &'b T` implies `'b: 'a, T: 'b` +and this completely changes that. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +This design is a further refinement of RFC 1327 with intent to stabilize. It +tries to avoid introducing too many new concepts, but does attempt to integrate +the lessons learned from RFC 1327 into the type system - specifically, allowing +them to be checked by the compiler. + +In particular, this design explicitly prevents the user from doing unsound +operations in safe code, while also allowing `impl Drop` to be *safe* even in +the presence of liveness obligations/dropck bounds. + +The explicit goals are: + +- Safe `impl Drop` for self-referential structs. +- Preventing dropck mistakes, both with first-party and third-party collections. + +Explicit **non**-goals include: + +- Preventing double frees, use-after-free, etc in unsafe code. + +Leveraging borrowck is good. The fact that `: '!` acts akin to `?Sized` can be a +bit confusing, but is necessary for backwards compatibility, and further, if we +treat `may_dangle` as a bound, it's basically in the name: *may* dangle. + +For soundness reasons, `SafeToDrop` cannot be opted-out-of. In fact, due to +`drop_in_place` being `unsafe`, it's technically possible to hide it altogether, +make `PhantomData` require a `T: SafeToDrop`, and just add to the +`drop_in_place` contract that it must be "logically owned" (however one chooses +to define that). But simply exposing `SafeToDrop` is enough to avoid issues like +rust-lang/rust#99413. (Just because unsafe Rust is unsafe, doesn't mean we +should add known footguns.) + +# Prior art +[prior-art]: #prior-art + +- Compiler MCP 563: This RFC was supposed to come after the implementation of MCP 563 but that didn't happen. This RFC is basically a refinement of the ideas in the MCP. +- Unsound dropck elaboration for `BTreeMap`: +- `may_dangle`: RFC 1238, RFC 1327 + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +## Spooky-dropck-at-a-distance + +Currently, the following code is not accepted by the compiler: + +```rust +use core::cell::Cell; +use core::marker::PhantomData; + +struct Dropper(T); + +impl Drop for Dropper { + fn drop(&mut self) {} +} + +struct Foo<'a, T>(PhantomData>>>, T); + +fn main() { + fn make_selfref<'a, T>(x: &'a Foo<'a, T>){} + let x = Foo(PhantomData, String::new()); + make_selfref(&x); +} +``` + +At the same time, replacing `String::new()` with `()` makes it compile, which +is really surprising: after all, the error is seemingly unrelated to the +`String`. This is an interaction between at least 4 factors: `T` having drop +glue, the presence of `Dropper`, the invariance of `'a`, and the use of +`make_selfref`. + +This RFC recommends removing this spooky-dropck-at-a-distance behaviour +entirely, to make the language less surprising. Since this RFC ties +user-defined dropck obligations with a `Drop` impl, it automatically prevents +users from defining similar spooky-dropck behaviour. If removed, the above +example would be accepted by the compiler. + +However, this spooky-dropck behaviour can also be used in no-alloc crates to +detect potentially-unsound `Drop` impls in *current stable*. For example, the +`selfref` crate *could* do something like this: + +```rust +type UBCheck...; + +// paste selfref::opaque! ub_check here. +``` + +But this RFC provides an alternative way which does not require spooky-dropck, +and `selfref` explicitly does not rely on spooky-dropck as the behaviour of +spooky-dropck has changed in the past: namely, between Rust 1.35 and Rust 1.36, +in the 2015 edition: . +(Arguably, this is when it *became* "spooky".) + +## Behaviour of `[T; 0]` + +Should `[T; 0]` have dropck obligations? As above, this also basically falls +under spooky-dropck-at-a-distance, since `[T; 0]` lacks drop glue. This RFC +proposes treating `[T; 0]` the same as `PhantomData`, as above. + +# Future possibilities +[future-possibilities]: #future-possibilities + +## `dyn Trait` dropck obligations + +Currently, if you attempt to make a self-referential (e.g.) `dyn Iterator`, you +get an error: + +```rust +use core::cell::RefCell; + +#[derive(Default)] +struct Foo<'a> { + vec: RefCell>>>, + iter: RefCell>>>>, +} + +fn main() { + let x = Foo::default(); + x.vec.borrow_mut().insert(vec![]).push(&x); +} +``` + +```text +error[E0597]: `x` does not live long enough + --> src/main.rs:11:44 + | +11 | x.vec.borrow_mut().insert(vec![]).push(&x); + | ^^ borrowed value does not live long enough +12 | } + | - + | | + | `x` dropped here while still borrowed + | borrow might be used here, when `x` is dropped and runs the destructor for type `Foo<'_>` +``` + +A future RFC could build on this RFC to allow traits to demand dropck +obligations from their implementers. Adding these bounds to existing traits +would be semver-breaking, so it can't be done with `Iterator`, but it could +be useful for other traits. + +## Generalize to "bounds generics" or "associated bounds" (or typestate?) + +This proposal limits itself to dropck semantics, but a future proposal could +generalize these kinds of bounds to some sort of "bounds generics" or +"associated bounds" or "typestate" kind of system. diff --git a/text/3390-dropck-obligations.md b/text/3390-dropck-obligations.md deleted file mode 100644 index 18db60c90ee..00000000000 --- a/text/3390-dropck-obligations.md +++ /dev/null @@ -1,790 +0,0 @@ -- Feature Name: `dropck_obligations` -- Start Date: 2023-02-13 -- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) -- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) - -# Summary -[summary]: #summary - -Provide a flexible framework for understanding and resolving dropck obligations, -built upon implied bounds, simplify dropck elaboration, and effectively -stabilize (a refinement of) `may_dangle`. - -# Motivation -[motivation]: #motivation - -Rust's drop checker (dropck) is an invaluable tool for making sure `impl Drop` -is sound. However, it can be too strict sometimes. Consider a custom `Box`: - -```rust -struct MyBox { - inner: *const T, -} - -impl MyBox { - fn new(t: T) -> Self { ... } -} - -impl Drop for MyBox { - fn drop(&mut self) { ... } -} -``` - -This is... fine. However, actual `Box`es have the following property: - -```rust -let x = String::new(); -let y = Box::new(&x); -drop(x); -// y implicitly dropped here -``` - -Meanwhile, using `MyBox` produces the following error: - -```text -error[E0505]: cannot move out of `x` because it is borrowed - --> src/main.rs:16:10 - | -15 | let y = MyBox::new(&x); - | -- borrow of `x` occurs here -16 | drop(x); - | ^ move out of `x` occurs here -17 | // y implicitly dropped here -18 | } - | - borrow might be used here, when `y` is dropped and runs the `Drop` code for type `MyBox` -``` - -This is where `may_dangle` comes in: it allows the impl to say "I don't touch -this parameter in a way that may cause unsoundness", and in fact the real `Box` -does use it. However, `may_dangle` was introduced as a hack specifically to make -`Box` (and collections like `Vec`) work like this, and it was never intended to -be the final form. - -`may_dangle` is itself a refinement of "unguarded escape hatch" (or UGEH for -short), because UGEH was found to be too much of a footgun even for an internal -compiler feature. UGEH effectively applied `may_dangle` to *all* parameters. But -even `may_dangle` sometimes comes back to bite, for example when dropck was -simplified, causing a broken BTreeMap implementation to become unsound. (see -rust-lang/rust#99413) - -This RFC proposes a *safe* refinement of `may_dangle`, while also making it -resistant to the pitfalls observed with the existing `may_dangle` mechanism. -This refined mechanism can be called "Liveness obligations" or "Dropck bounds". -In particular, it tries to encode the soundness obligations of `may_dangle` in -the type system, directly, so that they can be checked by the compiler. - -## Custom Box and Custom Collections - -The perhaps main use-case for a stable `may_dangle` is custom collections. With -the `MyBox` above, we can have a `Drop` impl as below: - -```rust -impl Drop for MyBox { - fn drop(&mut self) { - unsafe { - drop_in_place(self.inner); - free(self.inner); - } - } -} -``` - -(N.B. this still uses `unsafe`! however, the unsafety is about upholding the -contract of `drop_in_place` and `free`, *not* the `: '!` mechanism.) - -## Self-referential types - -The second use-case for a stable `may_dangle` is the ability to have `Drop` for -self-referential types. This doesn't come up too often, but: - -```rust -struct Foo<'a> { - this: Cell>> -} - -impl<'a: '!> Drop for Foo<'a> { - fn drop(&mut self) { - ... - } -} -``` - -Without this proposal, such a struct cannot be dropped. (It is, in fact, -possible to write such a `Drop` impl. It just errors at site-of-use.) - -# Guide-level explanation -[guide-level-explanation]: #guide-level-explanation - -## Liveness obligations - -Liveness obligations are effectively bounds related to the liveness of a -type/lifetime. Lifetime bounds are themselves a form of liveness obligations: -a lifetime bound defines an outlives relationship between a type/lifetime and -another lifetime. However, there are also 3 kinds of liveness obligations which -are specially relevant to dropck. These can also be called dropck obligations, -and they are named as such: - -1. You must not touch the type/value in question. -2. You may only drop the type/value in question. -3. You may freely access the type/value in question. - -For a type which does not itself implement `Drop`, these are implied by what's -in the type. These obligations are akin to variance or traits like `Send` and -`Sync`. Unlike `Send`/`Sync` these cannot be overriden by non-`Drop` types. - -For a type which implements `Drop`, these obligations can be added to the type -parameters, in which case they must be explicitly specified on both the type -and the `Drop` impl. - -### You may only drop the type/value - -The most interesting of these obligations is probably obligation type 2: you may -only drop the type/value. This obligation is particularly interesting for -collection types. - -For example, consider a custom `Box`: - -```rust -struct MyBox { - inner: *const T, -} - -impl MyBox { - fn new(t: T) -> Self { ... } -} - -impl Drop for MyBox { - fn drop(&mut self) { ... } -} -``` - -This is... fine. However, actual `Box`es have the following property: - -```rust -let x = String::new(); -let y = Box::new(&x); -drop(x); -// y implicitly dropped here -``` - -Meanwhile, using `MyBox` produces the following error: - -```text -error[E0505]: cannot move out of `x` because it is borrowed - --> src/main.rs:16:10 - | -15 | let y = MyBox::new(&x); - | -- borrow of `x` occurs here -16 | drop(x); - | ^ move out of `x` occurs here -17 | // y implicitly dropped here -18 | } - | - borrow might be used here, when `y` is dropped and runs the `Drop` code for type `MyBox` -``` - -As the error says, "borrow might be used here, when `y` is dropped and runs the -`Drop` code for type `MyBox`". To allow `MyBox` to have this property, simply -put a "You may only drop the type/value in question." bound on `T`, like so: - -```rust -struct MyBox { - inner: *const T, -} - -// N.B: `'!` provides a *relaxation* of bounds, similarly to `?Sized`, hence -// neither `Droppable` nor `'!` appear here. -impl MyBox { - fn new(t: T) -> Self { ... } -} - -impl Drop for MyBox { - fn drop(&mut self) { ... } -} -``` - -This "You may only drop the type/value in question." bound, represented by -`T: Droppable + '!`, is only (directly) compatible with exactly one function: -`core::ptr::drop_in_place`, which has the same bound. (N.B. it's also perfectly -fine to wrap `drop_in_place` and call it indirectly, as long as the dropck -bound is carried around.) Trying to use `T` in any other way causes an error. -For our example, this bound prevents the `Drop` impl from using the borrow, -tho it can still be dropped. (Obviously, dropping a borrow is a no-op.) - -### You may freely access the type/value in question - -This obligation is the default obligation (requires no additional syntax), for -both backwards compatibility reasons and because it puts no restrictions on the -`Drop` impl: the `Drop` impl can do whatever it wants with the given type/value. - -For example, it could print something out on `Drop`: - -```rust -use core::fmt::Display; - -struct PrintOnDrop(T); - -impl Drop for PrintOnDrop { - fn drop(&mut self) { - println!("{}", self.0); - } -} -``` - -or even create new instances of `T`: - -```rust -struct Foo(T); - -impl Drop for Foo { - fn drop(&mut self) { - T::default(); - } -} -``` - -This is only possible while the type parameter is still live. As such, the -compiler rejects the following code: - -```rust -let x = String::new(); -let y = PrintOnDrop(&x); -drop(x); -// y dropped here -``` - -```text -error[E0505]: cannot move out of `x` because it is borrowed - --> src/main.rs:14:10 - | -13 | let y = PrintOnDrop(&x); - | -- borrow of `x` occurs here -14 | drop(x); - | ^ move out of `x` occurs here -15 | // y implicitly dropped here -16 | } - | - borrow might be used here, when `y` is dropped and runs the `Drop` code for type `PrintOnDrop` -``` - -And it also rejects an `T: Droppable + '!` bound: - -```rust -struct PrintOnDrop(T); - -impl Drop for PrintOnDrop { - fn drop(&mut self) { - println!("{}", self.0); - } -} -``` - -```text -`self.0` does not live long enough, [...] -``` - -### You must not touch the type/value in question - -This obligation prevents you from touching the type/value in your `Drop` impl -entirely. This is also the most flexible for the *user* of your type. - -So far, the other 2 dropck obligations have applied to type parameters. This -dropck obligation instead applies to lifetimes. - -Actually, there are 2 forms of it: As applied to one or more lifetimes, it -prevents accessing those lifetimes. As applied to a type, it -prevents accessing the *type* entirely. - -#### As applied to one (or more) lifetimes - -[modified from tests/ui/dropck/issue-28498-ugeh-with-lifetime-param.rs] - -[TODO explain] - -```rust -// run-pass - -// Demonstrate the use of the unguarded escape hatch with a lifetime param -// to assert that destructor will not access any dead data. -// -// Compare with ui/span/issue28498-reject-lifetime-param.rs - -#![feature(dropck_obligations)] - -#[derive(Debug)] -struct ScribbleOnDrop(String); - -impl Drop for ScribbleOnDrop { - fn drop(&mut self) { - self.0 = format!("DROPPED"); - } -} - -struct Foo<'a: '!>(u32, &'a ScribbleOnDrop); - -impl<'a: '!> Drop for Foo<'a> { - fn drop(&mut self) { - // Use of `'a: '!` means destructor cannot access `self.1`. - println!("Dropping Foo({}, _)", self.0); - } -} - -fn main() { - let (last_dropped, foo0); - let (foo1, first_dropped); - - last_dropped = ScribbleOnDrop(format!("last")); - first_dropped = ScribbleOnDrop(format!("first")); - foo0 = Foo(0, &last_dropped); - foo1 = Foo(1, &first_dropped); - - println!("foo0.1: {:?} foo1.1: {:?}", foo0.1, foo1.1); -} -``` - -#### As applied to a type - -When applied to a type: - -```rust -struct Bar<'a: '!, T: '!>(&'a T); - -impl<'a: '!, T: '!> Drop for Bar<'a, T> { - ... -} -``` - -This makes `T` completely unusable. - -# Reference-level explanation -[reference-level-explanation]: #reference-level-explanation - -## Syntax - -The core syntax for dropck obligations is `'!` in bound position. This is -further restricted to only being allowed in functions (including trait -functions), data types (structs, enums), and impls. - -## Extent of bound-ness and interactions with borrowck - -May dangle bounds aren't true bounds - they instead opt-out of a bound, similar -to `?Sized`. - -Specifically, given a function like - -```rust -fn foo<'a, T>(a: &'a (), t: T) -``` - -The borrowck implicitly introduces an fn-scope lifetime that basically exists -for the duration of the function call, and defines everything as needing to -outlive it. Effectively desugared as: - -```rust -fn foo<'a: 'fn, T: 'fn>(a: &'a (), t: T) -``` - -The may dangle bound, `'!`, simply opts-out of this mechanism. This requires -borrowck to treat the type/lifetime as if it were already dropped/moved, -just as how it would be if one tried to write: - -```rust -fn main() { - let y; - { - let x = String::new(); - y = &x; - } - y; -} -``` - -But this would still forbid our `MyBox` impl. We need a mechanism to identify -if a type is safe to drop in a given lifetime, without introducing that lifetime -as a bound. - -Given a hypothetical `T: SafeToDrop in 'a` syntax which is distinct from -`T: SafeToDrop<'a>`, and desugaring implicit borrowck lifetimes, we can discuss -whether a type is safe to drop: - -For a type `T` with no (type or lifetime) parameters, regardless of `Drop` impl, -we have: - -```rust -struct Foo { - field: Type, - ... -} - -impl SafeToDrop for Foo where Type: SafeToDrop in 'self {} -``` - -(This is effectively always true - Rust doesn't have "undroppable types", tho -it does have undroppable *values*.) - -For a type with lifetime parameters, but no type parameters, and no `Drop`: - -```rust -struct Foo<'a> { - field: &'a Type, - ... -} - -impl<'a> SafeToDrop for Foo<'a> where Type, 'a {} -``` - -(Note the lack of bounds on `Type`! This is because of `&'a T`, more on this -later.) - -For a type with type parameters and no `Drop`, we just extend the field rule to -type parameters, as such: - -```rust -struct Foo { - field: T, - ... -} - -impl SafeToDrop for Foo where T: SafeToDrop in 'self {} -``` - -When `Drop` is involved, we then add additional requirements, unless they've -been opted out: - -```rust -struct Foo<'live, 'opt_out: '!, Accessible, Dropped: Droppable + '!, Dead: '!> { - ... -} - -impl<...> Drop for Foo<...> { ... } - -impl<'live, 'opt_out, Accessible, Dropped, Dead> SafeToDrop for Foo<...> where - 'live: 'self, - 'opt_out, - Accessible: 'self, - Dropped: SafeToDrop in 'self, - Dead, - ... // + bounds generated by fields in Foo -{} -``` - -## Parametricity (or lack thereof) - -Dropck bounds are non-parametric. They effectively assert properties about the -concrete type `T`: either that it is safe to use, or that it is safe to drop. - -If it is safe to use, then the `Drop` impl is allowed to use it. If it's safe to -drop, then the `Drop` impl is only allowed to drop it. - -Given the classic example of parametric dropck unsoundness: -(rust-lang/rust#26656, adapted) - -```rust -// Using this instead of Fn etc. to take HRTB out of the equation. -trait Trigger { fn fire(&self, b: &mut B); } -impl Trigger for () { - fn fire(&self, b: &mut B) { - b.push(); - } -} - -// Still unsound Zook -trait Button { fn push(&self); } -struct Zook { button: B, trigger: Box+'static> } - -impl Drop for Zook { - fn drop(&mut self) { - self.trigger.fire(&mut self.button); - } -} - -// AND -struct Bomb { usable: bool } -impl Drop for Bomb { fn drop(&mut self) { self.usable = false; } } -impl Bomb { fn activate(&self) { assert!(self.usable) } } - -enum B<'a> { HarmlessButton, BigRedButton(&'a Bomb) } -impl<'a> Button for B<'a> { - fn push(&self) { - if let B::BigRedButton(borrowed) = *self { - borrowed.activate(); - } - } -} - -fn main() { - let (mut zook, ticking); - zook = Zook { button: B::HarmlessButton, - trigger: Box::new(()) }; - ticking = Bomb { usable: true }; - zook.button = B::BigRedButton(&ticking); -} -``` - -This errors directly in `Zook::drop`, since it attempts to call an -inappropriate function. (Using the desugaring mentioned in the previous section, -it attempts to use an `B: 'fn` function, but it only has an -`B: SafeToDrop in 'fn`.) - -## Interactions with `ManuallyDrop` and pointer types - -For some historical background, `may_dangle` interacts badly with -`ManuallyDrop`. This is unsound: - -```rust -struct Foo(ManuallyDrop); - -unsafe impl<#[may_dangle] T> Drop for Foo { - fn drop(&mut self) { - unsafe { ManuallyDrop::drop(&mut self.0) } - } -} -``` - -Because `may_dangle` just defers to the drop obligations of the fields, and -`ManuallyDrop` does not have drop obligations regardless of its contents, you -can give it a type that *does* have (invalid) drop obligations and cause UB: - -```rust -struct Bomb<'a>(&'a str); -impl<'a> Drop for Bomb<'a> { - fn drop(&mut self) { println!("{}", self.0); } -} - -let s = String::from("hello"); -let bomb = Foo(Bomb(&s)); -drop(s); -``` - -Meanwhile, this RFC requires the `Drop for Foo` to be annotated with "safe to -drop" obligations on `T` (i.e. `T: Droppable + '!`), preventing this mistake. -With `may_dangle`, one needs to remember to add a `PhantomData` for the -dropck obligations instead. Likewise for pointer types. - -While the compiler can't prevent double-frees here without real typestate, it -can at least prevent regressions like rust-lang/rust#99413. - -Using the previously mentioned desugaring syntax (see: interactions with -borrowck), we can describe the behaviour of `ManuallyDrop` and pointer types: - -```rust -// there is some bikeshedding about the existence of dangling references, but -// #[may_dangle] does allow it, and this is observable on stable through various -// means. -impl<'a, T> SafeToDrop for &'a T where 'a, T {} -impl<'a, T> SafeToDrop for &'a mut T where 'a, T {} - -impl SafeToDrop for ManuallyDrop where T {} -impl SafeToDrop for *const T where T {} -impl SafeToDrop for *mut T where T {} - -// this is arguably not a pointer type, but for internal purposes it is treated -// as one. -impl SafeToDrop for Box where T: SafeToDrop in 'self {} -``` - -## Dropck elaboration - -After processing all of the above, we now have our `SafeToDrop` impls. We then -evaluate them at the drop lifetime. As an exaple, the desugared `drop` fn is: - -```rust -fn drop(t: T) { -} -``` - -## Dropck obligations for built-in types - -The following types have the given dropck obligations (based on existing usage -of `#[may_dangle]`), using the "fully-desugared" syntax (since these - aside -from `ManuallyDrop` - aren't representable using `struct` syntax): - -```text -// The previously mentioned pointer types (and `ManuallyDrop`): -impl<'a, T> SafeToDrop for &'a T where 'a, T {} -impl<'a, T> SafeToDrop for &'a mut T where 'a, T {} -impl SafeToDrop for ManuallyDrop where T {} -impl SafeToDrop for *const T where T {} -impl SafeToDrop for *mut T where T {} -``` - -And the next types have the given dropck obligations, using the surface syntax: - -``` -// Various container types: -struct OnceLock -struct RawVec -struct Rc -struct rc::Weak -struct VecDeque -struct BTreeMap -struct LinkedList -struct Box -struct Vec -struct vec::IntoIter -struct Arc -struct sync::Weak -struct HashMap -// FIXME: other types which currently use may_dangle but were not found by grep -``` - -We can also explain the behaviour of a few oddities. `PhantomData` and arrays -currently behave as such: - -```rust -impl SafeToDrop for PhantomData where T: SafeToDrop in 'self {} -impl SafeToDrop for [T; N] where T: SafeToDrop in 'self {} -``` - -But these are only checked if they are actually drop-elaborated! Since they -don't have drop glue, they're not usually checked. For more details, see -rust-lang/rust#103413. However, this RFC proposes making them: - -```rust -impl SafeToDrop for PhantomData where T {} -impl SafeToDrop for [T; N] where T: SafeToDrop in 'self {} -// not actually possible (at this time). see also `Copy`. -impl SafeToDrop for [T; 0] where T {} -``` - -# Drawbacks -[drawbacks]: #drawbacks - -Why should we *not* do this? - -# Rationale and alternatives -[rationale-and-alternatives]: #rationale-and-alternatives - -This design is a further refinement of RFC 1327 with intent to stabilize. It -tries to avoid introducing too many new concepts, but does attempt to integrate -the lessons learned from RFC 1327 into the type system - specifically, allowing -them to be checked by the compiler. - -In particular, this design explicitly prevents the user from doing unsound -operations in safe code, while also allowing `impl Drop` to be *safe* even in -the presence of liveness obligations/dropck bounds. - -The explicit goals are: - -- Safe `impl Drop` for self-referential structs. -- Preventing dropck mistakes, both with first-party and third-party collections. - -Explicit **non**-goals include: - -- Preventing double frees, use-after-free, etc in unsafe code. - -Leveraging borrowck is good. The fact that `: '!` acts akin to `?Sized` can be a -bit confusing, but is necessary for backwards compatibility, and further, if we -treat `may_dangle` as a bound, it's basically in the name: *may* dangle. - -# Prior art -[prior-art]: #prior-art - -- Compiler MCP 563: This RFC was supposed to come after the implementation of MCP 563 but that didn't happen. This RFC is basically a refinement of the ideas in the MCP. -- Unsound dropck elaboration for `BTreeMap`: -- `may_dangle`: RFC 1238, RFC 1327 - -# Unresolved questions -[unresolved-questions]: #unresolved-questions - -## Spooky-dropck-at-a-distance - -Currently, the following code is not accepted by the compiler: - -```rust -use core::cell::Cell; -use core::marker::PhantomData; - -struct Dropper(T); - -impl Drop for Dropper { - fn drop(&mut self) {} -} - -struct Foo<'a, T>(PhantomData>>>, T); - -fn main() { - fn make_selfref<'a, T>(x: &'a Foo<'a, T>){} - let x = Foo(PhantomData, String::new()); - make_selfref(&x); -} -``` - -At the same time, replacing `String::new()` with `()` makes it compile, which -is really surprising: after all, the error is seemingly unrelated to the -`String`. This is an interaction between at least 4 factors: `T` having drop -glue, the presence of `Dropper`, the invariance of `'a`, and the use of -`make_selfref`. - -This RFC recommends removing this spooky-dropck-at-a-distance behaviour -entirely, to make the language less surprising. Since this RFC ties -user-defined dropck obligations with a `Drop` impl, it automatically prevents -users from defining similar spooky-dropck behaviour. If removed, the above -example would be accepted by the compiler. - -However, this spooky-dropck behaviour can also be used in no-alloc crates to -detect potentially-unsound `Drop` impls in *current stable*. For example, the -`selfref` crate *could* do something like this: - -```rust -type UBCheck...; - -// paste selfref::opaque! ub_check here. -``` - -But this RFC provides an alternative way which does not require spooky-dropck, -and `selfref` explicitly does not rely on spooky-dropck as the behaviour of -spooky-dropck has changed in the past: namely, between Rust 1.35 and Rust 1.36, -in the 2015 edition: . -(Arguably, this is when it *became* "spooky".) - -## Behaviour of `[T; 0]` - -Should `[T; 0]` have dropck obligations? As above, this also basically falls -under spooky-dropck-at-a-distance, since `[T; 0]` lacks drop glue. This RFC -proposes treating `[T; 0]` the same as `PhantomData`, as above. - -# Future possibilities -[future-possibilities]: #future-possibilities - -## `dyn Trait` dropck obligations - -Currently, if you attempt to make a self-referential (e.g.) `dyn Iterator`, you -get an error: - -```rust -use core::cell::RefCell; - -#[derive(Default)] -struct Foo<'a> { - vec: RefCell>>>, - iter: RefCell>>>>, -} - -fn main() { - let x = Foo::default(); - x.vec.borrow_mut().insert(vec![]).push(&x); -} -``` - -```text -error[E0597]: `x` does not live long enough - --> src/main.rs:11:44 - | -11 | x.vec.borrow_mut().insert(vec![]).push(&x); - | ^^ borrowed value does not live long enough -12 | } - | - - | | - | `x` dropped here while still borrowed - | borrow might be used here, when `x` is dropped and runs the destructor for type `Foo<'_>` -``` - -A future RFC could build on this RFC to allow traits to demand dropck -obligations from their implementers. Adding these bounds to existing traits -would be semver-breaking, so it can't be done with `Iterator`, but it could -be useful for other traits. - -## Generalize to "bounds generics" or "associated bounds" (or typestate?) - -This proposal limits itself to dropck semantics, but a future proposal could -generalize these kinds of bounds to some sort of "bounds generics" or -"associated bounds" or "typestate" kind of system. From 7a4889db7f83cf01771792f64172548cd184f4bd Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Fri, 24 Feb 2023 09:32:44 -0300 Subject: [PATCH 10/10] Clarify bounding rules --- text/3390-daddy-borrowck-little-dropck.md | 29 ++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/text/3390-daddy-borrowck-little-dropck.md b/text/3390-daddy-borrowck-little-dropck.md index aacf9babefd..4788959d104 100644 --- a/text/3390-daddy-borrowck-little-dropck.md +++ b/text/3390-daddy-borrowck-little-dropck.md @@ -220,7 +220,7 @@ References have a few special properties: This RFC relaxes those properties. For the first one, we have that `&'a &'b T` does no longer imply `'b: 'a` if `'b: '!`, and thus `&'b T: 'a` is also no -longer true. +longer true. That is, `&'a U` does not imply `U: 'a`, at least not inherently. The second one is a bit more complicated. However, there is an existing mechanism which also breaks this assumption: `#[may_dangle]`. Putting @@ -228,6 +228,23 @@ mechanism which also breaks this assumption: `#[may_dangle]`. Putting the code runs (but `#[may_dangle]` has its own set of issues, like allowing the relevant lifetimes to be interacted with). +## Interactions with implied lifetime bounds + +Rust has an implied lifetime bounds mechanism, particularly on impl blocks. + +Opting out of the implied lifetime also opts out of the implied lifetime bounds +that would apply to said lifetime, but otherwise this mechanism is not changed: + +```rust +impl<'a, 'b, T> Foo<'a, 'b, T> { + // here we have 'b: 'a, T: 'b +} + +impl<'a: '!, 'b: '!, T: '!> Bar<'a, 'b, T> { + // here we can no longer imply 'b: 'a, T: 'b +} +``` + ## Interactions with dropck This opt-out would enable implementing `Drop` for self-referential structs: @@ -355,12 +372,22 @@ to define that). But simply exposing `SafeToDrop` is enough to avoid issues like rust-lang/rust#99413. (Just because unsafe Rust is unsafe, doesn't mean we should add known footguns.) +Also, compared to other proposals using similar syntax, `'!` is explicitly not a +lifetime. Further, this RFC rejects the possibility of empty lifetimes +altogether: the intersection between a lifetime and an expired lifetime is an +anonymous expired lifetime. This is in alignment with existing borrowck rules, +in particular the disjoint block rule which is the basis of this RFC, and avoids +the soundness issues that would be introduced by these other proposals. + # Prior art [prior-art]: #prior-art - Compiler MCP 563: This RFC was supposed to come after the implementation of MCP 563 but that didn't happen. This RFC is basically a refinement of the ideas in the MCP. - Unsound dropck elaboration for `BTreeMap`: - `may_dangle`: RFC 1238, RFC 1327 +- empty lifetimes: (sorry, + couldn't find the proposals that use the `'!` syntax, but they're roughly + the same as this.) # Unresolved questions [unresolved-questions]: #unresolved-questions