Created
May 6, 2022 12:35
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! | |
} |
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
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 userPin<&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.