Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trait object with non-static lifetime is accepted where static lifetime is expected and required #72315

Closed
VFLashM opened this issue May 18, 2020 · 12 comments
Assignees
Labels
A-lifetimes Area: lifetime related A-traits Area: Trait system C-bug Category: This is a bug. E-needs-mcve Call for participation: This issue has a repro, but needs a Minimal Complete and Verifiable Example I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness ICEBreaker-Cleanup-Crew Helping to "clean up" bugs with minimal examples and bisections P-critical Critical priority regression-from-stable-to-stable Performance or correctness regression from one stable version to another. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@VFLashM
Copy link
Contributor

VFLashM commented May 18, 2020

Problem: function is explicitly defined as accepting Box<dyn Fn + 'static> and yet it accepts Box<dyn Fn + 'a> where 'a is definitely less than 'static.

It allows to keep reference to an object past object lifetime and therefore cause a crash.

Sorry for an extremely long example, but issue in question is very fragile and I couldn't reproduce it with smaller code.

use std::cell::RefCell;

// a simple trait with one function accepting two arguments:
// one by mutable reference and another one by value
trait Foo {
    type MutArg; // type of an argument passed by mutable reference
    type ValArg; // type of an argument passed by value

    fn foo(&self, marg: &mut Self::MutArg, varg: Self::ValArg);
}

// ValArg can be a complex function which can be used
// to pass MutArg from parent to child
// note that all trait objects must have 'static lifetime
pub type FnValArg<MutArg> = Box<dyn
    FnOnce(Box<dyn
        FnOnce(&mut MutArg) + 'static
    >) + 'static
>;

// FnFooBox is boxed Foo which ValArg is a function
type FnFooBox<MutArg1, MutArg2> = Box<dyn
    Foo<ValArg=FnValArg<MutArg1>, MutArg=MutArg2>
>;

// FnFoo factory creates boxed FnFoo value on the fly
type FnFooFactory<MutArg1, MutArg2> = Box<dyn
    Fn() -> FnFooBox<MutArg1, MutArg2>
>;

// FactoryFoo is a struct storing factory
// and implementing Foo
struct FactoryFoo<MutArg1, MutArg2> {
    factory: FnFooFactory<MutArg1, MutArg2>,
    // Note: if instead of factory I store `subfoo` directly,
    // bug does not reproduce (i.e. I get lifetime error as expected):
    // subfoo: FnFooBox<MutArg1, MutArg2>,
}

impl<MutArg1, MutArg2> Foo for FactoryFoo<MutArg1, MutArg2> {
    type MutArg = (MutArg1, MutArg2);
    type ValArg = i32; // irrelevant

    fn foo(&self, marg: &mut Self::MutArg, _varg: Self::ValArg) {
        let (marg1, marg2) = marg;
        let subfoo = (self.factory)();
        subfoo.foo(
            marg2,
            // `subfoo.foo` requires `varg` of type `FnValArg`
            // `FnValArg` defined as boxed closure with 'static lifetime
            //
            // this closure captures mutable `marg1`, and therefore
            // has the same lifetime as `marg`,
            // which is obviously less than 'static
            //
            // and yet it is accepted and everything works,
            // which allows `subfoo` to modify `marg1` if desired
            //
            // Note: if I move this expression into local variable
            // bug does not reproduce (i.e. I get lifetime error as expected)
            Box::new(move |subfun| subfun(marg1)),
        );
    }
}

// now let me illustrate that it's possible to cause a crash
// with this lifetime issue:
// idea is to capture reference to marg1 and store it globally

// global variable storing FnValArg, which in turn captures marg1 from FactoryFoo
thread_local! {
    static GLOBAL_FN: RefCell<Option<FnValArg<String>>> = RefCell::new(None);
}

// struct implementing Foo and storing varg
// (i.e. closure with captured `&mut marg1` inside)
// in global variable
struct CrashFnFoo {}

impl Foo for CrashFnFoo {
    type ValArg = FnValArg<String>;
    type MutArg = i32; // irrelevant

    fn foo(&self, _marg: &mut Self::MutArg, varg: Self::ValArg) {
        // store varg function in global variable
        GLOBAL_FN.with(|global_fn| {
            global_fn.replace(Some(varg))
        });
    }
}

fn main() {
    let factory = || -> FnFooBox<String, i32> { Box::new(CrashFnFoo{}) };
    let factory_foo = FactoryFoo { factory: Box::new(factory) };

    {
        let mut marg = (String::from("some data"), 0);
        // this captures `&mut marg.0` into `GLOBAL_FN`
        factory_foo.foo(&mut marg, 0);
    }

    // by now marg is gone, but reference to it is still
    // captured in closure inside of GLOBAL_FN
    // now we just have to access it
    GLOBAL_FN.with(|global_fn| {
        let fn_refcell = RefCell::new(None);
        global_fn.swap(&fn_refcell);

        if let Some(func) = fn_refcell.into_inner() {
            println!("crashing now");
            // modifying marg.0 String, which is long dead by now
            func(Box::new(|marg1: &mut String| {
                marg1.push_str("crash here, please");
            }));
        }
    });
}

I expected this error to happen (and it actually does happen if you change the example even a little bit):

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
  --> src/lib.rs:45:14
   |
45 |         let (marg1, marg2) = marg;
   |              ^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #2 defined on the method body at 44:5...
  --> src/lib.rs:44:5
   |
44 | /     fn foo(&self, marg: &mut Self::MutArg, _varg: Self::ValArg) {
45 | |         let (marg1, marg2) = marg;
46 | |         let subfoo = (self.factory)();
47 | |         self.subfoo.foo(
...  |
62 | |         );
63 | |     }
   | |_____^
note: ...so that reference does not outlive borrowed content
  --> src/lib.rs:45:14
   |
45 |         let (marg1, marg2) = marg;
   |              ^^^^^
   = note: but, the lifetime must be valid for the static lifetime...
note: ...so that the expression is assignable
  --> src/lib.rs:61:13
   |
61 |             Box::new(move |subfun| subfun(marg1)),
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: expected  `std::boxed::Box<(dyn std::ops::FnOnce(std::boxed::Box<(dyn for<'r> std::ops::FnOnce(&'r mut MutArg1) + 'static)>) + 'static)>`
              found  `std::boxed::Box<dyn std::ops::FnOnce(std::boxed::Box<(dyn for<'r> std::ops::FnOnce(&'r mut MutArg1) + 'static)>)>`

Instead this code is silently accepted and causes a segmentation error when executed.

Meta

I tried it both on stable and nightly rust:

$ rustc --version --verbose
rustc 1.45.0-nightly (7ebd87a7a 2020-05-08)
binary: rustc
commit-hash: 7ebd87a7a1e0e21767422e115c9455ef6e6d4bee
commit-date: 2020-05-08
host: x86_64-pc-windows-msvc
release: 1.45.0-nightly
LLVM version: 9.0
$ rustc --version --verbose
rustc 1.43.0 (4fb7144ed 2020-04-20)
binary: rustc
commit-hash: 4fb7144ed159f94491249e86d5bbd033b5d60550
commit-date: 2020-04-20
host: x86_64-pc-windows-msvc
release: 1.43.0
LLVM version: 9.0
@VFLashM VFLashM added the C-bug Category: This is a bug. label May 18, 2020
@matthewjasper matthewjasper self-assigned this May 18, 2020
@jonas-schievink jonas-schievink added A-lifetimes Area: lifetime related A-traits Area: Trait system I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 18, 2020
@nbdd0121
Copy link
Contributor

Seems to be an issue with type inference. Giving subfoo an explicit type would cause borrow checker to work properly.

@jonas-schievink jonas-schievink added the E-needs-mcve Call for participation: This issue has a repro, but needs a Minimal Complete and Verifiable Example label May 18, 2020
@matthewjasper
Copy link
Contributor

Maybe fixed by #71896 ?

@RalfJung
Copy link
Member

Wow, nice catch!

@spastorino spastorino added the I-prioritize Issue: Indicates that prioritization has been requested for this issue. label May 21, 2020
@spastorino
Copy link
Member

Would be nice to find and mcve ...

@rustbot ping cleanup

@rustbot
Copy link
Collaborator

rustbot commented May 21, 2020

Hey Cleanup Crew ICE-breakers! This bug has been identified as a good
"Cleanup ICE-breaking candidate". In case it's useful, here are some
instructions for tackling these sorts of bugs. Maybe take a look?
Thanks! <3

cc @AminArria @chrissimpkins @contrun @DutchGhost @elshize @ethanboxx @h-michael @HallerPatrick @hdhoang @hellow554 @imtsuki @jakevossen5 @kanru @KarlK90 @LeSeulArtichaut @MAdrianMattocks @matheus-consoli @mental32 @nmccarty @Noah-Kennedy @pard68 @PeytonT @pierreN @Redblueflame @RobbieClarken @RobertoSnap @robjtede @SarthakSingh31 @senden9 @shekohex @sinato @spastorino @turboladen @woshilapin @yerke

@rustbot rustbot added the ICEBreaker-Cleanup-Crew Helping to "clean up" bugs with minimal examples and bisections label May 21, 2020
@spastorino
Copy link
Member

Assigning P-critical as discussed as part of the Prioritization Working Group process and removing I-prioritize.

@LeSeulArtichaut
Copy link
Contributor

We should try to find out if this is a duplicate of #71550 or not.

@spastorino spastorino added P-critical Critical priority regression-from-stable-to-stable Performance or correctness regression from one stable version to another. and removed I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels May 21, 2020
@spastorino
Copy link
Member

I can confirm that the example code with #71896 applied gives ...

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
  --> test.rs:45:14
   |
45 |         let (marg1, marg2) = marg;
   |              ^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #2 defined on the method body at 44:5...
  --> test.rs:44:5
   |
44 | /     fn foo(&self, marg: &mut Self::MutArg, _varg: Self::ValArg) {
45 | |         let (marg1, marg2) = marg;
46 | |         let subfoo = (self.factory)();
47 | |         subfoo.foo(
...  |
62 | |         );
63 | |     }
   | |_____^
note: ...so that reference does not outlive borrowed content
  --> test.rs:45:14
   |
45 |         let (marg1, marg2) = marg;
   |              ^^^^^
   = note: but, the lifetime must be valid for the static lifetime...
note: ...so that the expression is assignable
  --> test.rs:61:13
   |
61 |             Box::new(move |subfun| subfun(marg1)),
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: expected  `std::boxed::Box<(dyn std::ops::FnOnce(std::boxed::Box<(dyn for<'r> std::ops::FnOnce(&'r mut MutArg1) + 'static)>) + 'static)>`
              found  `std::boxed::Box<dyn std::ops::FnOnce(std::boxed::Box<(dyn for<'r> std::ops::FnOnce(&'r mut MutArg1) + 'static)>)>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0495`.

@matthewjasper matthewjasper removed their assignment May 21, 2020
@senden9
Copy link

senden9 commented May 22, 2020

Knows anybody a version of rust where this example (from thread start) compiles & do not crash?
I tested with cargo bisect and found no working version from today till 2015. Either it did not compile or the result crashed.

So either my script-fu has an bug our there is no working rust version 🤔

@VFLashM
Copy link
Contributor Author

VFLashM commented May 22, 2020

Well, it is not supposed to compile. It's a broken app which is supposed to be rejected by compiler.

@pnkfelix
Copy link
Member

Discussed with @spastorino . This is probably a duplicate of #71550, but since it is P-critical and we have not 100% confirmed that these are exactly the same bug (in part because we do not have an MCVE for #72315).

Since @spastorino has confirmed that their PR #71896 does fix both issues, I'm going to assign this bug to them, just like #71550.

@Elinvynia Elinvynia added the I-prioritize Issue: Indicates that prioritization has been requested for this issue. label Jun 9, 2020
@LeSeulArtichaut LeSeulArtichaut removed the I-prioritize Issue: Indicates that prioritization has been requested for this issue. label Jun 9, 2020
bors added a commit to rust-lang-ci/rust that referenced this issue Jun 11, 2020
…riance, r=nikomatsakis

Relate existential associated types with variance Invariant

Fixes rust-lang#71550 rust-lang#72315

r? @nikomatsakis

The test case reported in that issue now errors with the following message ...

```
error[E0495]: cannot infer an appropriate lifetime for lifetime parameter 'a in function call due to conflicting requirements
  --> /tmp/test.rs:25:5
   |
25 |     bad(&Bar(PhantomData), x)
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the lifetime `'a` as defined on the function body at 24:11...
  --> /tmp/test.rs:24:11
   |
24 | fn extend<'a, T>(x: &'a T) -> &'static T {
   |           ^^
note: ...so that reference does not outlive borrowed content
  --> /tmp/test.rs:25:28
   |
25 |     bad(&Bar(PhantomData), x)
   |                            ^
   = note: but, the lifetime must be valid for the static lifetime...
note: ...so that the types are compatible
  --> /tmp/test.rs:25:9
   |
25 |     bad(&Bar(PhantomData), x)
   |         ^^^^^^^^^^^^^^^^^
   = note: expected  `&'static T`
              found  `&T`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0495`.
```

I could also add that test case if we want to have a weaponized one too.
@tesuji
Copy link
Contributor

tesuji commented Jun 11, 2020

Should be closed by #71896

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-lifetimes Area: lifetime related A-traits Area: Trait system C-bug Category: This is a bug. E-needs-mcve Call for participation: This issue has a repro, but needs a Minimal Complete and Verifiable Example I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness ICEBreaker-Cleanup-Crew Helping to "clean up" bugs with minimal examples and bisections P-critical Critical priority regression-from-stable-to-stable Performance or correctness regression from one stable version to another. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests