Skip to content

Instantly share code, notes, and snippets.

@y86-dev
Created May 6, 2022 12:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save y86-dev/777c962cd17a26bed3f78c171198034d to your computer and use it in GitHub Desktop.
Save y86-dev/777c962cd17a26bed3f78c171198034d to your computer and use it in GitHub Desktop.
Ensure via API that a value is indeed dropped, based on code from [this zulip discussion](https://rust-lang.zulipchat.com/#narrow/stream/136281-t-lang.2Fwg-unsafe-code-guidelines/topic/Drop.20guarantee.20as.20a.20type)
pub mod lib {
use core::marker::PhantomData;
/// This struct ensures that the inner value of `T` is dropped, when `'a` ends.
/// Just wrap your incoming type in your API with `&IsDropped<'env, T>`. The type system is
/// then used to ensure that the caller can only safely construct a `&IsDropped<'env, T>`, if
/// the contained value is dropped in time.
///
/// # Caveats
/// At the moment it does not guarantee that `T` is dropped, when
/// - a double panic occurs.
/// - `std::process::exit` is called.
/// - a panic occurs and `panic = "abort"` is set.
/// - `core::hint::unreachable_unchecked` and other UB
pub struct IsDropped<'a, T: ?Sized> {
_scope: PhantomData<fn(&'a ()) -> &'a ()>,
inner: T,
}
impl<'a, T> IsDropped<'a, T> {
/// Contruct a new `IsDropped`.
///
/// # Safety
///
/// The caller guarantees, that this is dropped when `'a` ends.
pub unsafe fn new_unchecked(inner: T) -> Self {
Self {
inner,
_scope: PhantomData,
}
}
}
impl<'a, T: ?Sized> core::ops::Deref for IsDropped<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
/// Helper type used inside of the `is_dropped!` macro to ensure that the inner value is
/// dropped.
#[doc(hidden)]
pub struct ScopeGuard<T>(PhantomData<fn(T) -> T>);
impl<T> Default for ScopeGuard<T> {
fn default() -> Self {
Self(PhantomData)
}
}
impl<'scope> ScopeGuard<&'scope ()> {
/// Binds this `ScopeGuard`'s lifetime to the given lifetime.
#[doc(hidden)]
pub fn bind(&self, _: &'scope ()) {}
/// Unifies the lifetime of the given `IsDropped` to our lifetime.
#[doc(hidden)]
pub fn unify<T>(&self, _: &IsDropped<'scope, T>) {}
}
impl<T> Drop for ScopeGuard<T> {
fn drop(&mut self) {}
}
#[macro_export]
macro_rules! is_dropped {
($value:ident) => {
// this variable will live until the end of the current scope
let scope = ();
// this drop guard will be bound to scope, such that when we later unify it, it ensures
// drop is called at the current scope's end.
let scope_guard = lib::ScopeGuard::default();
scope_guard.bind(&scope);
// SAFETY: we ensure using ScopeGuard that the given value will be dropped, for the
// time it exists until the end, the data will not move.
let ref mut $value = unsafe { lib::IsDropped::new_unchecked($value) };
scope_guard.unify($value);
};
}
}
pub struct Scope();
impl Scope {
pub fn new() -> Self {
println!("expect symmetrical drop call...");
Self()
}
}
impl Drop for Scope {
fn drop(&mut self) {
println!("dropped a scope!");
}
}
impl Scope {
fn spawn<'env>(_: &lib::IsDropped<'env, Self>, val: &'env str) {
println!("{val}");
}
}
fn main() {
minor_problem_5();
}
pub fn basic() {
let value: Scope = Scope::new();
let s = "Hello World".to_owned();
is_dropped!(value);
Scope::spawn(value, &s);
}
pub fn error() {
let value: Scope = Scope::new();
let s = "Hello World".to_owned();
is_dropped!(value);
Scope::spawn(value, &s);
// core::mem::forget(s);
// ^ error, cannot move s, because it is borrowed.
}
/// does not prevent us from exiting the process
pub fn problem_1() {
let value: Scope = Scope::new();
let s = "Hello World".to_owned();
is_dropped!(value);
Scope::spawn(value, &s);
std::process::exit(0);
// we never reach the end of this scope, so drop is never called!
}
/// does not prevent us from aboriting due to a double panic
pub fn problem_2() {
// create a struct that panics on drop
struct PanicOnDrop;
impl Drop for PanicOnDrop {
fn drop(&mut self) {
panic!();
}
}
// bind it to a variable before creating our scope, that way it will be dropped before scope is
// dropped, circumventing the guarantee by
let _later = PanicOnDrop;
let value: Scope = Scope::new();
let s = "Hello World".to_owned();
is_dropped!(value);
Scope::spawn(value, &s);
panic!();
}
/// does not prevent us from aboriting due to a panic and `panic = "abort"`
/// (you need to set `panic = "abort"` in your `Cargo.toml` in your
/// `[profile.dev]`/`[profile.release]` section)
pub fn problem_3() {
let value: Scope = Scope::new();
let s = "Hello World".to_owned();
is_dropped!(value);
Scope::spawn(value, &s);
panic!();
// we never reach the end of this scope, so drop is never called!
}
/// does not prevent us from encountering a `core::hint::unreachable_unchecked()`
pub fn minor_problem_4() {
let value: Scope = Scope::new();
let s = "Hello World".to_owned();
is_dropped!(value);
Scope::spawn(value, &s);
unsafe {
core::hint::unreachable_unchecked();
}
// we never reach the end of this scope, so drop is never called!
}
/// does not prevent us from encountering a segfault
pub fn minor_problem_5() {
let value: Scope = Scope::new();
let s = "Hello World".to_owned();
is_dropped!(value);
Scope::spawn(value, &s);
// well, what do you think is gonna happen?
#[allow(deref_nullptr)]
unsafe {
println!("{}", &*core::ptr::null::<String>());
}
// we never reach the end of this scope, so drop is never called!
}
@y86-dev
Copy link
Author

y86-dev commented May 6, 2022

While only providing &IsDropped<'_, T> might be a bit too restrictive (requiring all scope writers to use interior mutability), one could also just give the user Pin<&mut IsDropped<'_, T>> instead, that way scope writers would need to use pin-projection, but that is arguably easier with the right macro and more performant.

@y86-dev
Copy link
Author

y86-dev commented May 7, 2022

Sadly this does not prevent the leak when using futures:

mod async_helpers {
    use std::{
        future::Future,
        pin::Pin,
        task::{Context, Poll, Wake}, sync::Arc,
    };

    // Future that yields once
    pub struct Yield(pub bool);

    impl Future for Yield {
        type Output = ();

        fn poll(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<()> {
            if self.0 {
                Poll::Ready(())
            } else {
                self.0 = true;
                ctx.waker().wake_by_ref();
                Poll::Pending
            }
        }
    }

    pub struct VoidWaker;

    impl Wake for VoidWaker {
        fn wake(self: Arc<Self>) {}
    }
}

pub async fn async_madness() {
    use async_helpers::*;
    let value: Scope = Scope::new();
    let s = "Hello World".to_owned();
    is_dropped!(value);
    Scope::spawn(value, &s);
   Box::pin(Yield(false)).await;
}

/// inside of `async` we cannot guarantee that the local is dropped, because the future might be
/// passed to `mem::forget`.
pub fn problem_6() {
    use async_helpers::VoidWaker;
    use std::{sync::Arc,task::{Waker,Context, Poll}};
    let waker = Arc::new(VoidWaker);
    let waker = Waker::from(waker);
    let mut ctx = Context::from_waker(&waker);

    let mut fut = Box::pin(async_madness());
    // poll once to the .await
    match fut.as_mut().poll(&mut ctx) {
        Poll::Pending => {},
        Poll::Ready(()) => {},
    };
    // now forget the future and avoid running the deconstructor
    core::mem::forget(fut);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment