Raja Software Labs logo

Thread Sanitizer (TSan): Debugging iOS Data Race EXC_BAD_ACCESS Errors

If you are an iOS developer, at one point or another you will run into an EXC_BAD_ACCESS crash. If you have already tried enabling NSZombie without any luck (thus ruling out memory problems / dangling pointers), a potential cause could be a Data Race Condition in your code. This post discusses what a Data Race Condition is and how you can use the Thread Sanitizer to debug the issue.

What is a Data Race Condition?

A Data Race Condition occurs when multiple threads access the same memory location without synchronization and atleast one of these accesses is a write.

Data Race Conditions are notoriously hard to debug since they are timing dependent and will not occur every time the code is run. Step In — Thread Sanitizer.

What is Thread Sanitizer?

Thread Sanitizer (or TSan) is a part of a family of LLVM tools which combines compile time instrumentation and runtime monitoring to detect threading bugs in the code. It was introduced in Xcode 8 and has support for both Objective C and Swift. It can detect multiple types of threading bugs one of which is the Data Race Condition.

Running with the TSan enabled -

a) Tap on application scheme and then on “Edit Scheme”

demo-blog-post-edit-scheme.webp

b) Under the “Diagnostics” tab, in the “Runtime Sanitizer” section, enable the “Thread Sanitizer” checkbox. You can also choose to pause the program execution when a data race is detected by checking the “Pause on issues” checkbox. (Note — The “Thread Sanitizer” option is only available when you are running on an iOS Simulator. It is not supported on a device)

demo-blog-post-thread-sanitizer.webp

c) Run your application and execute the steps which cause the EXC_BAD_ACCESS crash.

If your application has a Data Race Condition, TSan will detect it and log it to the console. (Note —TSan might not detect all Data Race Conditions in a single attempt. In that case, try executing the steps to reproduce multiple times).

Interpreting the TSan log -

The TSan log for a Data Race Condition will look like this (important parts are highlighted in red) -

demo-blog-post-tsan-log.webp

Sample Thread Sanitizer Log

  1. TSan log has information about which exact threads participated in the Data Race Condition. In the above example, the main thread and thread T7 were involved in the Data Race Condition.
  2. The log contains a stack trace for each participating thread. Asterisk (*) in the logged stack traces points to the exact code which accessed / mutated the data. In the above log, the data race occurred when setting / reading the someBool variable. Also, if you go one level down in the call stack, you will get the exact method where the memory location in question was accessed / mutated (startConsumer() and updateBoolValue())
  3. The log also tells you if the memory location in question is present in the heap / stack / data segment of your program and which thread allocated this memory. In the above example, TSan log states — Location is heap block of size 880 at 0x7b5c00000380 allocated by main thread.

Please Note — The TSan log will contain at most 4 call stacks which accessed the memory location in question. This is because the TSan is designed to keep a track of upto 4 different accesses only.

Now that you have tracked down the Data Race Condition, lets see how it can be fixed.

Fixing a Data Race Condition -

The fix for a Data Race Condition largely depends on how the code is designed. There is no “correct / standard” way to fix this problem. This means only you will know what the correct fix should be.

Broadly speaking, there are 2 ways to fix a Data Race Condition -

  1. Prevent multiple threads from accessing the same memory location. To do this, you could — choose to dispatch all accesses (read / write) to the same queue using GCD functions (DispatchQueue.sync() / DispatchQueue.async()) or copy your objects when passing them between different threads.
  2. Synchronize access to memory locations used by multiple threads by using synchronization primitives provided by Apple like @synchronized(), NSLock etc.

Wrapping up (Few key points about TSan) -

  1. The TSan can a detect Data Race Condition even when it does not manifest itself in a particular application run.
  2. TSan might not detect a Data Race Condition in a single application run. This is especially true if a memory location is being accessed by more than 4 threads.
  3. TSan requires recompilation of the code so it cannot be used on pre-built binaries.
  4. Running your code with TSan checks enabled can result in CPU slowdown of 2⨉ to 20⨉, and an increase in memory usage by 5⨉ to 10⨉.
  5. The TSan only works with the iOS simulator and cannot be enabled when running on a real iOS device.

Hope this post helps you debug / fix Data Race Conditions in your code. To summarize — we discussed what a Data Race Condition is, how to debug this using TSan and some potential ways to this problem in your code.

Happy Coding!