From 39ee27eea6cff2d8d04cbdeed7edf092be47c786 Mon Sep 17 00:00:00 2001 From: Jonathan Schwender <55576758+jschwe@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:48:07 +0200 Subject: [PATCH] mach: Allow using ASAN on ohos (#37185) Allow using ASAN on OpenHarmony by adding target specific configuration. This also relaxes the version requirement for the clang compiler when using asan, since asan also has runtime checks, which should fail if instrumented code relies on an incompatible libasan version (not tested). Additional comments: - Remove `TARGET_LDFLAGS = -static-libasan` since anyway rustc invokes the linker, so this flag has no effect. - Enable frame pointers with ASAN, since we are anyway debugging. It should probably be the default anyway. - We pass the with_asan option also to mach package, since hvigor needs to know that we are building for ASAN, otherwise it leads to a crash at startup. Testing: Tested manually on arm64 OpenHarmony. Known issues: ASAN increases the stack usage, and this can cause segfaults due to hitting the stack protector on more complex pages. A follow-up PR could address this by increasing the stack size when compiled with asan in threads that hit this issue. --------- Signed-off-by: Jonathan Schwender --- python/servo/build_commands.py | 25 ++++++++++---- python/servo/command_base.py | 8 +++-- python/servo/package_commands.py | 3 ++ python/servo/platform/build_target.py | 48 +++++++++++++++++++++++++-- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/python/servo/build_commands.py b/python/servo/build_commands.py index e2a5ffc6a1a..e34fa2a2687 100644 --- a/python/servo/build_commands.py +++ b/python/servo/build_commands.py @@ -40,6 +40,7 @@ from servo.platform.build_target import BuildTarget SUPPORTED_ASAN_TARGETS = [ "aarch64-apple-darwin", "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-ohos", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", ] @@ -109,6 +110,8 @@ class MachCommands(CommandBase): opts += ["-v"] if very_verbose: opts += ["-vv"] + if with_asan: + self.config["build"]["with_asan"] = True env = self.build_env() self.ensure_bootstrapped() @@ -136,6 +139,10 @@ class MachCommands(CommandBase): opts += ["-Zbuild-std"] kwargs["target_override"] = target_triple + # With asan we also want frame pointers + if "force-frame-pointers" not in env["RUSTFLAGS"]: + env["RUSTFLAGS"] += " -C force-frame-pointers=yes" + # Note: We want to use the same clang/LLVM version as rustc. rustc_llvm_version = get_rustc_llvm_version() if rustc_llvm_version is None: @@ -144,11 +151,14 @@ 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: - raise RuntimeError(f"--with-asan requires `{target_clang}` and `{target_cxx}` to be in PATH") - env.setdefault("TARGET_CC", target_clang) - env.setdefault("TARGET_CXX", target_cxx) - # TODO: We should also parse the LLVM version from the clang compiler we chose. - # It's unclear if the major version being the same is sufficient. + env.setdefault("TARGET_CC", target_clang) + env.setdefault("TARGET_CXX", target_cxx) + 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++") # 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. @@ -156,7 +166,6 @@ class MachCommands(CommandBase): env.setdefault("TARGET_CXXFLAGS", "") env["TARGET_CFLAGS"] += " -fsanitize=address" env["TARGET_CXXFLAGS"] += " -fsanitize=address" - env["TARGET_LDFLAGS"] = "-static-libasan" # By default build mozjs from source to enable ASAN with mozjs. env.setdefault("MOZJS_FROM_SOURCE", "1") @@ -190,7 +199,9 @@ class MachCommands(CommandBase): built_binary = self.get_binary_path(build_type, asan=with_asan) if not no_package and self.target.needs_packaging(): - rv = Registrar.dispatch("package", context=self.context, build_type=build_type, flavor=flavor) + rv = Registrar.dispatch( + "package", context=self.context, build_type=build_type, flavor=flavor, with_asan=with_asan + ) if rv: return rv diff --git a/python/servo/command_base.py b/python/servo/command_base.py index abd193eda49..f0f28e5488e 100644 --- a/python/servo/command_base.py +++ b/python/servo/command_base.py @@ -305,6 +305,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("with_asan", False) self.config.setdefault("android", {}) self.config["android"].setdefault("sdk", "") @@ -803,15 +804,16 @@ class CommandBase(object): "--manifest-path", path.join(self.context.topdir, "ports", "servoshell", "Cargo.toml"), ] - if target_override: - args += ["--target", target_override] - elif self.target.is_cross_build(): + + if self.target.is_cross_build(): args += ["--target", self.target.triple()] if type(self.target) in [AndroidTarget, OpenHarmonyTarget]: # Note: in practice `cargo rustc` should just be used unconditionally. assert command != "build", "For Android / OpenHarmony `cargo rustc` must be used instead of cargo build" if command == "rustc": args += ["--lib", "--crate-type=cdylib"] + elif target_override: + args += ["--target", target_override] features = [] diff --git a/python/servo/package_commands.py b/python/servo/package_commands.py index 5e63e6549c0..9bc4d70232d 100644 --- a/python/servo/package_commands.py +++ b/python/servo/package_commands.py @@ -184,6 +184,9 @@ class PackageCommands(CommandBase): "-p", f"buildMode={build_mode}", ] + if with_asan: + hvigor_command.extend(["-p", "ohos-debug-asan=true"]) + # Detect if PATH already has hvigor, or else fallback to npm installation # provided via HVIGOR_PATH if "HVIGOR_PATH" not in env: diff --git a/python/servo/platform/build_target.py b/python/servo/platform/build_target.py index 5f0f500ad58..536b10a8d3d 100644 --- a/python/servo/platform/build_target.py +++ b/python/servo/platform/build_target.py @@ -350,9 +350,8 @@ class OpenHarmonyTarget(CrossBuildTarget): env[f"CXX_{clang_target_triple_underscore}"] = ndk_clangxx # rustc linker env[f"CARGO_TARGET_{rust_target_triple.upper()}_LINKER"] = ndk_clang - # We could also use a cross-compile wrapper - env["RUSTFLAGS"] += f" -Clink-arg=--target={clang_target_triple}" - env["RUSTFLAGS"] += f" -Clink-arg=--sysroot={ohos_sysroot_posix}" + + link_args = ["-fuse-ld=lld", f"--target={clang_target_triple}", f"--sysroot={ohos_sysroot_posix}"] env["HOST_CFLAGS"] = "" env["HOST_CXXFLAGS"] = "" @@ -398,6 +397,49 @@ 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"]["with_asan"]: + # 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()] + if len(children) != 1: + 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() + 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(): + raise RuntimeError(f"Couldn't find ASAN runtime library at {libasan_so_path}") + link_args.extend( + [ + "-fsanitize=address", + "--rtlib=compiler-rt", + "-shared-libasan", + str(libasan_so_path), + "-Wl,--whole-archive", + "-Wl," + str(libasan_preinit_path), + "-Wl,--no-whole-archive", + ] + ) + + # 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" + ) + + 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) + 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 + + link_args = [f"-Clink-arg={arg}" for arg in link_args] + env["RUSTFLAGS"] += " " + " ".join(link_args) + def binary_name(self) -> str: return "libservoshell.so"