Coder Social home page Coder Social logo

cryo's Introduction

Cryo — Extend the lifetime of a reference. Safely.

docs.rs

Requires Rust 1.34.0 or later.

This crate provides a cell-like type Cryo that is similar to RefCell except that it constrains the lifetime of its borrowed value through a runtime check mechanism, erasing the compile-time lifetime information. The lock guard CryoRef created from Cryo is 'static and therefore can be used in various situations that require 'static types, including:

  • Storing CryoRef temporarily in a std::any::Any-compatible container.
  • Capturing a reference to create a Objective-C block.

This works by, when a Cryo is dropped, not letting the current thread's execution move forward (at least¹) until all references to the expiring Cryo are dropped so that none of them can outlive the Cryo. This is implemented by readers-writer locks under the hood.

¹ SyncLock blocks the current thread's execution on lock failure. LocalLock, on the other hand, panics because it's designed for single-thread use cases and would deadlock otherwise.

Examples

with_cryo, Cryo, and LocalLock (single-thread lock implementation, used by default):

use std::{thread::spawn, pin::Pin};

let cell: usize = 42;

// `with_cryo` uses `LocalLock` by default
with_cryo(&cell, |cryo: Pin<&Cryo<'_, usize, _>>| {
    // Borrow `cryo` and move it into a `'static` closure.
    let borrow: CryoRef<usize, _> = cryo.borrow();
    let closure: Box<dyn Fn()> =
        Box::new(move || { assert_eq!(*borrow, 42); });
    closure();
    drop(closure);

    // Compile-time lifetime works as well.
    assert_eq!(*cryo.get(), 42);

    // When `cryo` is dropped, it will block until there are no other
    // references to `cryo`. In this case, the program will leave
    // this block immediately because `CryoRef` has already been dropped.
});

with_cryo, Cryo, and SyncLock (thread-safe lock implementation):

use std::{thread::spawn, pin::Pin};

let cell: usize = 42;

// This time we are specifying the lock implementation
with_cryo((&cell, lock_ty::<SyncLock>()), |cryo| {
    // Borrow `cryo` and move it into a `'static` closure.
    // `CryoRef` can be sent to another thread because
    // `SyncLock` is thread-safe.
    let borrow: CryoRef<usize, _> = cryo.borrow();
    spawn(move || { assert_eq!(*borrow, 42); });

    // Compile-time lifetime works as well.
    assert_eq!(*cryo.get(), 42);

    // When `cryo` is dropped, it will block until there are no other
    // references to `cryo`. In this case, the program will not leave
    // this block until the thread we just spawned completes execution.
});

with_cryo, CryoMut, and SyncLock:

with_cryo((&mut cell, lock_ty::<SyncLock>()), |cryo_mut| {
    // Borrow `cryo_mut` and move it into a `'static` closure.
    let mut borrow: CryoMutWriteGuard<usize, _> = cryo_mut.write();
    spawn(move || { *borrow = 1; });

    // When `cryo_mut` is dropped, it will block until there are no other
    // references to `cryo_mut`. In this case, the program will not leave
    // this block until the thread we just spawned completes execution
});
assert_eq!(cell, 1);

Don't do these:

// The following statement will DEADLOCK because it attempts to drop
// `Cryo` while a `CryoRef` is still referencing it, and `Cryo`'s
// destructor will wait for the `CryoRef` to be dropped first (which
// will never happen)
let borrow = with_cryo((&cell, lock_ty::<SyncLock>()), |cryo| cryo.borrow());
// The following statement will ABORT because it attempts to drop
// `Cryo` while a `CryoRef` is still referencing it, and `Cryo`'s
// destructor will panic, knowing no amount of waiting would cause
// the `CryoRef` to be dropped
let borrow = with_cryo(&cell, |cryo| cryo.borrow());

Caveats

  • While it's capable of extending the effective lifetime of a reference, it does not apply to nested references. For example, when &'a NonStaticType<'b> is supplied to Cryo's constructor, the borrowed type is CryoRef<NonStaticType<'b>>, which is still partially bound to the original lifetime.

Details

Feature flags

  • std (enabled by default) enables SyncLock.

  • lock_api enables the blanket implementation of Lock on all types implementing lock_api::RawRwLock, such as spin::RawRwLock and parking_lot::RawRwLock.

  • atomic (enabled by default) enables features that require full atomics, which is not supported by some targets (detecting such targets is still unstable (#32976)). This feature will be deprecated after the stabilization of #32976.

Overhead

Cryo<T, SyncLock>'s creation, destruction, borrowing, and unborrowing each take one or two atomic operations in the best cases.

Neither of SyncLock and LocalLock require dynamic memory allocation.

Nomenclature

From cryopreservation.

License: MIT/Apache-2.0

cryo's People

Contributors

yvt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

cryo's Issues

`CryoMutWriteGuard` should be `!UnwindSafe`

Unlike RwLockWriteGuard, it does not implement poisoning. As such, if a panic-unsafe owner of CryoMutWriteGuard panics, subsequent lock owners may observe inconsistent stored data.

Unsoundness from panic on lock failure.

The problem

When the with_cryo function completes, it must guarantee that any outstanding CryoRef is gone. If any are left, it can't safely continue execution under any circumstances. In some cases (LocalLock and AtomicLock for example), this is done by panicking (specifically, unwinding). However, panics don't actually guarantee that execution will freeze, and this is unsound. There are a few ways to exploit this:

When a panic is thrown in a thread and it isn't caught, it deallocated the Cryo and stops the execution of the thread. However, the rest of the threads keep running, which is unsafe because they might still hold the CryoRef. This is Exploit through multithreading.

Altertnatively, execution can continue using the catch_unwind, which is a safe, std function. It is guarded by UnwindSafe, but it's merely a heuristic helper, not a safety guard (see UnwindSafe). This leads to unsoundness even in single threaded contexts. This is exploit through catch_unwind.

In addition, we can use the destructors called by the panic to use the leaked CryoRef. This is Exploit through destructors.

Examples

Exploit through multithreading

use std::sync::{Arc, Mutex};
use std::{panic::*, pin::Pin};
use cryo::*;

fn illegal_pointer_atomic() -> CryoRef<usize, AtomicLock> {
    let to_leak: Arc<Mutex<Option<CryoRef<usize, AtomicLock>>>> = Arc::new(Mutex::new(None));
    let to_leak_clone = to_leak.clone();

    let _join_res = std::thread::spawn(move || {
        let cell: usize = 42;

        // This call will panic
        with_cryo(
            (&cell, lock_ty::<AtomicLock>()),
            |cryo: Pin<&Cryo<'_, usize, _>>| {
                // leak a reference outside
                *to_leak_clone.lock().unwrap() = Some(cryo.borrow());
                // `with_cryo` will panic because the `Cryo` still has outstanding references to it
            },
        )
        // The panic will kill the thread
    })
    .join() // Join the thread to ensure it completed (panicked). Alternatively, just wait a bit.
    .expect_err("expected join failure");

    // Obtain and return the leaked `CryoRef<usize, AtomicLock>`
    let cryo_ref: CryoRef<usize, AtomicLock> = to_leak
        .lock()
        .expect("Mutex not available")
        .take()
        .expect("expected Leaked CryoRef");
    cryo_ref
}

fn main() {
    let illegal_cryo = illegal_pointer_atomic();
    println!("Printing address: {}", &*illegal_cryo as *const _ as i32);
    println!("Uninitialized read from address: {}", *illegal_cryo); // reading from the stack of a thread that terminated, i.e., an uninitialized read.
}

Exploit through catch_unwind

use std::{panic::*, pin::Pin};
use cryo::*;

fn illegal_pointer_local() -> CryoRef<usize, LocalLock> {
    let cell: usize = 42;

    let mut to_leak: Option<CryoRef<usize, LocalLock>> = None;
    let mut to_leak_unwind_safe = AssertUnwindSafe(&mut to_leak);

    // `with_cryo` uses `LocalLock` by default
    let err = catch_unwind(move || {
        with_cryo(&cell, |cryo: Pin<&Cryo<'_, usize, _>>| {
            // leak a reference outside
            **to_leak_unwind_safe = Some(cryo.borrow());
            // `with_cryo` will panic because the `Cryo` still has outstanding references to it
        });
    });
    // The panic is caught, and we can proceed, with the `CryoRef` still inside `r`
    println!("Recovered from error: {:?}", err);

    // Return the now-leaked CryoRef
    to_leak.unwrap()
}



fn main() {
    let illegal_cryo = illegal_pointer_local();
    println!("Printing address: {}", &*illegal_cryo as *const _ as i32);
    println!("Uninitialized read from address: {}", *illegal_cryo); // reading from the stack of a function that returned, i.e., an uninitialized read.
}

Exploit through destructors

struct ReadOnDrop(Option<CryoRef<usize, LocalLock>>);

impl Drop for ReadOnDrop {
    fn drop(&mut self) {
        if let Some(illegal_cryo) = &self.0 {
            println!("Printing address: {}", &*illegal_cryo as *const _ as i32);
            println!("Uninitialized read from address: {}", **illegal_cryo);
        }
    }
}

fn main() {
    let mut to_leak = ReadOnDrop(None);

    let mut cell = 42;

    // `with_cryo` uses `LocalLock` by default
    with_cryo(&mut cell, |cryo: Pin<&CryoMut<'_, usize, _>>| {
        // leak a reference outside
        to_leak = ReadOnDrop(Some(cryo.read()));
        // `with_cryo` will panic because the `Cryo` still has outstanding references to it
    });
    // The panic will drop the `to_leak` value
}

Other locks

This is most obvious in LocalLock and AtomicLock, which panic on purpose. However, any edgecase that triggers a panic during lock acquisition will lead to the same unsoundness.

In addition, this also applies to user-defined locks from outside this library, implemented through the Cryo::Lock trait. It is an unsafe trait, but its interface doesn't restrict panicking in any way (and in fact, panicking is the expected behavior of the lock_exclusive function). And, with the lock_api feature, all the locks implementing RawRwLock are also available, which can also panic freely.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.