Catch Memory Errors with Address Sanitizer on Cloud Mac CI

Catch Memory Errors with Address Sanitizer on Cloud Mac CI

An iOS project that combines Swift, Objective-C, and C/C++ dependencies may pass its regular test suite repeatedly yet still trigger an out-of-bounds access, double free, or use-after-free on a rarely executed path. After moving the job to a cloud Mac, do not simply rerun the existing tests. A more effective approach is to add an Address Sanitizer (ASan) pipeline that fails the build immediately on memory access errors and preserves a reproducible result bundle.

Define What the Gate Should Catch

ASan primarily detects stack and heap out-of-bounds accesses, use-after-free errors, double frees, and some invalid pointer accesses. It is especially useful for testing Objective-C bridging layers, image-processing libraries, database wrappers, and hand-written C interfaces. Pure Swift code can also benefit, but a successful run does not prove that the code is free of memory issues: branches not exercised by the tests remain unchecked, while leaks and data races require additional tools.

Start with a stable, non-interactive set of tests as the gate entry point instead of adding the entire UI test suite immediately. Give the highest priority to core parsers, caching layers, and cross-language interfaces.

Test layer Run frequency Failure handling
Core unit tests Every merge request Block the merge
Integration tests Main branch or merge request Block the release
Full UI tests Scheduled runs or release candidate stage File a defect and reproduce it

A sanitizer is valuable not only because it turns a job red, but because the same input can trigger the same error again while preserving the call stack, test name, and build environment.

Create a Dedicated Scheme and Directories

In Xcode, duplicate the existing test Scheme, name it App-ASan, and mark it as Shared. Do not modify the Scheme developers use every day, or local debugging, performance measurements, and CI settings will interfere with one another. Commit the Scheme at:

App.xcodeproj/xcshareddata/xcschemes/App-ASan.xcscheme

Use separate DerivedData for every job. This prevents object files from regular builds from being reused accidentally by ASan and stops parallel jobs from competing for indexes and intermediate artifacts. Store result bundles in directories named after the job ID rather than repeatedly overwriting a fixed file.

Pin the Test Destination

First, list the currently available simulators:

xcrun simctl list devices available

In CI, pin the device type and major OS version while allowing the patch version to follow the installed runtime. If the team must validate multiple OS versions, split them into matrix jobs instead of relying on automatic destination selection within a single command.

Run the Check with xcodebuild

The script below uses a dedicated build directory and result bundle. Replace the workspace, Scheme, and simulator name with the actual values for the project.

#!/bin/zsh
set -u

ROOT="${PWD}"
OUT="${ROOT}/artifacts/asan"
DERIVED="${ROOT}/.ci/DerivedData-ASan"

rm -rf "${OUT}" "${DERIVED}"
mkdir -p "${OUT}"

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme App-ASan \
  -configuration Debug \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -derivedDataPath "${DERIVED}" \
  -resultBundlePath "${OUT}/App-ASan.xcresult" \
  -enableAddressSanitizer YES \
  CODE_SIGNING_ALLOWED=NO \
  >"${OUT}/xcodebuild.log" 2>&1

status=$?
exit ${status}

Do not append an unconditional || true to the test command. If the CI platform requires log post-processing, save the exit code first and return it unchanged after archiving is complete. CODE_SIGNING_ALLOWED=NO is appropriate for simulator tests. Jobs involving physical devices or archives need a separate signing configuration and must not copy this setting blindly.

ASan increases memory usage and execution time, so concurrency on the same machine should be lower than for regular unit tests. If the system terminates a process, reduce concurrency before deciding whether the failure is a defect in the application under test.

Preserve Evidence That Others Can Investigate

At minimum, retain xcodebuild.log and the .xcresult bundle when a job fails. The former makes Sanitizer reports easy to search, while the latter contains the test hierarchy, failure attachments, and activity logs. Before archiving, you can verify their integrity:

test -s artifacts/asan/xcodebuild.log
test -d artifacts/asan/App-ASan.xcresult
xcrun xcresulttool get test-results summary \
  --path artifacts/asan/App-ASan.xcresult \
  > artifacts/asan/summary.json

The available xcresulttool subcommands may change between Xcode versions, so maintain tool invocations together with the Xcode version. If a command is incompatible, do not delete the original result bundle as a consequence.

When investigating a report, search for ERROR: AddressSanitizer first, then record the error type, the first application stack frame, the triggering test, and the input data. Stack frames from system libraries at the top usually indicate where the crash occurred; the write that corrupted memory may have happened earlier. If the error is not reliably reproducible, loop the affected test in isolation and disable randomized test ordering before changing multiple variables at once.

Run the Gate in Layers and Avoid Common Mistakes

The daily gate can run only the high-risk test set, while the main branch runs the complete ASan suite. Test tags or a Test Plan are easier to maintain than commands assembled from file names. Whenever a C interface, pointer arithmetic, or unsafe buffer handling is added, include the corresponding tests in the fast set as well.

There are three common mistakes. First, reusing a regular build cache, which results in incomplete instrumentation. Second, uploading only text logs without preserving the result bundle. Third, treating an ASan failure as a flaky test and simply retrying it. Retries can help confirm consistency, but the first report must still be retained, and repeated successful retries must not automatically clear an illegal access that was already captured.

Finally, define the acceptance criteria explicitly: the test process must exit with code zero, the result bundle must exist, the log must contain no ASan errors, and the job must not have been terminated because of a timeout or resource pressure. This makes the gate evaluate the complete execution result rather than a single test summary that merely appears successful.

Frequently asked questions

Should Address Sanitizer run on every commit?

Run focused unit tests and high-risk modules on each change. Put longer UI suites on merge requests, the main branch, or a scheduled job to control feedback time.

Does a passing Address Sanitizer job prove the app is memory-safe?

No. It only checks code paths exercised by that run and a specific class of memory faults. Broader tests, leak analysis, race detection, and device validation remain necessary.

Dedicated physical Mac mini

Choose a cloud Mac rental period based on your task

Rent either M4 configuration by the day, week, month, or quarter. Node availability and real-time details are provided by the console.

Choose a configuration and order