TSan instrumentation of module code (#8474)

This implements TSan instrumentation in the IREE compiler, and adds a IREE_BYTECODE_MODULE_ENABLE_TSAN flag, which when set causes iree_bytecode_module to enable TSan instrumentation.

When IREE_BUILD_TESTS is on, we enforce early that IREE_ENABLE_TSAN and IREE_BYTECODE_MODULE_ENABLE_TSAN agree, as we would otherwise get test crashes anyway.

IREE_BYTECODE_MODULE_ENABLE_TSAN also requires IREE_BYTECODE_MODULE_FORCE_SYSTEM_DYLIB_LINKER to be set. That too is enforced early.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6ed11ac..b320b20 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -126,6 +126,29 @@
 option(IREE_ENABLE_ASAN "Enable address sanitizer" OFF)
 option(IREE_ENABLE_MSAN "Enable memory sanitizer" OFF)
 option(IREE_ENABLE_TSAN "Enable thread sanitizer" OFF)
+option(IREE_BYTECODE_MODULE_ENABLE_TSAN "Enable thread sanitizer in IREE modules in tests" OFF)
+
+# STREQUAL feels wrong here - we don't care about the exact true-value used,
+# ON or TRUE or something else. But we haven't been able to think of a less bad
+# alternative. https://github.com/google/iree/pull/8474#discussion_r840790062
+if(NOT IREE_ENABLE_TSAN STREQUAL IREE_BYTECODE_MODULE_ENABLE_TSAN)
+  message(SEND_ERROR
+      "IREE_ENABLE_TSAN and IREE_BYTECODE_MODULE_ENABLE_TSAN must be "
+      "simultaneously ON or OFF. "
+      "A discrepancy between the two would cause tests to crash as IREE "
+      "runtime code (controlled by IREE_ENABLE_TSAN) calls into test IREE "
+      "modules (controlled by IREE_BYTECODE_MODULE_ENABLE_TSAN)")
+endif()
+
+if(IREE_BYTECODE_MODULE_ENABLE_TSAN)
+  if(NOT IREE_BYTECODE_MODULE_FORCE_SYSTEM_DYLIB_LINKER)
+    message(SEND_ERROR
+        "When IREE_BYTECODE_MODULE_ENABLE_TSAN is ON, "
+        "IREE_BYTECODE_MODULE_FORCE_SYSTEM_DYLIB_LINKER must also be ON. "
+        "TSAN instrumentation is not currently supported in embedded modules.")
+  endif()
+endif()
+
 option(IREE_ENABLE_CCACHE "Use ccache if installed to speed up rebuilds." OFF)
 
 if(${IREE_ENABLE_CCACHE})
diff --git a/build_tools/cmake/iree_bytecode_module.cmake b/build_tools/cmake/iree_bytecode_module.cmake
index c4a9d1c..fedc2ba 100644
--- a/build_tools/cmake/iree_bytecode_module.cmake
+++ b/build_tools/cmake/iree_bytecode_module.cmake
@@ -116,10 +116,17 @@
     # Note: -iree-llvm-system-linker-path is left unspecified.
   endif()
 
-  if (IREE_BYTECODE_MODULE_FORCE_SYSTEM_DYLIB_LINKER)
+  if(IREE_BYTECODE_MODULE_FORCE_SYSTEM_DYLIB_LINKER)
     list(APPEND _ARGS "-iree-llvm-link-embedded=false")
   endif()
 
+  # Support testing in TSan build dirs. Unlike other sanitizers, TSan is an
+  # ABI break: when the host code is built with TSan, the module must be too,
+  # otherwise we get crashes calling module code.
+  if(IREE_BYTECODE_MODULE_ENABLE_TSAN)
+    list(APPEND _ARGS "-iree-llvm-sanitize=thread")
+  endif()
+
   # Depending on the binary instead of the target here given we might not have
   # a target in this CMake invocation when cross-compiling.
   add_custom_command(
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMAOTTarget.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMAOTTarget.cpp
index 2818949..36a491c 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMAOTTarget.cpp
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMAOTTarget.cpp
@@ -284,6 +284,12 @@
           function.addFnAttr(llvm::Attribute::SanitizeAddress);
         }
       } break;
+      case SanitizerKind::kThread: {
+        libraryBuilder.setSanitizerKind(LibraryBuilder::SanitizerKind::THREAD);
+        for (auto &function : llvmModule->getFunctionList()) {
+          function.addFnAttr(llvm::Attribute::SanitizeThread);
+        }
+      } break;
     }
     auto align16 = llvm::Attribute::getWithAlignment(context, llvm::Align(16));
     for (auto entryPointOp :
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMIRPasses.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMIRPasses.cpp
index 49da9da..7d184ed 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMIRPasses.cpp
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMIRPasses.cpp
@@ -20,6 +20,7 @@
 #include "llvm/Support/Host.h"
 #include "llvm/Support/raw_ostream.h"
 #include "llvm/Transforms/Instrumentation/AddressSanitizer.h"
+#include "llvm/Transforms/Instrumentation/ThreadSanitizer.h"
 
 namespace mlir {
 namespace iree_compiler {
@@ -98,9 +99,19 @@
                 Opts, moduleUseAfterScope, useOdrIndicator));
           });
     } break;
+    case SanitizerKind::kThread: {
+      passBuilder.registerOptimizerLastEPCallback(
+          [](llvm::ModulePassManager &modulePassManager,
+             llvm::OptimizationLevel Level) {
+            modulePassManager.addPass(llvm::ModuleThreadSanitizerPass());
+            modulePassManager.addPass(llvm::createModuleToFunctionPassAdaptor(
+                llvm::ThreadSanitizerPass()));
+          });
+    } break;
   }
 
-  if (options.optLevel != llvm::OptimizationLevel::O0) {
+  if (options.optLevel != llvm::OptimizationLevel::O0 ||
+      options.sanitizerKind != SanitizerKind::kNone) {
     llvm::ModulePassManager modulePassManager;
     modulePassManager =
         passBuilder.buildPerModuleDefaultPipeline(options.optLevel);
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.cpp
index 7cdd79b..2534ea3 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.cpp
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.cpp
@@ -109,7 +109,9 @@
       "iree-llvm-sanitize", llvm::cl::desc("Apply LLVM sanitize feature"),
       llvm::cl::init(SanitizerKind::kNone),
       llvm::cl::values(clEnumValN(SanitizerKind::kAddress, "address",
-                                  "Address sanitizer support")));
+                                  "Address sanitizer support"),
+                       clEnumValN(SanitizerKind::kThread, "thread",
+                                  "Thread sanitizer support")));
   targetOptions.sanitizerKind = clSanitizerKind;
 
   static llvm::cl::opt<std::string> clTargetABI(
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h
index 2a833db..bb51176 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h
@@ -20,6 +20,7 @@
 enum class SanitizerKind {
   kNone = 0,
   kAddress,
+  kThread,
 };
 
 struct LLVMTargetOptions {
diff --git a/iree/hal/local/loaders/system_library_loader.c b/iree/hal/local/loaders/system_library_loader.c
index b18acc6..ebd0213 100644
--- a/iree/hal/local/loaders/system_library_loader.c
+++ b/iree/hal/local/loaders/system_library_loader.c
@@ -179,6 +179,18 @@
           "runtime is not compiled with it enabled; add -fsanitize=address to "
           "the runtime compilation options");
 #endif  // IREE_SANITIZER_ADDRESS
+#if defined(IREE_SANITIZER_THREAD)
+    case IREE_HAL_EXECUTABLE_LIBRARY_SANITIZER_THREAD:
+      // TSAN is compiled into the host and we can load this library.
+      break;
+#else
+    case IREE_HAL_EXECUTABLE_LIBRARY_SANITIZER_THREAD:
+      return iree_make_status(
+          IREE_STATUS_UNAVAILABLE,
+          "executable library is compiled with TSAN support but the host "
+          "runtime is not compiled with it enabled; add -fsanitize=thread to "
+          "the runtime compilation options");
+#endif  // IREE_SANITIZER_THREAD
     default:
       return iree_make_status(
           IREE_STATUS_UNAVAILABLE,