blob: 10b4bb7fc1725dc52c0bc960a7a2261299b17251 [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("//fuzzing/private:binary.bzl", "fuzzing_binary_transition")
load("//fuzzing/private:util.bzl", "runfile_path")
# 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, target, sanitizer_flags):
script = ctx.actions.declare_file(ctx.label.name)
# The script is split into two parts: The first is emitted as-is, the second
# is a template that is passed to format(). Without the split, curly braces
# in the first part would need to be escaped.
script_literal_part = """#!/bin/bash
# LLVMFuzzerTestOneInput - OSS-Fuzz needs this string literal to appear
# somewhere in the script so it is recognized as a fuzz target.
# Bazel-provided code snippet that should be copy-pasted as is at use sites.
# Taken from @bazel_tools//tools/bash/runfiles.
# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v3 ---
# Export the env variables required for subprocesses to find their runfiles.
runfiles_export_envvars
# When the runfiles tree exists but does not contain local_jdk, this script is
# executing on OSS-Fuzz. Link the current JAVA_HOME into the runfiles tree.
if [ -d "$0.runfiles" ] && [ ! -d "$0.runfiles/local_jdk" ]; then
ln -s "$JAVA_HOME" "$0.runfiles/local_jdk"
fi
"""
script_format_part = """
source "$(rlocation {sanitizer_options})"
if [[ ! -z "{sanitizer_runtime}" ]]; then
export JAZZER_NATIVE_SANITIZERS_DIR=$(dirname "$(rlocation "{sanitizer_runtime}")")
fi
exec "$(rlocation {target})" {sanitizer_flags} "$@"
"""
script_content = script_literal_part + script_format_part.format(
target = runfile_path(ctx, target),
sanitizer_flags = " ".join(sanitizer_flags),
sanitizer_options = runfile_path(ctx, ctx.file.sanitizer_options),
sanitizer_runtime = runfile_path(ctx, ctx.file.sanitizer_runtime) if ctx.file.sanitizer_runtime else "",
)
ctx.actions.write(script, script_content, is_executable = True)
return script
def _jazzer_fuzz_binary_impl(ctx):
sanitizer = ctx.attr.sanitizer
sanitizer_flags = []
if sanitizer in ["asan", "ubsan"]:
sanitizer_flags.append("--" + sanitizer)
if not sanitizer_flags and ctx.attr.target[0][JavaInfo].transitive_native_libraries:
sanitizer_flags.append("--native")
# Used by the wrapper script created in _jazzer_fuzz_binary_script.
transitive_runfiles = [
ctx.attr.target[0][DefaultInfo].default_runfiles,
ctx.attr._bash_runfiles_library[DefaultInfo].default_runfiles,
ctx.runfiles(
[
ctx.file.sanitizer_options,
] + ([ctx.file.sanitizer_runtime] if ctx.file.sanitizer_runtime else []),
),
]
runfiles = ctx.runfiles().merge_all(transitive_runfiles)
target = ctx.attr.target[0][DefaultInfo].files_to_run.executable
script = _jazzer_fuzz_binary_script(ctx, target, sanitizer_flags)
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 = {
"sanitizer": attr.string(
values = ["asan", "ubsan", "none"],
),
"sanitizer_options": attr.label(
doc = "A shell script that can export environment variables with " +
"sanitizer options.",
allow_single_file = [".sh"],
),
"sanitizer_runtime": attr.label(
doc = "The sanitizer runtime to preload.",
allow_single_file = [".dylib", ".so"],
),
"target": attr.label(
doc = "The fuzz target.",
mandatory = True,
providers = [JavaInfo],
cfg = fuzzing_binary_transition,
),
"use_oss_fuzz": attr.bool(),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
"_bash_runfiles_library": attr.label(
default = "@bazel_tools//tools/bash/runfiles",
),
},
executable = True,
)