What do the Address, Thread and Undefined Behavior sanitizers catch, what do they cost, and when do you use them in Swift code?

23.3.1 · L5 · Interview

Predict first

A program reads from a pointer after deallocating it. Built with Address Sanitizer, what is reported?

// === main.swift ===
let p = UnsafeMutablePointer<Int>.allocate(capacity: 4)
p.initialize(repeating: 1, count: 4)
p.deallocate()
print(p[0])
  • heap-use-after-free
  • Nothing: it prints the old value
  • A Swift fatal error

Predict first

A program reads one element past a four-element buffer. What does ASan report?

// === main.swift ===
let p = UnsafeMutablePointer<Int>.allocate(capacity: 4)
p.initialize(repeating: 1, count: 4)
print(p[4])
  • heap-buffer-overflow
  • stack-overflow
  • A Swift index-out-of-range trap

Predict first

Can Address and Thread Sanitizer be enabled in the same build?

// === main.swift ===
let p = UnsafeMutablePointer<Int>.allocate(capacity: 4)
p.initialize(repeating: 1, count: 4)
p.deallocate()
print(p[0])
  • No: the compiler rejects the combination
  • Yes, they stack
  • Only on a real device

Mental model. A sanitizer is a building inspector who is present during construction: the crooked wall is flagged the moment it goes up, not discovered when the ceiling cracks a year later.

Sanitizers instrument your build so that certain bugs are detected at the moment they happen, with a report naming both the access and the allocation or other party. Address Sanitizer (ASan) finds memory errors: use-after-free, heap and stack buffer overflow, double free and (optionally) use-after-return, with stacks for the bad access and for where the memory was allocated and freed. Swift itself is memory safe, so ASan earns its keep with UnsafePointer code, C and Objective-C libraries, and interop; the check below catches a read of freed memory and a read one element past a buffer, both of which would otherwise “work” and corrupt something later. Thread Sanitizer (TSan) finds data races even on runs where nothing visibly went wrong (Q301 covers using it). Undefined Behavior Sanitizer (UBSan) flags things like misaligned access and invalid conversions in C-family code. Xcode also has lighter, always-available checkers: the Main Thread Checker (UIKit used off the main thread, Q218) and the Thread Performance Checker (slow work or priority inversion on the main thread).

The costs and rules: sanitizers slow the program and use more memory (ASan is commonly around twice as slow and several times the memory, TSan more), they need a rebuild with the option on (-sanitize=address, or Scheme ▸ Run ▸ Diagnostics), and some cannot be combined: the compiler rejects address plus thread, as the check below shows, so you run the tests once per sanitizer. They only see code paths that execute, so they find bugs your tests exercise: a good habit is a CI job that runs the unit tests under ASan and another under TSan, with the sanitizer’s failure failing the build. Run them on the simulator or Mac for speed; they are for development and CI, never for shipping. When one fires, read the report’s first frame in your code, then both stacks for a race.

Without ASan the read often “works”. With it the access is caught immediately and reported with allocation and free stacks.

Unsafe pointers have no bounds checks, so the read is a heap overflow that only a sanitizer notices.

They use incompatible runtimes, so you run separate builds.

Common traps

  • Enabling ASan and TSan together. They conflict; use separate builds and test runs.
  • Assuming “Swift is safe” so no sanitizer is needed. Unsafe pointers, C libraries and unchecked interop are where the errors live.

They might follow up with

  • When does ASan find nothing? When the bad access is on a path your tests never run.
  • Where do sanitizers run in CI? As separate test jobs, one per sanitizer, failing the build on a report.

Related questions

  • 23.3.2 Why do some bugs appear only in Release builds, how do assert, precondition and fatalError differ, and how do…
  • 23.2.2 What are zombie objects and malloc stack logging, and how do you debug dangling references and over-released…
  • 10.7.1 How do you find and fix deadlocks, data races and slow main-actor hops?
  • 6.3.3 What is the main-thread contract, and why does DispatchQueue.main.sync from the main thread crash?

Why do some bugs appear only in Release builds, how do assert… →