Skip to content

Commit df74063

Browse files
committed
Make getting references bound by the autorelease pool unsafe
The current design of `autoreleasepool` does not prevent the user from "smuggling" the lifetime from an outer pool and using that inside an inner pool, even with the unstable auto traits guard. So let's make using the pool to construct a reference `unsafe` (making the invocation of `autoreleasepool` itself `unsafe` would be cumbersome for a lot of applications, especially so because of the extra nesting). Fixes #540, see that for details on why this is unsound. We _might_ be able to make this pattern safe again with something like contexts and capabilities, but that's future work.
1 parent 0cd79ee commit df74063

File tree

15 files changed

+132
-106
lines changed

15 files changed

+132
-106
lines changed

crates/objc2/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
114114
Classes now always use interior mutability.
115115
* **BREAKING**: Removed `DerefMut` implementation for `Retained<T>` when the
116116
`Retained` was mutable.
117+
* **BREAKING**: Mark `Retained::autorelease` as `unsafe`, since we cannot
118+
ensure that the given pool is actually the innermost pool.
117119

118120
### Fixed
119121
* Remove an incorrect assertion when adding protocols to classes in an unexpected

crates/objc2/src/macros/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,16 +1153,16 @@ macro_rules! msg_send_bool {
11531153
/// return a `Result<Retained<T>, Retained<E>>`, see below.
11541154
///
11551155
/// The `retain`, `release` and `autorelease` selectors are not supported, use
1156-
/// [`Retained::retain`], [`Retained::drop`] and [`Retained::autorelease`] for
1157-
/// that.
1156+
/// [`Retained::retain`], [`Retained::drop`] and [`Retained::autorelease_ptr`]
1157+
/// for that.
11581158
///
11591159
/// [sel-families]: https://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-method-families
11601160
/// [`MessageReceiver`]: crate::runtime::MessageReceiver
11611161
/// [`Retained::retain_autoreleased`]: crate::rc::Retained::retain_autoreleased
11621162
/// [arc-retainable]: https://clang.llvm.org/docs/AutomaticReferenceCounting.html#retainable-object-pointers-as-operands-and-arguments
11631163
/// [`Retained::retain`]: crate::rc::Retained::retain
11641164
/// [`Retained::drop`]: crate::rc::Retained::drop
1165-
/// [`Retained::autorelease`]: crate::rc::Retained::autorelease
1165+
/// [`Retained::autorelease_ptr`]: crate::rc::Retained::autorelease_ptr
11661166
///
11671167
///
11681168
/// # Errors

crates/objc2/src/rc/autorelease.rs

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,21 @@ impl Drop for Pool {
108108
/// use objc2::runtime::NSObject;
109109
/// use objc2::msg_send;
110110
///
111-
/// fn needs_lifetime_from_pool<'p>(pool: AutoreleasePool<'p>) -> &'p NSObject {
111+
/// unsafe fn needs_lifetime_from_pool<'p>(pool: AutoreleasePool<'p>) -> &'p NSObject {
112112
/// let obj = NSObject::new();
113113
/// // Do action that returns an autoreleased object
114114
/// let description: *mut NSObject = unsafe { msg_send![&obj, description] };
115-
/// // Bound the lifetime of the reference to the pool
115+
/// // Bound the lifetime of the reference to that of the pool.
116+
/// //
117+
/// // Note that this only helps ensuring soundness, our function is still
118+
/// // unsafe because the pool cannot be guaranteed to be the innermost
119+
/// // pool.
116120
/// unsafe { pool.ptr_as_ref(description) }
117121
/// }
118122
///
119123
/// autoreleasepool(|pool| {
120-
/// let obj = needs_lifetime_from_pool(pool);
124+
/// // SAFETY: The given pool is the innermost pool.
125+
/// let obj = unsafe { needs_lifetime_from_pool(pool) };
121126
/// println!("{obj:?}");
122127
/// });
123128
/// ```
@@ -178,10 +183,8 @@ impl<'pool> AutoreleasePool<'pool> {
178183
}
179184
}
180185

181-
/// This will be removed in a future version.
182186
#[inline]
183-
#[doc(hidden)]
184-
pub fn __verify_is_inner(self) {
187+
pub(crate) fn __verify_is_inner(self) {
185188
#[cfg(all(debug_assertions, not(feature = "unstable-autoreleasesafe")))]
186189
if let Some(pool) = &self.inner {
187190
POOLS.with(|c| {
@@ -200,11 +203,24 @@ impl<'pool> AutoreleasePool<'pool> {
200203
/// objects, since it binds the lifetime of the reference to the pool, and
201204
/// does some extra checks when debug assertions are enabled.
202205
///
206+
/// Note that this is helpful, but not sufficient, for ensuring that the
207+
/// lifetime of the reference does not exceed the lifetime of the
208+
/// autorelease pool. When calling this, you must also ensure that the
209+
/// pool is actually the current innermost pool.
210+
///
211+
///
212+
/// # Panics
213+
///
214+
/// If the pool is not the innermost pool, this function may panic when
215+
/// the `"std"` Cargo feature and debug assertions are enabled.
216+
///
203217
///
204218
/// # Safety
205219
///
206220
/// This is equivalent to `&*ptr`, and shares the unsafety of that, except
207221
/// the lifetime is bound to the pool instead of being unbounded.
222+
///
223+
/// The pool must be the innermost pool for the lifetime to be correct.
208224
#[inline]
209225
pub unsafe fn ptr_as_ref<T: ?Sized>(self, ptr: *const T) -> &'pool T {
210226
self.__verify_is_inner();
@@ -304,51 +320,66 @@ impl !AutoreleaseSafe for AutoreleasePool<'_> {}
304320
/// see [Apple's documentation][apple-autorelease] for general information on
305321
/// when to use those.
306322
///
307-
/// The pool is passed as a parameter to the closure to give you a lifetime
323+
/// [The pool] is passed as a parameter to the closure to give you a lifetime
308324
/// parameter that autoreleased objects can refer to.
309325
///
310326
/// Note that this is mostly useful for preventing leaks (as any Objective-C
311327
/// method may autorelease internally - see also [`autoreleasepool_leaking`]).
312328
/// If implementing an interface to an object, you should try to return
313329
/// retained pointers with [`msg_send_id!`] wherever you can instead, since
314-
/// it is usually more efficient, and having to use this function can be quite
315-
/// cumbersome for users.
330+
/// it is usually more efficient, safer, and having to use this function can
331+
/// be quite cumbersome for users.
316332
///
317333
/// [apple-autorelease]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html
334+
/// [The pool]: AutoreleasePool
318335
/// [`msg_send_id!`]: crate::msg_send_id
319336
///
320337
///
321338
/// # Restrictions
322339
///
323-
/// The given parameter must not be used in an inner `autoreleasepool` - doing
324-
/// so will panic with debug assertions enabled, and be a compile error in a
325-
/// future release.
340+
/// The given parameter must not be used inside a nested `autoreleasepool`,
341+
/// since doing so will give the objects that it is used with an incorrect
342+
/// lifetime bound.
326343
///
327-
/// Note that this means that **this function is currently unsound**, since it
328-
/// doesn't disallow wrong usage in all cases. Enabling the assertions in
329-
/// release mode would be prohibitively expensive though, so this is the
330-
/// least-bad solution.
344+
/// You can try to enable the `"unstable-autoreleasesafe"` Cargo feature using
345+
/// nightly Rust - if your use of this function compiles with that, it is more
346+
/// likely to be correct, though note that Rust does not have a way to express
347+
/// the lifetimes involved, so we cannot detect all invalid usage. See issue
348+
/// [#540] for details.
331349
///
332-
/// You can try to compile your crate with the `"unstable-autoreleasesafe"`
333-
/// crate feature enabled on nightly Rust - if your crate compiles with that,
334-
/// its autoreleasepool usage is guaranteed to be correct.
350+
/// [#540]: https://github.com/madsmtm/objc2/issues/540
335351
///
336352
///
337353
/// # Examples
338354
///
339-
/// Basic usage:
355+
/// Use an external API, and ensure that the memory that it used is cleaned
356+
/// up afterwards.
357+
///
358+
/// ```
359+
/// use objc2::rc::autoreleasepool;
360+
/// # fn example_function() {}
361+
/// # #[cfg(for_illustrative_purposes)]
362+
/// use example_crate::example_function;
363+
///
364+
/// autoreleasepool(|_| {
365+
/// // Call `example_function` in the context of a pool
366+
/// example_function();
367+
/// }); // Memory released into the pool is cleaned up when the scope ends
368+
/// ```
369+
///
370+
/// Autorelease an object into an autorelease pool:
340371
///
341372
/// ```
342373
/// use objc2::rc::{autoreleasepool, Retained};
343374
/// use objc2::runtime::NSObject;
344375
///
345376
/// autoreleasepool(|pool| {
346377
/// // Create `obj` and autorelease it to the pool
347-
/// let obj = Retained::autorelease(NSObject::new(), pool);
378+
/// // SAFETY: The pool is the current innermost pool
379+
/// let obj = unsafe { Retained::autorelease(NSObject::new(), pool) };
348380
/// // We now have a reference that we can freely use
349381
/// println!("{obj:?}");
350-
/// // `obj` is deallocated when the pool ends
351-
/// });
382+
/// }); // `obj` is deallocated when the pool ends
352383
/// // And is no longer usable outside the closure
353384
/// ```
354385
///
@@ -359,7 +390,7 @@ impl !AutoreleaseSafe for AutoreleasePool<'_> {}
359390
/// use objc2::rc::{autoreleasepool, Retained};
360391
/// use objc2::runtime::NSObject;
361392
///
362-
/// let obj = autoreleasepool(|pool| {
393+
/// let obj = autoreleasepool(|pool| unsafe {
363394
/// Retained::autorelease(NSObject::new(), pool)
364395
/// });
365396
/// ```
@@ -375,7 +406,8 @@ impl !AutoreleaseSafe for AutoreleasePool<'_> {}
375406
///
376407
/// autoreleasepool(|outer_pool| {
377408
/// let obj = autoreleasepool(|inner_pool| {
378-
/// Retained::autorelease(NSObject::new(), outer_pool)
409+
/// // SAFETY: NOT safe, the pool is _not_ the innermost pool!
410+
/// unsafe { Retained::autorelease(NSObject::new(), outer_pool) }
379411
/// });
380412
/// // `obj` could wrongly be used here because its lifetime was
381413
/// // assigned to the outer pool, even though it was released by the
@@ -444,7 +476,8 @@ where
444476
///
445477
/// autoreleasepool(|outer_pool| {
446478
/// let obj = autoreleasepool_leaking(|inner_pool| {
447-
/// Retained::autorelease(NSObject::new(), outer_pool)
479+
/// // SAFETY: The given `outer_pool` is the actual innermost pool.
480+
/// unsafe { Retained::autorelease(NSObject::new(), outer_pool) }
448481
/// });
449482
/// // `obj` is still usable here, since the leaking pool doesn't actually
450483
/// // do anything.
@@ -461,8 +494,8 @@ where
461494
/// use objc2::rc::{autoreleasepool_leaking, Retained};
462495
/// use objc2::runtime::NSObject;
463496
///
464-
/// let obj = autoreleasepool_leaking(|pool| {
465-
/// Retained::autorelease(NSObject::new(), pool)
497+
/// let obj = autoreleasepool_leaking(|pool| unsafe {
498+
/// unsafe { Retained::autorelease(NSObject::new(), pool) }
466499
/// });
467500
/// ```
468501
///
@@ -476,7 +509,8 @@ where
476509
///
477510
/// autoreleasepool_leaking(|outer_pool| {
478511
/// let obj = autoreleasepool(|inner_pool| {
479-
/// Retained::autorelease(NSObject::new(), outer_pool)
512+
/// // SAFETY: NOT safe, the pool is _not_ the innermost pool!
513+
/// unsafe { Retained::autorelease(NSObject::new(), outer_pool) }
480514
/// });
481515
/// });
482516
/// #
@@ -564,7 +598,7 @@ mod tests {
564598
let mut expected = expected;
565599

566600
autoreleasepool(|pool| {
567-
let _autoreleased = Retained::autorelease(obj.0, pool);
601+
let _autoreleased = unsafe { Retained::autorelease(obj.0, pool) };
568602
expected.autorelease += 1;
569603
expected.assert_current();
570604
panic!("unwind");

crates/objc2/src/rc/id.rs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -502,17 +502,20 @@ impl<T: Message> Retained<T> {
502502
/// Autoreleases the [`Retained`], returning a pointer.
503503
///
504504
/// The object is not immediately released, but will be when the innermost
505-
/// / current autorelease pool (given as a parameter) is drained.
505+
/// / current autorelease pool is drained.
506506
///
507507
/// This is useful when defining your own classes and you have some error
508508
/// parameter passed as `Option<&mut *mut NSError>`, and you want to
509509
/// create and autorelease an error before returning.
510510
///
511-
/// See [`Retained::autorelease`] for an alternative that yields safe
512-
/// references.
513-
///
514511
/// This is an associated method, and must be called as
515512
/// `Retained::autorelease_ptr(obj)`.
513+
///
514+
/// # Safety
515+
///
516+
/// This method is safe to call, but the returned pointer is only
517+
/// guaranteed to be valid until the innermost autorelease pool is
518+
/// drained.
516519
#[doc(alias = "objc_autorelease")]
517520
#[must_use = "if you don't intend to use the object any more, drop it as usual"]
518521
#[inline]
@@ -535,11 +538,17 @@ impl<T: Message> Retained<T> {
535538
///
536539
/// This is an associated method, and must be called as
537540
/// `Retained::autorelease(obj, pool)`.
541+
///
542+
/// # Safety
543+
///
544+
/// The given pool must represent the innermost pool, to ensure that the
545+
/// reference is not moved outside the autorelease pool into which it has
546+
/// been put in.
538547
#[doc(alias = "objc_autorelease")]
539548
#[must_use = "if you don't intend to use the object any more, drop it as usual"]
540549
#[inline]
541550
#[allow(clippy::needless_lifetimes)]
542-
pub fn autorelease<'p>(this: Self, pool: AutoreleasePool<'p>) -> &'p T {
551+
pub unsafe fn autorelease<'p>(this: Self, pool: AutoreleasePool<'p>) -> &'p T {
543552
let ptr = Self::autorelease_ptr(this);
544553
// SAFETY: The pointer is valid as a reference
545554
unsafe { pool.ptr_as_ref(ptr) }
@@ -569,14 +578,10 @@ impl<T: Message> Retained<T> {
569578
/// will often find yourself in need of returning autoreleased objects to
570579
/// properly follow [Cocoa's Memory Management Policy][mmRules].
571580
///
572-
/// To that end, you could use [`Retained::autorelease`], but that would require
573-
/// you to have an [`AutoreleasePool`] object at hand, which you often
574-
/// won't have in such cases. This function doesn't require a `pool`
575-
/// object (but as a downside returns a pointer instead of a reference).
576-
///
577-
/// This is also more efficient than a normal `autorelease`, since it
578-
/// makes a best effort attempt to hand off ownership of the retain count
579-
/// to a subsequent call to `objc_retainAutoreleasedReturnValue` /
581+
/// To that end, you could also use [`Retained::autorelease_ptr`], but
582+
/// this is more efficient than a normal `autorelease`, since it makes a
583+
/// best effort attempt to hand off ownership of the retain count to a
584+
/// subsequent call to `objc_retainAutoreleasedReturnValue` /
580585
/// [`Retained::retain_autoreleased`] in the enclosing call frame.
581586
///
582587
/// This optimization relies heavily on this function being tail called,
@@ -803,7 +808,7 @@ mod tests {
803808
let mut expected = ThreadTestData::current();
804809

805810
autoreleasepool(|pool| {
806-
let _ref = Retained::autorelease(obj, pool);
811+
let _ref = unsafe { Retained::autorelease(obj, pool) };
807812
expected.autorelease += 1;
808813
expected.assert_current();
809814
assert_eq!(cloned.retainCount(), 2);
@@ -813,7 +818,7 @@ mod tests {
813818
assert_eq!(cloned.retainCount(), 1);
814819

815820
autoreleasepool(|pool| {
816-
let _ref = Retained::autorelease(cloned, pool);
821+
let _ref = unsafe { Retained::autorelease(cloned, pool) };
817822
expected.autorelease += 1;
818823
expected.assert_current();
819824
});

crates/objc2/src/rc/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
//! autoreleasepool(|pool| {
4040
//! // Autorelease consumes the Retained, but won't actually
4141
//! // release it until the end of the autoreleasepool
42-
//! let obj_ref: &NSObject = Retained::autorelease(cloned, pool);
42+
//! // SAFETY: The given is the innermost pool.
43+
//! let obj_ref: &NSObject = unsafe { Retained::autorelease(cloned, pool) };
4344
//! });
4445
//!
4546
//! // Weak references won't retain the object

crates/objc2/src/topics/about_generated/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4848
* **BREAKING**: Removed `NSSet` methods `contains`, `is_subset`, `is_superset`
4949
and `is_disjoint` that were simple wrappers over the original methods.
5050
* **BREAKING**: Renamed `from_id_slice` to `from_retained_slice`.
51+
* **BREAKING**: Renamed `NSString::as_str` to `to_str`, and made it `unsafe`,
52+
since we cannot ensure that the given pool is actually the innermost pool.
5153

5254
### Deprecated
5355
* Moved `MainThreadMarker` from `objc2-foundation` to `objc2`.

crates/test-fuzz/fuzz_targets/nsstring.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ use objc2::rc::autoreleasepool;
33
use objc2_foundation::NSString;
44

55
fn run(s: &str) {
6-
autoreleasepool(|pool| {
6+
autoreleasepool(|pool| unsafe {
77
let obj = NSString::from_str(s);
8-
assert_eq!(obj.as_str(pool), s);
8+
assert_eq!(obj.to_str(pool), s);
99
});
1010
}
1111

crates/test-ui/ui/nsstring_as_str_use_after_release.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ use objc2::rc::autoreleasepool;
33
use objc2_foundation::NSString;
44

55
fn main() {
6-
autoreleasepool(|pool| {
6+
autoreleasepool(|pool| unsafe {
77
let ns_string = NSString::new();
8-
let s = ns_string.as_str(pool);
8+
let s = ns_string.to_str(pool);
99
drop(ns_string);
1010
println!("{}", s);
1111
});

crates/test-ui/ui/nsstring_as_str_use_after_release.stderr

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/test-ui/ui/nsstring_as_str_use_outside_pool.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ use objc2_foundation::NSString;
44

55
fn main() {
66
let ns_string = NSString::new();
7-
let _s = autoreleasepool(|pool| ns_string.as_str(pool));
7+
let _s = autoreleasepool(|pool| unsafe { ns_string.to_str(pool) });
88
}

0 commit comments

Comments
 (0)