Skip to content

Commit 4a94a22

Browse files
committed
feat: detect sanitizer instrumentation in build
1 parent 476df47 commit 4a94a22

File tree

4 files changed

+82
-15
lines changed

4 files changed

+82
-15
lines changed

src/ffpuppet/core.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from .checks import CheckLogContents, CheckLogSize, CheckMemoryUsage
2626
from .display import DISPLAYS, DisplayMode
2727
from .exceptions import BrowserExecutionError, InvalidPrefs, LaunchError
28-
from .helpers import prepare_environment, wait_on_files
28+
from .helpers import detect_sanitizer, prepare_environment, wait_on_files
2929
from .minidump_parser import MDSW_URL, MinidumpParser
3030
from .process_tree import ProcessTree
3131
from .profile import Profile
@@ -292,7 +292,10 @@ def available_logs(self) -> frozenset[str]:
292292
return self._logs.available_logs()
293293

294294
def build_launch_cmd(
295-
self, bin_path: str, additional_args: Sequence[str] | None = None
295+
self,
296+
bin_path: str,
297+
additional_args: Sequence[str] | None = None,
298+
sanitizer: str | None = None,
296299
) -> list[str]:
297300
"""Build a command that can be used to launch the browser.
298301
@@ -402,9 +405,9 @@ def build_launch_cmd(
402405
"rr",
403406
"record",
404407
]
405-
if getenv("RR_ASAN") == "1":
408+
if sanitizer == "asan" or getenv("RR_ASAN") == "1":
406409
rr_cmd.append("--asan")
407-
if getenv("RR_TSAN") == "1":
410+
if sanitizer == "tsan" or getenv("RR_TSAN") == "1":
408411
rr_cmd.append("--tsan")
409412
if getenv("RR_CHAOS") == "1":
410413
rr_cmd.append("--chaos")
@@ -730,6 +733,9 @@ def launch(
730733
bin_path = bin_path.resolve()
731734
if not bin_path.is_file() or not access(bin_path, X_OK):
732735
raise OSError(f"{bin_path} is not an executable")
736+
detected_sanitizer = detect_sanitizer(bin_path)
737+
LOG.debug("detected sanitizer: %s", detected_sanitizer)
738+
733739
# need the path to help find symbols
734740
self._bin_path = bin_path.parent
735741

@@ -805,7 +811,11 @@ def launch(
805811

806812
self.profile.add_prefs(prefs)
807813

808-
cmd = self.build_launch_cmd(str(bin_path), additional_args=launch_args)
814+
cmd = self.build_launch_cmd(
815+
str(bin_path),
816+
additional_args=launch_args,
817+
sanitizer=detected_sanitizer,
818+
)
809819

810820
# open logs
811821
self._logs.add_log("stdout")
@@ -827,6 +837,7 @@ def launch(
827837
env=prepare_environment(
828838
self._logs.path / self._logs.PREFIX_SAN,
829839
env_mod=env_mod,
840+
sanitizer=detected_sanitizer,
830841
),
831842
shell=False,
832843
stderr=stderr,

src/ffpuppet/helpers.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99
from contextlib import suppress
1010
from logging import getLogger
11+
from mmap import ACCESS_READ, mmap
1112
from os import environ
1213
from pathlib import Path
1314
from subprocess import STDOUT, CalledProcessError, check_output
@@ -28,14 +29,16 @@
2829
else:
2930
IS_WINDOWS = False
3031

32+
__author__ = "Tyson Smith"
33+
3134
CERTUTIL = "certutil.exe" if IS_WINDOWS else "certutil"
3235
LOG = getLogger(__name__)
3336

34-
__author__ = "Tyson Smith"
35-
3637

3738
def _configure_sanitizers(
38-
orig_env: Mapping[str, str], log_path: Path
39+
orig_env: Mapping[str, str],
40+
log_path: Path,
41+
symbolize: bool = False,
3942
) -> dict[str, str]:
4043
"""Copy environment and update default values in *SAN_OPTIONS entries.
4144
These values are only updated if they are not provided, with the exception of
@@ -44,13 +47,15 @@ def _configure_sanitizers(
4447
Args:
4548
orig_env: Current environment.
4649
log_path: Location to write sanitizer logs to.
50+
symbolize: Enable automatic symbolizing. This should only used when required to
51+
minimize memory usage.
4752
4853
Returns:
4954
Environment with *SAN_OPTIONS defaults set.
5055
"""
5156
env = dict(orig_env)
5257
# https://github.com/google/sanitizers/wiki/SanitizerCommonFlags
53-
common_flags = [
58+
common_flags = (
5459
("abort_on_error", "false"),
5560
("allocator_may_return_null", "true"),
5661
("disable_coredump", "true"),
@@ -64,10 +69,7 @@ def _configure_sanitizers(
6469
("handle_sigfpe", "true"),
6570
# set to be safe
6671
("handle_sigill", "true"),
67-
# do not automatically symbolize
68-
# this should be done after to avoid hitting memory limitations
69-
("symbolize", "false"),
70-
]
72+
)
7173

7274
# setup Address Sanitizer options ONLY if not set manually in environment
7375
# https://github.com/google/sanitizers/wiki/AddressSanitizerFlags
@@ -99,6 +101,7 @@ def _configure_sanitizers(
99101
asan_config.add("strict_init_order", "true")
100102
# temporarily revert to default (false) until https://bugzil.la/1767068 is fixed
101103
# asan_config.add("strict_string_checks", "true")
104+
asan_config.add("symbolize", "1" if symbolize else "0")
102105
env["ASAN_OPTIONS"] = str(asan_config)
103106

104107
# setup Leak Sanitizer options ONLY if not set manually in environment
@@ -126,6 +129,7 @@ def _configure_sanitizers(
126129
tsan_config.add("log_path", f"'{log_path}'", overwrite=True)
127130
# This is an experimental feature added in Bug 1792757
128131
tsan_config.add("rss_limit_heap_profile", "true")
132+
tsan_config.add("symbolize", "1" if symbolize else "0")
129133
env["TSAN_OPTIONS"] = str(tsan_config)
130134

131135
# setup Undefined Behavior Sanitizer options ONLY if not set manually in environment
@@ -140,6 +144,7 @@ def _configure_sanitizers(
140144
ubsan_config.add("log_path", f"'{log_path}'", overwrite=True)
141145
ubsan_config.add("print_stacktrace", "1")
142146
ubsan_config.add("report_error_type", "1")
147+
ubsan_config.add("symbolize", "1" if symbolize else "0")
143148
env["UBSAN_OPTIONS"] = str(ubsan_config)
144149

145150
return env
@@ -186,6 +191,28 @@ def certutil_find(browser_bin: Path | None = None) -> str:
186191
return CERTUTIL
187192

188193

194+
def detect_sanitizer(binary: Path) -> str | None:
195+
"""Detect sanitizer instrumentation in browser build.
196+
197+
Args:
198+
binary: Location of browser binary.
199+
200+
Returns:
201+
Name of sanitizer in use or None.
202+
"""
203+
with (
204+
binary.open("rb") as bin_fp,
205+
mmap(bin_fp.fileno(), 0, access=ACCESS_READ) as bmm,
206+
):
207+
if bmm.find(b"__tsan_") != -1:
208+
return "tsan"
209+
if bmm.find(b"__asan_") != -1:
210+
return "asan"
211+
if bmm.find(b"__ubsan_") != -1:
212+
return "ubsan"
213+
return None
214+
215+
189216
def files_in_use(files: Iterable[Path]) -> Generator[tuple[Path, int, str]]:
190217
"""Check if any of the given files are open.
191218
WARNING: This can be slow on Windows.
@@ -226,6 +253,7 @@ def files_in_use(files: Iterable[Path]) -> Generator[tuple[Path, int, str]]:
226253
def prepare_environment(
227254
sanitizer_log: Path,
228255
env_mod: Mapping[str, str | None] | None = None,
256+
sanitizer: str | None = None,
229257
) -> dict[str, str]:
230258
"""Create environment that can be used when launching the browser.
231259
@@ -235,6 +263,7 @@ def prepare_environment(
235263
env_mod: Environment modifier. Add, remove and update entries
236264
in the prepared environment. Add/update by setting
237265
value or remove entry by setting value to None.
266+
sanitizer: Sanitizer in use.
238267
239268
Returns:
240269
Environment to use when launching browser.
@@ -302,7 +331,9 @@ def prepare_environment(
302331
env.pop("MOZ_CRASHREPORTER_NO_REPORT", None)
303332
env.pop("MOZ_CRASHREPORTER_SHUTDOWN", None)
304333

305-
env = _configure_sanitizers(env, sanitizer_log)
334+
# automatically symbolize traces when TSan is in use
335+
# it is required for runtime TSan suppressions
336+
env = _configure_sanitizers(env, sanitizer_log, symbolize=sanitizer == "tsan")
306337
# filter environment to avoid leaking sensitive information
307338
return {k: v for k, v in env.items() if "_SECRET" not in k}
308339

src/ffpuppet/test_ffpuppet.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This Source Code Form is subject to the terms of the Mozilla Public
22
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
33
# You can obtain one at http://mozilla.org/MPL/2.0/.
4-
# pylint: disable=invalid-name,missing-docstring,protected-access
4+
# pylint: disable=missing-docstring,protected-access
55
"""ffpuppet tests"""
66

77
import os
@@ -32,6 +32,7 @@
3232

3333

3434
class ReqHandler(BaseHTTPRequestHandler):
35+
# pylint: disable=invalid-name
3536
def do_GET(self):
3637
self.send_response(200)
3738
self.end_headers()

src/ffpuppet/test_helpers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
_configure_sanitizers,
1515
certutil_available,
1616
certutil_find,
17+
detect_sanitizer,
1718
files_in_use,
1819
prepare_environment,
1920
wait_on_files,
@@ -33,6 +34,13 @@ def test_helpers_01(tmp_path):
3334
assert opts.get("log_path") == f"'{tmp_path}'"
3435
assert "LSAN_OPTIONS" in env
3536
assert "UBSAN_OPTIONS" in env
37+
# test symbolize
38+
env = _configure_sanitizers({}, tmp_path, symbolize=False)
39+
assert "symbolize=0" in env["ASAN_OPTIONS"]
40+
assert "symbolize=0" in env["TSAN_OPTIONS"]
41+
env = _configure_sanitizers({}, tmp_path, symbolize=True)
42+
assert "symbolize=1" in env["ASAN_OPTIONS"]
43+
assert "symbolize=1" in env["TSAN_OPTIONS"]
3644
# test with presets environment
3745
env = _configure_sanitizers(
3846
{
@@ -239,3 +247,19 @@ def test_certutil_find_01(tmp_path):
239247
certutil_bin.parent.mkdir()
240248
certutil_bin.touch()
241249
assert certutil_find(browser_bin) == str(certutil_bin)
250+
251+
252+
@mark.parametrize(
253+
"bin_content, result",
254+
[
255+
(b"_foo", None),
256+
(b"foo __asan_foo", "asan"),
257+
(b"foo __tsan_foo", "tsan"),
258+
(b"foo __ubsan_foo", "ubsan"),
259+
],
260+
)
261+
def test_detect_sanitizer_01(tmp_path, bin_content, result):
262+
"""test detect_sanitizer()"""
263+
binary = tmp_path / "file.bin"
264+
binary.write_bytes(bin_content)
265+
assert detect_sanitizer(binary) == result

0 commit comments

Comments
 (0)