Index: chrome/test/kasko/syzyasan_integration_test.py |
diff --git a/chrome/test/kasko/syzyasan_integration_test.py b/chrome/test/kasko/syzyasan_integration_test.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..2cbcc534b69b574e04bb867b6d423ba3ebf89e65 |
--- /dev/null |
+++ b/chrome/test/kasko/syzyasan_integration_test.py |
@@ -0,0 +1,275 @@ |
+#!/usr/bin/env python |
+# Copyright 2016 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""A Windows-only end-to-end integration test for Kasko and SyzyAsan. |
+ |
+This test ensures that the interface between SyzyAsan, Kasko and Chrome works |
+as expected. The test causes a crash that should be detected by SyzyAsan and |
+delivered via Kasko to a locally hosted test crash server. |
+ |
+Note that this test only works against non-component Release and Official builds |
+of Chrome with Chrome branding, and attempting to use it with anything else will |
+most likely lead to constant failures. |
+ |
+Typical usage (assuming in root 'src' directory): |
+ |
+- generate project files with the following GYP variables: |
+ syzyasan=1 win_z7=0 chromium_win_pch=0 |
+- build the release Chrome binaries: |
+ ninja -C out\Release chrome.exe |
+- run the test: |
+ python chrome/test/kasko/syzyasan_integration_test.py |
+""" |
+ |
+import logging |
+import os |
+import optparse |
+import re |
+import shutil |
+import subprocess |
+import sys |
+ |
+# Bring in the Kasko module. |
+KASKO_DIR = os.path.join(os.path.dirname(__file__), 'py') |
+sys.path.append(KASKO_DIR) |
+import kasko |
+ |
+ |
+_LOGGER = logging.getLogger(os.path.basename(__file__)) |
+_CHROME_DLL = 'chrome.dll' |
+_INSTRUMENT = 'instrument.exe' |
+_SYZYASAN_RTL = 'syzyasan_rtl.dll' |
+ |
+ |
+def _ParseCommandLine(): |
+ self_dir = os.path.dirname(__file__) |
+ src_dir = os.path.abspath(os.path.join(self_dir, '..', '..', '..')) |
+ |
+ option_parser = kasko.config.GenerateOptionParser() |
+ option_parser.add_option('--instrumented-dir', dest='instrumented_dir', |
+ type='string', |
+ help='Path where instrumented binaries will be placed. If instrumented ' |
+ 'binaries already exist here they will be reused.') |
+ option_parser.add_option('--skip-instrumentation', |
+ dest='skip_instrumentation', action='store_true', default=False, |
+ help='Skips instrumentation if specified. To be used when testing ' |
+ 'against an already instrumented build of Chrome.') |
+ option_parser.add_option('--syzygy-dir', dest='syzygy_dir', type='string', |
+ default=os.path.join(src_dir, 'third_party', 'syzygy', 'binaries', 'exe'), |
+ help='Path to Syzygy binaries. By default will look in third_party.') |
+ options = kasko.config.ParseCommandLine(option_parser) |
+ |
+ if not os.path.isdir(options.syzygy_dir): |
+ option_parser.error('Invalid syzygy directory.') |
+ for basename in [_INSTRUMENT, _SYZYASAN_RTL]: |
+ path = os.path.join(options.syzygy_dir, basename) |
+ if not os.path.isfile(path): |
+ option_parser.error('Missing syzygy binary: %s' % path) |
+ |
+ _LOGGER.debug('Using syzygy path: %s', options.syzygy_dir) |
+ |
+ return options |
+ |
+ |
+def _DecorateFilename(name, deco): |
+ """Decorates a filename, transforming 'foo.baz.bar' to 'foo.dec.baz.bar'.""" |
+ d = os.path.dirname(name) |
+ b = os.path.basename(name) |
+ b = b.split('.', 1) |
+ b.insert(1, deco) |
+ return os.path.join(d, '.'.join(b)) |
+ |
+ |
+def _BackupFile(path, dst_dir): |
+ """Creates a backup of a file in the specified directory.""" |
+ bak = os.path.abspath(os.path.join(dst_dir, os.path.basename(path))) |
+ if os.path.exists(bak): |
+ os.remove(bak) |
+ # Copy the file, with its permissions and timestamps, etc. |
+ _LOGGER.debug('Copying "%s" to "%s".' % (path, bak)) |
+ shutil.copyfile(path, bak) |
+ shutil.copystat(path, bak) |
+ return bak |
+ |
+ |
+def _RestoreFile(path, backup): |
+ """Restores a file from its backup. Leaves the backup file.""" |
+ if not os.path.exists(backup): |
+ raise Exception('Backup does not exist: %s' % backup) |
+ if os.path.exists(path): |
+ os.remove(path) |
+ _LOGGER.debug('Restoring "%s" from "%s".' % (path, backup)) |
+ shutil.copyfile(backup, path) |
+ shutil.copystat(backup, path) |
+ |
+ |
+class _ScopedInstrumentedChrome(object): |
+ """SyzyAsan Instruments a Chrome installation in-place.""" |
+ |
+ def __init__(self, chrome_dir, syzygy_dir, temp_dir, instrumented_dir=None, |
+ verbose=False, skip_instrumentation=False): |
+ self.chrome_dir_ = chrome_dir |
+ self.syzygy_dir_ = syzygy_dir |
+ self.temp_dir_ = temp_dir |
+ self.instrumented_dir_ = instrumented_dir |
+ self.verbose_ = verbose |
+ self.skip_instrumentation_ = skip_instrumentation |
+ |
+ def _ProduceInstrumentedBinaries(self): |
+ # Generate the instrumentation command-line. This will place the |
+ # instrumented binaries in the temp directory. |
+ instrument = os.path.abspath(os.path.join(self.syzygy_dir_, _INSTRUMENT)) |
+ cmd = [instrument, |
+ '--mode=asan', |
+ '--input-image=%s' % self.chrome_dll_bak_, |
+ '--input-pdb=%s' % self.chrome_dll_pdb_bak_, |
+ '--output-image=%s' % self.chrome_dll_inst_, |
+ '--output-pdb=%s' % self.chrome_dll_pdb_inst_, |
+ '--no-augment-pdb'] |
+ |
+ _LOGGER.debug('Instrumenting Chrome binaries.') |
+ |
+ # If in verbose mode then let the instrumentation produce output directly. |
+ if self.verbose_: |
+ result = subprocess.call(cmd) |
+ else: |
+ # Otherwise run the command with all output suppressed. |
+ proc = subprocess.call(cmd, stdout=subprocess.PIPE, |
+ stderr=subprocess.PIPE) |
+ stdout, stderr = proc.communicate() |
+ result = proc.returncode |
+ if result != 0: |
+ sys.stdout.write(stdout) |
+ sys.stderr.write(stderr) |
+ |
+ if result != 0: |
+ raise Exception('Failed to instrument: %s' % chrome_dll) |
+ |
+ return |
+ |
+ def __enter__(self): |
+ """In-place instruments a Chrome installation with SyzyAsan.""" |
+ # Do nothing if instrumentation is to be skipped entirely. |
+ if self.skip_instrumentation_: |
+ _LOGGER.debug('Assuming binaries already instrumented.') |
+ return self |
+ |
+ # Build paths to the original Chrome binaries. |
+ self.chrome_dll_ = os.path.abspath(os.path.join( |
+ self.chrome_dir_, _CHROME_DLL)) |
+ self.chrome_dll_pdb_ = self.chrome_dll_ + '.pdb' |
+ |
+ # Backup the original Chrome binaries to the temp directory. |
+ orig_dir = os.path.join(self.temp_dir_, 'orig') |
+ os.makedirs(orig_dir) |
+ self.chrome_dll_bak_ = _BackupFile(self.chrome_dll_, orig_dir) |
+ self.chrome_dll_pdb_bak_ = _BackupFile(self.chrome_dll_pdb_, orig_dir) |
+ |
+ # Generate the path to the instrumented binaries. |
+ inst_dir = os.path.join(self.temp_dir_, 'inst') |
+ if self.instrumented_dir_: |
+ inst_dir = self.instrumented_dir_ |
+ if not os.path.isdir(inst_dir): |
+ os.makedirs(inst_dir) |
+ self.chrome_dll_inst_ = os.path.abspath(os.path.join( |
+ inst_dir, _DecorateFilename(_CHROME_DLL, 'inst'))) |
+ self.chrome_dll_pdb_inst_ = os.path.abspath(os.path.join( |
+ inst_dir, _DecorateFilename(_CHROME_DLL + '.pdb', 'inst'))) |
+ |
+ # Only generate the instrumented binaries if they don't exist. |
+ if (os.path.isfile(self.chrome_dll_inst_) and |
+ os.path.isfile(self.chrome_dll_pdb_inst_)): |
+ _LOGGER.debug('Using existing instrumented binaries.') |
+ else: |
+ self._ProduceInstrumentedBinaries() |
+ |
+ # Replace the original chrome binaries with the instrumented versions. |
+ _RestoreFile(self.chrome_dll_, self.chrome_dll_inst_) |
+ _RestoreFile(self.chrome_dll_pdb_, self.chrome_dll_pdb_inst_) |
+ |
+ # Copy the runtime library into the Chrome directory. |
+ syzyasan_rtl = os.path.abspath(os.path.join(self.syzygy_dir_, |
+ _SYZYASAN_RTL)) |
+ self.syzyasan_rtl_ = os.path.abspath(os.path.join(self.chrome_dir_, |
+ _SYZYASAN_RTL)) |
+ _RestoreFile(self.syzyasan_rtl_, syzyasan_rtl) |
+ |
+ return self |
+ |
+ def __exit__(self, *args, **kwargs): |
+ # Do nothing if instrumentation is to be skipped entirely. |
+ if self.skip_instrumentation_: |
+ return |
+ |
+ # Remove the RTL and restore the original Chrome binaries. |
+ os.remove(self.syzyasan_rtl_) |
+ _RestoreFile(self.chrome_dll_, self.chrome_dll_bak_) |
+ _RestoreFile(self.chrome_dll_pdb_, self.chrome_dll_pdb_bak_) |
+ |
+ |
+def Main(): |
+ options = _ParseCommandLine() |
+ |
+ # Generate a temporary directory for use in the tests. |
+ with kasko.util.ScopedTempDir() as temp_dir: |
+ # Prevent the temporary directory from self cleaning if requested. |
+ if options.keep_temp_dirs: |
+ temp_dir_path = temp_dir.release() |
+ else: |
+ temp_dir_path = temp_dir.path |
+ |
+ # Use the specified user data directory if requested. |
+ if options.user_data_dir: |
+ user_data_dir = options.user_data_dir |
+ else: |
+ user_data_dir = os.path.join(temp_dir_path, 'user-data-dir') |
+ |
+ kasko_dir = os.path.join(temp_dir_path, 'kasko') |
+ os.makedirs(kasko_dir) |
+ |
+ # Launch the test server. |
+ server = kasko.crash_server.CrashServer() |
+ with kasko.util.ScopedStartStop(server): |
+ _LOGGER.info('Started server on port %d', server.port) |
+ |
+ # Configure the environment so Chrome can find the test crash server. |
+ os.environ['KASKO_CRASH_SERVER_URL'] = ( |
+ 'http://127.0.0.1:%d/crash' % server.port) |
+ |
+ # Configure the environment to disable feature randomization, which can |
+ # result in Kasko being randomly disabled. Append to any existing options. |
+ k = 'SYZYGY_ASAN_OPTIONS' |
+ v = '--disable_feature_randomization' |
+ if k in os.environ: |
+ os.environ[k] += ' ' + v |
+ else: |
+ os.environ[k] = v |
+ |
+ # SyzyAsan instrument the Chrome installation. |
+ chrome_dir = os.path.dirname(options.chrome) |
+ with _ScopedInstrumentedChrome(chrome_dir, options.syzygy_dir, |
+ temp_dir_path, instrumented_dir=options.instrumented_dir, |
+ verbose=(options.log_level == logging.DEBUG), |
+ skip_instrumentation=options.skip_instrumentation) as asan_chrome: |
+ # Launch Chrome and navigate it to the test URL. |
+ chrome = kasko.process.ChromeInstance(options.chromedriver, |
+ options.chrome, user_data_dir) |
+ with kasko.util.ScopedStartStop(chrome): |
+ _LOGGER.info('Navigating to SyzyAsan debug URL') |
+ chrome.navigate_to('chrome://crash/browser-use-after-free') |
+ |
+ _LOGGER.info('Waiting for Kasko report') |
+ if not server.wait_for_report(10): |
+ raise Exception('No Kasko report received.') |
+ |
+ report = server.crash(0) |
+ kasko.report.LogCrashKeys(report) |
+ kasko.report.ValidateCrashReport(report, {'asan-error-type': 'SyzyAsan'}) |
+ |
+ return 0 |
+ |
+ |
+if __name__ == '__main__': |
+ sys.exit(Main()) |