Trying and failing to leak memory with Rust and Foundation

Created On:

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.


  1. Apple uses the term “strong” and “weak” instead of owned and borrowed.↩︎