Skip to content

Commit

Permalink
Consolidate all fuzz test info (corpus, dictionaries, etc.) in one ru…
Browse files Browse the repository at this point in the history
…le. (#94)

* Consolidate all fuzz test info (corpus, dictionaries, etc.) in one rule.

The new rule supersedes the instrumented_fuzzing_binary rule and gets to incorporate all the other metadata about the fuzz test, such as corpus and dictionary paths, and the info about the fuzzing engine used. This simplifies the creation of additional rules that depend on fuzz test binaries, which can retrieve this metadata from the new CcFuzzingBinaryInfo provider.

* Removed unused symbol.

* Remove the fuzz test with the invalid dict, as it fails to build.

* Formatted bzl file.

* Add back source comments.
  • Loading branch information
stefanbucur authored Dec 16, 2020
1 parent 7ddfece commit 6ddf1e5
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 268 deletions.
3 changes: 2 additions & 1 deletion docs/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ bzl_library(
srcs = [
"//fuzzing:cc_deps.bzl",
"//fuzzing:instrum_opts.bzl",
"//fuzzing/private:binary.bzl",
"//fuzzing/private:common.bzl",
"//fuzzing/private:engine.bzl",
"//fuzzing/private:fuzz_test.bzl",
"//fuzzing/private:instrument.bzl",
"//fuzzing/private:instrum_opts.bzl",
],
deps = [
":rules_cc",
Expand Down
10 changes: 0 additions & 10 deletions examples/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,6 @@ cc_fuzz_test(
dicts = ["dict_dir/valid.dict"],
)

# This target shows a fuzz test target with an invalid dictionary.
# Building and runing empty_fuzz_test_with_invalid_dict should be ok,
# but building and running empty_fuzz_test_with_invalid_dict_run are expected to get an error message
# due to the invalid dictionary.
cc_fuzz_test(
name = "empty_fuzz_test_with_invalid_dict",
srcs = ["empty_fuzz_test.cc"],
dicts = ["dict_dir/invalid.dict"],
)

cc_fuzz_test(
name = "fuzzed_data_provider_fuzz_test",
srcs = ["fuzzed_data_provider_fuzz_test.cc"],
Expand Down
103 changes: 15 additions & 88 deletions fuzzing/instrum_opts.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,96 +19,23 @@ Each fuzzing engine or sanitizer instrumentation recognized by the
configuration flag should be defined here.
"""

def _is_string_list(value):
if type(value) != type([]):
return False
if any([type(element) != type("") for element in value]):
return False
return True

def instrumentation_opts(
copts = [],
conlyopts = [],
cxxopts = [],
linkopts = []):
"""Creates new instrumentation options.
The struct fields mirror the argument names of this function.
Args:
copts: A list of C/C++ compilation options passed as `--copt`
configuration flags.
conlyopts: A list of C-only compilation options passed as `--conlyopt`
configuration flags.
cxxopts: A list of C++-only compilation options passed as `--cxxopts`
configuration flags.
linkopts: A list of linker options to pass as `--linkopt`
configuration flags.
Returns:
A struct with the given instrumentation options.
"""
if not _is_string_list(copts):
fail("copts should be a list of strings")
if not _is_string_list(conlyopts):
fail("conlyopts should be a list of strings")
if not _is_string_list(cxxopts):
fail("cxxopts should be a list of strings")
if not _is_string_list(linkopts):
fail("linkopts should be a list of strings")
return struct(
copts = copts,
conlyopts = conlyopts,
cxxopts = cxxopts,
linkopts = linkopts,
)

# Instrumentation applied to all fuzz test executables when built in fuzzing
# mode. This mode is controlled by the `//fuzzing:cc_fuzzing_build_mode` config
# flag.
fuzzing_build_opts = instrumentation_opts(
copts = ["-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION"],
load(
"@rules_fuzzing//fuzzing/private:instrum_opts.bzl",
"instrum_defaults",
"instrum_opts",
)

# Engine-specific instrumentation.
fuzzing_engine_opts = {
"none": instrumentation_opts(),
"libfuzzer": instrumentation_opts(
copts = ["-fsanitize=fuzzer-no-link"],
),
# Reflects the set of options at
# https://github.com/google/honggfuzz/blob/master/hfuzz_cc/hfuzz-cc.c
"honggfuzz": instrumentation_opts(
copts = [
"-mllvm",
"-inline-threshold=2000",
"-fno-builtin",
"-fno-omit-frame-pointer",
"-D__NO_STRING_INLINES",
"-fsanitize-coverage=trace-pc-guard,trace-cmp,trace-div,indirect-calls",
"-fno-sanitize=fuzzer",
],
linkopts = [
"-fno-sanitize=fuzzer",
],
),
# Fuzz test binary instrumentation configurations.
instrum_configs = {
"none": instrum_opts.make(),
"libfuzzer": instrum_defaults.libfuzzer,
"honggfuzz": instrum_defaults.honggfuzz,
}

# Sanitizer-specific instrumentation.
sanitizer_opts = {
"none": instrumentation_opts(),
"asan": instrumentation_opts(
copts = ["-fsanitize=address"],
linkopts = ["-fsanitize=address"],
),
"msan": instrumentation_opts(
copts = ["-fsanitize=memory"],
linkopts = ["-fsanitize=memory"],
),
"msan-origin-tracking": instrumentation_opts(
copts = [
"-fsanitize=memory",
"-fsanitize-memory-track-origins=2",
],
linkopts = ["-fsanitize=memory"],
),
# Sanitizer configurations.
sanitizer_configs = {
"none": instrum_opts.make(),
"asan": instrum_defaults.asan,
"msan": instrum_defaults.msan,
"msan-origin-tracking": instrum_defaults.msan_origin_tracking,
}
3 changes: 2 additions & 1 deletion fuzzing/private/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ engine_test_suite(
)

exports_files([
"binary.bzl",
"common.bzl",
"engine.bzl",
"fuzz_test.bzl",
"instrument.bzl",
"instrum_opts.bzl",
])
163 changes: 163 additions & 0 deletions fuzzing/private/binary.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Copyright 2020 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
#
# https://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.

"""Defines a rule for creating an instrumented fuzzing executable."""

load("//fuzzing/private:engine.bzl", "CcFuzzingEngineInfo")
load(
"//fuzzing/private:instrum_opts.bzl",
"instrum_defaults",
"instrum_opts",
)
load(
"//fuzzing:instrum_opts.bzl",
"instrum_configs",
"sanitizer_configs",
)

CcFuzzingBinaryInfo = provider(
doc = """
Provider for storing information about a fuzz test binary.
""",
fields = {
"binary_file": "The instrumented fuzz test executable.",
"binary_runfiles": "The runfiles of the fuzz test executable.",
"corpus_dir": "The directory of the corpus files used as input seeds.",
"dictionary_file": "The dictionary file to use in fuzzing runs.",
"engine_info": "The `CcFuzzingEngineInfo` provider of the fuzzing engine used in the fuzz test.",
},
)

def _fuzzing_binary_transition_impl(settings, attr):
opts = instrum_opts.make(
copts = settings["//command_line_option:copt"],
conlyopts = settings["//command_line_option:conlyopt"],
cxxopts = settings["//command_line_option:cxxopt"],
linkopts = settings["//command_line_option:linkopt"],
)

is_fuzzing_build_mode = settings["@rules_fuzzing//fuzzing:cc_fuzzing_build_mode"]
if is_fuzzing_build_mode:
opts = instrum_opts.merge(opts, instrum_defaults.fuzzing_build)

instrum_config = settings["@rules_fuzzing//fuzzing:cc_engine_instrumentation"]
if instrum_config in instrum_configs:
opts = instrum_opts.merge(opts, instrum_configs[instrum_config])
else:
fail("unsupported engine instrumentation '%s'" % instrum_config)

sanitizer_config = settings["@rules_fuzzing//fuzzing:cc_engine_sanitizer"]
if sanitizer_config in sanitizer_configs:
opts = instrum_opts.merge(opts, sanitizer_configs[sanitizer_config])
else:
fail("unsupported sanitizer '%s'" % sanitizer_config)

return {
"//command_line_option:copt": opts.copts,
"//command_line_option:linkopt": opts.linkopts,
"//command_line_option:conlyopt": opts.conlyopts,
"//command_line_option:cxxopt": opts.cxxopts,
# Make sure binaries are built statically, to maximize the scope of the
# instrumentation.
"//command_line_option:dynamic_mode": "off",
}

fuzzing_binary_transition = transition(
implementation = _fuzzing_binary_transition_impl,
inputs = [
"@rules_fuzzing//fuzzing:cc_engine_instrumentation",
"@rules_fuzzing//fuzzing:cc_engine_sanitizer",
"@rules_fuzzing//fuzzing:cc_fuzzing_build_mode",
"//command_line_option:copt",
"//command_line_option:conlyopt",
"//command_line_option:cxxopt",
"//command_line_option:linkopt",
],
outputs = [
"//command_line_option:copt",
"//command_line_option:conlyopt",
"//command_line_option:cxxopt",
"//command_line_option:linkopt",
"//command_line_option:dynamic_mode",
],
)

def _fuzzing_binary_impl(ctx):
output_file = ctx.actions.declare_file(ctx.label.name)
ctx.actions.symlink(
output = output_file,
target_file = ctx.executable.binary,
is_executable = True,
)
binary_runfiles = ctx.attr.binary[0][DefaultInfo].default_runfiles
other_runfiles = []
if ctx.file.corpus:
other_runfiles.append(ctx.file.corpus)
if ctx.file.dictionary:
other_runfiles.append(ctx.file.dictionary)
return [
DefaultInfo(
executable = output_file,
runfiles = binary_runfiles.merge(ctx.runfiles(files = other_runfiles)),
),
CcFuzzingBinaryInfo(
binary_file = ctx.executable.binary,
binary_runfiles = binary_runfiles,
corpus_dir = ctx.file.corpus,
dictionary_file = ctx.file.dictionary,
engine_info = ctx.attr.engine[CcFuzzingEngineInfo],
),
]

fuzzing_binary = rule(
implementation = _fuzzing_binary_impl,
doc = """
Creates an instrumented fuzzing executable.
The executable runfiles include the corpus directory and the dictionary file,
if specified.
The instrumentation is controlled by the following flags:
* `@rules_fuzzing//fuzzing:cc_engine_instrumentation`
* `@rules_fuzzing//fuzzing:cc_engine_sanitizer`
* `@rules_fuzzing//fuzzing:cc_fuzzing_build_mode`
""",
attrs = {
"binary": attr.label(
executable = True,
doc = "The fuzz test executable to instrument.",
cfg = fuzzing_binary_transition,
mandatory = True,
),
"engine": attr.label(
doc = "The specification of the fuzzing engine used in the binary.",
providers = [CcFuzzingEngineInfo],
mandatory = True,
),
"corpus": attr.label(
doc = "A directory of corpus files used as input seeds.",
allow_single_file = True,
),
"dictionary": attr.label(
doc = "A dictionary file to use in fuzzing runs.",
allow_single_file = True,
),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
executable = True,
provides = [CcFuzzingBinaryInfo],
)
39 changes: 11 additions & 28 deletions fuzzing/private/common.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

"""Common building blocks for fuzz test definitions."""

load("//fuzzing/private:engine.bzl", "CcFuzzingEngineInfo")
load("//fuzzing/private:binary.bzl", "CcFuzzingBinaryInfo")

def _fuzzing_launcher_script(ctx):
engine_info = ctx.attr.engine[CcFuzzingEngineInfo]
binary_info = ctx.attr.binary[CcFuzzingBinaryInfo]
script = ctx.actions.declare_file(ctx.label.name)

script_template = """
Expand All @@ -33,31 +33,26 @@ exec "{launcher}" \
script_content = script_template.format(
environment = "\n".join([
"export %s='%s'" % (var, file.short_path)
for var, file in engine_info.environment.items()
for var, file in binary_info.engine_info.environment.items()
]),
launcher = ctx.executable._launcher.short_path,
binary_path = ctx.executable.binary.short_path,
engine_launcher = engine_info.launcher.short_path,
engine_name = engine_info.display_name,
corpus_dir = ctx.file.corpus.short_path if ctx.attr.corpus else "",
dictionary_path = ctx.file.dictionary.short_path if ctx.attr.dictionary else "",
engine_launcher = binary_info.engine_info.launcher.short_path,
engine_name = binary_info.engine_info.display_name,
corpus_dir = binary_info.corpus_dir.short_path if binary_info.corpus_dir else "",
dictionary_path = binary_info.dictionary_file.short_path if binary_info.dictionary_file else "",
)
ctx.actions.write(script, script_content, is_executable = True)
return script

def _fuzzing_launcher_impl(ctx):
script = _fuzzing_launcher_script(ctx)

engine_info = ctx.attr.engine[CcFuzzingEngineInfo]
runfiles = ctx.runfiles(files = [engine_info.launcher])
runfiles = runfiles.merge(engine_info.runfiles)
binary_info = ctx.attr.binary[CcFuzzingBinaryInfo]
runfiles = ctx.runfiles(files = [binary_info.engine_info.launcher])
runfiles = runfiles.merge(binary_info.engine_info.runfiles)
runfiles = runfiles.merge(ctx.attr._launcher[DefaultInfo].default_runfiles)
runfiles = runfiles.merge(ctx.attr.binary[DefaultInfo].default_runfiles)
if ctx.attr.corpus:
runfiles = runfiles.merge(ctx.attr.corpus[DefaultInfo].default_runfiles)
if ctx.attr.dictionary:
runfiles = runfiles.merge(ctx.attr.dictionary[DefaultInfo].default_runfiles)

return [DefaultInfo(executable = script, runfiles = runfiles)]

fuzzing_launcher = rule(
Expand All @@ -72,25 +67,13 @@ Rule for creating a script to run the fuzzing test.
executable = True,
cfg = "host",
),
"engine": attr.label(
doc = "The specification of the fuzzing engine to execute.",
providers = [CcFuzzingEngineInfo],
mandatory = True,
),
"binary": attr.label(
executable = True,
doc = "The executable of the fuzz test to run.",
providers = [CcFuzzingBinaryInfo],
cfg = "target",
mandatory = True,
),
"corpus": attr.label(
doc = "A directory of corpus files to use as input seeds.",
allow_single_file = True,
),
"dictionary": attr.label(
doc = "A dictionary file to use in fuzzing runs.",
allow_single_file = True,
),
},
executable = True,
)
Expand Down
Loading

0 comments on commit 6ddf1e5

Please sign in to comment.