Trying and failing to leak memory with Rust and Foundation
One of the oldest frameworks on macOS is Foundation. It’s impossible to interact with an Apple provided API that is not built upon Foundation in some way. This API is only exposed via Objective-C which makes calling it from Rust a complicated endeavour. Most of this complexity stems from managing memory and ensuring that memory does not leak or using previously freed memory.
Objective-C has a complicated history of memory management. Initially Objective-C only had manual reference counting. It was expected that programmers use a combination of retain
and release
to adjust the reference count, and when the last release
on the object was run, the Objective-C runtime would free the object. The runtime also contains an Autorelease Pool where objects could be added to the pool with autorelease
. Eventually drain
could be called on the pool and it would call release
on everything added to the pool. This provides a tool that allows some APIs to allocate an object and then immediately call autorelease
on it. The caller can choose to call retain
it or if the use is temporary, do nothing and expect the object to be freed when the pool is drained as some point in the future.
Complicating matters is a brief use of Garbage Collection with Objective-C on macOS, first available on macOS 10.5 and then later deprecated on 10.8. The goal here was to remove manual the use of release
retain
and autorelease
in application code. However it was deprecated and replaced with Automatic Reference Counting where the compiler would infer ownership of objects1 and insert retain
and release
statements as needed. ARC also introduced special syntax in Objective-C for managing Autorelease Pools, which would ensure that the pool was drained at the end of a scope instead of requiring the programmer to call drain
.
Now that ARC is the preferred way of interacting with Foundation APIs in Objective-C, it makes calling Foundation APIs from Rust difficult. The Rust side has to carefully manage calls to release
and retain
to emulate ARC.
Further Apple’s documentation says the Rust side has to manage an autorelease
pool and call drain
when using Foundation outside of AppKit or Objective-C and it might not be safe to send objects added to a pool across threads.
Threads
If you are making Cocoa calls outside of the Application Kit’s main thread—for example if you create a Foundation-only application or if you detach a thread—you need to create your own autorelease pool.
If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should periodically drain and create autorelease pools (like the Application Kit does on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If, however, your detached thread does not make Cocoa calls, you do not need to create an autorelease pool.
With all of this in mind, I would expect a very naive Rust program which converts a Rust String
to NSString
to leak and observe the leaks using the built in leaks
tool from XCode. However, a naive program doesn’t leak as Apple’s documentation implies.
A program that should leak but doesn’t
Lets create a Rust program called a-leaky-bucket
which tries to leak memory.
$ cargo new --bin a-leaky-bucket
Created binary (application) `a-leaky-bucket` package
From my previous post on bindgen it’s pretty easy to create some bindings to NSString
. For Objective-C related code, it’s important to note that bindgen
expects the application to depend on the objc crate which provides macros that the generated code will use.
Generating bindings is pretty straight forward with a wrapper.h
that references NSString.h
.
#include <Foundation/NSString.h>
The wrapper.h
to give bindgen
Feeding the wrapper.h
to bindgen
with support for Objective-C gives us the bindings. Due to some bugs in the bindgen
heuristics, it’s important to pass --no-derive-copy
and --no-derive-debug
otherwise the generated bindings will not compile.
$ bindgen --no-derive-copy --no-derive-debug wrapper.h -- -isysroot$(xcrun --sdk macosx --show-sdk-path) -x objective-c > ./src/nsstring.rs
Generating the bindings to NSString
To use these bindings the objc
crate needs to be added to the Cargo.toml
.
[package]
name = "a-leaky-bucket"
version = "0.1.0"
edition = "2018"
[dependencies]
objc = '0.2.7'
Cargo.toml
Then main.rs
can use these bindings to convert a Rust String
to NSString
.
#[macro_use]
extern crate objc;
use std::ffi::{CString, CStr};
#[allow(dead_code)]
#[allow(non_camel_case_types)]
#[allow(non_snake_case)]
#[allow(non_upper_case_globals)]
mod nsstring;
use nsstring::NSString;
use nsstring::NSString_NSStringExtensionMethods;
use nsstring::NSUTF8StringEncoding;
fn main() {
let s = leak();
unsafe {
let cstr = CStr::from_ptr(s.cStringUsingEncoding_(NSUTF8StringEncoding)).to_str().unwrap();
println!("{}", cstr);
}
}
fn leak() -> NSString {
let i = CString::new("a leaky string").unwrap();
unsafe {
NSString(NSString::stringWithUTF8String_(i.into_boxed_c_str().as_ptr()))
}
}
#[link(name = "Foundation", kind = "framework")]
extern "C" {
}
main.rs
Running the code results in “a leaky string” printed as expected.
$ ./target/debug/a-leaky-bucket
a leaky string
However the leaks
tool from XCode does not show any leaks in the code despite the code not making a single release
call after the NSString
was allocated.
$ leaks --atExit -- ./target/debug/a-leaky-bucket
a leaky string
Process: a-leaky-bucket [47670]
Path: /Users/USER/*/a-leaky-bucket
Load Address: 0x109093000
Identifier: a-leaky-bucket
Version: ???
Code Type: X86-64
Platform: macOS
Parent Process: leaks [47669]
Date/Time: 2021-07-18 22:14:24.959 -0400
Launch Time: 2021-07-18 22:14:24.358 -0400
OS Version: macOS 11.4 (20F71)
Report Version: 7
Analysis Tool: /Applications/Xcode.app/Contents/Developer/usr/bin/leaks
Analysis Tool Version: Xcode 12.5.1 (12E507)
Physical footprint: 660K
Physical footprint (peak): 660K
----
leaks Report Version: 4.0
Process 47670: 503 nodes malloced for 35 KB
Process 47670: 0 leaks for 0 total leaked bytes.
Looking under the hood
Trying to understand this behaviour requires taking a look into Foundation. With rust-lldb
it’s pretty straight forward to set some breakpoints and try to understand what is calling release
eventually on the created NSString
. rust-lldb
is distributed along with rustc
and cargo
so it should be available. We can set a breakpoint at main
and once it is hit, break at any Objective-C runtime function with the word release
in it.
$ rust-lldb ./target/debug/a-leaky-bucket
(lldb) command script import "/Users/zmanji/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/etc/lldb_lookup.py"
(lldb) command source -s 0 '/Users/zmanji/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/etc/lldb_commands'
Executing commands in '/Users/zmanji/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/etc/lldb_commands'.
(lldb) type synthetic add -l lldb_lookup.synthetic_lookup -x ".*" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(alloc::([a-z_]+::)+)String$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^&str$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^&\\[.+\\]$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(std::ffi::([a-z_]+::)+)OsString$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(alloc::([a-z_]+::)+)Vec<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(alloc::([a-z_]+::)+)VecDeque<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(alloc::([a-z_]+::)+)BTreeSet<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(alloc::([a-z_]+::)+)BTreeMap<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(std::collections::([a-z_]+::)+)HashMap<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(std::collections::([a-z_]+::)+)HashSet<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(alloc::([a-z_]+::)+)Rc<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(alloc::([a-z_]+::)+)Arc<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(core::([a-z_]+::)+)Cell<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(core::([a-z_]+::)+)Ref<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(core::([a-z_]+::)+)RefMut<.+>$" --category Rust
(lldb) type summary add -F lldb_lookup.summary_lookup -e -x -h "^(core::([a-z_]+::)+)RefCell<.+>$" --category Rust
(lldb) type category enable Rust
(lldb) target create "./target/debug/a-leaky-bucket"
Current executable set to '/Users/zmanji/code/a-leaky-bucket/target/debug/a-leaky-bucket' (x86_64).
(lldb) break set --name main
Breakpoint 1: 11 locations.
(lldb) r
Process 56685 launched: '/Users/zmanji/code/a-leaky-bucket/target/debug/a-leaky-bucket' (x86_64)
Process 56685 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2
frame #0: 0x0000000100001cc0 a-leaky-bucket`main
a-leaky-bucket`main:
-> 0x100001cc0 <+0>: pushq %rbp
0x100001cc1 <+1>: movq %rsp, %rbp
0x100001cc4 <+4>: movq %rsi, %rdx
0x100001cc7 <+7>: movslq %edi, %rsi
Target 0: (a-leaky-bucket) stopped.
(lldb) c
Process 56685 resuming
Process 56685 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100001b1b a-leaky-bucket`a_leaky_bucket::main::h00757089d677f6a2 at main.rs:18:13
15 use crate::nsstring::PNSObject;
16
17 fn main() {
-> 18 let s = leak();
19 unsafe {
20 let cstr = CStr::from_ptr(s.cStringUsingEncoding_(NSUTF8StringEncoding)).to_str().unwrap();
21 println!("{}", cstr);
Target 0: (a-leaky-bucket) stopped.
(lldb) break set -r .*release.* -s libobjc.A.dylib
Breakpoint 2: 46 locations.
Using rust-lldb
to prepare breakpoints in the Objective-C
runtime
With the breakpoints set some curious pieces of code were hit. First there is proof that the stringWithUTF8String
method calls autorelease
on the returned NSString
.
(lldb) c
Process 56685 resuming
Process 56685 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.37
frame #0: 0x00007fff2020e5e0 libobjc.A.dylib`objc_autorelease
libobjc.A.dylib`objc_autorelease:
-> 0x7fff2020e5e0 <+0>: testq %rdi, %rdi
0x7fff2020e5e3 <+3>: je 0x7fff2020e659 ; <+121>
0x7fff2020e5e5 <+5>: movl %edi, %eax
0x7fff2020e5e7 <+7>: andl $0x1, %eax
Target 0: (a-leaky-bucket) stopped.
(lldb) bt
error: need to add support for DW_TAG_base_type '()' encoded with DW_ATE = 0x7, bit_size = 0
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.37
* frame #0: 0x00007fff2020e5e0 libobjc.A.dylib`objc_autorelease
frame #1: 0x00000001000019a5 a-leaky-bucket`_$LT$$LP$A$C$$RP$$u20$as$u20$objc..message..MessageArguments$GT$::invoke::h330b94c840fc95e2(imp=(libobjc.A.dylib`objc_msgSend), obj=0x00007fff800dc8d0, sel=Sel @ 0x00007ffeefbfee40, (null)=(*const i8) @ 0x00007ffeefbfee70) at mod.rs:128:17
frame #2: 0x0000000100001884 a-leaky-bucket`objc::message::platform::send_unverified::hdd48c548e61778ec(obj=0x00007fff800dc8d0, sel=Sel @ 0x00007ffeefbfeee0, args=(*const i8) @ 0x00007ffeefbfef08) at mod.rs:27:9
frame #3: 0x0000000100000fe5 a-leaky-bucket`a_leaky_bucket::nsstring::NSString_NSStringExtensionMethods::stringWithUTF8String_::h13adb976a6c4f01a [inlined] objc::message::send_message::hb975109fc5b4cbdd(obj=0x00007fff800dc8d0, sel=Sel @ 0x00007ffeefbff128, args=(*const i8) @ 0x00007ffeefbff148) at mod.rs:178:5
frame #4: 0x0000000100000fcd a-leaky-bucket`a_leaky_bucket::nsstring::NSString_NSStringExtensionMethods::stringWithUTF8String_::h13adb976a6c4f01a(nullTerminatedCString="a leaky string") at nsstring.rs:7034
frame #5: 0x0000000100001c74 a-leaky-bucket`a_leaky_bucket::leak::h9a1e6714fee9d36c at main.rs:28:17
frame #6: 0x0000000100001b20 a-leaky-bucket`a_leaky_bucket::main::h00757089d677f6a2 at main.rs:18:13
frame #7: 0x000000010000156e a-leaky-bucket`core::ops::function::FnOnce::call_once::haeff37c118186a6c((null)=(a-leaky-bucket`a_leaky_bucket::main::h00757089d677f6a2 at main.rs:17), (null)=<unavailable>) at function.rs:227:5
frame #8: 0x0000000100001351 a-leaky-bucket`std::sys_common::backtrace::__rust_begin_short_backtrace::hfd92fb564fd3b2e4(f=(a-leaky-bucket`a_leaky_bucket::main::h00757089d677f6a2 at main.rs:17)) at backtrace.rs:125:18
frame #9: 0x0000000100001924 a-leaky-bucket`std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h72100a97160f9dee at rt.rs:66:18
frame #10: 0x0000000100020fe4 a-leaky-bucket`std::rt::lang_start_internal::h0c37a46739a0311d [inlined] core::ops::function::impls::_$LT$impl$u20$core..ops..function..FnOnce$LT$A$GT$$u20$for$u20$$RF$F$GT$::call_once::h3b22ce68aa2879c2 at function.rs:259:13 [opt]
frame #11: 0x0000000100020fdd a-leaky-bucket`std::rt::lang_start_internal::h0c37a46739a0311d [inlined] std::panicking::try::do_call::h51a4853c94b1bdea at panicking.rs:379 [opt]
frame #12: 0x0000000100020fdd a-leaky-bucket`std::rt::lang_start_internal::h0c37a46739a0311d [inlined] std::panicking::try::ha45bc5eab1f103eb at panicking.rs:343 [opt]
frame #13: 0x0000000100020fdd a-leaky-bucket`std::rt::lang_start_internal::h0c37a46739a0311d [inlined] std::panic::catch_unwind::h191bc002afc126a7 at panic.rs:431 [opt]
frame #14: 0x0000000100020fdd a-leaky-bucket`std::rt::lang_start_internal::h0c37a46739a0311d at rt.rs:51 [opt]
frame #15: 0x00000001000018fe a-leaky-bucket`std::rt::lang_start::h62e28d4b373c8b88(main=(a-leaky-bucket`a_leaky_bucket::main::h00757089d677f6a2 at main.rs:17), argc=1, argv=0x00007ffeefbff418) at rt.rs:65:5
frame #16: 0x0000000100001cd6 a-leaky-bucket`main + 22
frame #17: 0x00007fff20386f5d libdyld.dylib`start + 1
frame #18: 0x00007fff20386f5d libdyld.dylib`start + 1
The first breakpoint hit in the Objective-C runtime.
The backtrace shows that the autorelease
method was called from inside stringWithUTF8String
method. I would expect this to fail or leak memory since there is no Autorelease Pool created. However continuing a few times results in a backtrace deep in the Objective-C runtime.
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.13
* frame #0: 0x00007fff20228562 libobjc.A.dylib`AutoreleasePoolPage::autoreleaseNoPage(objc_object*)
frame #1: 0x00007fff2020d49c libobjc.A.dylib`objc_object::rootAutorelease2() + 32
frame #2: 0x00000001000019a5 a-leaky-bucket`_$LT$$LP$A$C$$RP$$u20$as$u20$objc..message..MessageArguments$GT$::invoke::h330b94c840fc95e2(imp=(libobjc.A.dylib`objc_msgSend), obj=0x00007fff800dc8d0, sel=Sel @ 0x00007ffeefbfee40, (null)=(*const i8) @ 0x00007ffeefbfee70) at mod.rs:128:17
frame #3: 0x0000000100001884 a-leaky-bucket`objc::message::platform::send_unverified::hdd48c548e61778ec(obj=0x00007fff800dc8d0, sel=Sel @ 0x00007ffeefbfeee0, args=(*const i8) @ 0x00007ffeefbfef08) at mod.rs:27:9
frame #4: 0x0000000100000fe5 a-leaky-bucket`a_leaky_bucket::nsstring::NSString_NSStringExtensionMethods::stringWithUTF8String_::h13adb976a6c4f01a [inlined] objc::message::send_message::hb975109fc5b4cbdd(obj=0x00007fff800dc8d0, sel=Sel @ 0x00007ffeefbff128, args=(*const i8) @ 0x00007ffeefbff148) at mod.rs:178:5
frame #5: 0x0000000100000fcd a-leaky-bucket`a_leaky_bucket::nsstring::NSString_NSStringExtensionMethods::stringWithUTF8String_::h13adb976a6c4f01a(nullTerminatedCString="a leaky string") at nsstring.rs:7034
frame #6: 0x0000000100001c74 a-leaky-bucket`a_leaky_bucket::leak::h9a1e6714fee9d36c at main.rs:28:17
frame #7: 0x0000000100001b20 a-leaky-bucket`a_leaky_bucket::main::h00757089d677f6a2 at main.rs:18:13
frame #8: 0x000000010000156e a-leaky-bucket`core::ops::function::FnOnce::call_once::haeff37c118186a6c((null)=(a-leaky-bucket`a_leaky_bucket::main::h00757089d677f6a2 at main.rs:17), (null)=<unavailable>) at function.rs:227:5
frame #9: 0x0000000100001351 a-leaky-bucket`std::sys_common::backtrace::__rust_begin_short_backtrace::hfd92fb564fd3b2e4(f=(a-leaky-bucket`a_leaky_bucket::main::h00757089d677f6a2 at main.rs:17)) at backtrace.rs:125:18
frame #10: 0x0000000100001924 a-leaky-bucket`std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h72100a97160f9dee at rt.rs:66:18
frame #11: 0x0000000100020fe4 a-leaky-bucket`std::rt::lang_start_internal::h0c37a46739a0311d [inlined] core::ops::function::impls::_$LT$impl$u20$core..ops..function..FnOnce$LT$A$GT$$u20$for$u20$$RF$F$GT$::call_once::h3b22ce68aa2879c2 at function.rs:259:13 [opt]
frame #12: 0x0000000100020fdd a-leaky-bucket`std::rt::lang_start_internal::h0c37a46739a0311d [inlined] std::panicking::try::do_call::h51a4853c94b1bdea at panicking.rs:379 [opt]
frame #13: 0x0000000100020fdd a-leaky-bucket`std::rt::lang_start_internal::h0c37a46739a0311d [inlined] std::panicking::try::ha45bc5eab1f103eb at panicking.rs:343 [opt]
frame #14: 0x0000000100020fdd a-leaky-bucket`std::rt::lang_start_internal::h0c37a46739a0311d [inlined] std::panic::catch_unwind::h191bc002afc126a7 at panic.rs:431 [opt]
frame #15: 0x0000000100020fdd a-leaky-bucket`std::rt::lang_start_internal::h0c37a46739a0311d at rt.rs:51 [opt]
frame #16: 0x00000001000018fe a-leaky-bucket`std::rt::lang_start::h62e28d4b373c8b88(main=(a-leaky-bucket`a_leaky_bucket::main::h00757089d677f6a2 at main.rs:17), argc=1, argv=0x00007ffeefbff418) at rt.rs:65:5
frame #17: 0x0000000100001cd6 a-leaky-bucket`main + 22
frame #18: 0x00007fff20386f5d libdyld.dylib`start + 1
frame #19: 0x00007fff20386f5d libdyld.dylib`start + 1
The AutoreleasePoolPage
seems to be a C++ class which is the implementation of the autorelease functionality in Objective-C. Since the code does not allocate an Autorelease Pool, this code should not be running.
Fortunately Apple publishes the source of the Objective-C runtime. Searching for AutoreleasePoolPage
in the source code reveals the implementation in NSObject.mm
. Within this implementation there are two interesting snippets.
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
// Also further down
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
ASSERT(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
objc_thread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
return page->add(obj);
}
A snippet from NSObject.mm
in the Autorelease Pool implementation
It seems that when autorelease
is called for the first time and no “page” has been allocated before, a page will be allocated so the object can be added to the page. Since other parts of the code make references to thread local storage, and Apple’s documentation implies Autorelease Pools are thread specific, I assume the pool will be drained when the main
thread dies, which explains why leaks
does not report any leaks.
Triggering the leak as expected
In the snippet above, there is a hint indicating we can prevent allocating a pool on demand.
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
objc_thread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
The DebugMissingPools
is an Objective-C runtime configuration flag that is controlled by the OBJC_DEBUG_MISSING_POOLS
environment variable.
Running a-leaky-bucket
with this variable set shows the warning message printed out along side with our expected output.
$ OBJC_DEBUG_MISSING_POOLS=YES ./target/debug/a-leaky-bucket
objc[60670]: MISSING POOLS: (0x10c64de00) Object 0x7f974940a470 of class __NSCFString autoreleased with no pool in place - just leaking - break on objc_autoreleaseNoPool() to debug
a leaky string
Running our program with leaks
and this environment variable set shows the leak as expected.
OBJC_DEBUG_MISSING_POOLS=YES leaks --atExit -- ./target/debug/a-leaky-bucket
objc[61039]: MISSING POOLS: (0x1037b7e00) Object 0x7fa0ed40a470 of class __NSCFString autoreleased with no pool in place - just leaking - break on objc_autoreleaseNoPool() to debug
a leaky string
Process: a-leaky-bucket [61039]
Path: /Users/USER/*/a-leaky-bucket
Load Address: 0x102f82000
Identifier: a-leaky-bucket
Version: ???
Code Type: X86-64
Platform: macOS
Parent Process: leaks [61038]
Date/Time: 2021-07-19 20:32:57.550 -0400
Launch Time: 2021-07-19 20:32:56.905 -0400
OS Version: macOS 11.4 (20F71)
Report Version: 7
Analysis Tool: /Applications/Xcode.app/Contents/Developer/usr/bin/leaks
Analysis Tool Version: Xcode 12.5.1 (12E507)
Physical footprint: 656K
Physical footprint (peak): 660K
----
leaks Report Version: 4.0
Process 61039: 502 nodes malloced for 31 KB
Process 61039: 1 leak for 32 total leaked bytes.
1 (32 bytes) ROOT LEAK: <CFString 0x7fa0ed40a470> [32] length: 14 "a leaky string"
Conclusion
Despite Apple’s documentation, it is not required to allocate an Autorelease Pool when calling Foundation APIs to prevent memory leaks. However, failure to do so creates a thread local Autorelease Pool, preventing release of the objects in the pool until the thread exits. When calling Foundation APIs from Rust, careful care has to be taken to ensure a pool is allocated and drained appropriately, otherwise memory will not be freed until the thread exits.
Also the OBJC_DEBUG_MISSING_POOLS
environment variable will prevent automatically creating pools, allowing for detection of missing Autorelease Pools via leaks
.
Apple uses the term “strong” and “weak” instead of owned and borrowed.↩︎