This commit is contained in:
Jonathan Schwender 2025-06-03 19:11:09 +02:00 committed by GitHub
commit 38f85a9314
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 203 additions and 68 deletions

View file

@ -17,7 +17,7 @@ import subprocess
import sys import sys
from time import time from time import time
from typing import Optional, List from typing import Optional, List, Dict
import notifypy import notifypy
@ -37,9 +37,12 @@ from servo.command_base import BuildType, CommandBase, call, check_call
from servo.gstreamer import windows_dlls, windows_plugins, package_gstreamer_dylibs from servo.gstreamer import windows_dlls, windows_plugins, package_gstreamer_dylibs
from servo.platform.build_target import BuildTarget from servo.platform.build_target import BuildTarget
from python.servo.platform.build_target import SanitizerKind
SUPPORTED_ASAN_TARGETS = [ SUPPORTED_ASAN_TARGETS = [
"aarch64-apple-darwin", "aarch64-apple-darwin",
"aarch64-unknown-linux-gnu", "aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-ohos",
"x86_64-apple-darwin", "x86_64-apple-darwin",
"x86_64-unknown-linux-gnu", "x86_64-unknown-linux-gnu",
] ]
@ -90,7 +93,7 @@ class MachCommands(CommandBase):
no_package=False, no_package=False,
verbose=False, verbose=False,
very_verbose=False, very_verbose=False,
with_asan=False, sanitizer: SanitizerKind = SanitizerKind.NONE,
flavor=None, flavor=None,
**kwargs, **kwargs,
): ):
@ -109,6 +112,8 @@ class MachCommands(CommandBase):
opts += ["-v"] opts += ["-v"]
if very_verbose: if very_verbose:
opts += ["-vv"] opts += ["-vv"]
self.config["build"]["sanitizer"] = sanitizer
assert sanitizer.is_tsan()
env = self.build_env() env = self.build_env()
self.ensure_bootstrapped() self.ensure_bootstrapped()
@ -117,52 +122,8 @@ class MachCommands(CommandBase):
host = servo.platform.host_triple() host = servo.platform.host_triple()
target_triple = self.target.triple() target_triple = self.target.triple()
if with_asan: if sanitizer is not None:
if target_triple not in SUPPORTED_ASAN_TARGETS: self.build_sanitizer_env(env, opts, kwargs, target_triple, sanitizer)
print(
"AddressSanitizer is currently not supported on this platform\n",
"See https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html",
)
sys.exit(1)
# do not use crown (clashes with different rust version)
env["RUSTC"] = "rustc"
# Enable usage of unstable rust flags
env["RUSTC_BOOTSTRAP"] = "1"
# Enable asan
env["RUSTFLAGS"] = env.get("RUSTFLAGS", "") + " -Zsanitizer=address"
opts += ["-Zbuild-std"]
kwargs["target_override"] = target_triple
# 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:
raise RuntimeError("Unable to determine necessary clang version for ASAN support")
llvm_major: int = rustc_llvm_version[0]
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.
# 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["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")
# 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")
build_start = time() build_start = time()
@ -187,10 +148,12 @@ class MachCommands(CommandBase):
status = self.run_cargo_build_like_command("rustc", opts, env=env, verbose=verbose, **kwargs) status = self.run_cargo_build_like_command("rustc", opts, env=env, verbose=verbose, **kwargs)
if status == 0: if status == 0:
built_binary = self.get_binary_path(build_type, asan=with_asan) built_binary = self.get_binary_path(build_type, sanitizer=sanitizer)
if not no_package and self.target.needs_packaging(): 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, sanitizer=sanitizer
)
if rv: if rv:
return rv return rv
@ -247,6 +210,72 @@ class MachCommands(CommandBase):
opts += params opts += params
return check_call(["cargo", "clean"] + opts, env=self.build_env(), verbose=verbose) return check_call(["cargo", "clean"] + opts, env=self.build_env(), verbose=verbose)
def build_sanitizer_env(
self, env: Dict, opts: List[str], kwargs, target_triple, sanitizer: Optional[SanitizerKind] = None
):
if sanitizer is None:
return None
# do not use crown (clashes with different rust version)
env["RUSTC"] = "rustc"
# Enable usage of unstable rust flags
env["RUSTC_BOOTSTRAP"] = "1"
# std library should also be instrumented
opts += ["-Zbuild-std"]
# We need to always set the target triple, even when building for host.
kwargs["target_override"] = target_triple
# When sanitizers are used we also want framepointers to help with backtraces.
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:
raise RuntimeError("Unable to determine necessary clang version for Sanitizer support")
llvm_major: int = rustc_llvm_version[0]
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", '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", 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")
# 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",
"See https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html",
)
sys.exit(1)
# Enable asan
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): def notify(self, title: str, message: str):
"""Generate desktop notification when build is complete and the """Generate desktop notification when build is complete and the
elapsed build time was longer than 30 seconds. elapsed build time was longer than 30 seconds.

View file

@ -43,6 +43,8 @@ from servo.util import download_file, get_default_cache_dir
import servo.platform import servo.platform
import servo.util as util import servo.util as util
from python.servo.platform.build_target import SanitizerKind
NIGHTLY_REPOSITORY_URL = "https://servo-builds2.s3.amazonaws.com/" NIGHTLY_REPOSITORY_URL = "https://servo-builds2.s3.amazonaws.com/"
ASAN_LEAK_SUPPRESSION_FILE = "support/suppressed_leaks_for_asan.txt" ASAN_LEAK_SUPPRESSION_FILE = "support/suppressed_leaks_for_asan.txt"
@ -305,6 +307,7 @@ class CommandBase(object):
self.config["build"].setdefault("incremental", None) self.config["build"].setdefault("incremental", None)
self.config["build"].setdefault("webgl-backtrace", False) self.config["build"].setdefault("webgl-backtrace", False)
self.config["build"].setdefault("dom-backtrace", False) self.config["build"].setdefault("dom-backtrace", False)
self.config["build"].setdefault("sanitizer", SanitizerKind.NONE)
self.config.setdefault("android", {}) self.config.setdefault("android", {})
self.config["android"].setdefault("sdk", "") self.config["android"].setdefault("sdk", "")
@ -327,9 +330,9 @@ class CommandBase(object):
def get_top_dir(self): def get_top_dir(self):
return self.context.topdir return self.context.topdir
def get_binary_path(self, build_type: BuildType, asan: bool = False): def get_binary_path(self, build_type: BuildType, sanitizer=None):
base_path = util.get_target_dir() base_path = util.get_target_dir()
if asan or self.target.is_cross_build(): if sanitizer is not None or self.target.is_cross_build():
base_path = path.join(base_path, self.target.triple()) base_path = path.join(base_path, self.target.triple())
binary_name = self.target.binary_name() binary_name = self.target.binary_name()
binary_path = path.join(base_path, build_type.directory_name(), binary_name) binary_path = path.join(base_path, build_type.directory_name(), binary_name)
@ -536,7 +539,23 @@ class CommandBase(object):
help="Build in release mode without debug assertions", help="Build in release mode without debug assertions",
), ),
CommandArgument("--profile", group="Build Type", help="Build with custom Cargo profile"), CommandArgument("--profile", group="Build Type", help="Build with custom Cargo profile"),
CommandArgument("--with-asan", action="store_true", help="Build with AddressSanitizer"), CommandArgumentGroup("Sanitizer"),
CommandArgument(
"--with-asan",
group="Sanitizer",
dest="sanitizer",
action="store_const",
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: if build_configuration:
@ -648,13 +667,13 @@ class CommandBase(object):
kwargs["servo_binary"] = ( kwargs["servo_binary"] = (
kwargs.get("bin") kwargs.get("bin")
or self.get_nightly_binary_path(kwargs.get("nightly")) or self.get_nightly_binary_path(kwargs.get("nightly"))
or self.get_binary_path(kwargs.get("build_type"), asan=kwargs.get("with_asan")) or self.get_binary_path(kwargs.get("build_type"), sanitizer=kwargs.get("sanitizer"))
) )
kwargs.pop("bin") kwargs.pop("bin")
kwargs.pop("nightly") kwargs.pop("nightly")
if not build_type: if not build_type:
kwargs.pop("build_type") kwargs.pop("build_type")
kwargs.pop("with_asan") kwargs.pop("sanitizer")
return original_function(self, *args, **kwargs) return original_function(self, *args, **kwargs)
@ -803,15 +822,16 @@ class CommandBase(object):
"--manifest-path", "--manifest-path",
path.join(self.context.topdir, "ports", "servoshell", "Cargo.toml"), path.join(self.context.topdir, "ports", "servoshell", "Cargo.toml"),
] ]
if target_override:
args += ["--target", target_override] if self.target.is_cross_build():
elif self.target.is_cross_build():
args += ["--target", self.target.triple()] args += ["--target", self.target.triple()]
if type(self.target) in [AndroidTarget, OpenHarmonyTarget]: if type(self.target) in [AndroidTarget, OpenHarmonyTarget]:
# Note: in practice `cargo rustc` should just be used unconditionally. # 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" assert command != "build", "For Android / OpenHarmony `cargo rustc` must be used instead of cargo build"
if command == "rustc": if command == "rustc":
args += ["--lib", "--crate-type=cdylib"] args += ["--lib", "--crate-type=cdylib"]
elif target_override:
args += ["--target", target_override]
features = [] features = []

View file

@ -237,7 +237,7 @@ def find_non_system_dependencies_with_otool(binary_path: str) -> Set[str]:
# No need to do any processing for system libraries. They should be # No need to do any processing for system libraries. They should be
# present on all macOS systems. # present on all macOS systems.
if not is_macos_system_library(dependency): if not (is_macos_system_library(dependency) or 'librustc-stable_rt' in dependency):
output.add(dependency) output.add(dependency)
return output return output

View file

@ -10,7 +10,7 @@
from datetime import datetime from datetime import datetime
import random import random
import time import time
from typing import List from typing import List, Optional
from github import Github from github import Github
import hashlib import hashlib
@ -41,6 +41,8 @@ from servo.command_base import (
) )
from servo.util import delete, get_target_dir from servo.util import delete, get_target_dir
from python.servo.platform.build_target import SanitizerKind
PACKAGES = { PACKAGES = {
"android": [ "android": [
"android/aarch64-linux-android/release/servoapp.apk", "android/aarch64-linux-android/release/servoapp.apk",
@ -108,9 +110,9 @@ class PackageCommands(CommandBase):
@CommandArgument("--target", "-t", default=None, help="Package for given target platform") @CommandArgument("--target", "-t", default=None, help="Package for given target platform")
@CommandBase.common_command_arguments(build_configuration=False, build_type=True, package_configuration=True) @CommandBase.common_command_arguments(build_configuration=False, build_type=True, package_configuration=True)
@CommandBase.allow_target_configuration @CommandBase.allow_target_configuration
def package(self, build_type: BuildType, flavor=None, with_asan=False): def package(self, build_type: BuildType, flavor=None, sanitizer: Optional[SanitizerKind] = None):
env = self.build_env() env = self.build_env()
binary_path = self.get_binary_path(build_type, asan=with_asan) binary_path = self.get_binary_path(build_type, sanitizer=sanitizer)
dir_to_root = self.get_top_dir() dir_to_root = self.get_top_dir()
target_dir = path.dirname(binary_path) target_dir = path.dirname(binary_path)
if self.is_android(): if self.is_android():
@ -184,6 +186,11 @@ class PackageCommands(CommandBase):
"-p", "-p",
f"buildMode={build_mode}", f"buildMode={build_mode}",
] ]
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 # Detect if PATH already has hvigor, or else fallback to npm installation
# provided via HVIGOR_PATH # provided via HVIGOR_PATH
if "HVIGOR_PATH" not in env: if "HVIGOR_PATH" not in env:
@ -388,17 +395,19 @@ class PackageCommands(CommandBase):
@CommandArgument("--target", "-t", default=None, help="Install the given target platform") @CommandArgument("--target", "-t", default=None, help="Install the given target platform")
@CommandBase.common_command_arguments(build_configuration=False, build_type=True, package_configuration=True) @CommandBase.common_command_arguments(build_configuration=False, build_type=True, package_configuration=True)
@CommandBase.allow_target_configuration @CommandBase.allow_target_configuration
def install(self, build_type: BuildType, emulator=False, usb=False, with_asan=False, flavor=None): def install(
self, build_type: BuildType, emulator=False, usb=False, sanitizer: Optional[SanitizerKind] = None, flavor=None
):
env = self.build_env() env = self.build_env()
try: try:
binary_path = self.get_binary_path(build_type, asan=with_asan) binary_path = self.get_binary_path(build_type, sanitizer=sanitizer)
except BuildNotFound: except BuildNotFound:
print("Servo build not found. Building servo...") print("Servo build not found. Building servo...")
result = Registrar.dispatch("build", context=self.context, build_type=build_type, flavor=flavor) result = Registrar.dispatch("build", context=self.context, build_type=build_type, flavor=flavor)
if result: if result:
return result return result
try: try:
binary_path = self.get_binary_path(build_type, asan=with_asan) binary_path = self.get_binary_path(build_type, sanitizer=sanitizer)
except BuildNotFound: except BuildNotFound:
print("Rebuilding Servo did not solve the missing build problem.") print("Rebuilding Servo did not solve the missing build problem.")
return 1 return 1

View file

@ -15,6 +15,7 @@ import platform
import shutil import shutil
import subprocess import subprocess
import sys import sys
from enum import Enum
from os import path from os import path
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -24,6 +25,24 @@ import servo.platform
import servo.util as util import servo.util as util
class SanitizerKind(Enum):
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): class BuildTarget(object):
def __init__(self, target_triple: str): def __init__(self, target_triple: str):
self.target_triple = target_triple self.target_triple = target_triple
@ -350,9 +369,8 @@ class OpenHarmonyTarget(CrossBuildTarget):
env[f"CXX_{clang_target_triple_underscore}"] = ndk_clangxx env[f"CXX_{clang_target_triple_underscore}"] = ndk_clangxx
# rustc linker # rustc linker
env[f"CARGO_TARGET_{rust_target_triple.upper()}_LINKER"] = ndk_clang 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}" link_args = ["-fuse-ld=lld", f"--target={clang_target_triple}", f"--sysroot={ohos_sysroot_posix}"]
env["RUSTFLAGS"] += f" -Clink-arg=--sysroot={ohos_sysroot_posix}"
env["HOST_CFLAGS"] = "" env["HOST_CFLAGS"] = ""
env["HOST_CXXFLAGS"] = "" env["HOST_CXXFLAGS"] = ""
@ -398,6 +416,65 @@ class OpenHarmonyTarget(CrossBuildTarget):
bindgen_extra_clangs_args = bindgen_extra_clangs_args + " " + ohos_cflags_str bindgen_extra_clangs_args = bindgen_extra_clangs_args + " " + ohos_cflags_str
env[bindgen_extra_clangs_args_var] = bindgen_extra_clangs_args env[bindgen_extra_clangs_args_var] = bindgen_extra_clangs_args
sanitizer: SanitizerKind = config["build"]["sanitizer"]
san_compile_flags = []
if sanitizer.is_some():
# Lookup `<sdk>/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()
# 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():
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",
]
)
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():
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}`")
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: def binary_name(self) -> str:
return "libservoshell.so" return "libservoshell.so"