Skip to content

Commit

Permalink
Initial implementation of honggfuzz.
Browse files Browse the repository at this point in the history
Part of #1128.
  • Loading branch information
oliverchang committed Nov 7, 2019
1 parent 75d2dfc commit 3b61745
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 1 deletion.
13 changes: 13 additions & 0 deletions src/python/bot/fuzzers/honggfuzz/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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.
169 changes: 169 additions & 0 deletions src/python/bot/fuzzers/honggfuzz/engine.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.
"""honggfuzz engine interface."""

import glob
import os
import re

from bot.fuzzers import dictionary_manager
from bot.fuzzers import engine
from system import environment
from system import new_process

_DEFAULT_ARGUMENTS = [
'-n',
'1', # single threaded
'--exit_upon_crash',
'-v', # output to stderr
'-z', # use clang instrumentation
'-P', # persistent mode
'-S', # enable sanitizers
'--rlimit_rss',
'2048',
'--timeout',
'25',
]

_CRASH_REGEX = re.compile('Crash: saved as \'(.*)\'')
_HF_SANITIZER_LOG_PREFIX = 'HF.sanitizer.log'


def _get_runner():
"""Get the honggfuzz runner."""
honggfuzz_path = os.path.join(environment.get_value('BUILD_DIR'), 'honggfuzz')
return new_process.ProcessRunner(honggfuzz_path)


def _find_sanitizer_stacktrace(reproducers_dir):
"""Find the sanitizer stacktrace from the reproducers dir."""
for stacktrace_path in glob.glob(
os.path.join(reproducers_dir, _HF_SANITIZER_LOG_PREFIX + '*')):
with open(stacktrace_path, 'rb') as f:
return f.read()

return None


class HonggfuzzEngine(engine.Engine):
"""honggfuzz engine implementation."""

@property
def name(self):
return 'honggfuzz'

def prepare(self, corpus_dir, target_path, build_dir):
"""Prepare for a fuzzing session, by generating options. Returns a
FuzzOptions object.
Args:
corpus_dir: The main corpus directory.
target_path: Path to the target.
build_dir: Path to the build directory.
Returns:
A FuzzOptions object.
"""
arguments = []
dict_path = dictionary_manager.get_default_dictionary_path(target_path)
if os.path.exists(dict_path):
arguments.extend(['--dict', dict_path])

return engine.FuzzOptions(corpus_dir, arguments, [])

def fuzz(self, target_path, options, reproducers_dir, max_time):
"""Run a fuzz session.
Args:
target_path: Path to the target.
options: The FuzzOptions object returned by prepare().
reproducers_dir: The directory to put reproducers in when crashes
are found.
max_time: Maximum allowed time for the fuzzing to run.
Returns:
A FuzzResult object.
"""
runner = _get_runner()
arguments = _DEFAULT_ARGUMENTS[:]
arguments.extend(options.arguments)
arguments.extend([
'-i',
options.corpus_dir,
'-W',
reproducers_dir,
'--run_time',
str(max_time),
'--',
target_path,
])

fuzz_result = runner.run_and_wait(
additional_args=arguments, timeout=max_time + 10)
log_lines = fuzz_result.output.splitlines()
sanitizer_stacktrace = _find_sanitizer_stacktrace(reproducers_dir)

crashes = []
for line in log_lines:
crash_match = _CRASH_REGEX.match(line)
if not crash_match:
continue

reproducer_path = crash_match.group(1)
crashes.append(
engine.Crash(reproducer_path, sanitizer_stacktrace, [],
int(fuzz_result.time_executed)))
break

# TODO(ochang): Parse stats.
return engine.FuzzResult(fuzz_result.output, fuzz_result.command, crashes,
{}, fuzz_result.time_executed)

def reproduce(self, target_path, input_path, arguments, max_time):
"""Reproduce a crash given an input.
Args:
target_path: Path to the target.
input_path: Path to the reproducer input.
arguments: Additional arguments needed for reproduction.
max_time: Maximum allowed time for the reproduction.
Returns:
A ReproduceResult.
"""
runner = new_process.ProcessRunner(target_path)
with open(input_path) as f:
result = runner.run_and_wait(timeout=max_time, stdin=f)

return engine.ReproduceResult(result.command, result.return_code,
result.time_executed, result.output)

def minimize_corpus(self, target_path, arguments, input_dirs, output_dir,
reproducers_dir, max_time):
"""Optional (but recommended): run corpus minimization.
Args:
target_path: Path to the target.
arguments: Additional arguments needed for corpus minimization.
input_dirs: Input corpora.
output_dir: Output directory to place minimized corpus.
reproducers_dir: The directory to put reproducers in when crashes are
found.
max_time: Maximum allowed time for the minimization.
Returns:
A FuzzResult object.
"""
# TODO(ochang): Implement this.
raise NotImplementedError
3 changes: 2 additions & 1 deletion src/python/system/new_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ def run_and_wait(self,
input_data=None,
max_stdout_len=None,
extra_env=None,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
**popen_args):
Expand Down Expand Up @@ -328,7 +329,7 @@ def run_and_wait(self,
additional_args,
max_stdout_len=max_stdout_len,
extra_env=extra_env,
stdin=subprocess.PIPE,
stdin=stdin,
stdout=stdout,
stderr=stderr,
**popen_args)
Expand Down
1 change: 1 addition & 0 deletions src/python/tests/core/bot/fuzzers/honggfuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
temp
13 changes: 13 additions & 0 deletions src/python/tests/core/bot/fuzzers/honggfuzz/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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.
145 changes: 145 additions & 0 deletions src/python/tests/core/bot/fuzzers/honggfuzz/honggfuzz_engine_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# 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.
"""Tests for honggfuzz engine."""
# pylint: disable=unused-argument

import os
import shutil
import unittest

from bot.fuzzers import engine_common
from bot.fuzzers.honggfuzz import engine
from tests.test_libs import helpers as test_helpers
from tests.test_libs import test_utils

TEST_PATH = os.path.abspath(os.path.dirname(__file__))
DATA_DIR = os.path.join(TEST_PATH, 'test_data')
TEMP_DIR = os.path.join(TEST_PATH, 'temp')


def clear_temp_dir():
"""Clear temp directory."""
if os.path.exists(TEMP_DIR):
shutil.rmtree(TEMP_DIR)

os.mkdir(TEMP_DIR)


def setup_testcase_and_corpus(testcase, corpus):
"""Setup testcase and corpus."""
clear_temp_dir()
copied_testcase_path = os.path.join(TEMP_DIR, testcase)
shutil.copy(os.path.join(DATA_DIR, testcase), copied_testcase_path)

copied_corpus_path = os.path.join(TEMP_DIR, corpus)
src_corpus_path = os.path.join(DATA_DIR, corpus)

if os.path.exists(src_corpus_path):
shutil.copytree(src_corpus_path, copied_corpus_path)
else:
os.mkdir(copied_corpus_path)

return copied_testcase_path, copied_corpus_path


@test_utils.integration
class IntegrationTest(unittest.TestCase):
"""Integration tests."""

def setUp(self):
self.maxDiff = None # pylint: disable=invalid-name
test_helpers.patch_environ(self)

os.environ['BUILD_DIR'] = DATA_DIR

def test_reproduce(self):
"""Tests reproducing a crash."""
testcase_path, _ = setup_testcase_and_corpus('crash', 'empty_corpus')
engine_impl = engine.HonggfuzzEngine()
target_path = engine_common.find_fuzzer_path(DATA_DIR, 'test_fuzzer')
result = engine_impl.reproduce(target_path, testcase_path,
['-timeout=60', '-rss_limit_mb=2048'], 65)
self.assertListEqual([target_path], result.command)
self.assertIn('ERROR: AddressSanitizer: heap-use-after-free', result.output)

@test_utils.slow
def test_fuzz_no_crash(self):
"""Test fuzzing (no crash)."""
_, corpus_path = setup_testcase_and_corpus('empty', 'corpus')
engine_impl = engine.HonggfuzzEngine()
target_path = engine_common.find_fuzzer_path(DATA_DIR, 'test_fuzzer')
options = engine_impl.prepare(corpus_path, target_path, DATA_DIR)
results = engine_impl.fuzz(target_path, options, TEMP_DIR, 10)
self.assertListEqual([
os.path.join(DATA_DIR, 'honggfuzz'),
'-n',
'1',
'--exit_upon_crash',
'-v',
'-z',
'-P',
'-S',
'--rlimit_rss',
'2048',
'--timeout',
'60',
'--dict',
os.path.join(DATA_DIR, 'test_fuzzer.dict'),
'-i',
os.path.join(TEMP_DIR, 'corpus'),
'-W',
TEMP_DIR,
'--run_time',
'10',
'--',
target_path,
], results.command)

def test_fuzz_crash(self):
"""Test fuzzing that results in a crash."""
_, corpus_path = setup_testcase_and_corpus('empty', 'corpus')
engine_impl = engine.HonggfuzzEngine()
target_path = engine_common.find_fuzzer_path(DATA_DIR,
'always_crash_fuzzer')
options = engine_impl.prepare(corpus_path, target_path, DATA_DIR)
results = engine_impl.fuzz(target_path, options, TEMP_DIR, 10)
self.assertListEqual([
os.path.join(DATA_DIR, 'honggfuzz'),
'-n',
'1',
'--exit_upon_crash',
'-v',
'-z',
'-P',
'-S',
'--rlimit_rss',
'2048',
'--timeout',
'60',
'-i',
os.path.join(TEMP_DIR, 'corpus'),
'-W',
TEMP_DIR,
'--run_time',
'10',
'--',
target_path,
], results.command)

self.assertIn('Seen a crash. Terminating all fuzzing threads', results.logs)
self.assertEqual(1, len(results.crashes))
crash = results.crashes[0]
self.assertEqual(TEMP_DIR, os.path.dirname(crash.input_path))
self.assertIn('ERROR: AddressSanitizer: heap-use-after-free',
crash.stacktrace)
Binary file not shown.
Binary file not shown.
Empty file.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"token"

0 comments on commit 3b61745

Please sign in to comment.