blob: 3b05cd93f7c337e4a46ccf86508f2f0ddaa655c3 [file] [log] [blame]
# Copyright 2021 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.
"""Utilities and helper rules for Java fuzz tests."""
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
load("//fuzzing/private:binary.bzl", "fuzzing_binary_transition")
load(
"//fuzzing/private:instrum_opts.bzl",
"instrum_defaults",
"instrum_opts",
)
load(
"//fuzzing:instrum_opts.bzl",
"sanitizer_configs",
)
# A Starlark reimplementation of a part of Bazel's JavaCommon#determinePrimaryClass.
def determine_primary_class(srcs, name):
main_source_path = _get_java_main_source_path(srcs, name)
return _get_java_full_classname(main_source_path)
# A Starlark reimplementation of a part of Bazel's JavaCommon#determinePrimaryClass.
def _get_java_main_source_path(srcs, name):
main_source_basename = name + ".java"
for source_file in srcs:
if source_file[source_file.rfind("/") + 1:] == main_source_basename:
main_source_basename = source_file
break
return native.package_name() + "/" + main_source_basename[:-len(".java")]
# A Starlark reimplementation of Bazel's JavaUtil#getJavaFullClassname.
def _get_java_full_classname(main_source_path):
java_path = _get_java_path(main_source_path)
if java_path != None:
return java_path.replace("/", ".")
return None
# A Starlark reimplementation of Bazel's JavaUtil#getJavaPath.
def _get_java_path(main_source_path):
path_segments = main_source_path.split("/")
index = _java_segment_index(path_segments)
if index >= 0:
return "/".join(path_segments[index + 1:])
return None
_KNOWN_SOURCE_ROOTS = ["java", "javatests", "src", "testsrc"]
# A Starlark reimplementation of Bazel's JavaUtil#javaSegmentIndex.
def _java_segment_index(path_segments):
root_index = -1
for pos, segment in enumerate(path_segments):
if segment in _KNOWN_SOURCE_ROOTS:
root_index = pos
break
if root_index == -1:
return root_index
is_src = "src" == path_segments[root_index]
check_maven_index = root_index if is_src else -1
max = len(path_segments) - 1
if root_index == 0 or is_src:
for i in range(root_index + 1, max):
segment = path_segments[i]
if "src" == segment or (is_src and ("javatests" == segment or "java" == segment)):
next = path_segments[i + 1]
if ("com" == next or "org" == next or "net" == next):
root_index = i
elif "src" == segment:
check_maven_index = i
break
if check_maven_index >= 0 and check_maven_index + 2 < len(path_segments):
next = path_segments[check_maven_index + 1]
if "main" == next or "test" == next:
next = path_segments[check_maven_index + 2]
if "java" == next or "resources" == next:
root_index = check_maven_index + 2
return root_index
def _jazzer_fuzz_binary_script(ctx):
script = ctx.actions.declare_file(ctx.label.name)
script_template = """
exec "{driver}" \
--agent_path="{agent}" \
--cp="{deploy_jar}" \
--jvm_args="-Djava.library.path={library_path}" \
"$@"
"""
# Perform feature detection for
# https://github.com/bazelbuild/bazel/commit/381a519dfc082d4c62096c4ce77ead1c2e0410d8.
target_info = ctx.attr.target[0][JavaInfo]
if "transitive_native_libraries" in dir(target_info):
# The current version of Bazel contains the commit, which means that
# the JavaInfo of the target includes information about all transitive
# native library dependencies.
native_libraries_list = target_info.transitive_native_libraries.to_list()
native_paths = [
lib.dynamic_library.short_path
for lib in native_libraries_list
]
else:
# Fall back to the list of native library dependencies specified by the user.
native_files = [
native_dep[DefaultInfo].files
for native_dep in ctx.attr.transitive_native_deps
]
native_paths = [
file.short_path
for file in depset(transitive = native_files).to_list()
]
native_dirs = [path[:path.rfind("/")] for path in native_paths]
script_content = script_template.format(
driver = ctx.executable.driver.short_path,
agent = ctx.file._agent.short_path,
deploy_jar = ctx.file.target_deploy_jar.short_path,
library_path = ":".join(native_dirs),
)
ctx.actions.write(script, script_content, is_executable = True)
return script
def _is_required_runfile(target, runtime_classpath, runfile):
# The jars in the runtime classpath are all merged into the deploy jar and
# thus don't need to be included in the runfiles for the fuzzer.
if runfile in runtime_classpath:
return False
# A java_binary target has a dependency on the local JDK. Since the Jazzer
# driver launches its own JVM, these runfiles are not needed.
if runfile.owner != None and runfile.owner.workspace_name == "local_jdk":
return False
return True
def _filter_target_runfiles(ctx, target):
compilation_info = target[JavaInfo].compilation_info
runtime_classpath = compilation_info.runtime_classpath.to_list()
all_runfiles = target[DefaultInfo].default_runfiles
return ctx.runfiles([
runfile
for runfile in all_runfiles.files.to_list()
if _is_required_runfile(target, runtime_classpath, runfile)
])
def _jazzer_fuzz_binary_impl(ctx):
script = _jazzer_fuzz_binary_script(ctx)
runfiles = ctx.runfiles()
runfiles = runfiles.merge(ctx.attr.driver[DefaultInfo].default_runfiles)
runfiles = runfiles.merge(ctx.runfiles([ctx.file._agent]))
runfiles = runfiles.merge(_filter_target_runfiles(ctx, ctx.attr.target[0]))
runfiles = runfiles.merge(ctx.runfiles([ctx.file.target_deploy_jar]))
for native_dep in ctx.attr.transitive_native_deps:
runfiles = runfiles.merge(native_dep[DefaultInfo].default_runfiles)
return [DefaultInfo(executable = script, runfiles = runfiles)]
jazzer_fuzz_binary = rule(
implementation = _jazzer_fuzz_binary_impl,
doc = """
Rule that creates a binary that invokes Jazzer on the specified target.
""",
attrs = {
"_agent": attr.label(
default = Label("@jazzer//agent:jazzer_agent_deploy.jar"),
doc = "The Jazzer agent used to instrument the target.",
allow_single_file = [".jar"],
),
"driver": attr.label(
default = Label("@jazzer//driver:jazzer_driver"),
doc = "The Jazzer driver binary used to fuzz the target.",
executable = True,
# Build in target configuration rather than host because the driver
# uses transitions to set the correct C++ standard for its
# dependencies.
cfg = "target",
),
"transitive_native_deps": attr.label_list(
doc = "The native libraries the fuzz target transitively depends " +
"on. The libraries are automatically instrumented for " +
"fuzzing.",
providers = [CcInfo],
cfg = fuzzing_binary_transition,
),
"target": attr.label(
doc = "The fuzz target.",
mandatory = True,
providers = [JavaInfo],
cfg = fuzzing_binary_transition,
),
"target_deploy_jar": attr.label(
doc = "The deploy jar of the fuzz target.",
allow_single_file = [".jar"],
mandatory = True,
),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
executable = True,
)