Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
84882e0
Add mutator plugins
jonathanmetzman Mar 19, 2019
894d3ba
fix production
jonathanmetzman Mar 20, 2019
56c52f6
fmt
jonathanmetzman Mar 20, 2019
92fb0e0
lnt
jonathanmetzman Mar 20, 2019
00bd0a1
add TODO
jonathanmetzman Mar 20, 2019
a33ba61
add strategy
jonathanmetzman Mar 20, 2019
6e309a2
fix integration
jonathanmetzman Mar 20, 2019
088e2f8
fix comments
jonathanmetzman Mar 20, 2019
cbbbaed
fix failing libfuzzer_run_test
jonathanmetzman Mar 20, 2019
8b0956b
add mutator plugins bucket
jonathanmetzman Mar 20, 2019
9018d32
try to go without bucket in testing
jonathanmetzman Mar 20, 2019
9dd22d0
fix failing test
jonathanmetzman Mar 20, 2019
87ee234
Disable failing test
jonathanmetzman Mar 20, 2019
3bb1279
fmt
jonathanmetzman Mar 20, 2019
66b8752
make staticmethod a function
jonathanmetzman Mar 21, 2019
c88acd8
fmt
jonathanmetzman Mar 21, 2019
deffd5f
make olivers suggestions
jonathanmetzman Mar 22, 2019
7b53d97
fix tests
jonathanmetzman Mar 22, 2019
021d73d
fmt
jonathanmetzman Mar 22, 2019
2e2d487
lnt
jonathanmetzman Mar 22, 2019
6344c0d
fix nonterminating unittests
jonathanmetzman Mar 22, 2019
246a445
make abhishek's suggested changes.
jonathanmetzman Mar 22, 2019
8d7d710
fmt
jonathanmetzman Mar 22, 2019
b8ed1b2
remove old code
jonathanmetzman Mar 22, 2019
7a26dc2
fix mild issue
jonathanmetzman Mar 22, 2019
8e6f954
WIP: get working with minijail (need to refactor tests to duplicate l…
jonathanmetzman Mar 22, 2019
bb0ca8c
fmt
jonathanmetzman Mar 27, 2019
2939d6c
fix
jonathanmetzman Mar 27, 2019
9ae33fb
undo accidental
jonathanmetzman Mar 27, 2019
50fbd32
dont repeat code
jonathanmetzman Mar 27, 2019
ed1b0af
improve comments
jonathanmetzman Mar 27, 2019
dd9d9dc
fix nits
jonathanmetzman Mar 27, 2019
8c0cf2c
fmt
jonathanmetzman Mar 27, 2019
27646a4
add TODO
jonathanmetzman Mar 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bot/inputs/mutator-plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Placeholder for mutator plugins.
3 changes: 3 additions & 0 deletions configs/test/project.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ env:
# the testcases against the target application.
FUZZ_LOGS_BUCKET: test-fuzz-logs-bucket

# Default bucket to store mutator plugins.
MUTATOR_PLUGINS_BUCKET: test-mutator-plugins-bucket
Comment thread
jonathanmetzman marked this conversation as resolved.

# Default issue tracker name unless overridden in a job definition. Current only supports monorail
# issue tracker (disabled by default).
# ISSUE_TRACKER: issue-tracker-name
Expand Down
2 changes: 2 additions & 0 deletions src/local/butler/create_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ def execute(args):
('test-shared-corpus-bucket',
project_bucket(args.project_id, 'shared-corpus')),
('test-fuzz-logs-bucket', project_bucket(args.project_id, 'fuzz-logs')),
('test-mutator-plugins-bucket',
project_bucket(args.project_id, 'mutator-plugins')),
)

# Write new configs.
Expand Down
2 changes: 2 additions & 0 deletions src/local/butler/run_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def bootstrap_gcs(storage_path):
config.get('env.SHARED_CORPUS_BUCKET'))
create_local_bucket(local_gcs_buckets_path,
config.get('env.FUZZ_LOGS_BUCKET'))
create_local_bucket(local_gcs_buckets_path,
config.get('env.MUTATOR_PLUGINS_BUCKET'))

# Symlink local GCS bucket path to appengine src dir to bypass sandboxing
# issues.
Expand Down
27 changes: 26 additions & 1 deletion src/python/bot/fuzzers/libFuzzer/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from bot.fuzzers import dictionary_manager
from bot.fuzzers import engine_common
from bot.fuzzers import libfuzzer
from bot.fuzzers import mutator_plugin
from bot.fuzzers import strategy
from bot.fuzzers import utils as fuzzer_utils
from bot.fuzzers.ml.rnn import generator as ml_rnn_generator
Expand Down Expand Up @@ -100,6 +101,8 @@

FORK_PROBABILITY = 0.1

MUTATOR_PLUGIN_PROBABILITY = 0.50


class Generator(object):
"""Generators we can use."""
Expand Down Expand Up @@ -174,6 +177,17 @@ def do_fork():
strategy.FORK_STRATEGY, default=FORK_PROBABILITY))


def do_mutator_plugin():
"""Return whether or not to use a mutator_plugin."""
# TODO(metzman): Support Windows.
if environment.platform() == 'WINDOWS':
return False

return engine_common.decide_with_probability(
engine_common.get_strategy_probability(
strategy.MUTATOR_PLUGIN_STRATEGY, default=MUTATOR_PLUGIN_PROBABILITY))


def add_recommended_dictionary(arguments, fuzzer_name, fuzzer_path):
"""Add recommended dictionary from GCS to existing .dict file or create
a new one and update the arguments as needed.
Expand Down Expand Up @@ -815,11 +829,22 @@ def main(argv):
fuzzing_strategies.append(
'%s_%d' % (strategy.FORK_STRATEGY, num_fuzz_processes))

extra_env = {}
if do_mutator_plugin():
mutator_plugin_path = mutator_plugin.get_mutator_plugin(target_name)
logs.log('Using mutator plugin: %s' % mutator_plugin_path)
if mutator_plugin_path:
# TODO(metzman): Change the strategy to log which plugin was used, and not
Comment thread
jonathanmetzman marked this conversation as resolved.
# simply that a plugin was used.
fuzzing_strategies.append(strategy.MUTATOR_PLUGIN_STRATEGY)
extra_env['LD_PRELOAD'] = mutator_plugin_path

# Execute the fuzzer binary with original arguments.
fuzz_result = runner.fuzz(
corpus_directories,
fuzz_timeout=fuzz_timeout,
additional_args=arguments + [artifact_prefix])
additional_args=arguments + [artifact_prefix],
extra_env=extra_env)

if (not use_minijail and
fuzz_result.return_code == constants.LIBFUZZER_ERROR_EXITCODE):
Expand Down
3 changes: 3 additions & 0 deletions src/python/bot/fuzzers/libFuzzer/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ def parse_line_for_strategy_prefix(line, strategy_name):
stats['strategy_corpus_mutations_radamsa'] = 1
if strategy.CORPUS_MUTATION_ML_RNN_STRATEGY in strategies:
stats['strategy_corpus_mutations_ml_rnn'] = 1
if strategy.MUTATOR_PLUGIN_STRATEGY in strategies:
stats['strategy_mutator_plugin'] = 1
if strategy.RANDOM_MAX_LENGTH_STRATEGY in strategies:
stats['strategy_random_max_len'] = 1
if strategy.RECOMMENDED_DICTIONARY_STRATEGY in strategies:
Expand Down Expand Up @@ -189,6 +191,7 @@ def parse_performance_features(log_lines, strategies, arguments):
'strategy_corpus_mutations_ml_rnn': 0,
'strategy_corpus_subset': 0,
'strategy_fork': 0,
'strategy_mutator_plugin': 0,
'strategy_random_max_len': 0,
'strategy_recommended_dict': 0,
'strategy_value_profile': 0,
Expand Down
18 changes: 12 additions & 6 deletions src/python/bot/fuzzers/libfuzzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ def fuzz(self,
corpus_directories,
fuzz_timeout,
artifact_prefix=None,
additional_args=None):
additional_args=None,
extra_env=None):
"""Running fuzzing command.

Args:
Expand All @@ -116,6 +117,8 @@ def fuzz(self,
timeouts, slow units)
additional_args: A sequence of additional arguments to be passed to the
executable.
extra_env: A dictionary containing environment variables and their values.
These will be added to the environment of the new process.

Returns:
A process.ProcessResult.
Expand Down Expand Up @@ -147,7 +150,8 @@ def fuzz(self,
timeout=fuzz_timeout - self.SIGTERM_WAIT_TIME,
terminate_before_kill=True,
terminate_wait_time=self.SIGTERM_WAIT_TIME,
max_stdout_len=MAX_OUTPUT_LEN)
max_stdout_len=MAX_OUTPUT_LEN,
extra_env=extra_env)

def merge(self,
corpus_directories,
Expand Down Expand Up @@ -309,14 +313,15 @@ def fuzz(self,
corpus_directories,
fuzz_timeout,
artifact_prefix=None,
additional_args=None):
additional_args=None,
extra_env=None):
"""LibFuzzerCommon.fuzz override."""
additional_args = copy.copy(additional_args)
if additional_args is None:
additional_args = []

return LibFuzzerCommon.fuzz(self, corpus_directories, fuzz_timeout,
artifact_prefix, additional_args)
artifact_prefix, additional_args, extra_env)


class MinijailLibFuzzerRunner(engine_common.MinijailEngineFuzzerRunner,
Expand Down Expand Up @@ -383,11 +388,12 @@ def fuzz(self,
corpus_directories,
fuzz_timeout,
artifact_prefix=None,
additional_args=None):
additional_args=None,
extra_env=None):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like extra_env is not passed to LibFuzzerCommon.fuzz

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also need a test that shows LD_PRELOAD set to that path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like extra_env is not passed to LibFuzzerCommon.fuzz

I think this was fixed in a more recent upload.

also need a test that shows LD_PRELOAD set to that path.

I'll see how easy this is but I already test for this implicitly in the integration test I added.

"""LibFuzzerCommon.fuzz override."""
corpus_directories = self._get_chroot_corpus_paths(corpus_directories)
return LibFuzzerCommon.fuzz(self, corpus_directories, fuzz_timeout,
artifact_prefix, additional_args)
artifact_prefix, additional_args, extra_env)

def merge(self,
corpus_directories,
Expand Down
169 changes: 169 additions & 0 deletions src/python/bot/fuzzers/mutator_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Getting and using custom mutator plugins for libFuzzer."""

import os

from base import utils
from google_cloud_utils import storage
from metrics import logs
from system import archive
from system import environment
from system import shell

MUTATOR_SHARED_OBJECT_FILENAME = 'mutator-plugin.so'
ARCHIVES_SUBDIR_NAME = 'archives'
PLUGINS_SUBDIR_NAME = 'plugins'


def _get_mutator_plugins_bucket_url():
"""Returns the url of the mutator plugin's cloud storage bucket."""
return 'gs://%s' % environment.get_value('MUTATOR_PLUGINS_BUCKET')


def _get_mutator_plugins_subdir(subdir):
"""Returns the path of the subdirectory named |subdir| in
MUTATOR_PLUGINS_DIR."""
return os.path.join(environment.get_value('MUTATOR_PLUGINS_DIR'), subdir)


def _get_mutator_plugins_archives_dir():
"""Returns path of the archives subdirectory in MUTATOR_PLUGINS_DIR"""
return _get_mutator_plugins_subdir(ARCHIVES_SUBDIR_NAME)


def _get_mutator_plugins_unpacked_dir():
"""Returns path of the unpacked plugins subdirectory in MUTATOR_PLUGINS_DIR"""
return _get_mutator_plugins_subdir(PLUGINS_SUBDIR_NAME)


def _get_mutator_plugins_from_bucket():
"""Returns list of the mutator plugin archives in the mutator plugin storage
bucket."""
return storage.list_blobs(_get_mutator_plugins_bucket_url())


def _download_mutator_plugin_archive(mutator_plugin_archive):
"""Downloads the |mutator_plugin_archive| from the mutator plugin storage
bucket to the plugin archives directory. Returns the path that the archive was
downloaded to."""
file_path = os.path.join(_get_mutator_plugins_archives_dir(),
mutator_plugin_archive)
url = '%s/%s' % (_get_mutator_plugins_bucket_url(), mutator_plugin_archive)
if not storage.copy_file_from(url, file_path):
logs.log_error(
'Failed to copy plugin archive from %s to %s' % (url, file_path))
return None

return file_path


def _unpack_mutator_plugin(mutator_plugin_archive_path):
"""Unpacks |mutator_plugin_archive_path| in the unpacked plugins directory and
returns the path it was unpacked into."""
mutator_plugin_name = os.path.basename(
os.path.splitext(mutator_plugin_archive_path)[0])
unpacked_plugin_dir = os.path.join(_get_mutator_plugins_unpacked_dir(),
mutator_plugin_name)
archive.unpack(mutator_plugin_archive_path, unpacked_plugin_dir)
return unpacked_plugin_dir
Comment thread
jonathanmetzman marked this conversation as resolved.


def _extract_name_from_archive(plugin_archive_filename):
"""Parses |plugin_archive_filename| which should be named using the schema:
'$NAME-$JOB-$FUZZ_TARGET.zip'. Returns tuple containing
Comment thread
jonathanmetzman marked this conversation as resolved.
Comment thread
jonathanmetzman marked this conversation as resolved.
($NAME, $JOB-$FUZZ_TARGET)."""
# TODO(metzman): Get rid of this when we create an upload page for custom
# mutator plugins.
plugin_archive_name = os.path.splitext(plugin_archive_filename)[0]
idx = plugin_archive_name.index('-')
plugin_name = plugin_archive_name[:idx]
return plugin_name, plugin_archive_name[idx + 1:]


class PluginGetter(object):
"""Class that gets a usable mutator plugin for |fuzzer_binary_name| from
GCS."""

def __init__(self, fuzzer_binary_name):
self.fuzzer_binary_name = fuzzer_binary_name
self.job_name = environment.get_value('JOB_NAME')
self.create_directories()

def create_directories(self):
"""Creates directories needed to use mutator plugins."""

# TODO(320): Change mutator plugin downloads so that they don't need to be
# deleted and redownloaded on each run of launcher.py.
def recreate_directory(directory_path):
shell.create_directory_if_needed(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very hacky, to do create_directory_if_needed first and then remove_directory. Why do we need to remove and re-download plugin everytime ? Is this for every launcher instance ? We need to make sure we dont even unpack if it was unpacked once.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very hacky, to do create_directory_if_needed first and then remove_directory. Why do we need to remove and re-download plugin everytime ?

What if the plugin gets updated?

Is this for every launcher instance ?

Yes.

We need to make sure we dont even unpack if it was unpacked once.

I intentionally do this. I agree it would be less wasteful to unpack once for every 5 runs of launcher.py. I believe the code did this initially, but it was suggested that I change this so that mutator plugins would more easily work on OSS-Fuzz.

directory_path, create_intermediates=True)
shell.remove_directory(directory_path, recreate=True)

recreate_directory(environment.get_value('MUTATOR_PLUGINS_DIR'))
recreate_directory(_get_mutator_plugins_archives_dir())
recreate_directory(_get_mutator_plugins_unpacked_dir())

def _is_plugin_usable(self, plugin_archive_filename):
"""Returns True if |plugin_archive_filename| is a usable plugin for this
job, fuzz target combination."""
_, plugin_job_and_fuzzer = _extract_name_from_archive(
plugin_archive_filename)
expected_name = '%s-%s' % (self.job_name, self.fuzzer_binary_name)
return expected_name == plugin_job_and_fuzzer

def get_mutator_plugin(self):
"""Downloads and unpacks a usable mutator plugin for this job and fuzz
target if one is available in GCS"""
mutator_plugins = _get_mutator_plugins_from_bucket()
usable_mutator_plugins = [
plugin_archive for plugin_archive in mutator_plugins
if self._is_plugin_usable(plugin_archive)
]

# Quit if no usable plugins are available.
if not usable_mutator_plugins:
return None

plugin_archive_name = utils.random_element_from_list(usable_mutator_plugins)

# Handle a failed download.
plugin_archive_path = _download_mutator_plugin_archive(plugin_archive_name)
if not plugin_archive_path:
return None

_unpack_mutator_plugin(plugin_archive_path)
mutator_plugin_path = find_mutator_plugin()
if mutator_plugin_path is None:
logs.log_error('Could not find plugin in %s' % plugin_archive_path)

return mutator_plugin_path


def find_mutator_plugin():
"""Sets LD_PRELOAD to the path of a usable mutator plugin shared object.
This should only be called after a call to get_mutator_plugin."""
paths = shell.get_files_list(_get_mutator_plugins_unpacked_dir())
# This function should not be called unless there is an unpacked plugin.
for path in paths:
if os.path.basename(path) == MUTATOR_SHARED_OBJECT_FILENAME:
return path
return None


def get_mutator_plugin(fuzzer_binary_name):
"""Downloads and unpacks a mutator plugin if a usable one for
|fuzzer_binary_name| is in the bucket."""
plugin_getter = PluginGetter(fuzzer_binary_name)
return plugin_getter.get_mutator_plugin()
1 change: 1 addition & 0 deletions src/python/bot/fuzzers/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CORPUS_MUTATION_ML_RNN_STRATEGY = 'corpus_mutations_ml_rnn'
CORPUS_SUBSET_STRATEGY = 'corpus_subset'
FORK_STRATEGY = 'fork'
MUTATOR_PLUGIN_STRATEGY = 'mutator_plugin'
RANDOM_MAX_LENGTH_STRATEGY = 'random_max_len'
RECOMMENDED_DICTIONARY_STRATEGY = 'recommended_dict'
VALUE_PROFILE_STRATEGY = 'value_profile'
1 change: 1 addition & 0 deletions src/python/bot/untrusted_runner/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
r'^ASAN_OPTIONS$',
r'^BACKUP_BUCKET$',
r'^CORPUS_BUCKET$',
r'^MUTATOR_PLUGINS_BUCKET$',
r'^FUZZ_CORPUS_DIR$',
r'^FUZZER_DIR$',
r'^FUZZER_NAME_REGEX$',
Expand Down
2 changes: 2 additions & 0 deletions src/python/system/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,8 @@ def set_bot_environment():
os.environ['FUZZ_INPUTS_MEMORY'] = os.environ['FUZZ_INPUTS']
os.environ['FUZZ_INPUTS_DISK'] = os.path.join(inputs_dir,
'fuzzer-testcases-disk')
os.environ['MUTATOR_PLUGINS_DIR'] = os.path.join(inputs_dir,
'mutator-plugins')
Comment thread
jonathanmetzman marked this conversation as resolved.
os.environ['FUZZ_DATA'] = os.path.join(inputs_dir,
'fuzzer-common-data-bundles')
os.environ['IMAGES_DIR'] = os.path.join(inputs_dir, 'images')
Expand Down
4 changes: 4 additions & 0 deletions src/python/system/minijail.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ def get_command(self, additional_args=None):
def run(self,
additional_args=None,
max_stdout_len=None,
extra_env=None,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand All @@ -341,6 +342,9 @@ def run(self,
passed_env = popen_args.pop('env', None)
from bot.untrusted_runner import environment as untrusted_environment
env = untrusted_environment.get_env_for_untrusted_process(passed_env)
if extra_env is not None:
env.update(extra_env)

env['PATH'] = self.PATH_ENVIRONMENT_VALUE

return MinijailChildProcess(
Expand Down
Loading