Skip to content

TimeCapsule&FrozenFuture is an unsound API, reliant on users to .await the .freeze(&mut …) future properly in-place #8

@steffahn

Description

@steffahn

So, well, TimeCapsule<T> can accept a reference of just any lifetime for freeze. It produces a FrozenFuture<'a, 'b, T> that does have some lifetime corresponding to that reference, but all that ensures is that the reference stays alive until that future is polled, nothing else. If you do end up just .awaiting that future in-place (in particular, converting the Poll::pending of FrozenFuture into an immediately propagated Poll::pending of the surrounding future), then all may be well, but who forces you to do this?

That’s an easy exploit: Just poll the future, then invalidate the reference, and only then “propagate” the Poll::pending. Below is a straightforward implementation of this using helper macros from the futures crate family:

use std::marker::PhantomData;
use nolife::{BoxScope, Family, TimeCapsule};
use futures_util::{poll, pending};

struct SingleFamily<T: 'static>(PhantomData<T>);
impl<'a, T: 'static> Family<'a> for SingleFamily<T> {
    type Family = T;
}

fn main() {
    {
        let mut scope = BoxScope::new(
            |mut time_capsule: TimeCapsule<SingleFamily<String>>| async move {
                let mut x = String::from("Hello World!");
                let fut = time_capsule.freeze(&mut x);
                let _ = poll!(fut); // puts reference into state
                drop(x); // then invalidates reference
                pending!(); // then yields
                panic!();
            },
        );
        scope.enter(|s| {
            println!("{s}");
        })
    }
}

outputs garbage data, e.g.:

b����

miri output

error: Undefined Behavior: out-of-bounds pointer use: alloc877 has been freed, so this pointer is dangling
   --> /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/slice/raw.rs:109:9
    |
109 |         &*ptr::slice_from_raw_parts(data, len)
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ out-of-bounds pointer use: alloc877 has been freed, so this pointer is dangling
    |
    = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
    = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
help: alloc877 was allocated here:
   --> src/main.rs:14:29
    |
14  |                 let mut x = String::from("Hello World!");
    |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: alloc877 was deallocated here:
   --> src/main.rs:17:17
    |
17  |                 drop(x); // then invalidates reference
    |                 ^^^^^^^
    = note: BACKTRACE (of the first span):
    = note: inside `std::slice::from_raw_parts::<'_, u8>` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/slice/raw.rs:109:9: 109:47
    = note: inside `<std::vec::Vec<u8> as std::ops::Deref>::deref` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:2709:18: 2709:64
    = note: inside `<std::string::String as std::ops::Deref>::deref` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/string.rs:2539:43: 2539:52
    = note: inside `<std::string::String as std::fmt::Display>::fmt` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/string.rs:2354:28: 2354:34
    = note: inside `<&mut std::string::String as std::fmt::Display>::fmt` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:2298:62: 2298:82
    = note: inside `core::fmt::rt::Argument::<'_>::fmt` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/rt.rs:142:9: 142:40
    = note: inside `std::fmt::write` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:1120:17: 1120:40
    = note: inside `<std::io::StdoutLock<'_> as std::io::Write>::write_fmt` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/mod.rs:1846:15: 1846:43
    = note: inside `<&std::io::Stdout as std::io::Write>::write_fmt` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:736:9: 736:36
    = note: inside `<std::io::Stdout as std::io::Write>::write_fmt` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:710:9: 710:33
    = note: inside `std::io::stdio::print_to::<std::io::Stdout>` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:1029:21: 1029:47
    = note: inside `std::io::_print` at /home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:1106:5: 1106:37
note: inside closure
   --> src/main.rs:23:13
    |
23  |             println!("{s}");
    |             ^^^^^^^^^^^^^^^
    = note: inside `nolife::scope::Scope::<SingleFamily<std::string::String>, {async block@src/main.rs:13:67: 20:14}>::enter::<'_, (), {closure@src/main.rs:22:21: 22:24}>` at /home/frank/.cargo/registry/src/index.crates.io-6f17d22bba15001f/nolife-0.3.3/src/scope.rs:166:22: 166:30
    = note: inside `nolife::BoxScope::<SingleFamily<std::string::String>, {async block@src/main.rs:13:67: 20:14}>::enter::<'_, (), {closure@src/main.rs:22:21: 22:24}>` at /home/frank/.cargo/registry/src/index.crates.io-6f17d22bba15001f/nolife-0.3.3/src/box_scope.rs:117:18: 117:41
note: inside `main`
   --> src/main.rs:22:9
    |
22  | /         scope.enter(|s| {
23  | |             println!("{s}");
24  | |         })
    | |__________^
    = note: this error originates in the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

Unfortunately, I cannot immediately think of straightforward fixes for soundness here.1 This might need more thought though. Let’s hope there is some alternative approach out there, and that his doesn’t mean this whole crate’s idea is fundamentally broken.

Footnotes

  1. At least without an API that involves some mandatory usage of a macro that calls unsafe, possibly-hidden, API.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions