Skip to content

Commit

Permalink
Add unstable_autoreleasesafe feature
Browse files Browse the repository at this point in the history
A nightly-only feature that adds the auto trait `AutoreleaseSafe` and uses it to prevent calling `autoreleasepool` with closures that capture an outer `AutoreleasePool`.

This fixes on nightly the unsoundness hole preventing `autoreleasepool` from being used to safely construct references to autoreleased objects.
  • Loading branch information
madsmtm committed Jun 13, 2021
1 parent 93ca34a commit a1b5455
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 68 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ exclude = [
[features]
exception = ["objc_exception"]
verify_message = []
unstable_autoreleasesafe = []

[dependencies]
malloc_buf = "1.0"
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ The bindings can be used on Linux or *BSD utilizing the
#![crate_name = "objc"]
#![crate_type = "lib"]

#![cfg_attr(feature = "unstable_autoreleasesafe", feature(negative_impls, auto_traits))]

#![warn(missing_docs)]

extern crate malloc_buf;
Expand Down
207 changes: 139 additions & 68 deletions src/rc/autorelease.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,73 +81,144 @@ impl Drop for AutoreleasePool {
}
}

// TODO:
// #![feature(negative_impls)]
// #![feature(auto_traits)]
// /// A trait for the sole purpose of ensuring we can't pass an `&AutoreleasePool`
// /// through to the closure inside `autoreleasepool`
// pub unsafe auto trait AutoreleaseSafe {}
// // TODO: Unsure how negative impls work exactly
// unsafe impl !AutoreleaseSafe for AutoreleasePool {}
// unsafe impl !AutoreleaseSafe for &AutoreleasePool {}
// unsafe impl !AutoreleaseSafe for &mut AutoreleasePool {}
/// A trait for the sole purpose of ensuring we can't pass an `&AutoreleasePool`
/// through to the closure inside `autoreleasepool`
#[cfg(feature = "unstable_autoreleasesafe")]
pub unsafe auto trait AutoreleaseSafe {}
// TODO: Unsure how negative impls work exactly
#[cfg(feature = "unstable_autoreleasesafe")]
impl !AutoreleaseSafe for AutoreleasePool {}

/// Execute `f` in the context of a new autorelease pool. The pool is drained
/// after the execution of `f` completes.
///
/// This corresponds to `@autoreleasepool` blocks in Objective-C and Swift.
///
/// The pool is passed as a reference to the enclosing function to give it a
/// lifetime parameter that autoreleased objects can refer to.
///
/// # Examples
///
/// ```rust
/// use objc::{class, msg_send};
/// use objc::rc::{autoreleasepool, AutoreleasePool};
/// use objc::runtime::Object;
///
/// fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object {
/// let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] };
/// let obj: *mut Object = unsafe { msg_send![obj, autorelease] };
/// // SAFETY: Lifetime bounded by the pool
/// unsafe { &mut *obj }
/// }
///
/// autoreleasepool(|pool| {
/// let obj = needs_lifetime_from_pool(pool);
/// // Use `obj`
/// });
///
/// // `obj` is deallocated when the pool ends
/// ```
///
/// ```rust,compile_fail
/// # use objc::{class, msg_send};
/// # use objc::rc::{autoreleasepool, AutoreleasePool};
/// # use objc::runtime::Object;
/// #
/// # fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object {
/// # let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] };
/// # let obj: *mut Object = unsafe { msg_send![obj, autorelease] };
/// # unsafe { &mut *obj }
/// # }
/// #
/// // Fails to compile because `obj` does not live long enough for us to
/// // safely take it out of the pool.
///
/// let obj = autoreleasepool(|pool| {
/// let obj = needs_lifetime_from_pool(pool);
/// // Use `obj`
/// obj
/// });
/// ```
///
/// TODO: More examples.
pub fn autoreleasepool<T, F>(f: F) -> T
where
for<'p> F: FnOnce(&'p AutoreleasePool) -> T, // + AutoreleaseSafe,
{
let pool = unsafe { AutoreleasePool::new() };
f(&pool)
#[cfg(feature = "unstable_autoreleasesafe")]
macro_rules! fn_autoreleasepool {
{$(#[$fn_meta:meta])* $v:vis fn $fn:ident($f:ident) $b:block} => {
$(#[$fn_meta])*
$v fn $fn<T, F>($f: F) -> T
where
for<'p> F: FnOnce(&'p AutoreleasePool) -> T + AutoreleaseSafe,
{
$b
}
}
}

#[cfg(not(feature = "unstable_autoreleasesafe"))]
macro_rules! fn_autoreleasepool {
{$(#[$fn_meta:meta])* $v:vis fn $fn:ident($f:ident) $b:block} => {
$(#[$fn_meta])*
$v fn $fn<T, F>($f: F) -> T
where
for<'p> F: FnOnce(&'p AutoreleasePool) -> T,
{
$b
}
}
}

fn_autoreleasepool!(
/// Execute `f` in the context of a new autorelease pool. The pool is
/// drained after the execution of `f` completes.
///
/// This corresponds to `@autoreleasepool` blocks in Objective-C and
/// Swift.
///
/// The pool is passed as a reference to the enclosing function to give it
/// a lifetime parameter that autoreleased objects can refer to.
///
/// The given reference must not be used in an inner `autoreleasepool`,
/// doing so will be a compile error in a future release. You can test
/// this guarantee with the `unstable_autoreleasesafe` crate feature on
/// nightly Rust.
///
/// So using `autoreleasepool` is unsound right now because of this
/// specific problem.
///
/// # Examples
///
/// Basic usage:
///
/// ```rust
/// use objc::{class, msg_send};
/// use objc::rc::{autoreleasepool, AutoreleasePool};
/// use objc::runtime::Object;
///
/// fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object {
/// let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] };
/// let obj: *mut Object = unsafe { msg_send![obj, autorelease] };
/// // SAFETY: Lifetime bounded by the pool
/// unsafe { &mut *obj }
/// }
///
/// autoreleasepool(|pool| {
/// // Create `obj` and autorelease it to the pool
/// let obj = needs_lifetime_from_pool(pool);
/// // ... use `obj` here
/// // `obj` is deallocated when the pool ends
/// });
/// ```
///
/// Fails to compile because `obj` does not live long enough for us to
/// safely take it out of the pool:
///
/// ```rust,compile_fail
/// # use objc::{class, msg_send};
/// # use objc::rc::{autoreleasepool, AutoreleasePool};
/// # use objc::runtime::Object;
/// #
/// # fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object {
/// # let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] };
/// # let obj: *mut Object = unsafe { msg_send![obj, autorelease] };
/// # unsafe { &mut *obj }
/// # }
/// #
/// let obj = autoreleasepool(|pool| {
/// let obj = needs_lifetime_from_pool(pool);
/// // Use `obj`
/// obj
/// });
/// ```
///
/// Incorrect usage which causes undefined behaviour:
///
#[cfg_attr(feature = "unstable_autoreleasesafe", doc = "```rust,compile_fail")]
#[cfg_attr(not(feature = "unstable_autoreleasesafe"), doc = "```rust")]
/// # use objc::{class, msg_send};
/// # use objc::rc::{autoreleasepool, AutoreleasePool};
/// # use objc::runtime::Object;
/// #
/// # fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object {
/// # let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] };
/// # let obj: *mut Object = unsafe { msg_send![obj, autorelease] };
/// # unsafe { &mut *obj }
/// # }
/// #
/// autoreleasepool(|outer_pool| {
/// let obj = autoreleasepool(|inner_pool| {
/// let obj = needs_lifetime_from_pool(outer_pool);
/// obj
/// });
/// // `obj` can wrongly be used here because it's lifetime was
/// // assigned to the outer pool, even though it was released by the
/// // inner pool already.
/// });
/// ```
pub fn autoreleasepool(f) {
let pool = unsafe { AutoreleasePool::new() };
f(&pool)
}
);

#[cfg(all(test, feature = "unstable_autoreleasesafe"))]
mod tests {
use super::AutoreleaseSafe;
use crate::runtime::Object;

fn requires_autoreleasesafe<T: AutoreleaseSafe>() {}

#[test]
fn test_autoreleasesafe() {
requires_autoreleasesafe::<usize>();
requires_autoreleasesafe::<*mut Object>();
requires_autoreleasesafe::<&mut Object>();
}
}

0 comments on commit a1b5455

Please sign in to comment.