Under XCTest teardown we hit a repeatable 'pointer being freed was not allocated' malloc crash when deallocating async/Combine-heavy DependencyContainer and view models. This post explains what was happening and why we fixed it by deliberately leaking those objects in DEBUG tests only.
This is Part 5 in our Advanced SwiftUI: Lessons From Mistakes series:
-
Part 1 – ObservedOrStateObject
walks through the property-wrapper bug that kicked off this learning arc. -
Part 2 – SwiftUI Alerts: When Multiple Alerts Collide
covers how conflicting alerts prevented save failures from reaching users. -
Part 3 – SwiftUI @Published Teardown: Pointer Being Freed Was Not Allocated
documents the teardown crash we treated as a Swift runtime edge case. -
Part 4 – SwiftUI @MainActor Initializers: Safe Default Parameters and Actor Isolation
explains how default parameter evaluation interacts with actor isolation.with actor isolation.
Who should read this
- iOS devs working with app-wide dependency containers and async/Combine-heavy view models
- Anyone wondering why we saw
malloc: *** error for object ... pointer being freed was not allocatedonly in tests. - Folks learning how async/Combine-heavy singletons and view models can crash during teardown.
Prerequisites
You should be comfortable with:
- Basic Swift classes and
finaltypes. - What
ObservableObjectand@Publishedare. - The idea of
@MainActorand the main thread.
You do not need deep Swift Concurrency internals.
Summary
During test runs we repeatedly hit crashes like this:
malloc: *** error for object 0x262c5a6f0: pointer being freed was not allocated
malloc: *** set a breakpoint in malloc_error_break to debugThe crash only happened when tests tore down objects created via our dependency container: Root cause (as best we can tell):
- We were deallocating async/Combine-heavy objects (
DependencyContainer,ProductListViewModel,VendorDashboardViewModel, etc.) under XCTest teardown. - Somewhere in the Swift runtime / Combine / actor teardown path, a double-free or invalid free occurs.
- The crash is not caused by our business logic, but by deallocation timing under the current Swift/Xcode toolchain.
Fix:
- In DEBUG tests only, we:
- Keep previous
DependencyContainerinstances in a staticleakedContainersarray when resetting. - Keep factory-created view models and testing containers in static arrays on
DependencyContainerTests.
- Keep previous
- This prevents the problematic objects from deallocating during test teardown.
- Release/runtime behavior is unchanged.
What broke
The failing tests all lived in DependencyContainerTests and looked innocuous:
@MainActor
final class DependencyContainerTests: XCTestCase {
override func setUp() async throws {
try await super.setUp()
DependencyContainerBootstrapper.resetForTesting()
}
override func tearDown() async throws {
DependencyContainerBootstrapper.resetForTesting()
try await super.tearDown()
}
func testMakeProductListViewModel_CreatesCorrectInstance() {
let container = DependencyContainerBootstrapper.bootstrap()
let store = MockProductStore()
let item = ProductItem(
id: UUID(),
createdAt: Date(),
title: "Test Product",
productId: "TEST-001",
imageFilename: "test.jpg",
uploaded: false,
persistenceOrigin: .local,
classification: nil,
productImportId: nil,
storeId: nil,
position: nil,
rowHash: nil
)
store.items = [item]
let viewModel = container.makeProductListViewModel(store: store)
XCTAssertNotNil(viewModel)
}
}All these tests:
- Bootstrap or construct a
DependencyContainer. - Call a factory method.
- Let the test end without holding on to the created object.
The crash consistently happened after the test body finished, as XCTest tore down the container and view models.
The core objects involved
DependencyContainer
Key points:
- A shared instance is exposed via
DependencyContainerBootstrapper.sharedContainer.
@MainActor
final class DependencyContainerBootstrapper {
private(set) static var sharedContainer: DependencyContainer?
#if DEBUG
private static var leakedContainers: [DependencyContainer] = []
#endif
static func bootstrap() -> DependencyContainer {
dispatchPrecondition(condition: .onQueue(.main))
if let existing = sharedContainer {
return existing
}
let container = DependencyContainer()
sharedContainer = container
return container
}
}
@MainActor
final class DependencyContainer {
let authSessionManager: AuthSessionManager
lazy var printerSettingsStore: any PrinterSettingsStoreProtocol = PrinterSettingsStore()
lazy var printerService: any PrinterServiceProtocol = PrinterService()
// ... many other services, repositories, and use cases ...
}In tests we also use a dedicated helper:
#if DEBUG
@MainActor
extension DependencyContainer {
static func makeTestingContainer(
printerService: some PrinterServiceProtocol,
printerSettingsStore: some PrinterSettingsStoreProtocol,
configure: ((DependencyContainer) -> Void)? = nil
) -> DependencyContainer {
let container = DependencyContainer()
container.printerService = printerService as any PrinterServiceProtocol
container.printerSettingsStore = printerSettingsStore as any PrinterSettingsStoreProtocol
configure?(container)
return container
}
}
#endifProductListViewModel and VendorDashboardViewModel
Both are ObservableObjects with async/Combine behavior.
ProductListViewModel:
- Holds multiple
Task<Void, Never>properties. - Subscribes to
store.itemsPublisherwith Combine. - Has a
Debouncerthat internally usesTask.sleep.
@MainActor
final class ProductListViewModel: ObservableObject {
@Published var products: [RemoteProduct] = []
var uploadTask: Task<Void, Never>?
var fetchTask: Task<Void, Never>?
var searchTask: Task<Void, Never>?
var loadImportsTask: Task<Void, Never>?
var importsLoadTask: Task<Void, Never>?
var remoteImagePrefetchTasks: [String: Task<Void, Never>] = [:]
var cancellables: Set<AnyCancellable> = []
init( /* many dependencies */ ) {
observeStoreItems()
}
deinit {
uploadTask?.cancel()
fetchTask?.cancel()
searchTask?.cancel()
loadImportsTask?.cancel()
importsLoadTask?.cancel()
remoteImagePrefetchTasks.values.forEach { $0.cancel() }
}
}These patterns are safe in normal app usage, but they make teardown timing much more sensitive under XCTest.
What the crash looked like
The error always appeared right after one of the factory tests finished, for example:
Test Case '-[PalletonTests.DependencyContainerTests testMakeProductListViewModel_CreatesCorrectInstance]' started.
Palleton(38027,0x102f1de40) malloc: *** error for object 0x262c5a6f0: pointer being freed was not allocated
Palleton(38027,0x102f1de40) malloc: *** set a breakpoint in malloc_error_break to debugA typical stack trace (simplified) showed:
Thread 1 Queue: com.apple.main-thread (serial)
0 malloc_error_break
1 swift::RefCounts<swift::RefCountBitsT<swift::RefCountIsInline>>::doDecrement
2 ProductListViewModel.__deallocating_deinit
3 DependencyContainerTests.testMakeProductListViewModel_CreatesCorrectInstance
...Or, for the vendor dashboard test:
Test Case '-[PalletonTests.DependencyContainerTests testMakeVendorDashboardViewModel_CreatesCorrectInstance]' started.
Palleton(...) malloc: *** error for object 0x262c5a6f0: pointer being freed was not allocatedKey observation:
- The crash occurred after the test’s
XCTAssertNotNilsucceeded. - The deallocation path (view model or container deinit) is where the runtime tripped.
Why this happens under XCTest teardown
Short version:
Deallocating async/Combine-heavy
ObservableObjects that are wired to shared singletons, tasks, and publishers during XCTest teardown tickles undefined behavior in the current Swift runtime.
More detailed mental model:
- A test constructs a
DependencyContainerand a view model via a factory method. - The view model subscribes to publishers, creates async tasks, etc.
- The test ends. XCTest begins tearing down the test case and any referenced singletons.
DependencyContainerBootstrapper.resetForTesting()setssharedContainer = nil, deallocating its container (and thus its dependencies).- Swift runtime tears down:
ObservableObjectstate.@Publishedstorage.- Any task locals tied to the main actor.
- Some combination of deallocation order + runtime bug leads to freeing memory twice.
Important:
- The crash is not in our explicit deinit logic (we do minimal cancelation); it is inside Swift’s internal teardown for tasks/Combine/actor isolation.
- The same object graphs behave fine in the running app; the crash appears only under the aggressive create/destroy cycles of the test runner.
Given this, we treated it as a toolchain/runtime edge case rather than a business-logic bug.
The concrete fix: leak in DEBUG tests
Instead of fighting the runtime, we made a pragmatic decision for tests only:
In DEBUG builds, do not deallocate the problematic singletons and view models during the lifetime of the test process.
This is implemented in two places.
1. Leak containers in DependencyContainerBootstrapper (DEBUG only)
We modified DependencyContainerBootstrapper so resetForTesting() and useForTesting(_:) keep strong references to old containers instead of letting them deallocate:
@MainActor
final class DependencyContainerBootstrapper {
private(set) static var sharedContainer: DependencyContainer?
#if DEBUG
private static var leakedContainers: [DependencyContainer] = []
#endif
static func bootstrap() -> DependencyContainer {
dispatchPrecondition(condition: .onQueue(.main))
if let existing = sharedContainer { return existing }
let container = DependencyContainer()
sharedContainer = container
return container
}
}
#if DEBUG
extension DependencyContainerBootstrapper {
static func resetForTesting() {
dispatchPrecondition(condition: .onQueue(.main))
if let container = sharedContainer {
leakedContainers.append(container)
}
sharedContainer = nil
}
static func useForTesting(_ container: DependencyContainer) {
dispatchPrecondition(condition: .onQueue(.main))
if let existing = sharedContainer, existing !== container {
leakedContainers.append(existing)
}
sharedContainer = container
}
}
#endifEffects:
- Tests still see the expected behavior:
sharedContainerbecomesnilafterresetForTesting().useForTestingswaps in a new container.
- Old containers are kept alive in
leakedContainersand never deallocated, so their async/Combine teardown paths are never executed. - Release builds ignore
leakedContainersentirely.
2. Leak view models and testing containers in DependencyContainerTests (DEBUG only)
We also updated DependencyContainerTests to retain factory-created view models and containers:
@MainActor
final class DependencyContainerTests: XCTestCase {
#if DEBUG
// Retain view models created via factory methods in tests
// to avoid deallocation-time malloc crashes in the current Swift/runtime combo.
private static var leakedProductListViewModels: [ProductListViewModel] = []
private static var leakedTestingContainers: [DependencyContainer] = []
#endif
// ... setUp / tearDown unchanged ...
func testMakeProductListViewModel_CreatesCorrectInstance() {
let container = DependencyContainerBootstrapper.bootstrap()
let store = MockProductStore()
// add an item, configure store...
let viewModel = container.makeProductListViewModel(store: store)
#if DEBUG
// Keep a strong reference for the duration of the test process to avoid
// triggering deallocation-time malloc issues in the test environment.
Self.leakedProductListViewModels.append(viewModel)
#endif
XCTAssertNotNil(viewModel)
}
func testMakeTestingContainer_AllowsDependencyOverride() {
let mockPrinterService = MockPrinterService()
let mockSettingsStore = MockPrinterSettingsStore()
let container = DependencyContainer.makeTestingContainer(
printerService: mockPrinterService,
printerSettingsStore: mockSettingsStore
)
#if DEBUG
Self.leakedTestingContainers.append(container)
#endif
XCTAssertNotNil(container)
XCTAssertTrue(container.printerService is MockPrinterService)
XCTAssertTrue(container.printerSettingsStore is MockPrinterSettingsStore)
}
}This pattern:
- Ensures that view models and testing containers are not destroyed while tests are still running.
- Avoids hitting the problematic runtime deallocation paths.
- Only applies in DEBUG builds.
Why this is acceptable
- The leaks are DEBUG-only:
- Guarded by
#if DEBUG. - They do not ship in production.
- Guarded by
- The objects being leaked are:
- A small number of containers and view models per test run.
- Perfectly fine to keep alive until the process exits.
- Tests still fully exercise the DI wiring and construction paths we care about.
- The alternative would be:
- Deep refactors to avoid
ObservableObject,@Published, or async tasks in these components, or - Highly fragile deinit/teardown code that tries to outsmart the runtime.
- Deep refactors to avoid
Given the limited scope and clear separation (test-only), this is a pragmatic, maintainable compromise.
General guidance for async/Combine-heavy teardown in tests
-
Be wary of deinit in async/Combine-heavy types
- Avoid complex logic in
deinitforObservableObjects that own tasks and publishers. - Prefer explicit
reset/tearDownmethods called from known-safe contexts (e.g. logout flows on@MainActor).
- Avoid complex logic in
-
For tests, it’s sometimes okay to leak
- If a crash only occurs during teardown and is clearly in runtime internals, retaining the object for the duration of the test process can be a reasonable workaround.
- Always:
- Scope leaks to
#if DEBUG. - Document the trade-off clearly.
- Scope leaks to
-
Centralize test-only helpers near the code under test
DependencyContainerBootstrapper.resetForTesting/useForTestinglive next to the container, so their behavior is obvious.DependencyContainerTestskeeps its own leaked view models and testing containers, making the workaround explicit in test code.
-
Document hard-to-fix runtime issues
- This post, together with earlier articles like SwiftUI @Published Teardown: Pointer Being Freed Was Not Allocated and ObservedOrStateObject: Choosing the Right Property Wrapper, forms a history of tricky SwiftUI/Concurrency issues we’ve hit.
- Future maintainers can:
- Understand why the code looks the way it does.
- Revisit decisions if new Swift/Xcode releases fix the underlying bugs.
For now, all container-related tests pass without malloc crashes, and the test-only leaks are a conscious, documented trade-off.
FAQ
- Why did DependencyContainer tests crash with 'pointer being freed was not allocated'?
- Because XCTest was tearing down async and Combine-heavy objects such as DependencyContainer and its view models. During deallocation under the current Swift and Xcode toolchain, the runtime occasionally hits a double free or invalid free, so the crash only appears during teardown rather than while normal application logic is running.
- How did you fix the malloc crash without rewriting the architecture?
- In DEBUG test builds only, previous DependencyContainer instances and related view models are kept alive in static arrays. That prevents them from being deallocated during XCTest teardown, where the crash occurred, while leaving release and runtime behavior unchanged so there are no leaks in production.
Welcome to The infinite monkey theorem
Somewhere a monkey just typed Shakespeare in TypeScript. Be the first to read the masterpieces (and the hilarious misfires) landing on the blog.

