How does explicit invocation of `drop` impact the borrow checker?

I'm confused by the question that the borrow checker ignores the effect of calling drop. Consider this example

struct Foo<T>(T);
impl<T> Drop for Foo<T> {
    fn drop(&mut self) {
        println!("dropping Foo");
    }
}
fn main(){
    let foo = Foo(0);  
    let s = String::from("123");
    println!("main");
}  // The compiler may implicitly insert the (pseudo)code at this point
  // Just for exposing where the point the last use of the variable is
   // drop(s);
   // drop(foo);

We can observe the "dropping Foo" will be printed after "main". If we change the code to

fn main(){
    let foo = Foo(0);  
    let s = String::from("123");
    drop(foo);  // #1
    println!("main");
}  // The explicit `drop(foo)` tells the compiler that we don't need to insert `drop(foo)` here.

We can observe "dropping Foo" will be printed before "main", which seems like the compiler won't implicitly drop(foo) at the end of the scope since we have explicitly drop foo at #1. Now, consider a confusing example

fn main(){
    let foo;
    let s = String::from("123");
    foo = Foo(&s);
    drop(foo);  // #2
}  // Since we explicitly drop foo, 
//the compiler won't insert `drop(foo)` at this point, 
// the use region of `foo` should be narrowed due to the explicit `drop(foo)`.

s will have a larger region than foo, however, the compiler will emit an error:

s does not live long enough

What's the reason? By logic, the last use of foo is at #2 while the last use of s will be at the end of the scope, NLL should apply to foo at #2.

How exactly does your Foo struct look like?

This works fine for me:

struct Foo<'a> {
    pub s: &'a str,
}

fn main(){
    let foo;
    let s = String::from("123");
    foo = Foo{ s: &s };
    drop(foo);
}

You didn't implement Drop for Foo. if you implement Drop, the code will be an error.

Yeah. But I think the error message explains why:

s does not live long enough
values in a scope are dropped in the opposite order they are defined
main.rs(65, 1): s dropped here while still borrowed
main.rs(65, 1): borrow might be used here, when foo is dropped and runs the Drop code

Item foo was defined first, so will be dropped last (after s).

Change code to this and it works:

fn main(){
    let s = String::from("123");
    let foo = Foo{ s: &s };
    drop(foo);
}

The question is, why the fact that foo is moved before the end of scope doesn't change this?

1 Like

From documentation of drop():

This effectively does nothing for types which implement Copy. Such values are copied and then moved into the function, so the value persists after this function call.

This function is not magic; it is literally defined as

pub fn drop<T>(_x: T) { }

I don't think Foo in this example (implicitly) implements Copy, though :thinking:

Uhh. Not related to the question itself, but this is invalid code.

struct Foo<T>(T);   // No `T: Debug` Here
impl<T: Debug> Drop for Foo<T> {
    fn drop(&mut self) {
        println!("dropping Foo");
    }
}

You can't conditional impl Drop with a where clause.

So… here’s the actual problem that explains the behavior of the borrow checker as far as I’m aware: What if the function exits early on panic?

struct Foo<T>(T);
impl<T> Drop for Foo<T> {
    fn drop(&mut self) {
        println!("dropping Foo");
    }
}

fn main() {
    let foo;
    let s = String::from("123");
    foo = Foo(&s);
    // what if the function panics at this point?
    something_that_might_panic();

    drop(foo);
}

fn something_that_might_panic() { /* … */ }

Of course in the real code, there is nothing there that could actually panic, but the borrow checking and drop checking will, as far as I’m aware, have little or maybe even no awareness of what operations could or could not panic, and they are designed in a way that would conservatively assume that panics could happen anywhere in the code execution, even in places where there is no code at all.


In other settings where panics at any place would not create a problem, then the borrow checker is able to reason about calls to drop by the way. E.g.

struct Foo<T>(T);
impl<T> Drop for Foo<T> {
    fn drop(&mut self) {
        println!("dropping Foo");
    }
}

fn main() {
    let mut s = String::from("123");
    let foo = Foo(&mut s);

    drop(foo); // will compile-error if this line is removed
    let bar = Foo(&mut s);
}
7 Likes

It's the same case shown in the nomicon and it's explained as:

Interestingly, only generic types need to worry about this. If they aren't generic, then the only lifetimes they can harbor are 'static, which will truly live forever. This is why this problem is referred to as sound generic drop. Sound generic drop is enforced by the drop checker. As of this writing, some of the finer details of how the drop checker (also called dropck) validates types is totally up in the air. However The Big Rule is the subtlety that we have focused on this whole section:

For a generic type to soundly implement drop, its generics arguments must strictly outlive it.

What does that mean? Consider the two functions:

fn f(){ // error
    let foo;
    let s = String::from("123");
    foo = Foo(&s); // &'tmp s -> Foo<&'tmp String>
    drop(foo);
}

// Big Rule: For a generic type to soundly implement drop, 
// its generics arguments must strictly outlive it.
fn g(){ // ok
    let s = String::from("123");
    let foo; // Foo<'foo>
    foo = Foo(&s); // &'tmp s -> &'foo s -> Foo<&'foo String> ie. 'tmp strictly outlives 'foo does exist
}

struct Foo<T>(T);
impl<T> Drop for Foo<T> {
    fn drop(&mut self) {
        println!("dropping Foo");
    }
}

The generics argument is T, and specifically &'foo String, the Big Rule requires &'tmp String strictly outlives Foo<&'foo String>, ie. 'tmp: 'foo but 'tmp can't be 'foo. The first function f can't do this, so fails.

1 Like

I have modified the code. No Debug is required.

What if we do not use the generic type?

struct Foo<'a>(&'a String);
impl<'a> Drop for Foo<'a> {
    fn drop(&mut self) {
        println!("dropping Foo");
    }
}

fn main() {
    let foo;
    let  s = String::from("123");
    foo = Foo(&s);
    drop(foo); 
}

The code is still an error. Or, Is it to say we consider 'a the generic type too, the Big Rule still applies to this example?

So, to spell things out explicitly: The idea is that if there was a panic between the lines
foo = Foo(&s);
and
drop(foo);
then all initialized local variables would be dropped in usual drop order, which is reverse order of initialization declaration.

This would mean that first s would be dropped, and then foo would be dropped. However, the destructor of Foo that would be run (after the String is already gone!) has access to the String field via the &mut self argument.

This has nothing to do with generic type arguments. Edit: Sorry if this statement caused confusion, see further discussion below.


For further illustration: if we were to circumvent the borrow checker and also insert a panic at the right place, as well as making the Drop impl actually access the String, we get really well-observable UB.

first without the panic, not quite UB yet

struct Foo<'a>(&'a String);
impl<'a> Drop for Foo<'a> {
    fn drop(&mut self) {
        println!("dropping Foo: {}", *self.0);
    }
}


fn circumvent_the_borrow_checker<'a, 'b, T>(r: &'a T) -> &'b T {
    unsafe {
        &*(r as *const T)
    }
}

fn main() {
    let foo;
    let s = String::from("123");
    foo = Foo(circumvent_the_borrow_checker(&s));
    // panic!();
    drop(foo); 
}

output:

dropping Foo: 123

with the panic

struct Foo<'a>(&'a String);
impl<'a> Drop for Foo<'a> {
    fn drop(&mut self) {
        println!("dropping Foo: {}", *self.0);
    }
}


fn circumvent_the_borrow_checker<'a, 'b, T>(r: &'a T) -> &'b T {
    unsafe {
        &*(r as *const T)
    }
}

fn main() {
    let foo;
    let s = String::from("123");
    foo = Foo(circumvent_the_borrow_checker(&s));
    panic!();
    drop(foo); 
}

output on current stable compiler, tested in playground, still looks somewhat harmless, but already questionable:

dropping Foo: 

but actually it’s undefined behavior:

error: Undefined Behavior: trying to retag from <3355> for SharedReadOnly permission at alloc1659[0x0], but that tag does not exist in the borrow stack for this location
  --> src/main.rs:4:38
   |
4  |         println!("dropping Foo: {}", *self.0);
   |                                      ^^^^^^^
   |                                      |
   |                                      trying to retag from <3355> for SharedReadOnly permission at alloc1659[0x0], but that tag does not exist in the borrow stack for this location
   |                                      this error occurs as part of retag at alloc1659[0x0..0x18]
   |
   = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
   = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <3355> was created by a SharedReadOnly retag at offsets [0x0..0x18]
  --> src/main.rs:18:5
   |
18 |     foo = Foo(circumvent_the_borrow_checker(&s));
   |     ^^^
help: <3355> was later invalidated at offsets [0x0..0x18] by a Unique retag
  --> src/main.rs:21:1
   |
21 | }
   | ^
   = note: BACKTRACE (of the first span):
   = note: inside `<Foo<'_> as std::ops::Drop>::drop` at src/main.rs:4:38: 4:45
   = note: inside `std::ptr::drop_in_place::<Foo<'_>> - shim(Some(Foo<'_>))` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:490:1: 490:56
note: inside `main`
  --> src/main.rs:21:1
   |
21 | }
   | ^
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

And if a longer string than "123" is used instead, we can even see visible garbage data coming from the allocator’s internals.

// ……
let s = String::from("Hello World, this string is longer!");
// ……

output (varies):

dropping Foo: ��9)\Us string is longer!
2 Likes

Thanks. Your interpretation is wonderful. Another question is, where is the part in rust reference says something like:

The idea is that if there was a panic between the lines, then all initialized local variables would be dropped in usual drop order, which is reverse order of initialization.

Destructors - The Rust Reference only says

Each variable or temporary is associated to a drop scope. When control flow leaves a drop scope all variables associated to that scope are dropped in reverse order of declaration (for variables) or creation (for temporaries).

When a well-formed code is executed step-by-step, the control flow will eventually leave the drop scope, which case will have the behavior specified in the above rule. However, is panic!() considered a case that results in the control flow leaving the scope? I'm looking for the relevant specification about this behavior for panic in reference, however, I cannot find the relevant wording.

Yes, as far as I understand, an (unwinding) panic!() is to be understood to be leaving the scope of the current function in the same manner as an early return would.

I just noticed a typo in my statement. I’ve of course meant to say “reverse order of declaration”.

I'm wrong on the Big Rule :frowning:
Do you have interpretations about it? @steffahn The sentence:

For a generic type to soundly implement drop, its generics arguments must strictly outlive it.

The nomicon is considering both types with generic type arguments such as Foo<T> as well as types with generic lifetime arguments such as Foo<'a> as a “generic types” with “generic arguments”.

Sorry for any confusion from my previous comment, I hadn’t looked into the context of your quotation from the nomicon until now.

There’s often multiple ways to explain the same issue around borrow checking and related checks in the compiler. Either by trying to describe the exact rules it follows, or by pointing out practical considerations of what kind of UB-causing scenarios are being prevented. I followed the latter approach, whilst thinking about the effects of generic type- or lifetime-arguments in Drop impls follows the former approach.


So after this reconsideration, I can answer this part

with a clear yes :innocent:

2 Likes

an (unwinding) panic!() is to be understood to be leaving the scope of the current function in the same manner as an early return would.

Ok. I'm looking for the relevant wording about this part. Do you know where it is in the rust reference?

It might be underdocumented, or at least unnecessarily hard to find. I am having trouble finding good documentation in the reference, too.

Here is a possibly useful perspective on drop() in general:

As far as compiler cares, drop() is just a function with signature fn(T) -> (). A function with this signature does not necessarily drop the value; it could, for example, pass the T to a newly created thread, or put it in some existing global storage.

Thus, for purposes of static analysis done by the compiler, drop() does not even guarantee the value is dropped. The only static guarantee is that when drop(foo) is executed, foo is moved out, which is equally true of any other function call. (And as already discussed, the drop() might not be executed if unwinding happens before then.)