diff --git a/python/servo/build_commands.py b/python/servo/build_commands.py index 346b6974dc7..826be76c8c6 100644 --- a/python/servo/build_commands.py +++ b/python/servo/build_commands.py @@ -93,7 +93,7 @@ class MachCommands(CommandBase): no_package=False, verbose=False, very_verbose=False, - sanitizer: Optional[SanitizerKind] = None, + sanitizer: SanitizerKind = SanitizerKind.NONE, flavor=None, **kwargs, ): @@ -113,6 +113,7 @@ class MachCommands(CommandBase): if very_verbose: opts += ["-vv"] self.config["build"]["sanitizer"] = sanitizer + assert sanitizer.is_tsan() env = self.build_env() self.ensure_bootstrapped() @@ -234,18 +235,25 @@ class MachCommands(CommandBase): target_clang = f"clang-{llvm_major}" target_cxx = f"clang++-{llvm_major}" if shutil.which(target_clang) is None or shutil.which(target_cxx) is None: - env.setdefault("TARGET_CC", target_clang) - env.setdefault("TARGET_CXX", target_cxx) + env.setdefault("TARGET_CC", 'clang') + env.setdefault("TARGET_CXX", 'clang++') else: # libasan can be compatible across multiple compiler versions and has a # runtime check, which would fail if we used incompatible compilers, so # we can try and fallback to the default clang. - env.setdefault("TARGET_CC", "clang") - env.setdefault("TARGET_CXX", "clang++") + env.setdefault("TARGET_CC", target_clang) + env.setdefault("TARGET_CXX", target_cxx) # By default, build mozjs from source to enable Sanitizers in mozjs. env.setdefault("MOZJS_FROM_SOURCE", "1") - if sanitizer == sanitizer.ASAN: + # We need to use `TARGET_CFLAGS`, since we don't want to compile host dependencies with ASAN, + # since that causes issues when building build-scripts / proc macros. + # The actual flags will be appended below depending on the sanitizer kind. + env.setdefault("TARGET_CFLAGS", "") + env.setdefault("TARGET_CXXFLAGS", "") + env.setdefault("RUSTFLAGS", "") + + if sanitizer is sanitizer.ASAN: if target_triple not in SUPPORTED_ASAN_TARGETS: print( "AddressSanitizer is currently not supported on this platform\n", @@ -254,18 +262,18 @@ class MachCommands(CommandBase): sys.exit(1) # Enable asan - env["RUSTFLAGS"] = env.get("RUSTFLAGS", "") + " -Zsanitizer=address" - - # We need to use `TARGET_CFLAGS`, since we don't want to compile host dependencies with ASAN, - # since that causes issues when building build-scripts / proc macros. - env.setdefault("TARGET_CFLAGS", "") - env.setdefault("TARGET_CXXFLAGS", "") + env["RUSTFLAGS"] += " -Zsanitizer=address" env["TARGET_CFLAGS"] += " -fsanitize=address" env["TARGET_CXXFLAGS"] += " -fsanitize=address" # asan replaces system allocator with asan allocator # we need to make sure that we do not replace it with jemalloc self.features.append("servo_allocator/use-system-allocator") + elif sanitizer is SanitizerKind.TSAN: + env["RUSTFLAGS"] += " -Zsanitizer=thread" + env["TARGET_CFLAGS"] += " -fsanitize=thread" + env["TARGET_CXXFLAGS"] += " -fsanitize=thread" + return None def notify(self, title: str, message: str): diff --git a/python/servo/command_base.py b/python/servo/command_base.py index bdb68aed99a..5265bd1a698 100644 --- a/python/servo/command_base.py +++ b/python/servo/command_base.py @@ -307,7 +307,7 @@ class CommandBase(object): self.config["build"].setdefault("incremental", None) self.config["build"].setdefault("webgl-backtrace", False) self.config["build"].setdefault("dom-backtrace", False) - self.config["build"].setdefault("sanitizer", None) + self.config["build"].setdefault("sanitizer", SanitizerKind.NONE) self.config.setdefault("android", {}) self.config["android"].setdefault("sdk", "") @@ -548,6 +548,14 @@ class CommandBase(object): const=SanitizerKind.ASAN, help="Build with AddressSanitizer", ), + CommandArgument( + "--with-tsan", + group="Sanitizer", + dest="sanitizer", + action="store_const", + const=SanitizerKind.TSAN, + help="Build with ThreadSanitizer", + ), ] if build_configuration: diff --git a/python/servo/package_commands.py b/python/servo/package_commands.py index b0e3eb41dca..45609daf4e8 100644 --- a/python/servo/package_commands.py +++ b/python/servo/package_commands.py @@ -186,8 +186,10 @@ class PackageCommands(CommandBase): "-p", f"buildMode={build_mode}", ] - if sanitizer == SanitizerKind.ASAN: + if sanitizer.is_asan(): hvigor_command.extend(["-p", "ohos-debug-asan=true"]) + elif sanitizer.is_tsan(): + hvigor_command.extend(["-p", "ohos-enable-tsan=true"]) # Detect if PATH already has hvigor, or else fallback to npm installation # provided via HVIGOR_PATH diff --git a/python/servo/platform/build_target.py b/python/servo/platform/build_target.py index 53b7ede0275..45b002ea6e9 100644 --- a/python/servo/platform/build_target.py +++ b/python/servo/platform/build_target.py @@ -26,8 +26,21 @@ import servo.util as util class SanitizerKind(Enum): - UNKNOWN = 0 + NONE = 0 ASAN = 1 + TSAN = 2 + + # Apparently enums don't always compare across modules, so we define + # helper methods. + def is_asan(self) -> bool: + return self is self.ASAN + + def is_tsan(self) -> bool: + return self is self.TSAN + + # Returns true if a sanitizer is enabled. + def is_some(self) -> bool: + return self is not self.NONE class BuildTarget(object): @@ -403,8 +416,9 @@ class OpenHarmonyTarget(CrossBuildTarget): bindgen_extra_clangs_args = bindgen_extra_clangs_args + " " + ohos_cflags_str env[bindgen_extra_clangs_args_var] = bindgen_extra_clangs_args - # On OpenHarmony we add some additional flags when asan is enabled - if config["build"]["sanitizer"] == SanitizerKind.ASAN: + sanitizer: SanitizerKind = config["build"]["sanitizer"] + san_compile_flags = [] + if sanitizer.is_some(): # Lookup `/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.asan.so` lib_clang = llvm_toolchain.joinpath("lib", "clang") children = [f.path for f in os.scandir(lib_clang) if f.is_dir()] @@ -412,6 +426,12 @@ class OpenHarmonyTarget(CrossBuildTarget): raise RuntimeError(f"Expected exactly 1 libclang version: `{children}`") lib_clang_version_dir = pathlib.Path(children[0]) libclang_arch = lib_clang_version_dir.joinpath("lib", clang_target_triple).resolve() + # Use the clangrt from the NDK to use the same library for both C++ and Rust. + env["RUSTFLAGS"] += " -Zexternal-clangrt" + san_compile_flags.append("-fno-omit-frame-pointer") + + # On OpenHarmony we add some additional flags when asan is enabled + if sanitizer.is_asan(): libasan_so_path = libclang_arch.joinpath("libclang_rt.asan.so") libasan_preinit_path = libclang_arch.joinpath("libclang_rt.asan-preinit.a") if not libasan_so_path.exists(): @@ -428,23 +448,32 @@ class OpenHarmonyTarget(CrossBuildTarget): ] ) - # Use the clangrt from the NDK to use the same library for both C++ and Rust. - env["RUSTFLAGS"] += " -Zexternal-clangrt" - - asan_compile_flags = ( - " -fsanitize=address -shared-libasan -fno-omit-frame-pointer -fsanitize-recover=address" - ) + san_compile_flags.extend([ "-fsanitize=address", "-shared-libasan", "-fsanitize-recover=address"]) arch_asan_ignore_list = lib_clang_version_dir.joinpath("share", "asan_ignorelist.txt") if arch_asan_ignore_list.exists(): - asan_compile_flags += " -fsanitize-system-ignorelist=" + str(arch_asan_ignore_list) + san_compile_flags.append("-fsanitize-system-ignorelist=" + str(arch_asan_ignore_list)) else: print(f"Warning: Couldn't find system ASAN ignorelist at `{arch_asan_ignore_list}`") - env["TARGET_CFLAGS"] += asan_compile_flags - env["TARGET_CXXFLAGS"] += asan_compile_flags + elif sanitizer.is_tsan(): + libtsan_so_path = libclang_arch.joinpath("libclang_rt.tsan.so") + builtins_path = libclang_arch.joinpath("libclang_rt.builtins.a") + + link_args.extend( + [ + "-fsanitize=thread", + "--rtlib=compiler-rt", + "-shared-libsan", + str(libtsan_so_path), + str(builtins_path) + ] + ) + san_compile_flags.append("-shared-libsan") link_args = [f"-Clink-arg={arg}" for arg in link_args] env["RUSTFLAGS"] += " " + " ".join(link_args) + env["TARGET_CFLAGS"] += " " + " ".join(san_compile_flags) + env["TARGET_CXXFLAGS"] += " " + " ".join(san_compile_flags) def binary_name(self) -> str: return "libservoshell.so"