aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com')
-rw-r--r--src/main/java/com/code_intelligence/jazzer/BUILD.bazel137
-rw-r--r--src/main/java/com/code_intelligence/jazzer/Jazzer.java515
-rw-r--r--src/main/java/com/code_intelligence/jazzer/agent/Agent.kt172
-rw-r--r--src/main/java/com/code_intelligence/jazzer/agent/AgentInstaller.java58
-rw-r--r--src/main/java/com/code_intelligence/jazzer/agent/AgentUtils.java45
-rw-r--r--src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel43
-rw-r--r--src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt223
-rw-r--r--src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt243
-rw-r--r--src/main/java/com/code_intelligence/jazzer/android/AndroidRuntime.java67
-rw-r--r--src/main/java/com/code_intelligence/jazzer/android/BUILD.bazel95
-rw-r--r--src/main/java/com/code_intelligence/jazzer/android/DexFileManager.java68
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java411
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java32
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java30
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel51
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java111
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java211
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Consumer1.java22
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Consumer2.java22
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Consumer3.java20
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Consumer4.java20
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Consumer5.java20
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Function1.java22
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Function2.java22
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Function3.java20
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Function4.java20
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Function5.java20
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java444
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java39
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java39
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java39
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java39
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/HookType.java25
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/Jazzer.java268
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/MethodHook.java207
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java31
-rw-r--r--src/main/java/com/code_intelligence/jazzer/api/SilentCloseable.java25
-rw-r--r--src/main/java/com/code_intelligence/jazzer/autofuzz/AccessibleObjectLookup.java147
-rw-r--r--src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java116
-rw-r--r--src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java31
-rw-r--r--src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel22
-rw-r--r--src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java363
-rw-r--r--src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java809
-rw-r--r--src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java229
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel177
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/Constants.java24
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/Driver.java161
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt215
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetFinder.java137
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetHolder.java62
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java542
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/FuzzedDataProviderImpl.java257
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/OfflineInstrumentor.java179
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/Opt.java220
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/OptParser.java212
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/RecordingFuzzedDataProvider.java213
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl28
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java85
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/SignalHandler.java31
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/junit/BUILD.bazel30
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/junit/ExitCodeException.java26
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/junit/JUnitRunner.java165
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel41
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt55
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt252
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt105
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt35
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt187
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt132
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt63
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt513
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt132
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt45
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java48
-rw-r--r--src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt268
-rw-r--r--src/main/java/com/code_intelligence/jazzer/jazzer_shade_rules.jarjar6
-rw-r--r--src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java72
-rw-r--r--src/main/java/com/code_intelligence/jazzer/junit/AgentConfiguringArgumentsProvider.java43
-rw-r--r--src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel99
-rw-r--r--src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java122
-rw-r--r--src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java282
-rw-r--r--src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java170
-rw-r--r--src/main/java/com/code_intelligence/jazzer/junit/FuzzingArgumentsProvider.java42
-rw-r--r--src/main/java/com/code_intelligence/jazzer/junit/SeedArgumentsProvider.java225
-rw-r--r--src/main/java/com/code_intelligence/jazzer/junit/SeedSerializer.java144
-rw-r--r--src/main/java/com/code_intelligence/jazzer/junit/Utils.java305
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java232
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel16
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/AppliesTo.java40
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/Ascii.java28
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/BUILD.bazel5
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/DoubleInRange.java32
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/FloatInRange.java32
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java35
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/NotNull.java29
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithLength.java33
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java34
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithUtf8Length.java43
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/AnySource.java40
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/BUILD.bazel21
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/WithDefaultInstance.java43
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/BUILD.bazel9
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/ChainedMutatorFactory.java48
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/Debuggable.java46
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/Detacher.java44
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/InPlaceMutator.java74
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/MutatorFactory.java80
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/PseudoRandom.java136
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/Serializer.java116
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingInPlaceMutator.java76
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingMutator.java35
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/api/ValueMutator.java75
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel11
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java516
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/combinator/PostComposedMutator.java89
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/combinator/ProductMutator.java138
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/engine/BUILD.bazel12
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/engine/SeededPseudoRandom.java288
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/BUILD.bazel14
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java81
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/BUILD.bazel13
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java226
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java243
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java28
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorFactory.java167
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorFactory.java205
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BUILD.bazel16
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BooleanMutatorFactory.java79
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ByteArrayMutatorFactory.java227
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/EnumMutatorFactory.java47
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/FloatingPointMutatorFactory.java597
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java399
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java30
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/NullableMutatorFactory.java126
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java172
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/BUILD.bazel15
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/LibFuzzerMutator.java92
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BUILD.bazel16
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderAdapters.java188
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java451
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ByteStringMutatorFactory.java41
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/MessageMutatorFactory.java52
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java34
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/TypeLibrary.java179
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel11
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/support/ExceptionSupport.java31
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/support/InputStreamSupport.java256
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterHolder.java63
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/support/Preconditions.java55
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/support/RandomSupport.java64
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/support/StreamSupport.java72
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/support/TypeHolder.java43
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java564
-rw-r--r--src/main/java/com/code_intelligence/jazzer/mutation/support/WeakIdentityHashMap.java168
-rw-r--r--src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel16
-rw-r--r--src/main/java/com/code_intelligence/jazzer/replay/Replayer.java144
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel180
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/Constants.java21
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java169
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/FuzzTargetRunnerNatives.java41
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java82
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java50
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/Mutator.java34
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java39
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java598
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java125
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java47
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java35
-rw-r--r--src/main/java/com/code_intelligence/jazzer/runtime/bootstrap_shade_rules4
-rwxr-xr-xsrc/main/java/com/code_intelligence/jazzer/runtime/verify_shading.sh27
-rw-r--r--src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel70
-rw-r--r--src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt66
-rw-r--r--src/main/java/com/code_intelligence/jazzer/utils/Log.java104
-rw-r--r--src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt50
-rw-r--r--src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt71
-rw-r--r--src/main/java/com/code_intelligence/jazzer/utils/UnsafeProvider.java56
-rw-r--r--src/main/java/com/code_intelligence/jazzer/utils/UnsafeUtils.java78
-rw-r--r--src/main/java/com/code_intelligence/jazzer/utils/Utils.kt45
-rw-r--r--src/main/java/com/code_intelligence/jazzer/utils/ZipUtils.java126
179 files changed, 21542 insertions, 0 deletions
diff --git a/src/main/java/com/code_intelligence/jazzer/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/BUILD.bazel
new file mode 100644
index 00000000..aed6769d
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/BUILD.bazel
@@ -0,0 +1,137 @@
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
+load("@com_github_johnynek_bazel_jar_jar//:jar_jar.bzl", "jar_jar")
+load("@rules_jvm_external//:defs.bzl", "javadoc")
+load("//:maven.bzl", "JAZZER_VERSION")
+load("//bazel:jar.bzl", "strip_jar")
+load("//sanitizers:sanitizers.bzl", "SANITIZER_CLASSES")
+
+java_binary(
+ name = "jazzer_standalone",
+ main_class = "com.code_intelligence.jazzer.Jazzer",
+ visibility = [
+ "//:__pkg__",
+ "//launcher:__pkg__",
+ ],
+ runtime_deps = [
+ ":jazzer_import",
+ "//deploy:jazzer-api",
+ ],
+)
+
+strip_jar(
+ name = "jazzer",
+ out = "jazzer.jar",
+ jar = ":jazzer_shaded",
+ paths_to_keep = [
+ "com/code_intelligence/jazzer/**",
+ "jaz/**",
+ "META-INF/MANIFEST.MF",
+ "win32-x86/**",
+ "win32-x86-64/**",
+ ],
+ visibility = ["//visibility:public"],
+)
+
+java_library(
+ name = "constants",
+ srcs = [":constants_java"],
+ visibility = ["//visibility:public"],
+)
+
+java_import(
+ name = "jazzer_import",
+ jars = [":jazzer"],
+ visibility = ["//:__subpackages__"],
+ deps = ["//deploy:jazzer-api"],
+)
+
+jar_jar(
+ name = "jazzer_shaded",
+ input_jar = "jazzer_unshaded_deploy.jar",
+ rules = "jazzer_shade_rules.jarjar",
+)
+
+java_binary(
+ name = "jazzer_unshaded",
+ # Note: We can't add
+ # //src/main/java/com/code_intelligence/jazzer/runtime:java_bootstrap_unshaded itself as
+ # that would also strip out external dependencies common between Jazzer and its bootstrap jar,
+ # such as e.g. RulesJni, which should be shaded into distinct classes.
+ deploy_env = [
+ "//src/main/java/com/code_intelligence/jazzer/api:api_deploy_env",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap_env",
+ ],
+ main_class = "com.code_intelligence.jazzer.Jazzer",
+ runtime_deps = [":jazzer_lib"],
+)
+
+# Docs are only generated for the com.code_intelligence.jazzer package. Everything else is not
+# considered a public interface.
+javadoc(
+ name = "jazzer-docs",
+ javadocopts = select({
+ "//deploy:emit_linked_javadoc": [
+ "-link",
+ "https://docs.oracle.com/en/java/javase/17/docs/api/",
+ "-link",
+ "https://codeintelligencetesting.github.io/jazzer-docs/jazzer-api/",
+ ],
+ "//conditions:default": [],
+ }),
+ visibility = ["//deploy:__pkg__"],
+ deps = [":jazzer_lib"],
+)
+
+strip_jar(
+ name = "jazzer-sources",
+ jar = ":jazzer_transitive_sources_deploy-src.jar",
+ paths_to_keep = [
+ "com/code_intelligence/jazzer/**",
+ "jaz/**",
+ "META-INF/MANIFEST.MF",
+ ],
+ visibility = ["//deploy:__pkg__"],
+)
+
+# The _deploy-src.jar for this target includes the sources for the jazzer_bootstrap library.
+java_binary(
+ name = "jazzer_transitive_sources",
+ main_class = "com.code_intelligence.jazzer.Jazzer",
+ runtime_deps = [
+ ":jazzer_lib",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap_lib",
+ ],
+)
+
+java_library(
+ name = "jazzer_lib",
+ srcs = ["Jazzer.java"],
+ visibility = ["//deploy:__pkg__"],
+ runtime_deps = select({
+ "@platforms//os:windows": [],
+ "//conditions:default": ["//src/main/native/com/code_intelligence/jazzer:jazzer_preload"],
+ }) + [
+ # Only used by JUnit, but including it here means we don't need to shade ASM in
+ # jazzer-junit.
+ "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_utils",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/android:android_runtime",
+ "//src/main/java/com/code_intelligence/jazzer/driver",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:constants",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ "//src/main/java/com/code_intelligence/jazzer/utils:zip_utils",
+ "@fmeum_rules_jni//jni/tools/native_loader",
+ ],
+)
+
+write_file(
+ name = "constants_java",
+ out = "Constants.java",
+ content = [
+ "package com.code_intelligence.jazzer;",
+ "public final class Constants {",
+ " public static final String JAZZER_VERSION = \"%s\";" % JAZZER_VERSION,
+ "}",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/Jazzer.java b/src/main/java/com/code_intelligence/jazzer/Jazzer.java
new file mode 100644
index 00000000..3eb316dd
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/Jazzer.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright 2022 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer;
+
+import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID;
+import static java.lang.System.exit;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import com.code_intelligence.jazzer.android.AndroidRuntime;
+import com.code_intelligence.jazzer.driver.Driver;
+import com.code_intelligence.jazzer.utils.Log;
+import com.code_intelligence.jazzer.utils.ZipUtils;
+import com.github.fmeum.rules_jni.RulesJni;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.management.ManagementFactory;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * The libFuzzer-compatible CLI entrypoint for Jazzer.
+ *
+ * <p>Arguments to Jazzer are passed as command-line arguments or {@code jazzer.*} system
+ * properties. For example, setting the property {@code jazzer.target_class} to
+ * {@code com.example.FuzzTest} is equivalent to passing the argument
+ * {@code --target_class=com.example.FuzzTest}.
+ *
+ * <p>Arguments to libFuzzer are passed as command-line arguments.
+ */
+public class Jazzer {
+ public static void main(String[] args) throws IOException, InterruptedException {
+ start(Arrays.stream(args).collect(toList()));
+ }
+
+ // Accessed by jazzer_main.cpp.
+ @SuppressWarnings("unused")
+ private static void main(byte[][] nativeArgs) throws IOException, InterruptedException {
+ start(Arrays.stream(nativeArgs)
+ .map(bytes -> new String(bytes, StandardCharsets.UTF_8))
+ .collect(toList()));
+ }
+
+ private static void start(List<String> args) throws IOException, InterruptedException {
+ // Lock in the output PrintStreams so that Jazzer can still emit output even if the fuzz target
+ // itself is "silenced" by redirecting System.out and/or System.err.
+ Log.fixOutErr(System.out, System.err);
+
+ parseJazzerArgsToProperties(args);
+
+ // --asan and --ubsan imply --native by default, but --native can also be used by itself to fuzz
+ // native libraries without sanitizers (e.g. to quickly grow a corpus).
+ final boolean loadASan = Boolean.parseBoolean(System.getProperty("jazzer.asan", "false"));
+ final boolean loadUBSan = Boolean.parseBoolean(System.getProperty("jazzer.ubsan", "false"));
+ final boolean loadHWASan = Boolean.parseBoolean(System.getProperty("jazzer.hwasan", "false"));
+ final boolean fuzzNative = Boolean.parseBoolean(
+ System.getProperty("jazzer.native", Boolean.toString(loadASan || loadUBSan || loadHWASan)));
+ if ((loadASan || loadUBSan || loadHWASan) && !fuzzNative) {
+ Log.error("--asan, --hwasan and --ubsan cannot be used without --native");
+ exit(1);
+ }
+ // No native fuzzing has been requested, fuzz in the current process.
+ if (!fuzzNative) {
+ if (IS_ANDROID) {
+ final String initOptions = getAndroidRuntimeOptions();
+ AndroidRuntime.initialize(initOptions);
+ }
+ // We only create a wrapper script if libFuzzer runs in a mode that creates subprocesses.
+ // In LibFuzzer's fork mode, the subprocesses created continuously by the main libFuzzer
+ // process do not create further subprocesses. Creating a wrapper script for each subprocess
+ // is an unnecessary overhead.
+ final boolean spawnsSubprocesses = args.stream().anyMatch(arg
+ -> (arg.startsWith("-fork=") && !arg.equals("-fork=0"))
+ || (arg.startsWith("-jobs=") && !arg.equals("-jobs=0"))
+ || (arg.startsWith("-merge=") && !arg.equals("-merge=0")));
+ // argv0 is printed by libFuzzer during reproduction, so have it contain "jazzer".
+ String arg0 = spawnsSubprocesses ? prepareArgv0(new HashMap<>()) : "jazzer";
+ args = Stream.concat(Stream.of(arg0), args.stream()).collect(toList());
+ exit(Driver.start(args, spawnsSubprocesses));
+ }
+
+ if (!isLinux() && !isMacOs()) {
+ Log.error("--asan, --ubsan, and --native are only supported on Linux and macOS");
+ exit(1);
+ }
+
+ // Run ourselves as a subprocess with `jazzer_preload` and (optionally) native sanitizers
+ // preloaded. By inheriting IO, this wrapping should become invisible for the user.
+ Set<String> argsToFilter =
+ Stream.of("--asan", "--ubsan", "--hwasan", "--native").collect(toSet());
+ ProcessBuilder processBuilder = new ProcessBuilder();
+ List<Path> preloadLibs = new ArrayList<>();
+ // We have to load jazzer_preload before we load ASan since the ASan includes no-op definitions
+ // of the fuzzer callbacks as weak symbols, but the dynamic linker doesn't distinguish between
+ // strong and weak symbols.
+ preloadLibs.add(RulesJni.extractLibrary("jazzer_preload", Jazzer.class));
+ if (loadASan) {
+ processBuilder.environment().compute("ASAN_OPTIONS",
+ (name, currentValue)
+ -> appendWithPathListSeparator(name,
+ // The JVM produces an extremely large number of false positive leaks, which makes
+ // it impossible to use LeakSanitizer.
+ // TODO: Investigate whether we can hook malloc/free only for JNI shared
+ // libraries, not the JVM itself.
+ "detect_leaks=0",
+ // We load jazzer_preload first.
+ "verify_asan_link_order=0"));
+ Log.warn("Jazzer is not compatible with LeakSanitizer. Leaks are not reported.");
+ preloadLibs.add(findLibrary(asanLibNames()));
+ }
+ if (loadHWASan) {
+ processBuilder.environment().compute("HWASAN_OPTIONS",
+ (name, currentValue)
+ -> appendWithPathListSeparator(name,
+ // The JVM produces an extremely large number of false positive leaks, which makes
+ // it impossible to use LeakSanitizer.
+ // TODO: Investigate whether we can hook malloc/free only for JNI shared
+ // libraries, not the JVM itself.
+ "detect_leaks=0",
+ // We load jazzer_preload first.
+ "verify_asan_link_order=0"));
+ Log.warn("Jazzer is not compatible with LeakSanitizer. Leaks are not reported.");
+ preloadLibs.add(findLibrary(hwasanLibNames()));
+ }
+ if (loadUBSan) {
+ preloadLibs.add(findLibrary(ubsanLibNames()));
+ }
+ // The launcher script we generate is executed by /bin/sh on macOS, which is codesigned without
+ // the allow-dyld-environment-variables entitlement. The dynamic linker would thus remove all
+ // DYLD_* variables. Instead, we pass these variables directly to the java executable by
+ // emitting them into the wrapper. The java binary has both the allow-dyld-environment-variables
+ // and the disable-library-validation entitlement, which allows any codesigned library to be
+ // preloaded.
+ processBuilder.environment().remove(preloadVariable());
+ Map<String, String> additionalEnvironment = new HashMap<>();
+ additionalEnvironment.put(preloadVariable(),
+ appendWithPathListSeparator(
+ preloadVariable(), preloadLibs.stream().map(Path::toString).toArray(String[] ::new)));
+ List<String> subProcessArgs =
+ Stream
+ .concat(Stream.of(prepareArgv0(additionalEnvironment)),
+ // Prevent a "fork bomb" by stripping all args that trigger this code path.
+ args.stream().filter(arg -> !argsToFilter.contains(arg.split("=")[0])))
+ .collect(toList());
+ processBuilder.command(subProcessArgs);
+ processBuilder.inheritIO();
+
+ exit(processBuilder.start().waitFor());
+ }
+
+ private static void parseJazzerArgsToProperties(List<String> args) {
+ args.stream()
+ .filter(arg -> arg.startsWith("--"))
+ .map(arg -> arg.substring("--".length()))
+ // Filter out "--", which can be used to declare that all further arguments aren't libFuzzer
+ // arguments.
+ .filter(arg -> !arg.isEmpty())
+ .map(Jazzer::parseSingleArg)
+ .forEach(e -> System.setProperty("jazzer." + e.getKey(), e.getValue()));
+ }
+
+ private static SimpleEntry<String, String> parseSingleArg(String arg) {
+ String[] nameAndValue = arg.split("=", 2);
+ if (nameAndValue.length == 2) {
+ // Example: --keep_going=10 --> (keep_going, 10)
+ return new SimpleEntry<>(nameAndValue[0], nameAndValue[1]);
+ } else if (nameAndValue[0].startsWith("no")) {
+ // Example: --nohooks --> (hooks, "false")
+ return new SimpleEntry<>(nameAndValue[0].substring("no".length()), "false");
+ } else {
+ // Example: --dedup --> (dedup, "true")
+ return new SimpleEntry<>(nameAndValue[0], "true");
+ }
+ }
+
+ // Create a wrapper script that faithfully recreates the current JVM. By using this script as
+ // libFuzzer's argv[0], libFuzzer modes that rely on subprocesses can work with the Java driver.
+ // This trick is also used to allow native sanitizers to be preloaded.
+ private static String prepareArgv0(Map<String, String> additionalEnvironment) throws IOException {
+ if (!isPosixOrAndroid() && !additionalEnvironment.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Setting environment variables in the wrapper is only supported on POSIX systems and Android");
+ }
+ char shellQuote = isPosixOrAndroid() ? '\'' : '"';
+ String launcherTemplate;
+ if (IS_ANDROID) {
+ launcherTemplate = "#!/system/bin/env sh\n%s LD_LIBRARY_PATH=%s \n%s $@\n";
+ } else if (isPosix()) {
+ launcherTemplate = "#!/usr/bin/env sh\n%s $@\n";
+ } else {
+ launcherTemplate = "@echo off\r\n%s %%*\r\n";
+ }
+
+ String launcherExtension = isPosix() ? ".sh" : ".bat";
+ FileAttribute<?>[] launcherScriptAttributes = isPosixOrAndroid()
+ ? new FileAttribute[] {PosixFilePermissions.asFileAttribute(
+ PosixFilePermissions.fromString("rwx------"))}
+ : new FileAttribute[] {};
+ String env = additionalEnvironment.entrySet()
+ .stream()
+ .map(e -> e.getKey() + "='" + e.getValue() + "'")
+ .collect(joining(" "));
+ String command =
+ Stream
+ .concat(Stream.of(IS_ANDROID ? "exec" : javaBinary().toString()), javaBinaryArgs())
+ // Escape individual arguments for the shell.
+ .map(str -> shellQuote + str + shellQuote)
+ .collect(joining(" "));
+
+ String invocation = env.isEmpty() ? command : env + " " + command;
+
+ // argv0 is printed by libFuzzer during reproduction, so have the launcher basename contain
+ // "jazzer".
+ Path launcher;
+ String launcherContent;
+ if (IS_ANDROID) {
+ String exportCommand = AndroidRuntime.getClassPathsCommand();
+ String ldLibraryPath = AndroidRuntime.getLdLibraryPath();
+ launcherContent = String.format(launcherTemplate, exportCommand, ldLibraryPath, invocation);
+ launcher = Files.createTempFile(
+ Paths.get("/data/local/tmp/"), "jazzer-", launcherExtension, launcherScriptAttributes);
+ } else {
+ launcherContent = String.format(launcherTemplate, invocation);
+ launcher = Files.createTempFile("jazzer-", launcherExtension, launcherScriptAttributes);
+ }
+
+ launcher.toFile().deleteOnExit();
+ Files.write(launcher, launcherContent.getBytes(StandardCharsets.UTF_8));
+ return launcher.toAbsolutePath().toString();
+ }
+
+ private static Path javaBinary() {
+ String javaBinaryName;
+ if (isPosix()) {
+ javaBinaryName = "java";
+ } else {
+ javaBinaryName = "java.exe";
+ }
+
+ return Paths.get(System.getProperty("java.home"), "bin", javaBinaryName);
+ }
+
+ private static Stream<String> javaBinaryArgs() throws IOException {
+ if (IS_ANDROID) {
+ // Add Android specific args
+ Path agentPath =
+ RulesJni.extractLibrary("android_native_agent", "/com/code_intelligence/jazzer/android");
+
+ String jazzerAgentPath = System.getProperty("jazzer.agent_path");
+ String bootclassClassOverrides =
+ System.getProperty("jazzer.android_bootpath_classes_overrides");
+
+ String jazzerBootstrapJarPath =
+ "com/code_intelligence/jazzer/android/jazzer_bootstrap_android.jar";
+ String jazzerBootstrapJarOut = "/data/local/tmp/jazzer_bootstrap_android.jar";
+
+ try {
+ ZipUtils.extractFile(jazzerAgentPath, jazzerBootstrapJarPath, jazzerBootstrapJarOut);
+ } catch (IOException ioe) {
+ Log.error(
+ "Could not extract jazzer_bootstrap_android.jar from Jazzer standalone agent", ioe);
+ exit(1);
+ }
+
+ String nativeAgentOptions = "injectJars=" + jazzerBootstrapJarOut;
+ if (bootclassClassOverrides != null && !bootclassClassOverrides.isEmpty()) {
+ nativeAgentOptions += ",bootstrapClassOverrides=" + bootclassClassOverrides;
+ }
+
+ // ManagementFactory wont work with Android
+ Stream<String> stream = Stream.of("app_process", "-Djdk.attach.allowAttachSelf=true",
+ "-Xplugin:libopenjdkjvmti.so",
+ "-agentpath:" + agentPath.toString() + "=" + nativeAgentOptions, "-Xcompiler-option",
+ "--debuggable", "/system/bin", Jazzer.class.getName());
+
+ return stream;
+ }
+
+ Stream<String> stream = Stream.of("-cp", System.getProperty("java.class.path"),
+ // Make ByteBuddyAgent's job simpler by allowing it to attach directly to the JVM
+ // rather than relying on an external helper. The latter fails on macOS 12 with JDK 11+
+ // (but not 8) and UBSan preloaded with:
+ // Caused by: java.io.IOException: Cannot run program
+ // "/Users/runner/hostedtoolcache/Java_Zulu_jdk/17.0.4-8/x64/bin/java": error=0, Failed
+ // to exec spawn helper: pid: 8227, signal: 9
+ // Presumably, this issue is caused by codesigning and the exec helper missing the
+ // entitlements required for library insertion.
+ "-Djdk.attach.allowAttachSelf=true", Jazzer.class.getName());
+
+ return Stream.concat(ManagementFactory.getRuntimeMXBean().getInputArguments().stream(), stream);
+ }
+
+ /**
+ * Append the given elements to the value of the environment variable {@code name} that contains a
+ * list of paths separated by the system path list separator.
+ */
+ private static String appendWithPathListSeparator(String name, String... options) {
+ if (options.length == 0) {
+ throw new IllegalArgumentException("options must not be empty");
+ }
+
+ String currentValue = Optional.ofNullable(System.getenv(name)).orElse("");
+ String additionalOptions = String.join(File.pathSeparator, options);
+ if (currentValue.isEmpty()) {
+ return additionalOptions;
+ }
+ return currentValue + File.pathSeparator + additionalOptions;
+ }
+
+ private static Path findLibrary(List<String> candidateNames) {
+ if (!IS_ANDROID) {
+ return findHostClangLibrary(candidateNames);
+ }
+
+ for (String candidateName : candidateNames) {
+ String candidateFullPath = "/apex/com.android.runtime/lib64/bionic/" + candidateName;
+ File f = new File(candidateFullPath);
+ if (f.exists()) {
+ return Paths.get(candidateFullPath);
+ }
+ }
+
+ Log.error(
+ String.format("Failed to find one of %s%n for Android", String.join(", ", candidateNames)));
+ Log.error("If fuzzing hwasan, make sure you have a hwasan build flashed to your device");
+
+ exit(1);
+ throw new IllegalStateException("not reached");
+ }
+
+ private static Path findHostClangLibrary(List<String> candidateNames) {
+ for (String name : candidateNames) {
+ Optional<Path> path = tryFindLibraryInJazzerNativeSanitizersDir(name);
+ if (path.isPresent()) {
+ return path.get();
+ }
+ }
+ for (String name : candidateNames) {
+ Optional<Path> path = tryFindLibraryUsingClang(name);
+ if (path.isPresent()) {
+ return path.get();
+ }
+ }
+ Log.error("Failed to find one of: " + String.join(", ", candidateNames));
+ exit(1);
+ throw new IllegalStateException("not reached");
+ }
+
+ private static Optional<Path> tryFindLibraryInJazzerNativeSanitizersDir(String name) {
+ String nativeSanitizersDir = System.getenv("JAZZER_NATIVE_SANITIZERS_DIR");
+ if (nativeSanitizersDir == null) {
+ return Optional.empty();
+ }
+ Path candidatePath = Paths.get(nativeSanitizersDir, name);
+ if (Files.exists(candidatePath)) {
+ return Optional.of(candidatePath);
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Given a library name such as "libclang_rt.asan-x86_64.so", get the full path to the library
+ * installed on the host from clang (or CC, if set). Returns Optional.empty() if clang does not
+ * find the library and exits with a message in case of any other error condition.
+ */
+ private static Optional<Path> tryFindLibraryUsingClang(String name) {
+ List<String> command = asList(hostClang(), "--print-file-name", name);
+ ProcessBuilder processBuilder = new ProcessBuilder(command);
+ byte[] output;
+ try {
+ Process process = processBuilder.start();
+ if (process.waitFor() != 0) {
+ Log.error(String.format(
+ "'%s' exited with exit code %d", String.join(" ", command), process.exitValue()));
+ copy(process.getInputStream(), System.out);
+ copy(process.getErrorStream(), System.err);
+ exit(1);
+ }
+ output = readAllBytes(process.getInputStream());
+ } catch (IOException | InterruptedException e) {
+ Log.error(String.format("Failed to run '%s'", String.join(" ", command)), e);
+ exit(1);
+ throw new IllegalStateException("not reached");
+ }
+ Path library = Paths.get(new String(output).trim());
+ if (Files.exists(library)) {
+ return Optional.of(library);
+ }
+ return Optional.empty();
+ }
+
+ private static String hostClang() {
+ return Optional.ofNullable(System.getenv("CC")).orElse("clang");
+ }
+
+ private static List<String> hwasanLibNames() {
+ if (!IS_ANDROID) {
+ Log.error("HWAsan is only supported for Android. Please try --asan");
+ exit(1);
+ }
+
+ return singletonList("libclang_rt.hwasan-aarch64-android.so");
+ }
+
+ private static List<String> asanLibNames() {
+ if (isLinux()) {
+ if (IS_ANDROID) {
+ Log.error("ASan is not supported for Android at this time. Use --hwasan for Address "
+ + "Sanitization on Android");
+ exit(1);
+ }
+
+ // Since LLVM 15 sanitizer runtimes no longer have the architecture in the filename.
+ return asList("libclang_rt.asan.so", "libclang_rt.asan-x86_64.so");
+ } else {
+ return singletonList("libclang_rt.asan_osx_dynamic.dylib");
+ }
+ }
+
+ private static List<String> ubsanLibNames() {
+ if (isLinux()) {
+ if (IS_ANDROID) {
+ // return asList("libclang_rt.ubsan_standalone-aarch64-android.so");
+ Log.error("ERROR: UBSan is not supported for Android at this time.");
+ exit(1);
+ }
+
+ return asList("libclang_rt.ubsan_standalone.so", "libclang_rt.ubsan_standalone-x86_64.so");
+ } else {
+ return singletonList("libclang_rt.ubsan_osx_dynamic.dylib");
+ }
+ }
+
+ private static String preloadVariable() {
+ return isLinux() ? "LD_PRELOAD" : "DYLD_INSERT_LIBRARIES";
+ }
+
+ private static boolean isLinux() {
+ return System.getProperty("os.name").startsWith("Linux");
+ }
+
+ private static boolean isMacOs() {
+ return System.getProperty("os.name").startsWith("Mac OS X");
+ }
+
+ private static boolean isPosix() {
+ return !IS_ANDROID && FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
+ }
+
+ private static String getAndroidRuntimeOptions() {
+ List<String> validInitOptions = Arrays.asList("use_platform_libs", "use_none", "");
+ String initOptString = System.getProperty("jazzer.android_init_options");
+ if (!validInitOptions.contains(initOptString)) {
+ Log.error("Invalid android_init_options set for Android Runtime.");
+ exit(1);
+ }
+ return initOptString;
+ }
+
+ private static boolean isPosixOrAndroid() {
+ if (isPosix()) {
+ return true;
+ }
+ return IS_ANDROID;
+ }
+
+ private static byte[] readAllBytes(InputStream in) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ copy(in, out);
+ return out.toByteArray();
+ }
+
+ private static void copy(InputStream source, OutputStream target) throws IOException {
+ byte[] buffer = new byte[64 * 104 * 1024];
+ int read;
+ while ((read = source.read(buffer)) != -1) {
+ target.write(buffer, 0, read);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt b/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt
new file mode 100644
index 00000000..9bcd744f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt
@@ -0,0 +1,172 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+@file:JvmName("Agent")
+
+package com.code_intelligence.jazzer.agent
+
+import com.code_intelligence.jazzer.driver.Opt
+import com.code_intelligence.jazzer.instrumentor.CoverageRecorder
+import com.code_intelligence.jazzer.instrumentor.Hooks
+import com.code_intelligence.jazzer.instrumentor.InstrumentationType
+import com.code_intelligence.jazzer.sanitizers.Constants
+import com.code_intelligence.jazzer.utils.ClassNameGlobber
+import com.code_intelligence.jazzer.utils.Log
+import com.code_intelligence.jazzer.utils.ManifestUtils
+import java.lang.instrument.Instrumentation
+import java.nio.file.Paths
+import kotlin.io.path.exists
+import kotlin.io.path.isDirectory
+
+fun install(instrumentation: Instrumentation) {
+ installInternal(instrumentation)
+}
+
+fun installInternal(
+ instrumentation: Instrumentation,
+ userHookNames: List<String> = findManifestCustomHookNames() + Opt.customHooks,
+ disabledHookNames: List<String> = Opt.disabledHooks,
+ instrumentationIncludes: List<String> = Opt.instrumentationIncludes.get(),
+ instrumentationExcludes: List<String> = Opt.instrumentationExcludes.get(),
+ customHookIncludes: List<String> = Opt.customHookIncludes.get(),
+ customHookExcludes: List<String> = Opt.customHookExcludes.get(),
+ trace: List<String> = Opt.trace,
+ idSyncFile: String? = Opt.idSyncFile,
+ dumpClassesDir: String = Opt.dumpClassesDir,
+ additionalClassesExcludes: List<String> = Opt.additionalClassesExcludes,
+) {
+ val allCustomHookNames = (Constants.SANITIZER_HOOK_NAMES + userHookNames).toSet()
+ check(allCustomHookNames.isNotEmpty()) { "No hooks registered; expected at least the built-in hooks" }
+ val customHookNames = allCustomHookNames - disabledHookNames.toSet()
+ val disabledCustomHooksToPrint = allCustomHookNames - customHookNames.toSet()
+ if (disabledCustomHooksToPrint.isNotEmpty()) {
+ Log.info("Not using the following disabled hooks: ${disabledCustomHooksToPrint.joinToString(", ")}")
+ }
+
+ val classNameGlobber = ClassNameGlobber(instrumentationIncludes, instrumentationExcludes + customHookNames)
+ CoverageRecorder.classNameGlobber = classNameGlobber
+ val customHookClassNameGlobber = ClassNameGlobber(customHookIncludes, customHookExcludes + customHookNames)
+ // FIXME: Setting trace to the empty string explicitly results in all rather than no trace types
+ // being applied - this is unintuitive.
+ val instrumentationTypes = (trace.takeIf { it.isNotEmpty() } ?: listOf("all")).flatMap {
+ when (it) {
+ "cmp" -> setOf(InstrumentationType.CMP)
+ "cov" -> setOf(InstrumentationType.COV)
+ "div" -> setOf(InstrumentationType.DIV)
+ "gep" -> setOf(InstrumentationType.GEP)
+ "indir" -> setOf(InstrumentationType.INDIR)
+ "native" -> setOf(InstrumentationType.NATIVE)
+ // Disable GEP instrumentation by default as it appears to negatively affect fuzzing
+ // performance. Our current GEP instrumentation only reports constant indices, but even
+ // when we instead reported non-constant indices, they tended to completely fill up the
+ // table of recent compares and value profile map.
+ "all" -> InstrumentationType.values().toSet() - InstrumentationType.GEP
+ else -> {
+ println("WARN: Skipping unknown instrumentation type $it")
+ emptySet()
+ }
+ }
+ }.toSet()
+
+ val idSyncFilePath = idSyncFile?.takeUnless { it.isEmpty() }?.let {
+ Paths.get(it).also { path ->
+ Log.info("Synchronizing coverage IDs in ${path.toAbsolutePath()}")
+ }
+ }
+ val dumpClassesDirPath = dumpClassesDir.takeUnless { it.isEmpty() }?.let {
+ Paths.get(it).toAbsolutePath().also { path ->
+ if (path.exists() && path.isDirectory()) {
+ Log.info("Dumping instrumented classes into $path")
+ } else {
+ Log.error("Cannot dump instrumented classes into $path; does not exist or not a directory")
+ }
+ }
+ }
+ val includedHookNames = instrumentationTypes
+ .mapNotNull { type ->
+ when (type) {
+ InstrumentationType.CMP -> "com.code_intelligence.jazzer.runtime.TraceCmpHooks"
+ InstrumentationType.DIV -> "com.code_intelligence.jazzer.runtime.TraceDivHooks"
+ InstrumentationType.INDIR -> "com.code_intelligence.jazzer.runtime.TraceIndirHooks"
+ InstrumentationType.NATIVE -> "com.code_intelligence.jazzer.runtime.NativeLibHooks"
+ else -> null
+ }
+ }
+ val coverageIdSynchronizer = if (idSyncFilePath != null) {
+ FileSyncCoverageIdStrategy(idSyncFilePath)
+ } else {
+ MemSyncCoverageIdStrategy()
+ }
+
+ // If we don't append the JARs containing the custom hooks to the bootstrap class loader,
+ // third-party hooks not contained in the agent JAR will not be able to instrument Java standard
+ // library classes. These classes are loaded by the bootstrap / system class loader and would
+ // not be considered when resolving references to hook methods, leading to NoClassDefFoundError
+ // being thrown.
+ Hooks.appendHooksToBootstrapClassLoaderSearch(instrumentation, customHookNames.toSet())
+ val (includedHooks, customHooks) = Hooks.loadHooks(additionalClassesExcludes, includedHookNames.toSet(), customHookNames.toSet())
+
+ val runtimeInstrumentor = RuntimeInstrumentor(
+ instrumentation,
+ classNameGlobber,
+ customHookClassNameGlobber,
+ instrumentationTypes,
+ includedHooks.hooks,
+ customHooks.hooks,
+ customHooks.additionalHookClassNameGlobber,
+ coverageIdSynchronizer,
+ dumpClassesDirPath,
+ )
+
+ // These classes are e.g. dependencies of the RuntimeInstrumentor or hooks and thus were loaded
+ // before the instrumentor was ready. Since we haven't enabled it yet, they can safely be
+ // "retransformed": They haven't been transformed yet.
+ val classesToRetransform = instrumentation.allLoadedClasses
+ .filter {
+ classNameGlobber.includes(it.name) ||
+ customHookClassNameGlobber.includes(it.name) ||
+ customHooks.additionalHookClassNameGlobber.includes(it.name)
+ }
+ .filter {
+ instrumentation.isModifiableClass(it)
+ }
+ .toTypedArray()
+
+ instrumentation.addTransformer(runtimeInstrumentor, true)
+
+ if (classesToRetransform.isNotEmpty()) {
+ if (instrumentation.isRetransformClassesSupported) {
+ retransformClassesWithRetry(instrumentation, classesToRetransform)
+ }
+ }
+}
+
+private fun retransformClassesWithRetry(instrumentation: Instrumentation, classesToRetransform: Array<Class<*>>) {
+ try {
+ instrumentation.retransformClasses(*classesToRetransform)
+ } catch (e: Throwable) {
+ if (classesToRetransform.size == 1) {
+ Log.warn("Error retransforming class ${classesToRetransform[0].name }", e)
+ } else {
+ // The docs state that no transformation was performed if an exception is thrown.
+ // Try again in a binary search fashion, until the not transformable classes have been isolated and reported.
+ retransformClassesWithRetry(instrumentation, classesToRetransform.copyOfRange(0, classesToRetransform.size / 2))
+ retransformClassesWithRetry(instrumentation, classesToRetransform.copyOfRange(classesToRetransform.size / 2, classesToRetransform.size))
+ }
+ }
+}
+
+private fun findManifestCustomHookNames() = ManifestUtils.combineManifestValues(ManifestUtils.HOOK_CLASSES)
+ .flatMap { it.split(':') }
+ .filter { it.isNotBlank() }
diff --git a/src/main/java/com/code_intelligence/jazzer/agent/AgentInstaller.java b/src/main/java/com/code_intelligence/jazzer/agent/AgentInstaller.java
new file mode 100644
index 00000000..5dd041ac
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/agent/AgentInstaller.java
@@ -0,0 +1,58 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.agent;
+
+import static com.code_intelligence.jazzer.agent.AgentUtils.extractBootstrapJar;
+import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID;
+
+import java.lang.instrument.Instrumentation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicBoolean;
+import net.bytebuddy.agent.ByteBuddyAgent;
+
+public class AgentInstaller {
+ private static final AtomicBoolean hasBeenInstalled = new AtomicBoolean();
+
+ /**
+ * Appends the parts of Jazzer that have to be visible to all classes, including those in the Java
+ * standard library, to the bootstrap class loader path. Additionally, if enableAgent is true,
+ * also enables the Jazzer agent that instruments classes for fuzzing.
+ */
+ public static void install(boolean enableAgent) {
+ // Only install the agent once.
+ if (!hasBeenInstalled.compareAndSet(false, true)) {
+ return;
+ }
+
+ if (IS_ANDROID) {
+ return;
+ }
+
+ Instrumentation instrumentation = ByteBuddyAgent.install();
+ instrumentation.appendToBootstrapClassLoaderSearch(extractBootstrapJar());
+ if (!enableAgent) {
+ return;
+ }
+ try {
+ Class<?> agent = Class.forName("com.code_intelligence.jazzer.agent.Agent");
+ Method install = agent.getMethod("install", Instrumentation.class);
+ install.invoke(null, instrumentation);
+ } catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException
+ | IllegalAccessException e) {
+ throw new IllegalStateException("Failed to run Agent.install", e);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/agent/AgentUtils.java b/src/main/java/com/code_intelligence/jazzer/agent/AgentUtils.java
new file mode 100644
index 00000000..e654252a
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/agent/AgentUtils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.agent;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.jar.JarFile;
+
+final class AgentUtils {
+ private static final String BOOTSTRAP_JAR =
+ "/com/code_intelligence/jazzer/runtime/jazzer_bootstrap.jar";
+
+ public static JarFile extractBootstrapJar() {
+ try (InputStream bootstrapJarStream = AgentUtils.class.getResourceAsStream(BOOTSTRAP_JAR)) {
+ if (bootstrapJarStream == null) {
+ throw new IllegalStateException("Failed to find Jazzer agent bootstrap jar in resources");
+ }
+ File bootstrapJar = Files.createTempFile("jazzer-agent-", ".jar").toFile();
+ bootstrapJar.deleteOnExit();
+ Files.copy(bootstrapJarStream, bootstrapJar.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ return new JarFile(bootstrapJar);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to extract Jazzer agent bootstrap jar", e);
+ }
+ }
+
+ private AgentUtils() {}
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel
new file mode 100644
index 00000000..89acbda3
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel
@@ -0,0 +1,43 @@
+load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+load("//bazel:kotlin.bzl", "ktlint")
+
+java_library(
+ name = "agent_installer",
+ srcs = ["AgentInstaller.java"],
+ resources = select({
+ "@platforms//os:android": [
+ "//src/main/java/com/code_intelligence/jazzer/android:jazzer_bootstrap_android",
+ ],
+ "//conditions:default": [
+ "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap",
+ ],
+ }),
+ visibility = ["//visibility:public"],
+ deps = [
+ ":agent_lib",
+ "//src/main/java/com/code_intelligence/jazzer/driver:opt",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:constants",
+ "@net_bytebuddy_byte_buddy_agent//jar",
+ ],
+)
+
+kt_jvm_library(
+ name = "agent_lib",
+ srcs = [
+ "Agent.kt",
+ "AgentUtils.java",
+ "CoverageIdStrategy.kt",
+ "RuntimeInstrumentor.kt",
+ ],
+ deps = [
+ "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:constants",
+ "//src/main/java/com/code_intelligence/jazzer/driver:opt",
+ "//src/main/java/com/code_intelligence/jazzer/instrumentor",
+ "//src/main/java/com/code_intelligence/jazzer/utils:class_name_globber",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ "//src/main/java/com/code_intelligence/jazzer/utils:manifest_utils",
+ "@com_github_classgraph_classgraph//:classgraph",
+ ],
+)
+
+ktlint()
diff --git a/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt b/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt
new file mode 100644
index 00000000..75d76003
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt
@@ -0,0 +1,223 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.agent
+
+import com.code_intelligence.jazzer.utils.Log
+import java.nio.ByteBuffer
+import java.nio.channels.FileChannel
+import java.nio.channels.FileLock
+import java.nio.file.Path
+import java.nio.file.StandardOpenOption
+import java.util.UUID
+
+/**
+ * Indicates a fatal failure to generate synchronized coverage IDs.
+ */
+class CoverageIdException(cause: Throwable? = null) :
+ RuntimeException("Failed to synchronize coverage IDs", cause)
+
+/**
+ * [CoverageIdStrategy] provides an abstraction to switch between context specific coverage ID generation.
+ *
+ * Coverage (i.e., edge) IDs differ from other kinds of IDs, such as those generated for call sites or cmp
+ * instructions, in that they should be consecutive, collision-free, and lie in a known, small range.
+ * This precludes us from generating them simply as hashes of class names.
+ */
+interface CoverageIdStrategy {
+
+ /**
+ * [withIdForClass] provides the initial coverage ID of the given [className] as parameter to the
+ * [block] to execute. [block] has to return the number of additionally used IDs.
+ */
+ @Throws(CoverageIdException::class)
+ fun withIdForClass(className: String, block: (Int) -> Int)
+}
+
+/**
+ * A memory synced strategy for coverage ID generation.
+ *
+ * This strategy uses a synchronized block to guard access to a global edge ID counter.
+ * Even though concurrent fuzzing is not fully supported this strategy enables consistent coverage
+ * IDs in case of concurrent class loading.
+ *
+ * It only prevents races within one VM instance.
+ */
+class MemSyncCoverageIdStrategy : CoverageIdStrategy {
+ private var nextEdgeId = 0
+
+ @Synchronized
+ override fun withIdForClass(className: String, block: (Int) -> Int) {
+ nextEdgeId += block(nextEdgeId)
+ }
+}
+
+/**
+ * A strategy for coverage ID generation that synchronizes the IDs assigned to a class with other processes via the
+ * specified [idSyncFile].
+ * This class takes care of synchronizing the access to the file between multiple processes as long as the general
+ * contract of [CoverageIdStrategy] is followed.
+ */
+class FileSyncCoverageIdStrategy(private val idSyncFile: Path) : CoverageIdStrategy {
+ private val uuid: UUID = UUID.randomUUID()
+ private var idFileLock: FileLock? = null
+
+ private var cachedFirstId: Int? = null
+ private var cachedClassName: String? = null
+ private var cachedIdCount: Int? = null
+
+ /**
+ * This method is synchronized to prevent concurrent access to the internal file lock which would result in
+ * [java.nio.channels.OverlappingFileLockException]. Furthermore, every coverage ID obtained by [obtainFirstId]
+ * is always committed back again to the sync file by [commitIdCount].
+ */
+ @Synchronized
+ override fun withIdForClass(className: String, block: (Int) -> Int) {
+ var actualNumEdgeIds = 0
+ try {
+ val firstId = obtainFirstId(className)
+ actualNumEdgeIds = block(firstId)
+ } finally {
+ commitIdCount(actualNumEdgeIds)
+ }
+ }
+
+ /**
+ * Obtains a coverage ID for [className] such that all cooperating agent processes will obtain the same ID.
+ * There are two cases to consider:
+ * - This agent process is the first to encounter [className], i.e., it does not find a record for that class in
+ * [idSyncFile]. In this case, a lock on the file is held until the class has been instrumented and a record with
+ * the required number of coverage IDs has been added.
+ * - Another agent process has already encountered [className], i.e., there is a record that class in [idSyncFile].
+ * In this case, the lock on the file is returned immediately and the extracted first coverage ID is returned to
+ * the caller. The caller is still expected to call [commitIdCount] so that desynchronization can be detected.
+ */
+ private fun obtainFirstId(className: String): Int {
+ try {
+ check(idFileLock == null) { "Already holding a lock on the ID file" }
+ val localIdFile = FileChannel.open(
+ idSyncFile,
+ StandardOpenOption.WRITE,
+ StandardOpenOption.READ,
+ )
+ // Wait until we have obtained the lock on the sync file. We hold the lock from this point until we have
+ // finished reading and writing (if necessary) to the file.
+ val localIdFileLock = localIdFile.lock()
+ check(localIdFileLock.isValid && !localIdFileLock.isShared)
+ // Parse the sync file, which consists of lines of the form
+ // <class name>:<first ID>:<num IDs>
+ val idInfo = localIdFileLock.channel().readFully()
+ .lineSequence()
+ .filterNot { it.isBlank() }
+ .map { line ->
+ val parts = line.split(':')
+ check(parts.size == 4) {
+ "Expected ID file line to be of the form '<class name>:<first ID>:<num IDs>:<uuid>', got '$line'"
+ }
+ val lineClassName = parts[0]
+ val lineFirstId = parts[1].toInt()
+ check(lineFirstId >= 0) { "Negative first ID in line: $line" }
+ val lineIdCount = parts[2].toInt()
+ check(lineIdCount >= 0) { "Negative ID count in line: $line" }
+ Triple(lineClassName, lineFirstId, lineIdCount)
+ }.toList()
+ cachedClassName = className
+ val idInfoForClass = idInfo.filter { it.first == className }
+ return when (idInfoForClass.size) {
+ 0 -> {
+ // We are the first to encounter this class and thus need to hold the lock until the class has been
+ // instrumented and we know the required number of coverage IDs.
+ idFileLock = localIdFileLock
+ // Compute the next free ID as the maximum over the sums of first ID and ID count, starting at 0 if
+ // this is the first ID to be assigned. In fact, since this is the only way new lines are added to
+ // the file, the maximum is always attained by the last line.
+ val nextFreeId = idInfo.asSequence().map { it.second + it.third }.lastOrNull() ?: 0
+ cachedFirstId = nextFreeId
+ nextFreeId
+ }
+ 1 -> {
+ // This class has already been instrumented elsewhere, so we just return the first ID and ID count
+ // reported from there and release the lock right away. The caller is still expected to call
+ // commitIdCount.
+ localIdFile.close()
+ cachedIdCount = idInfoForClass.single().third
+ idInfoForClass.single().second
+ }
+ else -> {
+ localIdFile.close()
+ Log.println(idInfo.joinToString("\n") { "${it.first}:${it.second}:${it.third}" })
+ throw IllegalStateException("Multiple entries for $className in ID file")
+ }
+ }
+ } catch (e: Exception) {
+ throw CoverageIdException(e)
+ }
+ }
+
+ /**
+ * Records the number of coverage IDs used to instrument the class specified in a previous call to [obtainFirstId].
+ * If instrumenting the class should fail, this function must still be called. In this case, [idCount] is set to 0.
+ */
+ private fun commitIdCount(idCount: Int) {
+ val localIdFileLock = idFileLock
+ try {
+ check(cachedClassName != null)
+ if (localIdFileLock == null) {
+ // We released the lock already in obtainFirstId since the class had already been instrumented
+ // elsewhere. As we know the expected number of IDs for the current class in this case, check for
+ // deviations.
+ check(cachedIdCount != null)
+ check(idCount == cachedIdCount) {
+ "$cachedClassName has $idCount edges, but $cachedIdCount edges reserved in ID file"
+ }
+ } else {
+ // We are the first to instrument this class and should record the number of IDs in the sync file.
+ check(cachedFirstId != null)
+ localIdFileLock.channel().append("$cachedClassName:$cachedFirstId:$idCount:$uuid\n")
+ localIdFileLock.channel().force(true)
+ }
+ idFileLock = null
+ cachedFirstId = null
+ cachedIdCount = null
+ cachedClassName = null
+ } catch (e: Exception) {
+ throw CoverageIdException(e)
+ } finally {
+ localIdFileLock?.channel()?.close()
+ }
+ }
+}
+
+/**
+ * Reads the [FileChannel] to the end as a UTF-8 string.
+ */
+fun FileChannel.readFully(): String {
+ check(size() <= Int.MAX_VALUE)
+ val buffer = ByteBuffer.allocate(size().toInt())
+ while (buffer.hasRemaining()) {
+ when (read(buffer)) {
+ 0 -> throw IllegalStateException("No bytes read")
+ -1 -> break
+ }
+ }
+ return String(buffer.array())
+}
+
+/**
+ * Appends [string] to the end of the [FileChannel].
+ */
+fun FileChannel.append(string: String) {
+ position(size())
+ write(ByteBuffer.wrap(string.toByteArray()))
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt
new file mode 100644
index 00000000..57410f30
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt
@@ -0,0 +1,243 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.agent
+
+import com.code_intelligence.jazzer.driver.Opt
+import com.code_intelligence.jazzer.instrumentor.ClassInstrumentor
+import com.code_intelligence.jazzer.instrumentor.CoverageRecorder
+import com.code_intelligence.jazzer.instrumentor.Hook
+import com.code_intelligence.jazzer.instrumentor.InstrumentationType
+import com.code_intelligence.jazzer.utils.ClassNameGlobber
+import com.code_intelligence.jazzer.utils.Log
+import io.github.classgraph.ClassGraph
+import java.io.File
+import java.lang.instrument.ClassFileTransformer
+import java.lang.instrument.Instrumentation
+import java.nio.file.Path
+import java.security.ProtectionDomain
+import kotlin.math.roundToInt
+import kotlin.system.exitProcess
+import kotlin.time.measureTimedValue
+
+class RuntimeInstrumentor(
+ private val instrumentation: Instrumentation,
+ private val classesToFullyInstrument: ClassNameGlobber,
+ private val classesToHookInstrument: ClassNameGlobber,
+ private val instrumentationTypes: Set<InstrumentationType>,
+ private val includedHooks: List<Hook>,
+ private val customHooks: List<Hook>,
+ // Dedicated name globber for additional classes to hook stated in hook annotations is needed due to
+ // existing include and exclude pattern of classesToHookInstrument. All classes are included in hook
+ // instrumentation except the ones from default excludes, like JDK and Kotlin classes. But additional
+ // classes to hook, based on annotations, are allowed to reference normally ignored ones, like JDK
+ // and Kotlin internals.
+ // FIXME: Adding an additional class to hook will apply _all_ hooks to it and not only the one it's
+ // defined in. At some point we might want to track the list of classes per custom hook rather than globally.
+ private val additionalClassesToHookInstrument: ClassNameGlobber,
+ private val coverageIdSynchronizer: CoverageIdStrategy,
+ private val dumpClassesDir: Path?,
+) : ClassFileTransformer {
+
+ @kotlin.time.ExperimentalTime
+ override fun transform(
+ loader: ClassLoader?,
+ internalClassName: String,
+ classBeingRedefined: Class<*>?,
+ protectionDomain: ProtectionDomain?,
+ classfileBuffer: ByteArray,
+ ): ByteArray? {
+ var pathPrefix = ""
+ if (!Opt.instrumentOnly.isEmpty() && protectionDomain != null) {
+ var outputPathPrefix = protectionDomain.getCodeSource().getLocation().getFile().toString()
+ if (outputPathPrefix.isNotEmpty()) {
+ if (outputPathPrefix.contains(File.separator)) {
+ outputPathPrefix = outputPathPrefix.substring(outputPathPrefix.lastIndexOf(File.separator) + 1, outputPathPrefix.length)
+ }
+
+ if (outputPathPrefix.endsWith(".jar")) {
+ outputPathPrefix = outputPathPrefix.substring(0, outputPathPrefix.lastIndexOf(".jar"))
+ }
+
+ if (outputPathPrefix.isNotEmpty()) {
+ pathPrefix = outputPathPrefix + File.separator
+ }
+ }
+ }
+
+ return try {
+ // Bail out early if we would instrument ourselves. This prevents ClassCircularityErrors as we might need to
+ // load additional Jazzer classes until we reach the full exclusion logic.
+ if (internalClassName.startsWith("com/code_intelligence/jazzer/")) {
+ return null
+ }
+ // Workaround for a JDK bug (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8299798):
+ // When retransforming a class in the Java standard library, the provided classfileBuffer does not contain
+ // any StackMapTable attributes. Our transformations require stack map frames to calculate the number of
+ // local variables and stack slots as well as when adding control flow.
+ //
+ // We work around this by reloading the class file contents if we are retransforming (classBeingRedefined
+ // is also non-null in this situation) and the class is provided by the bootstrap loader.
+ //
+ // Alternatives considered:
+ // Using ClassWriter.COMPUTE_FRAMES as an escape hatch isn't possible in the context of an agent as the
+ // computation may itself need to load classes, which leads to circular loads and incompatible class
+ // redefinitions.
+ transformInternal(internalClassName, classfileBuffer.takeUnless { loader == null && classBeingRedefined != null })
+ } catch (t: Throwable) {
+ // Throwables raised from transform are silently dropped, making it extremely hard to detect instrumentation
+ // failures. The docs advise to use a top-level try-catch.
+ // https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/ClassFileTransformer.html
+ if (dumpClassesDir != null) {
+ dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".failed", pathPrefix = pathPrefix)
+ }
+ Log.warn("Failed to instrument $internalClassName:", t)
+ throw t
+ }.also { instrumentedByteCode ->
+ // Only dump classes that were instrumented.
+ if (instrumentedByteCode != null && dumpClassesDir != null) {
+ dumpToClassFile(internalClassName, instrumentedByteCode, pathPrefix = pathPrefix)
+ dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".original", pathPrefix = pathPrefix)
+ }
+ }
+ }
+
+ private fun dumpToClassFile(internalClassName: String, bytecode: ByteArray, basenameSuffix: String = "", pathPrefix: String = "") {
+ val relativePath = "$pathPrefix$internalClassName$basenameSuffix.class"
+ val absolutePath = dumpClassesDir!!.resolve(relativePath)
+ val dumpFile = absolutePath.toFile()
+ dumpFile.parentFile.mkdirs()
+ dumpFile.writeBytes(bytecode)
+ }
+
+ @kotlin.time.ExperimentalTime
+ override fun transform(
+ module: Module?,
+ loader: ClassLoader?,
+ internalClassName: String,
+ classBeingRedefined: Class<*>?,
+ protectionDomain: ProtectionDomain?,
+ classfileBuffer: ByteArray,
+ ): ByteArray? {
+ try {
+ if (module != null && !module.canRead(RuntimeInstrumentor::class.java.module)) {
+ // Make all other modules read our (unnamed) module, which allows them to access the classes needed by the
+ // instrumentations, e.g. CoverageMap. If a module can't be modified, it should not be instrumented as the
+ // injected bytecode might throw NoClassDefFoundError.
+ // https://mail.openjdk.java.net/pipermail/jigsaw-dev/2021-May/014663.html
+ if (!instrumentation.isModifiableModule(module)) {
+ val prettyClassName = internalClassName.replace('/', '.')
+ Log.warn("Failed to instrument $prettyClassName in unmodifiable module ${module.name}, skipping")
+ return null
+ }
+ instrumentation.redefineModule(
+ module,
+ setOf(RuntimeInstrumentor::class.java.module), // extraReads
+ emptyMap(),
+ emptyMap(),
+ emptySet(),
+ emptyMap(),
+ )
+ }
+ } catch (t: Throwable) {
+ // Throwables raised from transform are silently dropped, making it extremely hard to detect instrumentation
+ // failures. The docs advise to use a top-level try-catch.
+ // https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/ClassFileTransformer.html
+ if (dumpClassesDir != null) {
+ dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".failed")
+ }
+ Log.warn("Failed to instrument $internalClassName:", t)
+ throw t
+ }
+ return transform(loader, internalClassName, classBeingRedefined, protectionDomain, classfileBuffer)
+ }
+
+ @kotlin.time.ExperimentalTime
+ fun transformInternal(internalClassName: String, maybeClassfileBuffer: ByteArray?): ByteArray? {
+ val (fullInstrumentation, printInfo) = when {
+ classesToFullyInstrument.includes(internalClassName) -> Pair(true, true)
+ classesToHookInstrument.includes(internalClassName) -> Pair(false, true)
+ // The classes to hook specified by hooks are more of an implementation detail of the hook. The list is
+ // always the same unless the set of hooks changes and doesn't help the user judge whether their classes are
+ // being instrumented, so we don't print info for them.
+ additionalClassesToHookInstrument.includes(internalClassName) -> Pair(false, false)
+ else -> return null
+ }
+ val className = internalClassName.replace('/', '.')
+ val classfileBuffer = maybeClassfileBuffer ?: ClassGraph()
+ .enableSystemJarsAndModules()
+ .ignoreClassVisibility()
+ .acceptClasses(className)
+ .scan()
+ .use {
+ it.getClassInfo(className)?.resource?.load() ?: run {
+ Log.warn("Failed to load bytecode of class $className")
+ return null
+ }
+ }
+ val (instrumentedBytecode, duration) = measureTimedValue {
+ try {
+ instrument(internalClassName, classfileBuffer, fullInstrumentation)
+ } catch (e: CoverageIdException) {
+ Log.error("Coverage IDs are out of sync")
+ e.printStackTrace()
+ exitProcess(1)
+ }
+ }
+ val durationInMs = duration.inWholeMilliseconds
+ val sizeIncrease = ((100.0 * (instrumentedBytecode.size - classfileBuffer.size)) / classfileBuffer.size).roundToInt()
+ if (printInfo) {
+ if (fullInstrumentation) {
+ Log.info("Instrumented $className (took $durationInMs ms, size +$sizeIncrease%)")
+ } else {
+ Log.info("Instrumented $className with custom hooks only (took $durationInMs ms, size +$sizeIncrease%)")
+ }
+ }
+ return instrumentedBytecode
+ }
+
+ private fun instrument(internalClassName: String, bytecode: ByteArray, fullInstrumentation: Boolean): ByteArray {
+ val classWithHooksEnabledField = if (Opt.conditionalHooks) {
+ // Let the hook instrumentation emit additional logic that checks the value of the
+ // hooksEnabled field on this class and skips the hook if it is false.
+ "com/code_intelligence/jazzer/runtime/JazzerInternal"
+ } else {
+ null
+ }
+ return ClassInstrumentor(internalClassName, bytecode).run {
+ if (fullInstrumentation) {
+ // Coverage instrumentation must be performed before any other code updates
+ // or there will be additional coverage points injected if any calls are inserted
+ // and JaCoCo will produce a broken coverage report.
+ coverageIdSynchronizer.withIdForClass(internalClassName) { firstId ->
+ coverage(firstId).also { actualNumEdgeIds ->
+ CoverageRecorder.recordInstrumentedClass(
+ internalClassName,
+ bytecode,
+ firstId,
+ actualNumEdgeIds,
+ )
+ }
+ }
+ // Hook instrumentation must be performed after data flow tracing as the injected
+ // bytecode would trigger the GEP callbacks for byte[].
+ traceDataFlow(instrumentationTypes)
+ hooks(includedHooks + customHooks, classWithHooksEnabledField)
+ } else {
+ hooks(customHooks, classWithHooksEnabledField)
+ }
+ instrumentedBytecode
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/android/AndroidRuntime.java b/src/main/java/com/code_intelligence/jazzer/android/AndroidRuntime.java
new file mode 100644
index 00000000..3a80c314
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/android/AndroidRuntime.java
@@ -0,0 +1,67 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.android;
+
+import com.code_intelligence.jazzer.utils.Log;
+import com.github.fmeum.rules_jni.RulesJni;
+
+/**
+ * Loads Android tooling library and registers native functions.
+ */
+public class AndroidRuntime {
+ private static final String DO_NOT_INITIALIZE = "use_none";
+ private static final String FUZZ_DIR = "/data/fuzz/";
+ private static final String PLATFORM_LIB_DIRS = ":/system/lib64/:/apex/com.android.i18n@1/lib64/";
+
+ public static void initialize(String runtimeLibs) {
+ if (runtimeLibs == null) {
+ return;
+ }
+
+ RulesJni.loadLibrary("jazzer_android_tooling", "/com/code_intelligence/jazzer/driver");
+ if (runtimeLibs.equals(DO_NOT_INITIALIZE)) {
+ Log.warn("Android Runtime (ART) is not being initialized for this fuzzer.");
+ } else {
+ registerNatives();
+ }
+ };
+
+ /**
+ * Returns a command to set the classpath for fuzzing.
+ *
+ * @return The classpath command.
+ */
+ public static String getClassPathsCommand() {
+ return "export CLASSPATH=" + System.getProperty("java.class.path");
+ }
+
+ /**
+ * Builds and returns the value to set for LD_LIBRARY_PATH.
+ * This value is consumed when launching jazzer on the device
+ * and specifies which directories to search for dependencies.
+ *
+ * @return The string for LD_LIBRARY_PATH.
+ */
+ public static String getLdLibraryPath() {
+ String initOptString = System.getProperty("jazzer.android_init_options");
+ if (initOptString.equals(DO_NOT_INITIALIZE) || initOptString.equals("")) {
+ return FUZZ_DIR;
+ }
+
+ return FUZZ_DIR + PLATFORM_LIB_DIRS;
+ }
+
+ private static native int registerNatives();
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/android/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/android/BUILD.bazel
new file mode 100644
index 00000000..1204c4ee
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/android/BUILD.bazel
@@ -0,0 +1,95 @@
+load("//bazel:compat.bzl", "SKIP_ON_WINDOWS")
+load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library")
+
+java_import(
+ name = "jazzer_bootstrap_android_import",
+ jars = [
+ "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap",
+ ],
+ tags = ["manual"],
+ target_compatible_with = SKIP_ON_WINDOWS,
+)
+
+android_library(
+ name = "jazzer_bootstrap_android_lib",
+ tags = ["manual"],
+ target_compatible_with = SKIP_ON_WINDOWS,
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__",
+ ],
+ exports = [
+ ":jazzer_bootstrap_android_import",
+ ],
+)
+
+android_binary(
+ name = "jazzer_bootstrap_android_bin",
+ manifest = "//launcher/android:android_manifest",
+ min_sdk_version = 26,
+ tags = ["manual"],
+ target_compatible_with = SKIP_ON_WINDOWS,
+ deps = [
+ ":jazzer_bootstrap_android_lib",
+ ],
+)
+
+copy_file(
+ name = "jazzer_bootstrap_android",
+ src = "jazzer_bootstrap_android_bin.apk",
+ out = "jazzer_bootstrap_android.jar",
+ tags = ["manual"],
+ target_compatible_with = SKIP_ON_WINDOWS,
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__",
+ ],
+)
+
+java_jni_library(
+ name = "dex_file_manager",
+ srcs = ["DexFileManager.java"],
+ native_libs = [
+ "//src/main/native/com/code_intelligence/jazzer/android:android_native_agent",
+ ],
+)
+
+android_library(
+ name = "jazzer_standalone_library",
+ tags = ["manual"],
+ target_compatible_with = SKIP_ON_WINDOWS,
+ exports = [
+ "//deploy:jazzer-api",
+ "//src/main/java/com/code_intelligence/jazzer:jazzer_import",
+ ],
+)
+
+android_binary(
+ name = "jazzer_standalone_android",
+ manifest = "//launcher/android:android_manifest",
+ min_sdk_version = 26,
+ tags = ["manual"],
+ target_compatible_with = SKIP_ON_WINDOWS,
+ visibility = [
+ "//:__pkg__",
+ "//launcher/android:__pkg__",
+ ],
+ deps = [
+ ":dex_file_manager",
+ ":jazzer_standalone_library",
+ ],
+)
+
+java_jni_library(
+ name = "android_runtime",
+ srcs = ["AndroidRuntime.java"],
+ native_libs = ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_android_tooling"],
+ target_compatible_with = SKIP_ON_WINDOWS,
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/driver:__subpackages__",
+ "//src/main/native/com/code_intelligence/jazzer/driver:__subpackages__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/android/DexFileManager.java b/src/main/java/com/code_intelligence/jazzer/android/DexFileManager.java
new file mode 100644
index 00000000..23d2eeec
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/android/DexFileManager.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.android;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.Math;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+public class DexFileManager {
+ private final static int MAX_READ_LENGTH = 2000000;
+
+ public static byte[] getBytecodeFromDex(String jarPath, String dexFile) throws IOException {
+ try (JarFile jarFile = new JarFile(jarPath)) {
+ JarEntry entry = jarFile.stream()
+ .filter(jarEntry -> jarEntry.getName().equals(dexFile))
+ .findFirst()
+ .orElse(null);
+
+ if (entry == null) {
+ throw new IOException("Could not find dex file: " + dexFile);
+ }
+
+ try (InputStream is = jarFile.getInputStream(entry)) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ byte[] buffer = new byte[64 * 104 * 1024];
+ int read;
+ while ((read = is.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+
+ return out.toByteArray();
+ }
+ }
+ }
+
+ public static String[] getDexFilesForJar(String jarpath) throws IOException {
+ try (JarFile jarFile = new JarFile(jarpath)) {
+ return jarFile.stream()
+ .map(JarEntry::getName)
+ .filter(entry -> entry.endsWith(".dex"))
+ .toArray(String[] ::new);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java b/src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java
new file mode 100644
index 00000000..77711ee9
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java
@@ -0,0 +1,411 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+
+/**
+ * Static helper functions that allow Jazzer fuzz targets to use Autofuzz.
+ */
+final public class Autofuzz {
+ private static final MethodHandle CONSUME;
+ private static final MethodHandle AUTOFUZZ_FUNCTION_1;
+ private static final MethodHandle AUTOFUZZ_FUNCTION_2;
+ private static final MethodHandle AUTOFUZZ_FUNCTION_3;
+ private static final MethodHandle AUTOFUZZ_FUNCTION_4;
+ private static final MethodHandle AUTOFUZZ_FUNCTION_5;
+ private static final MethodHandle AUTOFUZZ_CONSUMER_1;
+ private static final MethodHandle AUTOFUZZ_CONSUMER_2;
+ private static final MethodHandle AUTOFUZZ_CONSUMER_3;
+ private static final MethodHandle AUTOFUZZ_CONSUMER_4;
+ private static final MethodHandle AUTOFUZZ_CONSUMER_5;
+
+ static {
+ MethodHandle consume = null;
+ MethodHandle autofuzzFunction1 = null;
+ MethodHandle autofuzzFunction2 = null;
+ MethodHandle autofuzzFunction3 = null;
+ MethodHandle autofuzzFunction4 = null;
+ MethodHandle autofuzzFunction5 = null;
+ MethodHandle autofuzzConsumer1 = null;
+ MethodHandle autofuzzConsumer2 = null;
+ MethodHandle autofuzzConsumer3 = null;
+ MethodHandle autofuzzConsumer4 = null;
+ MethodHandle autofuzzConsumer5 = null;
+ try {
+ Class<?> metaClass = Class.forName("com.code_intelligence.jazzer.autofuzz.Meta");
+ MethodType consumeType =
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Class.class);
+ consume = MethodHandles.publicLookup().findStatic(metaClass, "consume", consumeType);
+
+ autofuzzFunction1 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Function1.class));
+ autofuzzFunction2 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Function2.class));
+ autofuzzFunction3 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Function3.class));
+ autofuzzFunction4 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Function4.class));
+ autofuzzFunction5 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Function5.class));
+ autofuzzConsumer1 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer1.class));
+ autofuzzConsumer2 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer2.class));
+ autofuzzConsumer3 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer3.class));
+ autofuzzConsumer4 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer4.class));
+ autofuzzConsumer5 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer5.class));
+ } catch (ClassNotFoundException ignore) {
+ // Not running in the context of the agent. This is fine as long as no methods are called on
+ // this class.
+ } catch (NoSuchMethodException | IllegalAccessException e) {
+ // This should never happen as the Jazzer API is loaded from the agent and thus should always
+ // match the version of the runtime classes.
+ // Does not use the Log class as it is unlikely it can be loaded if the Autofuzz classes
+ // couldn't be loaded.
+ System.err.println("ERROR: Incompatible version of the Jazzer API detected, please update.");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ CONSUME = consume;
+ AUTOFUZZ_FUNCTION_1 = autofuzzFunction1;
+ AUTOFUZZ_FUNCTION_2 = autofuzzFunction2;
+ AUTOFUZZ_FUNCTION_3 = autofuzzFunction3;
+ AUTOFUZZ_FUNCTION_4 = autofuzzFunction4;
+ AUTOFUZZ_FUNCTION_5 = autofuzzFunction5;
+ AUTOFUZZ_CONSUMER_1 = autofuzzConsumer1;
+ AUTOFUZZ_CONSUMER_2 = autofuzzConsumer2;
+ AUTOFUZZ_CONSUMER_3 = autofuzzConsumer3;
+ AUTOFUZZ_CONSUMER_4 = autofuzzConsumer4;
+ AUTOFUZZ_CONSUMER_5 = autofuzzConsumer5;
+ }
+
+ private Autofuzz() {}
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Function1} with (partially) specified
+ * type variables, e.g. {@code (Function1<String, ?>) String::new}.
+ * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke
+ * the function.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T1, R> R autofuzz(FuzzedDataProvider data, Function1<T1, R> func) {
+ try {
+ return (R) AUTOFUZZ_FUNCTION_1.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ // Not reached.
+ return null;
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Function2} with (partially) specified
+ * type variables.
+ * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke
+ * the function.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, R> R autofuzz(FuzzedDataProvider data, Function2<T1, T2, R> func) {
+ try {
+ return (R) AUTOFUZZ_FUNCTION_2.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ // Not reached.
+ return null;
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Function3} with (partially) specified
+ * type variables.
+ * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke
+ * the function.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, R> R autofuzz(FuzzedDataProvider data, Function3<T1, T2, T3, R> func) {
+ try {
+ return (R) AUTOFUZZ_FUNCTION_3.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ // Not reached.
+ return null;
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Function4} with (partially) specified
+ * type variables.
+ * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke
+ * the function.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4, R> R autofuzz(
+ FuzzedDataProvider data, Function4<T1, T2, T3, T4, R> func) {
+ try {
+ return (R) AUTOFUZZ_FUNCTION_4.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ // Not reached.
+ return null;
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Function5} with (partially) specified
+ * type variables.
+ * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke
+ * the function.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4, T5, R> R autofuzz(
+ FuzzedDataProvider data, Function5<T1, T2, T3, T4, T5, R> func) {
+ try {
+ return (R) AUTOFUZZ_FUNCTION_5.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ // Not reached.
+ return null;
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Consumer1} with explicitly specified
+ * type variable.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ public static <T1> void autofuzz(FuzzedDataProvider data, Consumer1<T1> func) {
+ try {
+ AUTOFUZZ_CONSUMER_1.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Consumer2} with (partially) specified
+ * type variables.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ public static <T1, T2> void autofuzz(FuzzedDataProvider data, Consumer2<T1, T2> func) {
+ try {
+ AUTOFUZZ_CONSUMER_2.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Consumer3} with (partially) specified
+ * type variables.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ public static <T1, T2, T3> void autofuzz(FuzzedDataProvider data, Consumer3<T1, T2, T3> func) {
+ try {
+ AUTOFUZZ_CONSUMER_3.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Consumer4} with (partially) specified
+ * type variables.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ public static <T1, T2, T3, T4> void autofuzz(
+ FuzzedDataProvider data, Consumer4<T1, T2, T3, T4> func) {
+ try {
+ AUTOFUZZ_CONSUMER_4.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Consumer5} with (partially) specified
+ * type variables.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ public static <T1, T2, T3, T4, T5> void autofuzz(
+ FuzzedDataProvider data, Consumer5<T1, T2, T3, T4, T5> func) {
+ try {
+ AUTOFUZZ_CONSUMER_5.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ }
+
+ /**
+ * Attempts to construct an instance of {@code type} from the fuzzer input using only public
+ * methods available on the classpath.
+ * <p>
+ * <b>Note:</b> This function is inherently heuristic and may fail to return meaningful values for
+ * a variety of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param type the {@link Class} to construct an instance of.
+ * @return an instance of {@code type} constructed from the fuzzer input, or {@code null} if
+ * autofuzz failed to create an instance.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T consume(FuzzedDataProvider data, Class<T> type) {
+ try {
+ return (T) CONSUME.invokeExact(data, type);
+ } catch (AutofuzzConstructionException ignored) {
+ return null;
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ // Not reached.
+ return null;
+ }
+ }
+
+ // Rethrows a (possibly checked) exception while avoiding a throws declaration.
+ @SuppressWarnings("unchecked")
+ private static <T extends Throwable> void rethrowUnchecked(Throwable t) throws T {
+ throw(T) t;
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java b/src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java
new file mode 100644
index 00000000..93340ee8
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java
@@ -0,0 +1,32 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+// An exception wrapping a Throwable thrown during the construction of parameters for, but not the
+// actual invocation of an autofuzzed method.
+/**
+ * Only used internally.
+ */
+public class AutofuzzConstructionException extends RuntimeException {
+ public AutofuzzConstructionException() {
+ super();
+ }
+ public AutofuzzConstructionException(String message) {
+ super(message);
+ }
+ public AutofuzzConstructionException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java b/src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java
new file mode 100644
index 00000000..eb936630
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java
@@ -0,0 +1,30 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+// An exception wrapping a {@link Throwable} thrown during the actual invocation of, but not the
+// construction of parameters for an autofuzzed method.
+/**
+ * Only used internally.
+ */
+public class AutofuzzInvocationException extends RuntimeException {
+ public AutofuzzInvocationException() {
+ super();
+ }
+
+ public AutofuzzInvocationException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel
new file mode 100644
index 00000000..03ac7917
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel
@@ -0,0 +1,51 @@
+load("@rules_jvm_external//:defs.bzl", "java_export")
+
+java_library(
+ name = "api",
+ srcs = [
+ "Autofuzz.java",
+ "AutofuzzConstructionException.java",
+ "AutofuzzInvocationException.java",
+ "BugDetectors.java",
+ "CannedFuzzedDataProvider.java",
+ "Consumer1.java",
+ "Consumer2.java",
+ "Consumer3.java",
+ "Consumer4.java",
+ "Consumer5.java",
+ "Function1.java",
+ "Function2.java",
+ "Function3.java",
+ "Function4.java",
+ "Function5.java",
+ "FuzzedDataProvider.java",
+ "SilentCloseable.java",
+ ],
+ visibility = ["//visibility:public"],
+ runtime_deps = [
+ ":hooks",
+ ],
+)
+
+java_binary(
+ name = "api_deploy_env",
+ create_executable = False,
+ visibility = ["//src/main/java/com/code_intelligence/jazzer:__pkg__"],
+ runtime_deps = [":api"],
+)
+
+java_library(
+ name = "hooks",
+ srcs = [
+ "FuzzerSecurityIssueCritical.java",
+ "FuzzerSecurityIssueHigh.java",
+ "FuzzerSecurityIssueLow.java",
+ "FuzzerSecurityIssueMedium.java",
+ "HookType.java",
+ "Jazzer.java",
+ "MethodHook.java",
+ "MethodHooks.java",
+ "//src/main/java/jaz",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java b/src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java
new file mode 100644
index 00000000..64f01931
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.api;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiPredicate;
+
+/**
+ * Provides static functions that configure the behavior of bug detectors provided by Jazzer.
+ */
+public final class BugDetectors {
+ private static final AtomicReference<BiPredicate<String, Integer>> currentPolicy =
+ getConnectionPermittedReference();
+
+ /**
+ * Allows all network connections.
+ *
+ * <p>See {@link #allowNetworkConnections(BiPredicate)} for an alternative that provides
+ * fine-grained control over which network connections are expected.
+ *
+ * <p>By default, all attempted network connections are considered unexpected and result in a
+ * finding being reported.
+ *
+ * <p>By wrapping the call into a try-with-resources statement, network connection permissions
+ * can be configured to apply to individual parts of the fuzz test only:
+ *
+ * <pre>{@code
+ * Image image = parseImage(bytes);
+ * Response response;
+ * try (SilentCloseable unused = BugDetectors.allowNetworkConnections()) {
+ * response = uploadImage(image);
+ * }
+ * handleResponse(response);
+ * }</pre>
+ *
+ * @return a {@link SilentCloseable} that restores the previously set permissions when closed
+ */
+ public static SilentCloseable allowNetworkConnections() {
+ return allowNetworkConnections((host, port) -> true);
+ }
+
+ /**
+ * Allows all network connections for which the provided predicate returns {@code true}.
+ *
+ * <p>By default, all attempted network connections are considered unexpected and result in a
+ * finding being reported.
+ *
+ * <p>By wrapping the call into a try-with-resources statement, network connection permissions
+ * can be configured to apply to individual parts of the fuzz test only:
+ *
+ * <pre>{@code
+ * Image image = parseImage(bytes);
+ * Response response;
+ * try (SilentCloseable unused = BugDetectors.allowNetworkConnections(
+ * (host, port) -> host.equals("example.org"))) {
+ * response = uploadImage(image, "example.org");
+ * }
+ * handleResponse(response);
+ * }</pre>
+ *
+ * @param connectionPermitted a predicate that evaluate to {@code true} if network connections to
+ * the provided combination of host and port are permitted
+ * @return a {@link SilentCloseable} that restores the previously set predicate when closed
+ */
+ public static SilentCloseable allowNetworkConnections(
+ BiPredicate<String, Integer> connectionPermitted) {
+ if (connectionPermitted == null) {
+ throw new IllegalArgumentException("connectionPermitted must not be null");
+ }
+ if (currentPolicy == null) {
+ throw new IllegalStateException("Failed to set network connection policy");
+ }
+ BiPredicate<String, Integer> previousPolicy = currentPolicy.getAndSet(connectionPermitted);
+ return () -> {
+ if (!currentPolicy.compareAndSet(connectionPermitted, previousPolicy)) {
+ throw new IllegalStateException(
+ "Failed to reset network connection policy - using try-with-resources is highly recommended");
+ }
+ };
+ }
+
+ private static AtomicReference<BiPredicate<String, Integer>> getConnectionPermittedReference() {
+ try {
+ Class<?> ssrfSanitizer =
+ Class.forName("com.code_intelligence.jazzer.sanitizers.ServerSideRequestForgery");
+ return (AtomicReference<BiPredicate<String, Integer>>) ssrfSanitizer
+ .getField("connectionPermitted")
+ .get(null);
+ } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
+ System.err.println("WARNING: ");
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private BugDetectors() {}
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java b/src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java
new file mode 100644
index 00000000..7209a497
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java
@@ -0,0 +1,211 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Replays recorded FuzzedDataProvider invocations that were executed while fuzzing.
+ * Note: This class is only meant to be used by Jazzer's generated reproducers.
+ */
+final public class CannedFuzzedDataProvider implements FuzzedDataProvider {
+ private final Iterator<Object> nextReply;
+
+ public CannedFuzzedDataProvider(String can) {
+ byte[] rawIn = Base64.getDecoder().decode(can);
+ ArrayList<Object> recordedReplies;
+ try (ByteArrayInputStream byteStream = new ByteArrayInputStream(rawIn)) {
+ try (ObjectInputStream objectStream = new ObjectInputStream(byteStream)) {
+ recordedReplies = (ArrayList<Object>) objectStream.readObject();
+ }
+ } catch (IOException | ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ nextReply = recordedReplies.iterator();
+ }
+
+ public static CannedFuzzedDataProvider create(List<Object> objects) {
+ try {
+ try (ByteArrayOutputStream bout = new ByteArrayOutputStream()) {
+ try (ObjectOutputStream out = new ObjectOutputStream(bout)) {
+ out.writeObject(new ArrayList<>(objects));
+ String base64 = Base64.getEncoder().encodeToString(bout.toByteArray());
+ return new CannedFuzzedDataProvider(base64);
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public boolean consumeBoolean() {
+ return (boolean) nextReply.next();
+ }
+
+ @Override
+ public boolean[] consumeBooleans(int maxLength) {
+ return (boolean[]) nextReply.next();
+ }
+
+ @Override
+ public byte consumeByte() {
+ return (byte) nextReply.next();
+ }
+
+ @Override
+ public byte consumeByte(byte min, byte max) {
+ return (byte) nextReply.next();
+ }
+
+ @Override
+ public short consumeShort() {
+ return (short) nextReply.next();
+ }
+
+ @Override
+ public short consumeShort(short min, short max) {
+ return (short) nextReply.next();
+ }
+
+ @Override
+ public short[] consumeShorts(int maxLength) {
+ return (short[]) nextReply.next();
+ }
+
+ @Override
+ public int consumeInt() {
+ return (int) nextReply.next();
+ }
+
+ @Override
+ public int consumeInt(int min, int max) {
+ return (int) nextReply.next();
+ }
+
+ @Override
+ public int[] consumeInts(int maxLength) {
+ return (int[]) nextReply.next();
+ }
+
+ @Override
+ public long consumeLong() {
+ return (long) nextReply.next();
+ }
+
+ @Override
+ public long consumeLong(long min, long max) {
+ return (long) nextReply.next();
+ }
+
+ @Override
+ public long[] consumeLongs(int maxLength) {
+ return (long[]) nextReply.next();
+ }
+
+ @Override
+ public float consumeFloat() {
+ return (float) nextReply.next();
+ }
+
+ @Override
+ public float consumeRegularFloat() {
+ return (float) nextReply.next();
+ }
+
+ @Override
+ public float consumeRegularFloat(float min, float max) {
+ return (float) nextReply.next();
+ }
+
+ @Override
+ public float consumeProbabilityFloat() {
+ return (float) nextReply.next();
+ }
+
+ @Override
+ public double consumeDouble() {
+ return (double) nextReply.next();
+ }
+
+ @Override
+ public double consumeRegularDouble(double min, double max) {
+ return (double) nextReply.next();
+ }
+
+ @Override
+ public double consumeRegularDouble() {
+ return (double) nextReply.next();
+ }
+
+ @Override
+ public double consumeProbabilityDouble() {
+ return (double) nextReply.next();
+ }
+
+ @Override
+ public char consumeChar() {
+ return (char) nextReply.next();
+ }
+
+ @Override
+ public char consumeChar(char min, char max) {
+ return (char) nextReply.next();
+ }
+
+ @Override
+ public char consumeCharNoSurrogates() {
+ return (char) nextReply.next();
+ }
+
+ @Override
+ public String consumeAsciiString(int maxLength) {
+ return (String) nextReply.next();
+ }
+
+ @Override
+ public String consumeString(int maxLength) {
+ return (String) nextReply.next();
+ }
+
+ @Override
+ public String consumeRemainingAsAsciiString() {
+ return (String) nextReply.next();
+ }
+
+ @Override
+ public String consumeRemainingAsString() {
+ return (String) nextReply.next();
+ }
+
+ @Override
+ public byte[] consumeBytes(int maxLength) {
+ return (byte[]) nextReply.next();
+ }
+
+ @Override
+ public byte[] consumeRemainingAsBytes() {
+ return (byte[]) nextReply.next();
+ }
+
+ @Override
+ public int remainingBytes() {
+ return (int) nextReply.next();
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Consumer1.java b/src/main/java/com/code_intelligence/jazzer/api/Consumer1.java
new file mode 100644
index 00000000..472c2efd
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Consumer1.java
@@ -0,0 +1,22 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+import java.util.function.Consumer;
+
+@FunctionalInterface
+public interface Consumer1<T1> extends Consumer<T1> {
+ @Override void accept(T1 t1);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Consumer2.java b/src/main/java/com/code_intelligence/jazzer/api/Consumer2.java
new file mode 100644
index 00000000..d951ade7
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Consumer2.java
@@ -0,0 +1,22 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+import java.util.function.BiConsumer;
+
+@FunctionalInterface
+public interface Consumer2<T1, T2> extends BiConsumer<T1, T2> {
+ @Override void accept(T1 t1, T2 t2);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Consumer3.java b/src/main/java/com/code_intelligence/jazzer/api/Consumer3.java
new file mode 100644
index 00000000..c508fe53
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Consumer3.java
@@ -0,0 +1,20 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+@FunctionalInterface
+public interface Consumer3<T1, T2, T3> {
+ void accept(T1 t1, T2 t2, T3 t3);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Consumer4.java b/src/main/java/com/code_intelligence/jazzer/api/Consumer4.java
new file mode 100644
index 00000000..6ee70141
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Consumer4.java
@@ -0,0 +1,20 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+@FunctionalInterface
+public interface Consumer4<T1, T2, T3, T4> {
+ void accept(T1 t1, T2 t2, T3 t3, T4 t4);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Consumer5.java b/src/main/java/com/code_intelligence/jazzer/api/Consumer5.java
new file mode 100644
index 00000000..523df53c
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Consumer5.java
@@ -0,0 +1,20 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+@FunctionalInterface
+public interface Consumer5<T1, T2, T3, T4, T5> {
+ void accept(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Function1.java b/src/main/java/com/code_intelligence/jazzer/api/Function1.java
new file mode 100644
index 00000000..43d68cc7
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Function1.java
@@ -0,0 +1,22 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+import java.util.function.Function;
+
+@FunctionalInterface
+public interface Function1<T1, R> extends Function<T1, R> {
+ @Override R apply(T1 t1);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Function2.java b/src/main/java/com/code_intelligence/jazzer/api/Function2.java
new file mode 100644
index 00000000..6e733b1c
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Function2.java
@@ -0,0 +1,22 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+import java.util.function.BiFunction;
+
+@FunctionalInterface
+public interface Function2<T1, T2, R> extends BiFunction<T1, T2, R> {
+ @Override R apply(T1 t1, T2 t2);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Function3.java b/src/main/java/com/code_intelligence/jazzer/api/Function3.java
new file mode 100644
index 00000000..07d593f9
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Function3.java
@@ -0,0 +1,20 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+@FunctionalInterface
+public interface Function3<T1, T2, T3, R> {
+ R apply(T1 t1, T2 t2, T3 t3);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Function4.java b/src/main/java/com/code_intelligence/jazzer/api/Function4.java
new file mode 100644
index 00000000..0e6ec75e
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Function4.java
@@ -0,0 +1,20 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+@FunctionalInterface
+public interface Function4<T1, T2, T3, T4, R> {
+ R apply(T1 t1, T2 t2, T3 t3, T4 t4);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Function5.java b/src/main/java/com/code_intelligence/jazzer/api/Function5.java
new file mode 100644
index 00000000..cd833f78
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Function5.java
@@ -0,0 +1,20 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+@FunctionalInterface
+public interface Function5<T1, T2, T3, T4, T5, R> {
+ R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java b/src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java
new file mode 100644
index 00000000..b1f38b50
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java
@@ -0,0 +1,444 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * A convenience wrapper turning the raw fuzzer input bytes into Java primitive types.
+ *
+ * <p>The methods defined by this interface behave similarly to {@link Random#nextInt()}, with all
+ * returned values depending deterministically on the fuzzer input for the current run.
+ */
+public interface FuzzedDataProvider {
+ /**
+ * Consumes a {@code boolean} from the fuzzer input.
+ *
+ * @return a {@code boolean}
+ */
+ boolean consumeBoolean();
+
+ /**
+ * Consumes a {@code boolean} array from the fuzzer input.
+ * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input
+ * is not sufficiently long.
+ *
+ * @param maxLength the maximum length of the array
+ * @return a {@code boolean} array of length at most {@code length}
+ */
+ boolean[] consumeBooleans(int maxLength);
+
+ /**
+ * Consumes a {@code byte} from the fuzzer input.
+ *
+ * @return a {@code byte}
+ */
+ byte consumeByte();
+
+ /**
+ * Consumes a {@code byte} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @param min the inclusive lower bound on the returned value
+ * @param max the inclusive upper bound on the returned value
+ * @return a {@code byte} in the range {@code [min, max]}
+ */
+ byte consumeByte(byte min, byte max);
+
+ /**
+ * Consumes a {@code byte} array from the fuzzer input.
+ * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input
+ * is not sufficiently long.
+ *
+ * @param maxLength the maximum length of the array
+ * @return a {@code byte} array of length at most {@code length}
+ */
+ byte[] consumeBytes(int maxLength);
+
+ /**
+ * Consumes the remaining fuzzer input as a {@code byte} array.
+ * <p><b>Note:</b> After calling this method, further calls to methods of this interface will
+ * return fixed values only.
+ *
+ * @return a {@code byte} array
+ */
+ byte[] consumeRemainingAsBytes();
+
+ /**
+ * Consumes a {@code short} from the fuzzer input.
+ *
+ * @return a {@code short}
+ */
+ short consumeShort();
+
+ /**
+ * Consumes a {@code short} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @param min the inclusive lower bound on the returned value
+ * @param max the inclusive upper bound on the returned value
+ * @return a {@code short} in the range {@code [min, max]}
+ */
+ short consumeShort(short min, short max);
+
+ /**
+ * Consumes a {@code short} array from the fuzzer input.
+ * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input
+ * is not sufficiently long.
+ *
+ * @param maxLength the maximum length of the array
+ * @return a {@code short} array of length at most {@code length}
+ */
+ short[] consumeShorts(int maxLength);
+
+ /**
+ * Consumes an {@code int} from the fuzzer input.
+ *
+ * @return an {@code int}
+ */
+ int consumeInt();
+
+ /**
+ * Consumes an {@code int} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @param min the inclusive lower bound on the returned value
+ * @param max the inclusive upper bound on the returned value
+ * @return an {@code int} in the range {@code [min, max]}
+ */
+ int consumeInt(int min, int max);
+
+ /**
+ * Consumes an {@code int} array from the fuzzer input.
+ * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input
+ * is not sufficiently long.
+ *
+ * @param maxLength the maximum length of the array
+ * @return an {@code int} array of length at most {@code length}
+ */
+ int[] consumeInts(int maxLength);
+
+ /**
+ * Consumes a {@code long} from the fuzzer input.
+ *
+ * @return a {@code long}
+ */
+ long consumeLong();
+
+ /**
+ * Consumes a {@code long} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @param min the inclusive lower bound on the returned value
+ * @param max the inclusive upper bound on the returned value
+ * @return a {@code long} in the range @{code [min, max]}
+ */
+ long consumeLong(long min, long max);
+
+ /**
+ * Consumes a {@code long} array from the fuzzer input.
+ * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input
+ * is not sufficiently long.
+ *
+ * @param maxLength the maximum length of the array
+ * @return a {@code long} array of length at most {@code length}
+ */
+ long[] consumeLongs(int maxLength);
+
+ /**
+ * Consumes a {@code float} from the fuzzer input.
+ *
+ * @return a {@code float} that may have a special value (e.g. a NaN or infinity)
+ */
+ float consumeFloat();
+
+ /**
+ * Consumes a regular {@code float} from the fuzzer input.
+ *
+ * @return a {@code float} that is not a special value (e.g. not a NaN or infinity)
+ */
+ float consumeRegularFloat();
+
+ /**
+ * Consumes a regular {@code float} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @return a {@code float} in the range {@code [min, max]}
+ */
+ float consumeRegularFloat(float min, float max);
+
+ /**
+ * Consumes a {@code float} between 0.0 and 1.0 (inclusive) from the fuzzer input.
+ *
+ * @return a {@code float} in the range {@code [0.0, 1.0]}
+ */
+ float consumeProbabilityFloat();
+
+ /**
+ * Consumes a {@code double} from the fuzzer input.
+ *
+ * @return a {@code double} that may have a special value (e.g. a NaN or infinity)
+ */
+ double consumeDouble();
+
+ /**
+ * Consumes a regular {@code double} from the fuzzer input.
+ *
+ * @return a {@code double} that is not a special value (e.g. not a NaN or infinity)
+ */
+ double consumeRegularDouble();
+
+ /**
+ * Consumes a regular {@code double} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @return a {@code double} in the range {@code [min, max]}
+ */
+ double consumeRegularDouble(double min, double max);
+
+ /**
+ * Consumes a {@code double} between 0.0 and 1.0 (inclusive) from the fuzzer input.
+ *
+ * @return a {@code double} in the range {@code [0.0, 1.0]}
+ */
+ double consumeProbabilityDouble();
+
+ /**
+ * Consumes a {@code char} from the fuzzer input.
+ */
+ char consumeChar();
+
+ /**
+ * Consumes a {@code char} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @param min the inclusive lower bound on the returned value
+ * @param max the inclusive upper bound on the returned value
+ * @return a {@code char} in the range {@code [min, max]}
+ */
+ char consumeChar(char min, char max);
+
+ /**
+ * Consumes a {@code char} from the fuzzer input that is never a UTF-16 surrogate character.
+ */
+ char consumeCharNoSurrogates();
+
+ /**
+ * Consumes a {@link String} from the fuzzer input.
+ * <p>The returned string may be of any length between 0 and {@code maxLength}, even if there is
+ * more fuzzer input available.
+ *
+ * @param maxLength the maximum length of the string
+ * @return a {@link String} of length between 0 and {@code maxLength} (inclusive)
+ */
+ String consumeString(int maxLength);
+
+ /**
+ * Consumes the remaining fuzzer input as a {@link String}.
+ * <p><b>Note:</b> After calling this method, further calls to methods of this interface will
+ * return fixed values only.
+ *
+ * @return a {@link String}
+ */
+ String consumeRemainingAsString();
+
+ /**
+ * Consumes an ASCII-only {@link String} from the fuzzer input.
+ * <p>The returned string may be of any length between 0 and {@code maxLength}, even if there is
+ * more fuzzer input available.
+ *
+ * @param maxLength the maximum length of the string
+ * @return a {@link String} of length between 0 and {@code maxLength} (inclusive) that contains
+ * only ASCII characters
+ */
+ String consumeAsciiString(int maxLength);
+
+ /**
+ * Consumes the remaining fuzzer input as an ASCII-only {@link String}.
+ * <p><b>Note:</b> After calling this method, further calls to methods of this interface will
+ * return fixed values only.
+ *
+ * @return a {@link String} that contains only ASCII characters
+ */
+ String consumeRemainingAsAsciiString();
+
+ /**
+ * Returns the number of unconsumed bytes in the fuzzer input.
+ *
+ * @return the number of unconsumed bytes in the fuzzer input
+ */
+ int remainingBytes();
+
+ /**
+ * Picks an element from {@code collection} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param collection the {@link Collection} to pick an element from.
+ * @param <T> the type of the collection element
+ * @return an element from {@code collection} chosen based on the fuzzer input
+ */
+ @SuppressWarnings("unchecked")
+ default<T> T pickValue(Collection<T> collection) {
+ int size = collection.size();
+ if (size == 0) {
+ throw new IllegalArgumentException("collection is empty");
+ }
+ if (collection instanceof List<?>) {
+ return ((List<T>) collection).get(consumeInt(0, size - 1));
+ } else {
+ return (T) pickValue(collection.toArray());
+ }
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @param <T> the type of the array element
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default<T> T pickValue(T[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default boolean pickValue(boolean[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default byte pickValue(byte[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default short pickValue(short[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default int pickValue(int[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default long pickValue(long[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default double pickValue(double[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default float pickValue(float[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default char pickValue(char[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks {@code numOfElements} elements from {@code collection} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param collection the {@link Collection} to pick an element from.
+ * @param numOfElements the number of elements to pick.
+ * @param <T> the type of the collection element
+ * @return an array of size {@code numOfElements} from {@code collection} chosen based on the
+ * fuzzer input
+ */
+ default<T> List<T> pickValues(Collection<T> collection, int numOfElements) {
+ int size = collection.size();
+ if (size == 0) {
+ throw new IllegalArgumentException("collection is empty");
+ }
+ if (numOfElements > collection.size()) {
+ throw new IllegalArgumentException("numOfElements exceeds collection.size()");
+ }
+
+ List<T> remainingElements = new ArrayList<>(collection);
+ List<T> pickedElements = new ArrayList<>();
+ for (int i = 0; i < numOfElements; i++) {
+ T element = pickValue(remainingElements);
+ pickedElements.add(element);
+ remainingElements.remove(element);
+ }
+ return pickedElements;
+ }
+
+ /**
+ * Picks {@code numOfElements} elements from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @param numOfElements the number of elements to pick.
+ * @param <T> the type of the array element
+ * @return an array of size {@code numOfElements} from {@code array} chosen based on the fuzzer
+ * input
+ */
+ default<T> List<T> pickValues(T[] array, int numOfElements) {
+ return pickValues(Arrays.asList(array), numOfElements);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java
new file mode 100644
index 00000000..fbde853b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java
@@ -0,0 +1,39 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+/**
+ * Thrown to indicate that a fuzz target has detected a critical severity security issue rather than
+ * a normal bug.
+ * <p>
+ * There is only a semantical but no functional difference between throwing exceptions of this type
+ * or any other. However, automated fuzzing platforms can use the extra information to handle the
+ * detected issues appropriately.
+ */
+public class FuzzerSecurityIssueCritical extends RuntimeException {
+ public FuzzerSecurityIssueCritical() {}
+
+ public FuzzerSecurityIssueCritical(String message) {
+ super(message);
+ }
+
+ public FuzzerSecurityIssueCritical(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public FuzzerSecurityIssueCritical(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java
new file mode 100644
index 00000000..05837b0e
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java
@@ -0,0 +1,39 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+/**
+ * Thrown to indicate that a fuzz target has detected a high severity security issue rather than a
+ * normal bug.
+ * <p>
+ * There is only a semantical but no functional difference between throwing exceptions of this type
+ * or any other. However, automated fuzzing platforms can use the extra information to handle the
+ * detected issues appropriately.
+ */
+public class FuzzerSecurityIssueHigh extends RuntimeException {
+ public FuzzerSecurityIssueHigh() {}
+
+ public FuzzerSecurityIssueHigh(String message) {
+ super(message);
+ }
+
+ public FuzzerSecurityIssueHigh(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public FuzzerSecurityIssueHigh(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java
new file mode 100644
index 00000000..364b3afb
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java
@@ -0,0 +1,39 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+/**
+ * Thrown to indicate that a fuzz target has detected a low severity security issue rather than a
+ * normal bug.
+ *
+ * There is only a semantical but no functional difference between throwing exceptions of this type
+ * or any other. However, automated fuzzing platforms can use the extra information to handle the
+ * detected issues appropriately.
+ */
+public class FuzzerSecurityIssueLow extends RuntimeException {
+ public FuzzerSecurityIssueLow() {}
+
+ public FuzzerSecurityIssueLow(String message) {
+ super(message);
+ }
+
+ public FuzzerSecurityIssueLow(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public FuzzerSecurityIssueLow(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java
new file mode 100644
index 00000000..be7c8c8f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java
@@ -0,0 +1,39 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+/**
+ * Thrown to indicate that a fuzz target has detected a medium severity security issue rather than a
+ * normal bug.
+ * <p>
+ * There is only a semantical but no functional difference between throwing exceptions of this type
+ * or any other. However, automated fuzzing platforms can use the extra information to handle the
+ * detected issues appropriately.
+ */
+public class FuzzerSecurityIssueMedium extends RuntimeException {
+ public FuzzerSecurityIssueMedium() {}
+
+ public FuzzerSecurityIssueMedium(String message) {
+ super(message);
+ }
+
+ public FuzzerSecurityIssueMedium(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public FuzzerSecurityIssueMedium(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/HookType.java b/src/main/java/com/code_intelligence/jazzer/api/HookType.java
new file mode 100644
index 00000000..8ed4337f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/HookType.java
@@ -0,0 +1,25 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+/**
+ * The type of a {@link MethodHook}.
+ */
+// Note: The order of entries is important and is used during instrumentation.
+public enum HookType {
+ BEFORE,
+ REPLACE,
+ AFTER,
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java b/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java
new file mode 100644
index 00000000..aad9ae01
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java
@@ -0,0 +1,268 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.InvocationTargetException;
+import java.security.SecureRandom;
+
+/**
+ * Static helper methods that hooks can use to provide feedback to the fuzzer.
+ */
+public final class Jazzer {
+ private static final Class<?> JAZZER_INTERNAL;
+
+ private static final MethodHandle ON_FUZZ_TARGET_READY;
+
+ private static final MethodHandle TRACE_STRCMP;
+ private static final MethodHandle TRACE_STRSTR;
+ private static final MethodHandle TRACE_MEMCMP;
+ private static final MethodHandle TRACE_PC_INDIR;
+
+ static {
+ Class<?> jazzerInternal = null;
+ MethodHandle onFuzzTargetReady = null;
+ MethodHandle traceStrcmp = null;
+ MethodHandle traceStrstr = null;
+ MethodHandle traceMemcmp = null;
+ MethodHandle tracePcIndir = null;
+ try {
+ jazzerInternal = Class.forName("com.code_intelligence.jazzer.runtime.JazzerInternal");
+ MethodType onFuzzTargetReadyType = MethodType.methodType(void.class, Runnable.class);
+ onFuzzTargetReady = MethodHandles.publicLookup().findStatic(
+ jazzerInternal, "registerOnFuzzTargetReadyCallback", onFuzzTargetReadyType);
+ Class<?> traceDataFlowNativeCallbacks =
+ Class.forName("com.code_intelligence.jazzer.runtime.TraceDataFlowNativeCallbacks");
+
+ // Use method handles for hints as the calls are potentially performance critical.
+ MethodType traceStrcmpType =
+ MethodType.methodType(void.class, String.class, String.class, int.class, int.class);
+ traceStrcmp = MethodHandles.publicLookup().findStatic(
+ traceDataFlowNativeCallbacks, "traceStrcmp", traceStrcmpType);
+ MethodType traceStrstrType =
+ MethodType.methodType(void.class, String.class, String.class, int.class);
+ traceStrstr = MethodHandles.publicLookup().findStatic(
+ traceDataFlowNativeCallbacks, "traceStrstr", traceStrstrType);
+ MethodType traceMemcmpType =
+ MethodType.methodType(void.class, byte[].class, byte[].class, int.class, int.class);
+ traceMemcmp = MethodHandles.publicLookup().findStatic(
+ traceDataFlowNativeCallbacks, "traceMemcmp", traceMemcmpType);
+ MethodType tracePcIndirType = MethodType.methodType(void.class, int.class, int.class);
+ tracePcIndir = MethodHandles.publicLookup().findStatic(
+ traceDataFlowNativeCallbacks, "tracePcIndir", tracePcIndirType);
+ } catch (ClassNotFoundException ignore) {
+ // Not running in the context of the agent. This is fine as long as no methods are called on
+ // this class.
+ } catch (NoSuchMethodException | IllegalAccessException e) {
+ // This should never happen as the Jazzer API is loaded from the agent and thus should always
+ // match the version of the runtime classes.
+ System.err.println("ERROR: Incompatible version of the Jazzer API detected, please update.");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ JAZZER_INTERNAL = jazzerInternal;
+ ON_FUZZ_TARGET_READY = onFuzzTargetReady;
+ TRACE_STRCMP = traceStrcmp;
+ TRACE_STRSTR = traceStrstr;
+ TRACE_MEMCMP = traceMemcmp;
+ TRACE_PC_INDIR = tracePcIndir;
+ }
+
+ private Jazzer() {}
+
+ /**
+ * A 32-bit random number that hooks can use to make pseudo-random choices
+ * between multiple possible mutations they could guide the fuzzer towards.
+ * Hooks <b>must not</b> base the decision whether or not to report a finding
+ * on this number as this will make findings non-reproducible.
+ * <p>
+ * This is the same number that libFuzzer uses as a seed internally, which
+ * makes it possible to deterministically reproduce a previous fuzzing run by
+ * supplying the seed value printed by libFuzzer as the value of the
+ * {@code -seed}.
+ */
+ public static final int SEED = getLibFuzzerSeed();
+
+ /**
+ * Instructs the fuzzer to guide its mutations towards making {@code current} equal to {@code
+ * target}.
+ * <p>
+ * If the relation between the raw fuzzer input and the value of {@code current} is relatively
+ * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to
+ * achieve equality.
+ *
+ * @param current a non-constant string observed during fuzz target execution
+ * @param target a string that {@code current} should become equal to, but currently isn't
+ * @param id a (probabilistically) unique identifier for this particular compare hint
+ */
+ public static void guideTowardsEquality(String current, String target, int id) {
+ if (TRACE_STRCMP == null) {
+ return;
+ }
+ try {
+ TRACE_STRCMP.invokeExact(current, target, 1, id);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Instructs the fuzzer to guide its mutations towards making {@code current} equal to {@code
+ * target}.
+ * <p>
+ * If the relation between the raw fuzzer input and the value of {@code current} is relatively
+ * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to
+ * achieve equality.
+ *
+ * @param current a non-constant byte array observed during fuzz target execution
+ * @param target a byte array that {@code current} should become equal to, but currently isn't
+ * @param id a (probabilistically) unique identifier for this particular compare hint
+ */
+ public static void guideTowardsEquality(byte[] current, byte[] target, int id) {
+ if (TRACE_MEMCMP == null) {
+ return;
+ }
+ try {
+ TRACE_MEMCMP.invokeExact(current, target, 1, id);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Instructs the fuzzer to guide its mutations towards making {@code haystack} contain {@code
+ * needle} as a substring.
+ * <p>
+ * If the relation between the raw fuzzer input and the value of {@code haystack} is relatively
+ * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to
+ * satisfy the substring check.
+ *
+ * @param haystack a non-constant string observed during fuzz target execution
+ * @param needle a string that should be contained in {@code haystack} as a substring, but
+ * currently isn't
+ * @param id a (probabilistically) unique identifier for this particular compare hint
+ */
+ public static void guideTowardsContainment(String haystack, String needle, int id) {
+ if (TRACE_STRSTR == null) {
+ return;
+ }
+ try {
+ TRACE_STRSTR.invokeExact(haystack, needle, id);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Instructs the fuzzer to attain as many possible values for the absolute value of {@code state}
+ * as possible.
+ * <p>
+ * Call this function from a fuzz target or a hook to help the fuzzer track partial progress
+ * (e.g. by passing the length of a common prefix of two lists that should become equal) or
+ * explore different values of state that is not directly related to code coverage (see the
+ * MazeFuzzer example).
+ * <p>
+ * <b>Note:</b> This hint only takes effect if the fuzzer is run with the argument
+ * {@code -use_value_profile=1}.
+ *
+ * @param state a numeric encoding of a state that should be varied by the fuzzer
+ * @param id a (probabilistically) unique identifier for this particular state hint
+ */
+ public static void exploreState(byte state, int id) {
+ if (TRACE_PC_INDIR == null) {
+ return;
+ }
+ // We only use the lower 7 bits of state, which allows for 128 different state values tracked
+ // per id. The particular amount of 7 bits of state is also used in libFuzzer's
+ // TracePC::HandleCmp:
+ // https://github.com/llvm/llvm-project/blob/c12d49c4e286fa108d4d69f1c6d2b8d691993ffd/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L390
+ // This value should be large enough for most use cases (e.g. tracking the length of a prefix in
+ // a comparison) while being small enough that the bitmap isn't filled up too quickly
+ // (65536 bits / 128 bits per id = 512 ids).
+
+ // We use tracePcIndir as a way to set a bit in libFuzzer's value profile bitmap. In
+ // TracePC::HandleCallerCallee, which is what this function ultimately calls through to, the
+ // lower 12 bits of each argument are combined into a 24-bit index into the bitmap, which is
+ // then reduced modulo a 16-bit prime. To keep the modulo bias small, we should fill as many
+ // of the relevant bits as possible.
+
+ // We pass state in the lowest bits of the caller address, which is used to form the lowest bits
+ // of the bitmap index. This should result in the best caching behavior as state is expected to
+ // change quickly in consecutive runs and in this way all its bitmap entries would be located
+ // close to each other in memory.
+ int lowerBits = (state & 0x7f) | (id << 7);
+ int upperBits = id >>> 5;
+ try {
+ TRACE_PC_INDIR.invokeExact(upperBits, lowerBits);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Make Jazzer report the provided {@link Throwable} as a finding.
+ * <p>
+ * <b>Note:</b> This method must only be called from a method hook. In a
+ * fuzz target, simply throw an exception to trigger a finding.
+ * @param finding the finding that Jazzer should report
+ */
+ public static void reportFindingFromHook(Throwable finding) {
+ try {
+ JAZZER_INTERNAL.getMethod("reportFindingFromHook", Throwable.class).invoke(null, finding);
+ } catch (NullPointerException | IllegalAccessException | NoSuchMethodException e) {
+ // We can only reach this point if the runtime is not on the classpath, e.g. in case of a
+ // reproducer. Just throw the finding.
+ rethrowUnchecked(finding);
+ } catch (InvocationTargetException e) {
+ rethrowUnchecked(e.getCause());
+ }
+ }
+
+ /**
+ * Register a callback to be executed right before the fuzz target is executed for the first time.
+ * <p>
+ * This can be used to disable hooks until after Jazzer has been fully initializing, e.g. to
+ * prevent Jazzer internals from triggering hooks on Java standard library classes.
+ *
+ * @param callback the callback to execute
+ */
+ public static void onFuzzTargetReady(Runnable callback) {
+ try {
+ ON_FUZZ_TARGET_READY.invokeExact(callback);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static int getLibFuzzerSeed() {
+ // The Jazzer driver sets this property based on the value of libFuzzer's -seed command-line
+ // option, which allows for fully reproducible fuzzing runs if set. If not running in the
+ // context of the driver, fall back to a random number instead.
+ String rawSeed = System.getProperty("jazzer.internal.seed");
+ if (rawSeed == null) {
+ return new SecureRandom().nextInt();
+ }
+ // If jazzer.internal.seed is set, we expect it to be a valid integer.
+ return Integer.parseUnsignedInt(rawSeed);
+ }
+
+ // Rethrows a (possibly checked) exception while avoiding a throws declaration.
+ @SuppressWarnings("unchecked")
+ private static <T extends Throwable> void rethrowUnchecked(Throwable t) throws T {
+ throw(T) t;
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java b/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java
new file mode 100644
index 00000000..3a1c5f39
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java
@@ -0,0 +1,207 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.invoke.MethodType;
+
+/**
+ * Registers the annotated method as a hook that should run before, instead or
+ * after the method specified by the annotation parameters.
+ * <p>
+ * Depending on {@link #type()} this method will be called after, instead or
+ * before every call to the target method and has
+ * access to its parameters and return value. The target method is specified by
+ * {@link #targetClassName()} and {@link #targetMethod()}. In case of an
+ * overloaded method, {@link #targetMethodDescriptor()} can be used to restrict
+ * the application of the hook to a particular overload.
+ * <p>
+ * The signature of the annotated method must be as follows (this does not
+ * restrict the method name and parameter names, which are arbitrary),
+ * depending on the value of {@link #type()}:
+ *
+ * <dl>
+ * <dt><span class="strong">{@link HookType#BEFORE}</span>
+ * <dd>
+ * <pre>{@code
+ * public static void hook(MethodHandle method, Object thisObject, Object[] arguments, int hookId)
+ * }</pre>
+ * Arguments:
+ * <p><ul>
+ * <li>{@code method}: A {@link java.lang.invoke.MethodHandle} representing the
+ * original method. The original method can be invoked via
+ * {@link java.lang.invoke.MethodHandle#invokeWithArguments(Object...)}. This
+ * requires passing {@code thisObject} as the first argument if the method is
+ * not static. This argument can be {@code null}.
+ * <li>{@code thisObject}: An {@link Object} containing the implicit
+ * {@code this} argument to the original method. If the original method is
+ * static, this argument will be {@code null}.
+ * <li>{@code arguments}: An array of {@link Object}s containing the arguments
+ * passed to the original method. Primitive types (e.g. {@code boolean}) will be
+ * wrapped into their corresponding wrapper type (e.g. {@link Boolean}).
+ * <li>{@code hookId}: A random {@code int} identifying the particular call
+ * site.This can be used to derive additional coverage information.
+ * </ul>
+ *
+ * <dt><span class="strong">{@link HookType#REPLACE}</span>
+ * <dd>
+ * <pre>{@code
+ * public static Object hook(MethodHandle method, Object thisObject, Object[] arguments, int hookId)
+ * }</pre>
+ * The return type may alternatively be taken to be the exact return type of
+ * target method or a wrapper type thereof. The returned object will be casted
+ * and unwrapped automatically.
+ * <p>
+ * Arguments:
+ * <p><ul>
+ * <li>{@code method}: A {@link java.lang.invoke.MethodHandle} representing the
+ * original method. The original method can be invoked via
+ * {@link java.lang.invoke.MethodHandle#invokeWithArguments(Object...)}. This
+ * requires passing {@code thisObject} as the first argument if the method is
+ * not static. This argument can be {@code null}.
+ * <li>{@code thisObject}: An {@link Object} containing the implicit
+ * {@code this} argument to the original method. If the original method is
+ * static, this argument will be {@code null}.
+ * <li>{@code arguments}: An array of {@link Object}s containing the arguments
+ * passed to the original method. Primitive types (e.g. {@code boolean}) will be
+ * wrapped into their corresponding wrapper type (e.g. {@link Boolean}).
+ * <li>{@code hookId}: A random {@code int} identifying the particular call
+ * site.This can be used to derive additional coverage information.
+ * </ul><p>
+ * <p>
+ * Return value: the value that should take the role of the value the target
+ * method would have returned
+ * <p>
+ * <dt><span class="strong">{@link HookType#AFTER}</span>
+ * <dd>
+ * <pre>{@code
+ * public static void hook(MethodHandle method, Object thisObject, Object[] arguments, int hookId,
+ * Object returnValue)
+ * }</pre>
+ * Arguments:
+ * <p><ul>
+ * <li>{@code method}: A {@link java.lang.invoke.MethodHandle} representing the
+ * original method. The original method can be invoked via
+ * {@link java.lang.invoke.MethodHandle#invokeWithArguments(Object...)}. This
+ * requires passing {@code thisObject} as the first argument if the method is
+ * not static. This argument can be {@code null}.
+ * <li>{@code thisObject}: An {@link Object} containing the implicit
+ * {@code this} argument to the original method. If the original method is
+ * static, this argument will be {@code null}.
+ * <li>{@code arguments}: An array of {@link Object}s containing the arguments
+ * passed to the original method. Primitive types (e.g. {@code boolean}) will be
+ * wrapped into their corresponding wrapper type (e.g. {@link Boolean}).
+ * <li>{@code hookId}: A random {@code int} identifying the particular call
+ * site.This can be used to derive additional coverage information.
+ * <li>{@code returnValue}: An {@link Object} containing the return value of the
+ * invocation of the original method. Primitive types (e.g. {@code boolean})
+ * will be wrapped into their corresponding wrapper type (e.g. {@link Boolean}).
+ * If the original method has return type {@code void}, this value will be
+ * {@code null}.
+ * <p>
+ * Multiple {@link HookType#BEFORE} and {@link HookType#AFTER} hooks are
+ * allowed to reference the same target method. Exclusively one
+ * {@link HookType#REPLACE} hook may reference a target method, no other types
+ * allowed. Attention must be paid to not guide the Fuzzer in different
+ * directions via {@link Jazzer}'s {@code guideTowardsXY} methods in the
+ * different hooks.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Repeatable(MethodHooks.class)
+@Documented
+public @interface MethodHook {
+ /**
+ * The time at which the annotated method should be called.
+ * <p>
+ * If this is {@link HookType#BEFORE}, the annotated method will be called
+ * before the target method and has access to its arguments.
+ * <p>
+ * If this is {@link HookType#REPLACE}, the annotated method will be called
+ * instead of the target method. It has access to its arguments and can
+ * return a value that will replace the target method's return value.
+ * <p>
+ * If this is {@link HookType#AFTER}, the annotated method will be called
+ * after the target method and has access to its arguments and return
+ * value.
+ *
+ * @return when the hook should be called
+ */
+ HookType type();
+
+ /**
+ * The name of the class that contains the method that should be hooked,
+ * as returned by {@link Class#getName()}.
+ * <p>
+ * If an interface or abstract class is specified, also calls to all
+ * implementations and subclasses available on the classpath during startup
+ * are hooked, respectively. Interfaces and subclasses are not taken into
+ * account for concrete classes.
+ * <p>
+ * Examples:
+ * <p><ul>
+ * <li>{@link String}: {@code "java.lang.String"}
+ * <li>{@link java.nio.file.FileSystem}: {@code "java.nio.file.FileSystem"}
+ * </ul><p>
+ *
+ * @return the name of the class containing the method to be hooked
+ */
+ String targetClassName();
+
+ /**
+ * The name of the method to be hooked. Use {@code "<init>"} for
+ * constructors.
+ * <p>
+ * Examples:
+ * <p><ul>
+ * <li>{@link String#equals(Object)}: {@code "equals"}
+ * <li>{@link String#String()}: {@code "<init>"}
+ * </ul><p>
+ *
+ * @return the name of the method to be hooked
+ */
+ String targetMethod();
+
+ /**
+ * The descriptor of the method to be hooked. This is only needed if there
+ * are multiple methods with the same name and not all of them should be
+ * hooked.
+ * <p>
+ * The descriptor of a method is an internal representation of the method's
+ * signature, which includes the types of its parameters and its return
+ * value. For more information on descriptors, see the
+ * <a href=https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-4.html#jvms-4.3.3>JVM
+ * Specification, Section 4.3.3</a> and {@link MethodType#toMethodDescriptorString()}
+ *
+ * @return the descriptor of the method to be hooked
+ */
+ String targetMethodDescriptor() default "";
+
+ /**
+ * Array of additional classes to hook.
+ * <p>
+ * Hooks are applied on call sites. This means that classes calling the one
+ * defined in this annotation need to be instrumented to actually execute
+ * the hook. This property can be used to hook normally ignored classes.
+ *
+ * @return fully qualified class names to hook
+ */
+ String[] additionalClassesToHook() default {};
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java b/src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java
new file mode 100644
index 00000000..7eec24b3
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java
@@ -0,0 +1,31 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.api;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Internal helper allowing to apply multiple {@link MethodHook} annotations to the same method.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface MethodHooks {
+ MethodHook[] value();
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/SilentCloseable.java b/src/main/java/com/code_intelligence/jazzer/api/SilentCloseable.java
new file mode 100644
index 00000000..3f2d6e3f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/api/SilentCloseable.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.api;
+
+/**
+ * A specialization of {@link AutoCloseable} without a {@code throws} declarations on
+ * {@link #close()}.
+ */
+public interface SilentCloseable extends AutoCloseable {
+ @Override void close();
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/AccessibleObjectLookup.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/AccessibleObjectLookup.java
new file mode 100644
index 00000000..a45a474b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/AccessibleObjectLookup.java
@@ -0,0 +1,147 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.autofuzz;
+
+import io.github.classgraph.ClassInfo;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+class AccessibleObjectLookup {
+ private static final Comparator<Class<?>> STABLE_CLASS_COMPARATOR =
+ Comparator.comparing(Class::getName);
+ private static final Comparator<Executable> STABLE_EXECUTABLE_COMPARATOR =
+ Comparator.comparing(Executable::getName).thenComparing(executable -> {
+ if (executable instanceof Method) {
+ return org.objectweb.asm.Type.getMethodDescriptor((Method) executable);
+ } else {
+ return org.objectweb.asm.Type.getConstructorDescriptor((Constructor<?>) executable);
+ }
+ });
+
+ private final Class<?> referenceClass;
+
+ public AccessibleObjectLookup(Class<?> referenceClass) {
+ this.referenceClass = referenceClass;
+ }
+
+ Class<?>[] getAccessibleClasses(Class<?> type) {
+ return Stream.concat(Arrays.stream(type.getDeclaredClasses()), Arrays.stream(type.getClasses()))
+ .distinct()
+ .filter(this::isAccessible)
+ .sorted(STABLE_CLASS_COMPARATOR)
+ .toArray(Class<?>[] ::new);
+ }
+
+ Constructor<?>[] getAccessibleConstructors(Class<?> type) {
+ // Neither of getDeclaredConstructors and getConstructors is a superset of the other: While
+ // getDeclaredConstructors returns constructors with all visibility modifiers, it does not
+ // return the implicit default constructor.
+ return Stream
+ .concat(
+ Arrays.stream(type.getDeclaredConstructors()), Arrays.stream(type.getConstructors()))
+ .distinct()
+ .filter(this::isAccessible)
+ .sorted(STABLE_EXECUTABLE_COMPARATOR)
+ .filter(constructor -> {
+ try {
+ constructor.setAccessible(true);
+ return true;
+ } catch (Exception e) {
+ // Can't make the constructor accessible, e.g. because it is in a standard library
+ // module. We can't do anything about this, so we skip the constructor.
+ return false;
+ }
+ })
+ .toArray(Constructor<?>[] ::new);
+ }
+
+ Method[] getAccessibleMethods(Class<?> type) {
+ return Stream.concat(Arrays.stream(type.getDeclaredMethods()), Arrays.stream(type.getMethods()))
+ .distinct()
+ .filter(this::isAccessible)
+ .sorted(STABLE_EXECUTABLE_COMPARATOR)
+ .filter(method -> {
+ try {
+ method.setAccessible(true);
+ return true;
+ } catch (Exception e) {
+ // Can't make the method accessible, e.g. because it is in a standard library module. We
+ // can't do anything about this, so we skip the method.
+ return false;
+ }
+ })
+ .toArray(Method[] ::new);
+ }
+
+ boolean isAccessible(Class<?> clazz, int modifiers) {
+ if (Modifier.isPublic(modifiers)) {
+ return true;
+ }
+ if (referenceClass == null) {
+ return false;
+ }
+ if (Modifier.isPrivate(modifiers)) {
+ return clazz.equals(referenceClass);
+ }
+ if (Modifier.isProtected(modifiers)) {
+ return clazz.isAssignableFrom(referenceClass);
+ }
+ // No visibility modifiers implies default visibility, which means visible in the same package.
+ return clazz.getPackage().equals(referenceClass.getPackage());
+ }
+
+ boolean isAccessible(ClassInfo clazz, int modifiers) {
+ if (Modifier.isPublic(modifiers)) {
+ return true;
+ }
+ if (referenceClass == null) {
+ return false;
+ }
+ if (Modifier.isPrivate(modifiers)) {
+ return clazz.getName().equals(referenceClass.getName());
+ }
+ if (Modifier.isProtected(modifiers)) {
+ return isAssignableFrom(clazz, referenceClass);
+ }
+ // No visibility modifiers implies default visibility, which means visible in the same package.
+ return clazz.getPackageName().equals(referenceClass.getPackage().getName());
+ }
+
+ boolean isAssignableFrom(ClassInfo clazz, Class<?> potentialSubclass) {
+ if (potentialSubclass.getName().equals(clazz.getName())) {
+ return true;
+ }
+ if (potentialSubclass.equals(Object.class)) {
+ return clazz.getName().equals(Object.class.getName());
+ }
+ if (potentialSubclass.getSuperclass() == null) {
+ return false;
+ }
+ return isAssignableFrom(clazz, potentialSubclass.getSuperclass());
+ }
+
+ private boolean isAccessible(Executable executable) {
+ return isAccessible(executable.getDeclaringClass(), executable.getModifiers());
+ }
+
+ private boolean isAccessible(Class<?> clazz) {
+ return isAccessible(clazz, clazz.getModifiers());
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java
new file mode 100644
index 00000000..2ea4e9b0
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java
@@ -0,0 +1,116 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.autofuzz;
+
+import java.util.Stack;
+import java.util.stream.Collectors;
+
+public class AutofuzzCodegenVisitor {
+ private final Stack<Group> groups = new Stack<>();
+ private int variableCounter = 0;
+
+ AutofuzzCodegenVisitor() {
+ init();
+ }
+
+ private void init() {
+ pushGroup("", "", "");
+ }
+
+ public void pushGroup(String prefix, String delimiter, String suffix) {
+ groups.push(new Group(prefix, delimiter, suffix));
+ }
+
+ public void pushElement(String element) {
+ groups.peek().push(element);
+ }
+
+ public void popElement() {
+ groups.peek().pop();
+ }
+
+ public void popGroup() {
+ if (groups.size() == 1) {
+ throw new AutofuzzError(
+ "popGroup must be called exactly once for every pushGroup: " + toDebugString());
+ }
+ pushElement(groups.pop().toString());
+ }
+
+ public String generate() {
+ if (groups.size() != 1) {
+ throw new AutofuzzError(
+ "popGroup must be called exactly once for every pushGroup: " + toDebugString());
+ }
+ return groups.pop().toString();
+ }
+
+ public void addCharLiteral(char c) {
+ pushElement("'" + escapeForLiteral(Character.toString(c)) + "'");
+ }
+
+ public void addStringLiteral(String string) {
+ pushElement('"' + escapeForLiteral(string) + '"');
+ }
+
+ public String uniqueVariableName() {
+ return String.format("autofuzzVariable%s", variableCounter++);
+ }
+
+ static String escapeForLiteral(String string) {
+ // The list of escape sequences is taken from:
+ // https://docs.oracle.com/javase/tutorial/java/data/characters.html
+ return string.replace("\\", "\\\\")
+ .replace("\t", "\\t")
+ .replace("\b", "\\b")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\f", "\\f")
+ .replace("\"", "\\\"")
+ .replace("'", "\\'");
+ }
+
+ private String toDebugString() {
+ return groups.stream()
+ .map(group -> group.elements.stream().collect(Collectors.joining(", ", "[", "]")))
+ .collect(Collectors.joining(", ", "[", "]"));
+ }
+
+ private static class Group {
+ private final String prefix;
+ private final String delimiter;
+ private final String suffix;
+ private final Stack<String> elements = new Stack<>();
+
+ Group(String prefix, String delimiter, String suffix) {
+ this.prefix = prefix;
+ this.delimiter = delimiter;
+ this.suffix = suffix;
+ }
+
+ public void push(String element) {
+ elements.push(element);
+ }
+
+ public void pop() {
+ elements.pop();
+ }
+
+ @Override
+ public String toString() {
+ return elements.stream().collect(Collectors.joining(delimiter, prefix, suffix));
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java
new file mode 100644
index 00000000..a94b385d
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java
@@ -0,0 +1,31 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.autofuzz;
+
+/**
+ * An error indicating an internal error in the autofuzz functionality.
+ */
+public class AutofuzzError extends Error {
+ private static final String MESSAGE_TRAILER = String.format(
+ "%nPlease file an issue at:%n https://github.com/CodeIntelligenceTesting/jazzer/issues/new/choose");
+
+ public AutofuzzError(String message) {
+ super(message + MESSAGE_TRAILER);
+ }
+
+ public AutofuzzError(String message, Throwable cause) {
+ super(message + MESSAGE_TRAILER, cause);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel
new file mode 100644
index 00000000..04a076e6
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel
@@ -0,0 +1,22 @@
+java_library(
+ name = "autofuzz",
+ srcs = [
+ "AccessibleObjectLookup.java",
+ "AutofuzzCodegenVisitor.java",
+ "AutofuzzError.java",
+ "FuzzTarget.java",
+ "Meta.java",
+ "YourAverageJavaClass.java",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap_compile_only",
+ "//src/main/java/com/code_intelligence/jazzer/utils",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ "//src/main/java/com/code_intelligence/jazzer/utils:simple_glob_matcher",
+ "@com_github_classgraph_classgraph//:classgraph",
+ "@com_github_jhalterman_typetools//:typetools",
+ "@org_ow2_asm_asm//jar",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java
new file mode 100644
index 00000000..885ebfbf
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java
@@ -0,0 +1,363 @@
+// Copyright 2023 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.autofuzz;
+
+import com.code_intelligence.jazzer.api.AutofuzzConstructionException;
+import com.code_intelligence.jazzer.api.AutofuzzInvocationException;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.utils.Log;
+import com.code_intelligence.jazzer.utils.SimpleGlobMatcher;
+import com.code_intelligence.jazzer.utils.Utils;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public final class FuzzTarget {
+ private static final String AUTOFUZZ_REPRODUCER_TEMPLATE = "public class Crash_%1$s {\n"
+ + " public static void main(String[] args) throws Throwable {\n"
+ + " Crash_%1$s.class.getClassLoader().setDefaultAssertionStatus(true);\n"
+ + " %2$s;\n"
+ + " }\n"
+ + "}";
+ private static final long MAX_EXECUTIONS_WITHOUT_INVOCATION = 100;
+
+ private static Meta meta;
+ private static String methodReference;
+ private static Executable[] targetExecutables;
+ private static Object targetInstance;
+ private static Map<Executable, Class<?>[]> throwsDeclarations;
+ private static Set<SimpleGlobMatcher> ignoredExceptionMatchers;
+ private static long executionsSinceLastInvocation = 0;
+
+ public static void fuzzerInitialize(String[] args) {
+ if (args.length == 0 || !args[0].contains("::")) {
+ Log.error(
+ "Expected the argument to --autofuzz to be a method reference (e.g. System.out::println)");
+ System.exit(1);
+ }
+ String methodSignature = args[0];
+ String[] parts = methodSignature.split("::", 2);
+ String className = parts[0];
+ String methodNameAndOptionalDescriptor = parts[1];
+ String methodName;
+ String descriptor;
+ int descriptorStart = methodNameAndOptionalDescriptor.indexOf('(');
+ if (descriptorStart != -1) {
+ methodName = methodNameAndOptionalDescriptor.substring(0, descriptorStart);
+ // URL decode the descriptor to allow copy-pasting from javadoc links such as:
+ // https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#valueOf(char%5B%5D)
+ try {
+ descriptor =
+ URLDecoder.decode(methodNameAndOptionalDescriptor.substring(descriptorStart), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // UTF-8 is always supported.
+ Log.error(e);
+ System.exit(1);
+ return;
+ }
+ } else {
+ methodName = methodNameAndOptionalDescriptor;
+ descriptor = null;
+ }
+
+ Class<?> targetClassTemp = null;
+ String targetClassName = className;
+ do {
+ try {
+ targetClassTemp = Class.forName(targetClassName);
+ } catch (ClassNotFoundException e) {
+ int classSeparatorIndex = targetClassName.lastIndexOf(".");
+ if (classSeparatorIndex == -1) {
+ Log.error(String.format(
+ "Failed to find class %s for autofuzz, please ensure it is contained in the classpath specified with --cp and specify the full package name",
+ className));
+ System.exit(1);
+ return;
+ }
+ StringBuilder classNameBuilder = new StringBuilder(targetClassName);
+ classNameBuilder.setCharAt(classSeparatorIndex, '$');
+ targetClassName = classNameBuilder.toString();
+ }
+ } while (targetClassTemp == null);
+ final Class<?> targetClass = targetClassTemp;
+
+ AccessibleObjectLookup lookup = new AccessibleObjectLookup(targetClass);
+
+ Executable[] executables;
+ boolean isConstructor = methodName.equals("new");
+ // We filter out inherited methods, which can lead to unexpected results when autofuzzing a
+ // method by name without a descriptor. If desired, these can be autofuzzed explicitly by
+ // referencing the parent class. If a descriptor is provided, we also allow fuzzing non-public
+ // methods. This is necessary e.g. when using Autofuzz on a package-private JUnit @FuzzTest
+ // method.
+ if (isConstructor) {
+ executables = Arrays.stream(lookup.getAccessibleConstructors(targetClass))
+ .filter(constructor -> constructor.getDeclaringClass().equals(targetClass))
+ .filter(constructor
+ -> (descriptor == null && Modifier.isPublic(constructor.getModifiers()))
+ || Utils.getReadableDescriptor(constructor).equals(descriptor))
+ .toArray(Executable[] ::new);
+ } else {
+ executables = Arrays.stream(lookup.getAccessibleMethods(targetClass))
+ .filter(method -> method.getDeclaringClass().equals(targetClass))
+ .filter(method
+ -> method.getName().equals(methodName)
+ && ((descriptor == null && Modifier.isPublic(method.getModifiers()))
+ || Utils.getReadableDescriptor(method).equals(descriptor)))
+ .toArray(Executable[] ::new);
+ }
+ if (executables.length == 0) {
+ if (isConstructor) {
+ if (descriptor == null) {
+ Log.error(
+ String.format("Failed to find constructors in class %s for autofuzz.%n", className));
+ } else {
+ Log.error(String.format(
+ "Failed to find constructors with signature %s in class %s for autofuzz.%n"
+ + "Public constructors declared by the class:%n%s",
+ descriptor, className,
+ Arrays.stream(lookup.getAccessibleConstructors(targetClass))
+ .filter(constructor -> Modifier.isPublic(constructor.getModifiers()))
+ .filter(constructor -> constructor.getDeclaringClass().equals(targetClass))
+ .map(method
+ -> String.format("%s::new%s", method.getDeclaringClass().getName(),
+ Utils.getReadableDescriptor(method)))
+ .distinct()
+ .collect(Collectors.joining(System.lineSeparator()))));
+ }
+ } else {
+ if (descriptor == null) {
+ Log.error(String.format("Failed to find methods named %s in class %s for autofuzz.%n"
+ + "Public methods declared by the class:%n%s",
+ methodName, className,
+ Arrays.stream(lookup.getAccessibleMethods(targetClass))
+ .filter(method -> Modifier.isPublic(method.getModifiers()))
+ .filter(method -> method.getDeclaringClass().equals(targetClass))
+ .map(method
+ -> String.format(
+ "%s::%s", method.getDeclaringClass().getName(), method.getName()))
+ .distinct()
+ .collect(Collectors.joining(System.lineSeparator()))));
+ } else {
+ Log.error(String.format(
+ "Failed to find public methods named %s with signature %s in class %s for autofuzz.%n"
+ + "Public methods with that name:%n%s",
+ methodName, descriptor, className,
+ Arrays.stream(lookup.getAccessibleMethods(targetClass))
+ .filter(method -> Modifier.isPublic(method.getModifiers()))
+ .filter(method -> method.getDeclaringClass().equals(targetClass))
+ .filter(method -> method.getName().equals(methodName))
+ .map(method
+ -> String.format("%s::%s%s", method.getDeclaringClass().getName(),
+ method.getName(), Utils.getReadableDescriptor(method)))
+ .distinct()
+ .collect(Collectors.joining(System.lineSeparator()))));
+ }
+ }
+ System.exit(1);
+ }
+
+ Set<SimpleGlobMatcher> ignoredExceptionGlobMatchers = Arrays.stream(args)
+ .skip(1)
+ .filter(s -> s.contains("*"))
+ .map(SimpleGlobMatcher::new)
+ .collect(Collectors.toSet());
+
+ List<Class<?>> alwaysIgnore =
+ Arrays.stream(args)
+ .skip(1)
+ .filter(s -> !s.contains("*"))
+ .map(name -> {
+ try {
+ return ClassLoader.getSystemClassLoader().loadClass(name);
+ } catch (ClassNotFoundException e) {
+ Log.error(String.format(
+ "Failed to find class '%s' specified in --autofuzz_ignore", name));
+ System.exit(1);
+ }
+ throw new Error("Not reached");
+ })
+ .collect(Collectors.toList());
+
+ Map<Executable, Class<?>[]> ignoredExceptionClasses =
+ Arrays.stream(executables)
+ .collect(Collectors.toMap(method
+ -> method,
+ method
+ -> Stream.concat(Arrays.stream(method.getExceptionTypes()), alwaysIgnore.stream())
+ .toArray(Class[] ::new)));
+
+ setTarget(
+ executables, null, methodSignature, ignoredExceptionGlobMatchers, ignoredExceptionClasses);
+ }
+
+ /**
+ * Set the target executables to (auto-)fuzz. This method is primarily used by the JUnit
+ * integration to set the target class and method passed in by the test framework.
+ */
+ public static void setTarget(Executable[] targetExecutables, Object targetInstance,
+ String methodReference, Set<SimpleGlobMatcher> ignoredExceptionMatchers,
+ Map<Executable, Class<?>[]> throwsDeclarations) {
+ Class<?> targetClass = null;
+ for (Executable executable : targetExecutables) {
+ if (targetClass != null && !targetClass.equals(executable.getDeclaringClass())) {
+ throw new IllegalStateException(
+ "All target executables must be declared in the same class");
+ }
+ targetClass = executable.getDeclaringClass();
+ executable.setAccessible(true);
+ }
+
+ FuzzTarget.meta = new Meta(targetClass);
+ FuzzTarget.targetExecutables = targetExecutables;
+ FuzzTarget.targetInstance = targetInstance;
+ FuzzTarget.methodReference = methodReference;
+ FuzzTarget.ignoredExceptionMatchers = ignoredExceptionMatchers;
+ FuzzTarget.throwsDeclarations = throwsDeclarations;
+ }
+
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Throwable {
+ AutofuzzCodegenVisitor codegenVisitor = null;
+ if (Meta.IS_DEBUG) {
+ codegenVisitor = new AutofuzzCodegenVisitor();
+ }
+ fuzzerTestOneInput(data, codegenVisitor);
+ if (codegenVisitor != null) {
+ Log.println(codegenVisitor.generate());
+ }
+ }
+
+ public static void dumpReproducer(FuzzedDataProvider data, String reproducerPath, String sha) {
+ AutofuzzCodegenVisitor codegenVisitor = new AutofuzzCodegenVisitor();
+ try {
+ fuzzerTestOneInput(data, codegenVisitor);
+ } catch (Throwable ignored) {
+ }
+ String javaSource = String.format(AUTOFUZZ_REPRODUCER_TEMPLATE, sha, codegenVisitor.generate());
+ Path javaPath = Paths.get(reproducerPath, String.format("Crash_%s.java", sha));
+ try {
+ Files.write(javaPath, javaSource.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ Log.error(String.format("Failed to write Java reproducer to %s%n", javaPath), e);
+ }
+ Log.println(String.format(
+ "reproducer_path='%s'; Java reproducer written to %s%n", reproducerPath, javaPath));
+ }
+
+ private static void fuzzerTestOneInput(
+ FuzzedDataProvider data, AutofuzzCodegenVisitor codegenVisitor) throws Throwable {
+ Executable targetExecutable;
+ if (FuzzTarget.targetExecutables.length == 1) {
+ targetExecutable = FuzzTarget.targetExecutables[0];
+ } else {
+ targetExecutable = data.pickValue(FuzzTarget.targetExecutables);
+ }
+ Object returnValue = null;
+ try {
+ if (targetExecutable instanceof Method) {
+ if (targetInstance != null) {
+ returnValue =
+ meta.autofuzz(data, (Method) targetExecutable, targetInstance, codegenVisitor);
+ } else {
+ returnValue = meta.autofuzz(data, (Method) targetExecutable, codegenVisitor);
+ }
+ } else {
+ // No targetInstance for constructors possible.
+ returnValue = meta.autofuzz(data, (Constructor<?>) targetExecutable, codegenVisitor);
+ }
+ executionsSinceLastInvocation = 0;
+ } catch (AutofuzzConstructionException e) {
+ if (Meta.IS_DEBUG) {
+ Log.error(e);
+ }
+ // Ignore exceptions thrown while constructing the parameters for the target method. We can
+ // only guess how to generate valid parameters and any exceptions thrown while doing so
+ // are most likely on us. However, if this happens too often, Autofuzz got stuck and we should
+ // let the user know.
+ executionsSinceLastInvocation++;
+ if (executionsSinceLastInvocation >= MAX_EXECUTIONS_WITHOUT_INVOCATION) {
+ Log.error(
+ String.format("Failed to generate valid arguments to '%s' in %d attempts; giving up",
+ methodReference, executionsSinceLastInvocation));
+ System.exit(1);
+ } else if (executionsSinceLastInvocation == MAX_EXECUTIONS_WITHOUT_INVOCATION / 2) {
+ // The application under test might perform classpath modifications or create classes
+ // dynamically that implement interfaces or extend abstract classes. Rescanning the
+ // classpath might help with constructing objects.
+ Meta.rescanClasspath();
+ }
+ } catch (AutofuzzInvocationException e) {
+ executionsSinceLastInvocation = 0;
+ Throwable cause = e.getCause();
+ Class<?> causeClass = cause.getClass();
+ // Do not report exceptions declared to be thrown by the method under test.
+ for (Class<?> declaredThrow :
+ throwsDeclarations.getOrDefault(targetExecutable, new Class[0])) {
+ if (declaredThrow.isAssignableFrom(causeClass)) {
+ return;
+ }
+ }
+
+ if (ignoredExceptionMatchers.stream().anyMatch(m -> m.matches(causeClass.getName()))) {
+ return;
+ }
+ cleanStackTraces(cause);
+ throw cause;
+ } catch (Throwable t) {
+ Log.error("Unexpected exception encountered during autofuzz", t);
+ System.exit(1);
+ } finally {
+ if (returnValue instanceof Closeable) {
+ ((Closeable) returnValue).close();
+ }
+ }
+ }
+
+ // Removes all stack trace elements that live in the Java reflection packages or the autofuzz
+ // package from the bottom of all stack frames.
+ private static void cleanStackTraces(Throwable t) {
+ Throwable cause = t;
+ while (cause != null) {
+ StackTraceElement[] elements = cause.getStackTrace();
+ int firstInterestingPos;
+ for (firstInterestingPos = elements.length - 1; firstInterestingPos > 0;
+ firstInterestingPos--) {
+ String className = elements[firstInterestingPos].getClassName();
+ if (!className.startsWith("com.code_intelligence.jazzer.autofuzz.")
+ && !className.startsWith("java.lang.reflect.")
+ && !className.startsWith("jdk.internal.reflect.")) {
+ break;
+ }
+ }
+ cause.setStackTrace(Arrays.copyOfRange(elements, 0, firstInterestingPos + 1));
+ cause = cause.getCause();
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java
new file mode 100644
index 00000000..543284f1
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java
@@ -0,0 +1,809 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.autofuzz;
+
+import com.code_intelligence.jazzer.api.AutofuzzConstructionException;
+import com.code_intelligence.jazzer.api.AutofuzzInvocationException;
+import com.code_intelligence.jazzer.api.Consumer1;
+import com.code_intelligence.jazzer.api.Consumer2;
+import com.code_intelligence.jazzer.api.Consumer3;
+import com.code_intelligence.jazzer.api.Consumer4;
+import com.code_intelligence.jazzer.api.Consumer5;
+import com.code_intelligence.jazzer.api.Function1;
+import com.code_intelligence.jazzer.api.Function2;
+import com.code_intelligence.jazzer.api.Function3;
+import com.code_intelligence.jazzer.api.Function4;
+import com.code_intelligence.jazzer.api.Function5;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.runtime.HardToCatchError;
+import com.code_intelligence.jazzer.utils.Utils;
+import io.github.classgraph.ClassGraph;
+import io.github.classgraph.ClassInfoList;
+import io.github.classgraph.ScanResult;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import net.jodah.typetools.TypeResolver;
+import net.jodah.typetools.TypeResolver.Unknown;
+
+public class Meta {
+ public static final boolean IS_DEBUG = isDebug();
+
+ private static final Meta PUBLIC_LOOKUP_INSTANCE = new Meta(null);
+ private static final boolean IS_TEST = isTest();
+ private static final WeakHashMap<Class<?>, List<Class<?>>> implementingClassesCache =
+ new WeakHashMap<>();
+ private static final WeakHashMap<Class<?>, List<Class<?>>> nestedBuilderClassesCache =
+ new WeakHashMap<>();
+ private static final WeakHashMap<Class<?>, List<Method>> originalObjectCreationMethodsCache =
+ new WeakHashMap<>();
+ private static final WeakHashMap<Class<?>, List<Method>> cascadingBuilderMethodsCache =
+ new WeakHashMap<>();
+
+ private final AccessibleObjectLookup lookup;
+
+ public Meta(Class<?> referenceClass) {
+ lookup = new AccessibleObjectLookup(referenceClass);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1> void autofuzz(FuzzedDataProvider data, Consumer1<T1> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Consumer1.class, func.getClass());
+ func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2> void autofuzz(FuzzedDataProvider data, Consumer2<T1, T2> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Consumer2.class, func.getClass());
+ func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
+ (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3> void autofuzz(FuzzedDataProvider data, Consumer3<T1, T2, T3> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Consumer3.class, func.getClass());
+ func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
+ (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
+ (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4> void autofuzz(
+ FuzzedDataProvider data, Consumer4<T1, T2, T3, T4> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Consumer4.class, func.getClass());
+ func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
+ (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
+ (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2),
+ (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4, T5> void autofuzz(
+ FuzzedDataProvider data, Consumer5<T1, T2, T3, T4, T5> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Consumer5.class, func.getClass());
+ func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
+ (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
+ (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2),
+ (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3),
+ (T5) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 4));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, R> R autofuzz(FuzzedDataProvider data, Function1<T1, R> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Function1.class, func.getClass());
+ return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, R> R autofuzz(FuzzedDataProvider data, Function2<T1, T2, R> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Function2.class, func.getClass());
+ return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
+ (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, R> R autofuzz(FuzzedDataProvider data, Function3<T1, T2, T3, R> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Function3.class, func.getClass());
+ return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
+ (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
+ (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4, R> R autofuzz(
+ FuzzedDataProvider data, Function4<T1, T2, T3, T4, R> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Function4.class, func.getClass());
+ return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
+ (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
+ (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2),
+ (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4, T5, R> R autofuzz(
+ FuzzedDataProvider data, Function5<T1, T2, T3, T4, T5, R> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Function5.class, func.getClass());
+ return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
+ (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
+ (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2),
+ (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3),
+ (T5) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 4));
+ }
+
+ public static Object consume(FuzzedDataProvider data, Class<?> type) {
+ return PUBLIC_LOOKUP_INSTANCE.consume(data, type, null);
+ }
+
+ static void rescanClasspath() {
+ implementingClassesCache.clear();
+ }
+
+ private static boolean isTest() {
+ String value = System.getenv("JAZZER_AUTOFUZZ_TESTING");
+ return value != null && !value.isEmpty();
+ }
+
+ private static boolean isDebug() {
+ String value = System.getenv("JAZZER_AUTOFUZZ_DEBUG");
+ return value != null && !value.isEmpty();
+ }
+
+ private static int consumeArrayLength(FuzzedDataProvider data, int sizeOfElement) {
+ // Spend at most half of the fuzzer input bytes so that the remaining arguments that require
+ // construction still have non-trivial data to work with.
+ int bytesToSpend = data.remainingBytes() / 2;
+ return bytesToSpend / Math.max(sizeOfElement, 1);
+ }
+
+ private static String deepToString(Object obj) {
+ if (obj == null) {
+ return "null";
+ }
+ if (obj.getClass().isArray()) {
+ return String.format("(%s[]) %s", obj.getClass().getComponentType().getName(),
+ Arrays.deepToString((Object[]) obj));
+ }
+ return obj.toString();
+ }
+
+ private static String getDebugSummary(
+ Executable executable, Object thisObject, Object[] arguments) {
+ return String.format("%nMethod: %s::%s%s%nthis: %s%nArguments: %s",
+ executable.getDeclaringClass().getName(), executable.getName(),
+ Utils.getReadableDescriptor(executable), thisObject,
+ Arrays.stream(arguments).map(Meta::deepToString).collect(Collectors.joining(", ")));
+ }
+
+ static Class<?> getRawType(Type genericType) {
+ if (genericType instanceof Class<?>) {
+ return (Class<?>) genericType;
+ } else if (genericType instanceof ParameterizedType) {
+ return getRawType(((ParameterizedType) genericType).getRawType());
+ } else if (genericType instanceof WildcardType) {
+ // TODO: Improve this.
+ return Object.class;
+ } else if (genericType instanceof TypeVariable<?>) {
+ throw new AutofuzzError("Did not expect genericType to be a TypeVariable: " + genericType);
+ } else if (genericType instanceof GenericArrayType) {
+ return Array
+ .newInstance(getRawType(((GenericArrayType) genericType).getGenericComponentType()), 0)
+ .getClass();
+ } else {
+ throw new AutofuzzError("Got unexpected class implementing Type: " + genericType);
+ }
+ }
+
+ public Object autofuzz(FuzzedDataProvider data, Method method) {
+ return autofuzz(data, method, null);
+ }
+
+ // Renamed so that it doesn't clash with the static method consume, which we don't want to rename
+ // as the api package depends on it by name.
+ public Object consumeNonStatic(FuzzedDataProvider data, Class<?> type) {
+ return consume(data, type, null);
+ }
+
+ Object autofuzz(FuzzedDataProvider data, Method method, AutofuzzCodegenVisitor visitor) {
+ Object result;
+ if (Modifier.isStatic(method.getModifiers())) {
+ if (visitor != null) {
+ // This group will always have two elements: The class name and the method call.
+ visitor.pushGroup(
+ String.format("%s.", method.getDeclaringClass().getCanonicalName()), "", "");
+ }
+ try {
+ result = autofuzz(data, method, null, visitor);
+ } finally {
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ }
+ } else {
+ if (visitor != null) {
+ // This group will always have two elements: The thisObject and the method call.
+ // Since the this object can be a complex expression, wrap it in parenthesis.
+ visitor.pushGroup("(", ").", "");
+ }
+ try {
+ Object thisObject = consume(data, method.getDeclaringClass(), visitor);
+ if (thisObject == null) {
+ throw new AutofuzzConstructionException();
+ }
+ result = autofuzz(data, method, thisObject, visitor);
+ } finally {
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ }
+ }
+ return result;
+ }
+
+ public Object autofuzz(FuzzedDataProvider data, Method method, Object thisObject) {
+ return autofuzz(data, method, thisObject, null);
+ }
+
+ Object autofuzz(
+ FuzzedDataProvider data, Method method, Object thisObject, AutofuzzCodegenVisitor visitor) {
+ if (visitor != null) {
+ visitor.pushGroup(String.format("%s(", method.getName()), ", ", ")");
+ }
+ Object[] arguments = consumeArguments(data, method, visitor);
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ try {
+ return method.invoke(thisObject, arguments);
+ } catch (IllegalAccessException | IllegalArgumentException | NullPointerException e) {
+ // We should ensure that the arguments fed into the method are always valid.
+ throw new AutofuzzError(getDebugSummary(method, thisObject, arguments), e);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof HardToCatchError) {
+ throw new AutofuzzInvocationException();
+ }
+ throw new AutofuzzInvocationException(e.getCause());
+ }
+ }
+
+ Object autofuzzForConsume(
+ FuzzedDataProvider data, Constructor<?> constructor, AutofuzzCodegenVisitor visitor) {
+ try {
+ return autofuzz(data, constructor, visitor);
+ } catch (AutofuzzConstructionException e) {
+ // Do not nest AutofuzzConstructionExceptions.
+ throw e;
+ } catch (AutofuzzInvocationException e) {
+ // If an invocation fails during consume and thus while trying to construct a valid object,
+ // the exception should not be reported as a finding, so we rewrap it.
+ throw new AutofuzzConstructionException(e.getCause());
+ } catch (Throwable t) {
+ throw new AutofuzzConstructionException(t);
+ }
+ }
+
+ Object autofuzzForConsume(
+ FuzzedDataProvider data, Method method, Object thisObject, AutofuzzCodegenVisitor visitor) {
+ try {
+ return autofuzz(data, method, thisObject, visitor);
+ } catch (AutofuzzConstructionException e) {
+ // Do not nest AutofuzzConstructionExceptions.
+ throw e;
+ } catch (AutofuzzInvocationException e) {
+ // If an invocation fails during consume and thus while trying to construct a valid object,
+ // the exception should not be reported as a finding, so we rewrap it.
+ throw new AutofuzzConstructionException(e.getCause());
+ } catch (Throwable t) {
+ throw new AutofuzzConstructionException(t);
+ }
+ }
+
+ public <R> R autofuzz(FuzzedDataProvider data, Constructor<R> constructor) {
+ return autofuzz(data, constructor, null);
+ }
+
+ <R> R autofuzz(
+ FuzzedDataProvider data, Constructor<R> constructor, AutofuzzCodegenVisitor visitor) {
+ if (visitor != null) {
+ // getCanonicalName is correct also for nested classes.
+ visitor.pushGroup(
+ String.format("new %s(", constructor.getDeclaringClass().getCanonicalName()), ", ", ")");
+ }
+ Object[] arguments = consumeArguments(data, constructor, visitor);
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ try {
+ return constructor.newInstance(arguments);
+ } catch (InstantiationException | IllegalAccessException | IllegalArgumentException e) {
+ // This should never be reached as the logic in consume should prevent us from e.g. calling
+ // constructors of abstract classes or private constructors.
+ throw new AutofuzzError(getDebugSummary(constructor, null, arguments), e);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof HardToCatchError) {
+ throw new AutofuzzInvocationException();
+ }
+ throw new AutofuzzInvocationException(e.getCause());
+ }
+ }
+
+ // Invariant: The Java source code representation of the returned object visited by visitor must
+ // represent an object of the same type as genericType. For example, a null value returned for
+ // the genericType Class<java.lang.String> should lead to the generated code
+ // "(java.lang.String) null", not just "null". This makes it possible to safely use consume in
+ // recursive argument constructions.
+ // Exception: Some Java libraries offer public methods that take private interfaces or abstract
+ // classes as parameters. In this case, a cast to the parent type would cause an
+ // IllegalAccessError. Since this case should be rare and there is no good alternative to
+ // disambiguate overloads, we omit the cast in this case.
+ Object consume(FuzzedDataProvider data, Type genericType, AutofuzzCodegenVisitor visitor) {
+ Class<?> type = getRawType(genericType);
+ if (type == byte.class || type == Byte.class) {
+ byte result = data.consumeByte();
+ if (visitor != null) {
+ visitor.pushElement(String.format("(byte) %s", result));
+ }
+ return result;
+ } else if (type == short.class || type == Short.class) {
+ short result = data.consumeShort();
+ if (visitor != null) {
+ visitor.pushElement(String.format("(short) %s", result));
+ }
+ return result;
+ } else if (type == int.class || type == Integer.class) {
+ int result = data.consumeInt();
+ if (visitor != null) {
+ visitor.pushElement(Integer.toString(result));
+ }
+ return result;
+ } else if (type == long.class || type == Long.class) {
+ long result = data.consumeLong();
+ if (visitor != null) {
+ visitor.pushElement(String.format("%sL", result));
+ }
+ return result;
+ } else if (type == float.class || type == Float.class) {
+ float result = data.consumeFloat();
+ if (visitor != null) {
+ visitor.pushElement(String.format("%sF", result));
+ }
+ return result;
+ } else if (type == double.class || type == Double.class) {
+ double result = data.consumeDouble();
+ if (visitor != null) {
+ visitor.pushElement(Double.toString(result));
+ }
+ return result;
+ } else if (type == boolean.class || type == Boolean.class) {
+ boolean result = data.consumeBoolean();
+ if (visitor != null) {
+ visitor.pushElement(Boolean.toString(result));
+ }
+ return result;
+ } else if (type == char.class || type == Character.class) {
+ char result = data.consumeChar();
+ if (visitor != null) {
+ visitor.addCharLiteral(result);
+ }
+ return result;
+ }
+ // Sometimes, but rarely return null for non-primitive and non-boxed types.
+ // TODO: We might want to return null for boxed types sometimes, but this is complicated by the
+ // fact that TypeUtils can't distinguish between a primitive type and its wrapper and may
+ // thus easily cause false-positive NullPointerExceptions.
+ if (!type.isPrimitive() && data.consumeByte() == 0) {
+ if (visitor != null) {
+ if (type == Object.class) {
+ visitor.pushElement("null");
+ } else {
+ visitor.pushElement(String.format("(%s) null", type.getCanonicalName()));
+ }
+ }
+ return null;
+ }
+ if (type == String.class || type == CharSequence.class) {
+ String result = data.consumeString(consumeArrayLength(data, 1));
+ if (visitor != null) {
+ visitor.addStringLiteral(result);
+ }
+ return result;
+ } else if (type.isArray()) {
+ if (type == byte[].class) {
+ byte[] result = data.consumeBytes(consumeArrayLength(data, Byte.BYTES));
+ if (visitor != null) {
+ visitor.pushElement(IntStream.range(0, result.length)
+ .mapToObj(i -> "(byte) " + result[i])
+ .collect(Collectors.joining(", ", "new byte[]{", "}")));
+ }
+ return result;
+ } else if (type == int[].class) {
+ int[] result = data.consumeInts(consumeArrayLength(data, Integer.BYTES));
+ if (visitor != null) {
+ visitor.pushElement(Arrays.stream(result)
+ .mapToObj(String::valueOf)
+ .collect(Collectors.joining(", ", "new int[]{", "}")));
+ }
+ return result;
+ } else if (type == short[].class) {
+ short[] result = data.consumeShorts(consumeArrayLength(data, Short.BYTES));
+ if (visitor != null) {
+ visitor.pushElement(IntStream.range(0, result.length)
+ .mapToObj(i -> "(short) " + result[i])
+ .collect(Collectors.joining(", ", "new short[]{", "}")));
+ }
+ return result;
+ } else if (type == long[].class) {
+ long[] result = data.consumeLongs(consumeArrayLength(data, Long.BYTES));
+ if (visitor != null) {
+ visitor.pushElement(Arrays.stream(result)
+ .mapToObj(e -> e + "L")
+ .collect(Collectors.joining(", ", "new long[]{", "}")));
+ }
+ return result;
+ } else if (type == boolean[].class) {
+ boolean[] result = data.consumeBooleans(consumeArrayLength(data, 1));
+ if (visitor != null) {
+ visitor.pushElement(
+ Arrays.toString(result).replace(']', '}').replace("[", "new boolean[]{"));
+ }
+ return result;
+ } else {
+ if (visitor != null) {
+ visitor.pushGroup(
+ String.format("new %s[]{", type.getComponentType().getName()), ", ", "}");
+ }
+ int remainingBytesBeforeFirstElementCreation = data.remainingBytes();
+ Object firstElement = consume(data, type.getComponentType(), visitor);
+ int remainingBytesAfterFirstElementCreation = data.remainingBytes();
+ int sizeOfElementEstimate =
+ remainingBytesBeforeFirstElementCreation - remainingBytesAfterFirstElementCreation;
+ Object array = Array.newInstance(
+ type.getComponentType(), consumeArrayLength(data, sizeOfElementEstimate));
+ for (int i = 0; i < Array.getLength(array); i++) {
+ if (i == 0) {
+ Array.set(array, i, firstElement);
+ } else {
+ Array.set(array, i, consume(data, type.getComponentType(), visitor));
+ }
+ }
+ if (visitor != null) {
+ if (Array.getLength(array) == 0) {
+ // We implicitly pushed the first element with the call to consume above, but it is not
+ // part of the array.
+ visitor.popElement();
+ }
+ visitor.popGroup();
+ }
+ return array;
+ }
+ } else if (type == ByteArrayInputStream.class || type == InputStream.class) {
+ byte[] array = data.consumeBytes(consumeArrayLength(data, Byte.BYTES));
+ if (visitor != null) {
+ visitor.pushElement(IntStream.range(0, array.length)
+ .mapToObj(i -> "(byte) " + array[i])
+ .collect(Collectors.joining(
+ ", ", "new java.io.ByteArrayInputStream(new byte[]{", "})")));
+ }
+ return new ByteArrayInputStream(array);
+ } else if (type == Map.class) {
+ ParameterizedType mapType = (ParameterizedType) genericType;
+ if (mapType.getActualTypeArguments().length != 2) {
+ throw new AutofuzzError(
+ "Expected Map generic type to have two type parameters: " + mapType);
+ }
+ Type keyType = mapType.getActualTypeArguments()[0];
+ Type valueType = mapType.getActualTypeArguments()[1];
+ if (visitor != null) {
+ // Do not use Collectors.toMap() since it cannot handle null values.
+ // Also annotate the type of the entry stream since it might be empty, in which case type
+ // inference on the accumulator could fail.
+ visitor.pushGroup(
+ String.format("java.util.stream.Stream.<java.util.AbstractMap.SimpleEntry<%s, %s>>of(",
+ keyType.getTypeName(), valueType.getTypeName()),
+ ", ",
+ ").collect(java.util.HashMap::new, (map, e) -> map.put(e.getKey(), e.getValue()), java.util.HashMap::putAll)");
+ }
+ int remainingBytesBeforeFirstEntryCreation = data.remainingBytes();
+ if (visitor != null) {
+ visitor.pushGroup("new java.util.AbstractMap.SimpleEntry<>(", ", ", ")");
+ }
+ Object firstKey = consume(data, keyType, visitor);
+ Object firstValue = consume(data, valueType, visitor);
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ int remainingBytesAfterFirstEntryCreation = data.remainingBytes();
+ int sizeOfElementEstimate =
+ remainingBytesBeforeFirstEntryCreation - remainingBytesAfterFirstEntryCreation;
+ int mapSize = consumeArrayLength(data, sizeOfElementEstimate);
+ Map<Object, Object> map = new HashMap<>(mapSize);
+ for (int i = 0; i < mapSize; i++) {
+ if (i == 0) {
+ map.put(firstKey, firstValue);
+ } else {
+ if (visitor != null) {
+ visitor.pushGroup("new java.util.AbstractMap.SimpleEntry<>(", ", ", ")");
+ }
+ map.put(consume(data, keyType, visitor), consume(data, valueType, visitor));
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ }
+ }
+ if (visitor != null) {
+ if (mapSize == 0) {
+ // We implicitly pushed the first entry with the call to consume above, but it is not
+ // part of the array.
+ visitor.popElement();
+ }
+ visitor.popGroup();
+ }
+ return map;
+ } else if (type.isEnum()) {
+ Enum<?> enumValue = (Enum<?>) data.pickValue(type.getEnumConstants());
+ if (visitor != null) {
+ visitor.pushElement(String.format("%s.%s", type.getName(), enumValue.name()));
+ }
+ return enumValue;
+ } else if (type == Class.class) {
+ if (visitor != null) {
+ visitor.pushElement(String.format("%s.class", YourAverageJavaClass.class.getName()));
+ }
+ return YourAverageJavaClass.class;
+ } else if (type == Method.class) {
+ if (visitor != null) {
+ throw new AutofuzzError("codegen has not been implemented for Method.class");
+ }
+ return data.pickValue(lookup.getAccessibleMethods(YourAverageJavaClass.class));
+ } else if (type == Constructor.class) {
+ if (visitor != null) {
+ throw new AutofuzzError("codegen has not been implemented for Constructor.class");
+ }
+ return data.pickValue(lookup.getAccessibleConstructors(YourAverageJavaClass.class));
+ } else if (type.isInterface() || Modifier.isAbstract(type.getModifiers())) {
+ List<Class<?>> implementingClasses = implementingClassesCache.get(type);
+ if (implementingClasses == null) {
+ // TODO: We may be scanning multiple times. Instead, we should keep the ScanResult around
+ // for as long as there is enough memory.
+ ClassGraph classGraph = new ClassGraph()
+ .enableClassInfo()
+ .ignoreClassVisibility()
+ .ignoreMethodVisibility()
+ .enableInterClassDependencies()
+ .rejectPackages("jaz");
+ if (!IS_TEST) {
+ classGraph.rejectPackages("com.code_intelligence.jazzer");
+ }
+ try (ScanResult result = classGraph.scan()) {
+ ClassInfoList children =
+ type.isInterface() ? result.getClassesImplementing(type) : result.getSubclasses(type);
+ implementingClasses = children.getStandardClasses()
+ .filter(info -> !Modifier.isAbstract(info.getModifiers()))
+ .filter(info -> lookup.isAccessible(info, info.getModifiers()))
+ // Filter out anonymous and local classes, which can't be
+ // instantiated in reproducers.
+ .filter(info -> info.getName() != null)
+ .loadClasses();
+ implementingClassesCache.put(type, implementingClasses);
+ }
+ }
+ if (implementingClasses.isEmpty()) {
+ if (IS_DEBUG) {
+ throw new AutofuzzConstructionException(String.format(
+ "Could not find classes implementing %s on the classpath", type.getName()));
+ } else {
+ throw new AutofuzzConstructionException();
+ }
+ }
+ if (visitor != null) {
+ // See the "Exception" note in the method comment.
+ if (Modifier.isPublic(type.getModifiers())) {
+ // This group will always have a single element: The instance of the implementing class.
+ visitor.pushGroup(String.format("(%s) ", type.getCanonicalName()), "", "");
+ }
+ }
+ Object result = consume(data, data.pickValue(implementingClasses), visitor);
+ if (visitor != null) {
+ if (Modifier.isPublic(type.getModifiers())) {
+ visitor.popGroup();
+ }
+ }
+ return result;
+ }
+ Constructor<?>[] constructors = lookup.getAccessibleConstructors(type);
+ if (constructors.length > 0) {
+ Constructor<?> constructor = data.pickValue(constructors);
+ boolean applySetters = constructor.getParameterCount() == 0;
+ if (visitor != null && applySetters) {
+ // Embed the instance creation and setters into an immediately invoked lambda expression to
+ // turn them into an expression.
+ String uniqueVariableName = visitor.uniqueVariableName();
+ visitor.pushGroup(String.format("((java.util.function.Supplier<%1$s>) (() -> {%1$s %2$s = ",
+ type.getCanonicalName(), uniqueVariableName),
+ String.format("; %s.", uniqueVariableName),
+ String.format("; return %s;})).get()", uniqueVariableName));
+ }
+ Object obj = autofuzzForConsume(data, constructor, visitor);
+ if (applySetters) {
+ List<Method> potentialSetters = getPotentialSetters(type);
+ if (!potentialSetters.isEmpty()) {
+ List<Method> pickedSetters =
+ data.pickValues(potentialSetters, data.consumeInt(0, potentialSetters.size()));
+ for (Method setter : pickedSetters) {
+ autofuzzForConsume(data, setter, obj, visitor);
+ }
+ }
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ }
+ return obj;
+ }
+ // We are out of more or less canonical ways to construct an instance of this class and have to
+ // resort to more heuristic approaches.
+
+ // First, try to find nested classes with names ending in Builder and call a subset of their
+ // chaining methods.
+ List<Class<?>> nestedBuilderClasses = getNestedBuilderClasses(type);
+ if (!nestedBuilderClasses.isEmpty()) {
+ Class<?> pickedBuilder = data.pickValue(nestedBuilderClasses);
+ List<Method> cascadingBuilderMethods = getCascadingBuilderMethods(pickedBuilder);
+ List<Method> originalObjectCreationMethods = getOriginalObjectCreationMethods(pickedBuilder);
+
+ int pickedMethodsNumber = data.consumeInt(0, cascadingBuilderMethods.size());
+ List<Method> pickedMethods = data.pickValues(cascadingBuilderMethods, pickedMethodsNumber);
+ Method builderMethod = data.pickValue(originalObjectCreationMethods);
+
+ if (visitor != null) {
+ // Group for the chain of builder methods.
+ visitor.pushGroup("", ".", "");
+ }
+ Object builderObj = autofuzzForConsume(
+ data, data.pickValue(lookup.getAccessibleConstructors(pickedBuilder)), visitor);
+ for (Method method : pickedMethods) {
+ builderObj = autofuzzForConsume(data, method, builderObj, visitor);
+ }
+
+ try {
+ Object obj = autofuzzForConsume(data, builderMethod, builderObj, visitor);
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ return obj;
+ } catch (Exception e) {
+ throw new AutofuzzConstructionException(e);
+ }
+ }
+
+ // We ran out of ways to construct an instance of the requested type. If in debug mode, report
+ // more detailed information.
+ if (IS_DEBUG) {
+ String summary = String.format(
+ "Failed to generate instance of %s:%nAccessible constructors: %s%nNested subclasses: %s%n",
+ type.getName(),
+ Arrays.stream(lookup.getAccessibleConstructors(type))
+ .map(Utils::getReadableDescriptor)
+ .collect(Collectors.joining(", ")),
+ Arrays.stream(lookup.getAccessibleClasses(type))
+ .map(Class::getName)
+ .collect(Collectors.joining(", ")));
+ throw new AutofuzzConstructionException(summary);
+ } else {
+ throw new AutofuzzConstructionException();
+ }
+ }
+
+ private List<Class<?>> getNestedBuilderClasses(Class<?> type) {
+ List<Class<?>> nestedBuilderClasses = nestedBuilderClassesCache.get(type);
+ if (nestedBuilderClasses == null) {
+ nestedBuilderClasses = Arrays.stream(lookup.getAccessibleClasses(type))
+ .filter(cls -> cls.getName().endsWith("Builder"))
+ .filter(cls -> !getOriginalObjectCreationMethods(cls).isEmpty())
+ .collect(Collectors.toList());
+ nestedBuilderClassesCache.put(type, nestedBuilderClasses);
+ }
+ return nestedBuilderClasses;
+ }
+
+ private List<Method> getOriginalObjectCreationMethods(Class<?> builder) {
+ List<Method> originalObjectCreationMethods = originalObjectCreationMethodsCache.get(builder);
+ if (originalObjectCreationMethods == null) {
+ originalObjectCreationMethods =
+ Arrays.stream(lookup.getAccessibleMethods(builder))
+ .filter(m -> m.getReturnType() == builder.getEnclosingClass())
+ .collect(Collectors.toList());
+ originalObjectCreationMethodsCache.put(builder, originalObjectCreationMethods);
+ }
+ return originalObjectCreationMethods;
+ }
+
+ private List<Method> getCascadingBuilderMethods(Class<?> builder) {
+ List<Method> cascadingBuilderMethods = cascadingBuilderMethodsCache.get(builder);
+ if (cascadingBuilderMethods == null) {
+ cascadingBuilderMethods = Arrays.stream(lookup.getAccessibleMethods(builder))
+ .filter(m -> m.getReturnType() == builder)
+ .collect(Collectors.toList());
+ cascadingBuilderMethodsCache.put(builder, cascadingBuilderMethods);
+ }
+ return cascadingBuilderMethods;
+ }
+
+ private List<Method> getPotentialSetters(Class<?> type) {
+ return Arrays.stream(lookup.getAccessibleMethods(type))
+ .filter(method -> void.class.equals(method.getReturnType()))
+ .filter(method -> method.getParameterCount() == 1)
+ .filter(method -> method.getName().startsWith("set"))
+ .collect(Collectors.toList());
+ }
+
+ public Object[] consumeArguments(
+ FuzzedDataProvider data, Executable executable, AutofuzzCodegenVisitor visitor) {
+ Object[] result;
+ try {
+ result = Arrays.stream(executable.getGenericParameterTypes())
+ .map(type -> consume(data, type, visitor))
+ .toArray();
+ return result;
+ } catch (AutofuzzConstructionException e) {
+ // Do not nest AutofuzzConstructionExceptions.
+ throw e;
+ } catch (AutofuzzInvocationException e) {
+ // If an invocation fails while creating the arguments for another invocation, the exception
+ // should not be reported, so we rewrap it.
+ throw new AutofuzzConstructionException(e.getCause());
+ } catch (Throwable t) {
+ throw new AutofuzzConstructionException(t);
+ }
+ }
+
+ private Object consumeChecked(FuzzedDataProvider data, Class<?>[] types, int i) {
+ if (types[i] == Unknown.class) {
+ throw new AutofuzzError("Failed to determine type of argument " + (i + 1));
+ }
+ Object result;
+ try {
+ result = consumeNonStatic(data, types[i]);
+ } catch (AutofuzzConstructionException e) {
+ // Do not nest AutofuzzConstructionExceptions.
+ throw e;
+ } catch (AutofuzzInvocationException e) {
+ // If an invocation fails while creating the arguments for another invocation, the exception
+ // should not be reported, so we rewrap it.
+ throw new AutofuzzConstructionException(e.getCause());
+ } catch (Throwable t) {
+ throw new AutofuzzConstructionException(t);
+ }
+ if (result != null && !types[i].isAssignableFrom(result.getClass())) {
+ throw new AutofuzzError("consume returned " + result.getClass() + ", but need " + types[i]);
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java
new file mode 100644
index 00000000..452ca878
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java
@@ -0,0 +1,229 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.autofuzz;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+// Returned by Meta when asked to construct a Class object. Its purpose is to be a relatively
+// "interesting" Java data class that can serve as the target of methods that perform some kind of
+// reflection or deserialization.
+public class YourAverageJavaClass implements Cloneable, Closeable, Serializable {
+ public byte aByte;
+ public boolean aBoolean;
+ public double aDouble;
+ public float aFloat;
+ public int anInt;
+ public transient int transientInt;
+ public long aLong;
+ public short aShort;
+ public volatile short volatileShort;
+ public String string;
+ public byte[] bytes;
+ public List<YourAverageJavaClass> list;
+ public Map<String, YourAverageJavaClass> map;
+
+ // Everything below has been automatically generated (apart from a minor modification to clone());
+
+ public YourAverageJavaClass(byte aByte, boolean aBoolean, double aDouble, float aFloat, int anInt,
+ int transientInt, long aLong, short aShort, short volatileShort, String string) {
+ this.aByte = aByte;
+ this.aBoolean = aBoolean;
+ this.aDouble = aDouble;
+ this.aFloat = aFloat;
+ this.anInt = anInt;
+ this.transientInt = transientInt;
+ this.aLong = aLong;
+ this.aShort = aShort;
+ this.volatileShort = volatileShort;
+ this.string = string;
+ }
+
+ public YourAverageJavaClass() {}
+
+ public YourAverageJavaClass(byte aByte, boolean aBoolean, double aDouble, float aFloat, int anInt,
+ int transientInt, long aLong, short aShort, short volatileShort, String string, byte[] bytes,
+ List<YourAverageJavaClass> list, Map<String, YourAverageJavaClass> map) {
+ this.aByte = aByte;
+ this.aBoolean = aBoolean;
+ this.aDouble = aDouble;
+ this.aFloat = aFloat;
+ this.anInt = anInt;
+ this.transientInt = transientInt;
+ this.aLong = aLong;
+ this.aShort = aShort;
+ this.volatileShort = volatileShort;
+ this.string = string;
+ this.bytes = bytes;
+ this.list = list;
+ this.map = map;
+ }
+
+ public byte getaByte() {
+ return aByte;
+ }
+
+ public void setaByte(byte aByte) {
+ this.aByte = aByte;
+ }
+
+ public boolean isaBoolean() {
+ return aBoolean;
+ }
+
+ public void setaBoolean(boolean aBoolean) {
+ this.aBoolean = aBoolean;
+ }
+
+ public double getaDouble() {
+ return aDouble;
+ }
+
+ public void setaDouble(double aDouble) {
+ this.aDouble = aDouble;
+ }
+
+ public float getaFloat() {
+ return aFloat;
+ }
+
+ public void setaFloat(float aFloat) {
+ this.aFloat = aFloat;
+ }
+
+ public int getAnInt() {
+ return anInt;
+ }
+
+ public void setAnInt(int anInt) {
+ this.anInt = anInt;
+ }
+
+ public int getTransientInt() {
+ return transientInt;
+ }
+
+ public void setTransientInt(int transientInt) {
+ this.transientInt = transientInt;
+ }
+
+ public long getaLong() {
+ return aLong;
+ }
+
+ public void setaLong(long aLong) {
+ this.aLong = aLong;
+ }
+
+ public short getaShort() {
+ return aShort;
+ }
+
+ public void setaShort(short aShort) {
+ this.aShort = aShort;
+ }
+
+ public short getVolatileShort() {
+ return volatileShort;
+ }
+
+ public void setVolatileShort(short volatileShort) {
+ this.volatileShort = volatileShort;
+ }
+
+ public String getString() {
+ return string;
+ }
+
+ public void setString(String string) {
+ this.string = string;
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+
+ public void setBytes(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ public List<YourAverageJavaClass> getList() {
+ return list;
+ }
+
+ public void setList(List<YourAverageJavaClass> list) {
+ this.list = list;
+ }
+
+ public Map<String, YourAverageJavaClass> getMap() {
+ return map;
+ }
+
+ public void setMap(Map<String, YourAverageJavaClass> map) {
+ this.map = map;
+ }
+
+ @Override
+ public YourAverageJavaClass clone() {
+ try {
+ YourAverageJavaClass clone = (YourAverageJavaClass) super.clone();
+ clone.transientInt = transientInt + 1;
+ clone.volatileShort = (short) (volatileShort - 1);
+ return clone;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (!(o instanceof YourAverageJavaClass))
+ return false;
+ YourAverageJavaClass that = (YourAverageJavaClass) o;
+ return aByte == that.aByte && aBoolean == that.aBoolean
+ && Double.compare(that.aDouble, aDouble) == 0 && Float.compare(that.aFloat, aFloat) == 0
+ && anInt == that.anInt && transientInt == that.transientInt && aLong == that.aLong
+ && aShort == that.aShort && volatileShort == that.volatileShort
+ && Objects.equals(string, that.string) && Arrays.equals(bytes, that.bytes)
+ && Objects.equals(list, that.list) && Objects.equals(map, that.map);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(aByte, aBoolean, aDouble, aFloat, anInt, transientInt, aLong, aShort,
+ volatileShort, string, list, map);
+ result = 31 * result + Arrays.hashCode(bytes);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "YourAverageJavaClass{"
+ + "aByte=" + aByte + ", aBoolean=" + aBoolean + ", aDouble=" + aDouble + ", aFloat="
+ + aFloat + ", anInt=" + anInt + ", transientInt=" + transientInt + ", aLong=" + aLong
+ + ", aShort=" + aShort + ", volatileShort=" + volatileShort + ", string='" + string + '\''
+ + ", bytes=" + Arrays.toString(bytes) + ", list=" + list + ", map=" + map + '}';
+ }
+
+ @Override
+ public void close() throws IOException {}
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel
new file mode 100644
index 00000000..37202c6d
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel
@@ -0,0 +1,177 @@
+load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library")
+load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+load("//bazel:kotlin.bzl", "ktlint")
+
+java_library(
+ name = "constants",
+ srcs = ["Constants.java"],
+ visibility = ["//src/main/java/com/code_intelligence/jazzer/driver:__subpackages__"],
+)
+
+java_library(
+ name = "driver",
+ srcs = ["Driver.java"],
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer:__pkg__",
+ ],
+ deps = [
+ ":fuzz_target_finder",
+ ":fuzz_target_holder",
+ ":fuzz_target_runner",
+ ":offline_instrumentor",
+ ":opt",
+ "//src/main/java/com/code_intelligence/jazzer/agent:agent_installer",
+ "//src/main/java/com/code_intelligence/jazzer/android:android_runtime",
+ "//src/main/java/com/code_intelligence/jazzer/driver/junit:junit_runner",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:constants",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ ],
+)
+
+java_library(
+ name = "offline_instrumentor",
+ srcs = ["OfflineInstrumentor.java"],
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer:__pkg__",
+ ],
+ deps = [
+ ":opt",
+ "//src/main/java/com/code_intelligence/jazzer/agent:agent_installer",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ "//src/main/java/com/code_intelligence/jazzer/utils:zip_utils",
+ ],
+)
+
+kt_jvm_library(
+ name = "exception_utils",
+ srcs = ["ExceptionUtils.kt"],
+ visibility = ["//src/main/java/com/code_intelligence/jazzer/driver:__subpackages__"],
+ deps = [
+ ":opt",
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:constants",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ ],
+)
+
+java_library(
+ name = "fuzz_target_finder",
+ srcs = ["FuzzTargetFinder.java"],
+ visibility = ["//src/test/java/com/code_intelligence/jazzer/driver:__pkg__"],
+ deps = [
+ ":fuzz_target_holder",
+ ":opt",
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:constants",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ "//src/main/java/com/code_intelligence/jazzer/utils:manifest_utils",
+ ],
+)
+
+java_library(
+ name = "fuzz_target_holder",
+ srcs = ["FuzzTargetHolder.java"],
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/junit:__pkg__",
+ "//src/test/java/com/code_intelligence/jazzer/driver:__pkg__",
+ ],
+ deps = [
+ ":opt",
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/autofuzz",
+ ],
+)
+
+java_jni_library(
+ name = "fuzz_target_runner",
+ srcs = ["FuzzTargetRunner.java"],
+ # This library is loaded by the classes in the agent runtime package as it needs to be available
+ # in the bootstrap class loader. It is packaged here rather than in jazzer_boostrap.jar since
+ # the bootstrap class loader doesn't support resources.
+ native_libs = [
+ "//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver",
+ ],
+ visibility = [
+ "//examples/junit/src/test/java/com/example:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/driver/junit:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/junit:__pkg__",
+ "//src/test:__subpackages__",
+ ],
+ deps = [
+ ":constants",
+ ":exception_utils",
+ ":fuzz_target_holder",
+ ":fuzzed_data_provider_impl",
+ ":opt",
+ ":recording_fuzzed_data_provider",
+ ":reproducer_template",
+ ":signal_handler",
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/autofuzz",
+ "//src/main/java/com/code_intelligence/jazzer/instrumentor",
+ "//src/main/java/com/code_intelligence/jazzer/mutation",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:constants",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap_compile_only",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ "//src/main/java/com/code_intelligence/jazzer/utils:manifest_utils",
+ "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider",
+ ],
+)
+
+java_jni_library(
+ name = "fuzzed_data_provider_impl",
+ srcs = ["FuzzedDataProviderImpl.java"],
+ native_libs = ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_fuzzed_data_provider"],
+ visibility = [
+ "//src:__subpackages__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider",
+ ],
+)
+
+java_library(
+ name = "reproducer_template",
+ srcs = ["ReproducerTemplate.java"],
+ resources = ["Reproducer.java.tmpl"],
+ deps = [
+ ":opt",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ ],
+)
+
+java_library(
+ name = "opt",
+ srcs = [
+ "Opt.java",
+ "OptParser.java",
+ ],
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/driver:__subpackages__",
+ "//src/main/java/com/code_intelligence/jazzer/junit:__pkg__",
+ "//src/test/java/com/code_intelligence/jazzer/driver:__subpackages__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer:constants",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ ],
+)
+
+java_library(
+ name = "recording_fuzzed_data_provider",
+ srcs = ["RecordingFuzzedDataProvider.java"],
+ visibility = ["//src/test/java/com/code_intelligence/jazzer/driver:__pkg__"],
+ deps = ["//src/main/java/com/code_intelligence/jazzer/api"],
+)
+
+java_jni_library(
+ name = "signal_handler",
+ srcs = ["SignalHandler.java"],
+ native_libs = ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_signal_handler"],
+ visibility = ["//src/main/native/com/code_intelligence/jazzer/driver:__pkg__"],
+ deps = [":opt"],
+)
+
+ktlint()
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Constants.java b/src/main/java/com/code_intelligence/jazzer/driver/Constants.java
new file mode 100644
index 00000000..80d476d8
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/Constants.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.driver;
+
+public final class Constants {
+ // Default value of the libFuzzer -error_exitcode flag.
+ public static final int JAZZER_FINDING_EXIT_CODE = 77;
+
+ private Constants(){};
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Driver.java b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java
new file mode 100644
index 00000000..8640e6c3
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2022 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.driver;
+
+import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID;
+import static java.lang.System.exit;
+
+import com.code_intelligence.jazzer.agent.AgentInstaller;
+import com.code_intelligence.jazzer.driver.junit.JUnitRunner;
+import com.code_intelligence.jazzer.utils.Log;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.Optional;
+
+public class Driver {
+ public static int start(List<String> args, boolean spawnsSubprocesses) throws IOException {
+ if (IS_ANDROID) {
+ if (!System.getProperty("jazzer.autofuzz", "").isEmpty()) {
+ Log.error("--autofuzz is not supported for Android");
+ return 1;
+ }
+ if (!System.getProperty("jazzer.coverage_report", "").isEmpty()) {
+ Log.warn("--coverage_report is not supported for Android and has been disabled");
+ System.clearProperty("jazzer.coverage_report");
+ }
+ if (!System.getProperty("jazzer.coverage_dump", "").isEmpty()) {
+ Log.warn("--coverage_dump is not supported for Android and has been disabled");
+ System.clearProperty("jazzer.coverage_dump");
+ }
+ }
+
+ if (spawnsSubprocesses) {
+ if (!System.getProperty("jazzer.coverage_report", "").isEmpty()) {
+ Log.warn("--coverage_report does not support parallel fuzzing and has been disabled");
+ System.clearProperty("jazzer.coverage_report");
+ }
+ if (!System.getProperty("jazzer.coverage_dump", "").isEmpty()) {
+ Log.warn("--coverage_dump does not support parallel fuzzing and has been disabled");
+ System.clearProperty("jazzer.coverage_dump");
+ }
+
+ String idSyncFileArg = System.getProperty("jazzer.id_sync_file", "");
+ Path idSyncFile;
+ if (idSyncFileArg.isEmpty()) {
+ // Create an empty temporary file used for coverage ID synchronization and
+ // pass its path to the agent in every child process. This requires adding
+ // the argument to argv for it to be picked up by libFuzzer, which then
+ // forwards it to child processes.
+ if (!IS_ANDROID) {
+ idSyncFile = Files.createTempFile("jazzer-", "");
+ } else {
+ File f = File.createTempFile("jazzer-", "", new File("/data/local/tmp/"));
+ idSyncFile = f.toPath();
+ }
+
+ args.add("--id_sync_file=" + idSyncFile.toAbsolutePath());
+ } else {
+ // Creates the file, truncating it if it exists.
+ idSyncFile = Files.write(Paths.get(idSyncFileArg), new byte[] {});
+ }
+ // This wouldn't run in case we exit the process with _Exit, but the parent process of a -fork
+ // run is expected to exit with a regular exit(0), which does cause JVM shutdown hooks to run:
+ // https://github.com/llvm/llvm-project/blob/940e178c0018b32af2f1478d331fc41a92a7dac7/compiler-rt/lib/fuzzer/FuzzerFork.cpp#L491
+ idSyncFile.toFile().deleteOnExit();
+ }
+
+ if (args.stream().anyMatch("-merge_inner=1" ::equals)) {
+ System.setProperty("jazzer.internal.merge_inner", "true");
+ }
+
+ // Jazzer's hooks use deterministic randomness and thus require a seed. Search for the last
+ // occurrence of a "-seed" argument as that is the one that is used by libFuzzer. If none is
+ // set, generate one and pass it to libFuzzer so that a fuzzing run can be reproduced simply by
+ // setting the seed printed by libFuzzer.
+ String seed = args.stream().reduce(
+ null, (prev, cur) -> cur.startsWith("-seed=") ? cur.substring("-seed=".length()) : prev);
+ if (seed == null) {
+ seed = Integer.toUnsignedString(new SecureRandom().nextInt());
+ // Only add the -seed argument to the command line if not running in a mode
+ // that spawns subprocesses. These would inherit the same seed, which might
+ // make them less effective.
+ if (!spawnsSubprocesses) {
+ args.add("-seed=" + seed);
+ }
+ }
+ System.setProperty("jazzer.internal.seed", seed);
+
+ if (args.stream().noneMatch(arg -> arg.startsWith("-rss_limit_mb="))) {
+ args.add(getDefaultRssLimitMbArg());
+ }
+
+ // Do not modify properties beyond this point, loading Opt locks in their values.
+ if (!Opt.instrumentOnly.isEmpty()) {
+ boolean instrumentationSuccess = OfflineInstrumentor.instrumentJars(Opt.instrumentOnly);
+ if (!instrumentationSuccess) {
+ exit(1);
+ }
+ exit(0);
+ }
+
+ Driver.class.getClassLoader().setDefaultAssertionStatus(true);
+
+ if (!Opt.autofuzz.isEmpty()) {
+ AgentInstaller.install(Opt.hooks);
+ FuzzTargetHolder.fuzzTarget = FuzzTargetHolder.AUTOFUZZ_FUZZ_TARGET;
+ return FuzzTargetRunner.startLibFuzzer(args);
+ }
+
+ String targetClassName = FuzzTargetFinder.findFuzzTargetClassName();
+ if (targetClassName == null) {
+ Log.error("Missing argument --target_class=<fuzz_target_class>");
+ exit(1);
+ }
+
+ // The JUnitRunner calls AgentInstaller.install itself after modifying flags affecting the
+ // agent.
+ if (JUnitRunner.isSupported()) {
+ Optional<JUnitRunner> runner = JUnitRunner.create(targetClassName, args);
+ if (runner.isPresent()) {
+ return runner.get().run();
+ }
+ }
+
+ // Installing the agent after the following "findFuzzTarget" leads to an asan error
+ // in it on "Class.forName(targetClassName)", but only during native fuzzing.
+ AgentInstaller.install(Opt.hooks);
+ FuzzTargetHolder.fuzzTarget = FuzzTargetFinder.findFuzzTarget(targetClassName);
+ return FuzzTargetRunner.startLibFuzzer(args);
+ }
+
+ private static String getDefaultRssLimitMbArg() {
+ // Java OutOfMemoryErrors are strictly more informative than libFuzzer's out of memory crashes.
+ // We thus want to scale the default libFuzzer memory limit, which includes all memory used by
+ // the process including Jazzer's native and non-native memory footprint, such that:
+ // 1. we never reach it purely by allocating memory on the Java heap;
+ // 2. it is still reached if the fuzz target allocates excessively on the native heap.
+ // As a heuristic, we set the overall memory limit to 2 * the maximum size of the Java heap and
+ // add a fixed 1 GiB on top for the fuzzer's own memory usage.
+ long maxHeapInBytes = Runtime.getRuntime().maxMemory();
+ return "-rss_limit_mb=" + ((2 * maxHeapInBytes / (1024 * 1024)) + 1024);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt b/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt
new file mode 100644
index 00000000..ed4b0569
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt
@@ -0,0 +1,215 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+@file:JvmName("ExceptionUtils")
+
+package com.code_intelligence.jazzer.driver
+
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow
+import com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID
+import com.code_intelligence.jazzer.utils.Log
+import java.lang.management.ManagementFactory
+import java.nio.ByteBuffer
+import java.security.MessageDigest
+
+private val JAZZER_PACKAGE_PREFIX = "com.code_intelligence.jazzer."
+private val PUBLIC_JAZZER_PACKAGES = setOf("api", "replay", "sanitizers")
+
+private val StackTraceElement.isInternalFrame: Boolean
+ get() = if (!className.startsWith(JAZZER_PACKAGE_PREFIX)) {
+ false
+ } else {
+ val jazzerSubPackage =
+ className.substring(JAZZER_PACKAGE_PREFIX.length).split(".", limit = 2)[0]
+ jazzerSubPackage !in PUBLIC_JAZZER_PACKAGES
+ }
+
+private fun hash(throwable: Throwable, passToRootCause: Boolean): ByteArray =
+ MessageDigest.getInstance("SHA-256").run {
+ // It suffices to hash the stack trace of the deepest cause as the higher-level causes only
+ // contain part of the stack trace (plus possibly a different exception type).
+ var rootCause = throwable
+ if (passToRootCause) {
+ while (true) {
+ rootCause = rootCause.cause ?: break
+ }
+ }
+ update(rootCause.javaClass.name.toByteArray())
+ rootCause.stackTrace
+ .takeWhile { !it.isInternalFrame }
+ .filterNot {
+ it.className.startsWith("jdk.internal.") ||
+ it.className.startsWith("java.lang.reflect.") ||
+ it.className.startsWith("sun.reflect.") ||
+ it.className.startsWith("java.lang.invoke.")
+ }
+ .forEach { update(it.toString().toByteArray()) }
+ if (throwable.suppressed.isNotEmpty()) {
+ update("suppressed".toByteArray())
+ for (suppressed in throwable.suppressed) {
+ update(hash(suppressed, passToRootCause))
+ }
+ }
+ digest()
+ }
+
+/**
+ * Computes a hash of the stack trace of [throwable] without messages.
+ *
+ * The hash can be used to deduplicate stack traces obtained on crashes. By not including the
+ * messages, this hash should not depend on the precise crashing input.
+ */
+fun computeDedupToken(throwable: Throwable): Long {
+ var passToRootCause = true
+ if (throwable is FuzzerSecurityIssueLow && throwable.cause is StackOverflowError) {
+ // Special handling for StackOverflowErrors as processed by preprocessThrowable:
+ // Only consider the repeated part of the stack trace and ignore the original stack trace in
+ // the cause.
+ passToRootCause = false
+ }
+ return ByteBuffer.wrap(hash(throwable, passToRootCause)).long
+}
+
+/**
+ * Annotates [throwable] with a severity and additional information if it represents a bug type
+ * that has security content.
+ */
+fun preprocessThrowable(throwable: Throwable): Throwable = when (throwable) {
+ is StackOverflowError -> {
+ // StackOverflowErrors are hard to deduplicate as the top-most stack frames vary wildly,
+ // whereas the information that is most useful for deduplication detection is hidden in the
+ // rest of the (truncated) stack frame.
+ // We heuristically clean up the stack trace by taking the elements from the bottom and
+ // stopping at the first repetition of a frame. The original error is returned as the cause
+ // unchanged.
+ val observedFrames = mutableSetOf<StackTraceElement>()
+ val bottomFramesWithoutRepetition = throwable.stackTrace.takeLastWhile { frame ->
+ (frame !in observedFrames).also { observedFrames.add(frame) }
+ }
+ var securityIssueMessage = "Stack overflow"
+ if (!IS_ANDROID) {
+ securityIssueMessage = "$securityIssueMessage (use '${getReproducingXssArg()}' to reproduce)"
+ }
+ FuzzerSecurityIssueLow(securityIssueMessage, throwable).apply {
+ stackTrace = bottomFramesWithoutRepetition.toTypedArray()
+ }
+ }
+ is OutOfMemoryError -> {
+ var securityIssueMessage = "Out of memory"
+ if (!IS_ANDROID) {
+ securityIssueMessage = "$securityIssueMessage (use '${getReproducingXmxArg()}' to reproduce)"
+ }
+ stripOwnStackTrace(FuzzerSecurityIssueLow(securityIssueMessage, throwable))
+ }
+ is VirtualMachineError -> stripOwnStackTrace(FuzzerSecurityIssueLow(throwable))
+ else -> throwable
+}.also { dropInternalFrames(it) }
+
+/**
+ * Recursively strips all Jazzer-internal stack frames from the given [Throwable] and its causes.
+ */
+private fun dropInternalFrames(throwable: Throwable?) {
+ throwable?.run {
+ stackTrace = stackTrace.takeWhile { !it.isInternalFrame }.toTypedArray()
+ suppressed.forEach { it.stackTrace = stackTrace.takeWhile { !it.isInternalFrame }.toTypedArray() }
+ dropInternalFrames(throwable.cause)
+ }
+}
+
+/**
+ * Strips the stack trace of [throwable] (e.g. because it was created in a utility method), but not
+ * the stack traces of its causes.
+ */
+private fun stripOwnStackTrace(throwable: Throwable) = throwable.apply {
+ stackTrace = emptyArray()
+}
+
+/**
+ * Returns a valid `-Xmx` JVM argument that sets the stack size to a value with which [StackOverflowError] findings can
+ * be reproduced, assuming the environment is sufficiently similar (e.g. OS and JVM version).
+ */
+private fun getReproducingXmxArg(): String? {
+ val maxHeapSizeInMegaBytes = (getNumericFinalFlagValue("MaxHeapSize") ?: return null) shr 20
+ val conservativeMaxHeapSizeInMegaBytes = (maxHeapSizeInMegaBytes * 0.9).toInt()
+ return "-Xmx${conservativeMaxHeapSizeInMegaBytes}m"
+}
+
+/**
+ * Returns a valid `-Xss` JVM argument that sets the stack size to a value with which [StackOverflowError] findings can
+ * be reproduced, assuming the environment is sufficiently similar (e.g. OS and JVM version).
+ */
+private fun getReproducingXssArg(): String? {
+ val threadStackSizeInKiloBytes = getNumericFinalFlagValue("ThreadStackSize") ?: return null
+ val conservativeThreadStackSizeInKiloBytes = (threadStackSizeInKiloBytes * 0.9).toInt()
+ return "-Xss${conservativeThreadStackSizeInKiloBytes}k"
+}
+
+private fun getNumericFinalFlagValue(arg: String): Long? {
+ val argPattern = "$arg\\D*(\\d*)".toRegex()
+ return argPattern.find(javaFullFinalFlags ?: return null)?.groupValues?.get(1)?.toLongOrNull()
+}
+
+private val javaFullFinalFlags by lazy {
+ readJavaFullFinalFlags()
+}
+
+private fun readJavaFullFinalFlags(): String? {
+ val javaHome = System.getProperty("java.home") ?: return null
+ val javaBinary = "$javaHome/bin/java"
+ val currentJvmArgs = ManagementFactory.getRuntimeMXBean().inputArguments
+ val javaPrintFlagsProcess = ProcessBuilder(
+ listOf(javaBinary) + currentJvmArgs + listOf(
+ "-XX:+PrintFlagsFinal",
+ "-version",
+ ),
+ ).start()
+ return javaPrintFlagsProcess.inputStream.bufferedReader().useLines { lineSequence ->
+ lineSequence
+ .filter { it.contains("ThreadStackSize") || it.contains("MaxHeapSize") }
+ .joinToString("\n")
+ }
+}
+
+fun dumpAllStackTraces() {
+ Log.println("\nStack traces of all JVM threads:")
+ for ((thread, stack) in Thread.getAllStackTraces()) {
+ Log.println(thread.toString())
+ // Remove traces of this method and the methods it calls.
+ stack.asList()
+ .asReversed()
+ .takeWhile {
+ !(
+ it.className == "com.code_intelligence.jazzer.driver.ExceptionUtils" &&
+ it.methodName == "dumpAllStackTraces"
+ )
+ }
+ .asReversed()
+ .forEach { frame ->
+ Log.println("\tat $frame")
+ }
+ Log.println("")
+ }
+
+ if (IS_ANDROID) {
+ // ManagementFactory is not supported on Android
+ return
+ }
+
+ Log.println("Garbage collector stats:")
+ Log.println(
+ ManagementFactory.getGarbageCollectorMXBeans().joinToString("\n", "\n", "\n") {
+ "${it.name}: ${it.collectionCount} collections took ${it.collectionTime}ms"
+ },
+ )
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetFinder.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetFinder.java
new file mode 100644
index 00000000..c2c41774
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetFinder.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2022 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.driver;
+
+import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID;
+import static java.lang.System.exit;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.driver.FuzzTargetHolder.FuzzTarget;
+import com.code_intelligence.jazzer.utils.Log;
+import com.code_intelligence.jazzer.utils.ManifestUtils;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+class FuzzTargetFinder {
+ private static final String FUZZER_TEST_ONE_INPUT = "fuzzerTestOneInput";
+ private static final String FUZZER_INITIALIZE = "fuzzerInitialize";
+ private static final String FUZZER_TEAR_DOWN = "fuzzerTearDown";
+
+ static String findFuzzTargetClassName() {
+ if (!Opt.targetClass.isEmpty()) {
+ return Opt.targetClass;
+ }
+ if (IS_ANDROID) {
+ // Fuzz target detection tools aren't supported on android
+ return null;
+ }
+ return ManifestUtils.detectFuzzTargetClass();
+ }
+
+ /**
+ * @throws IllegalArgumentException if the fuzz target method is invalid or couldn't be found
+ * @param targetClassName name of the fuzz target class
+ * @return a {@link FuzzTarget}
+ */
+ static FuzzTarget findFuzzTarget(String targetClassName) {
+ Class<?> fuzzTargetClass;
+ try {
+ fuzzTargetClass =
+ Class.forName(targetClassName, false, FuzzTargetFinder.class.getClassLoader());
+ } catch (ClassNotFoundException e) {
+ Log.error(String.format(
+ "'%s' not found on classpath:%n%n%s%n%nAll required classes must be on the classpath specified via --cp.",
+ targetClassName, System.getProperty("java.class.path")));
+ exit(1);
+ throw new IllegalStateException("Not reached");
+ }
+
+ return findFuzzTargetByMethodName(fuzzTargetClass);
+ }
+
+ // Finds the traditional static fuzzerTestOneInput fuzz target method.
+ private static FuzzTarget findFuzzTargetByMethodName(Class<?> clazz) {
+ Method fuzzTargetMethod;
+ if (Opt.experimentalMutator) {
+ List<Method> fuzzTargetMethods =
+ Arrays.stream(clazz.getMethods())
+ .filter(method -> "fuzzerTestOneInput".equals(method.getName()))
+ .filter(method -> Modifier.isStatic(method.getModifiers()))
+ .collect(Collectors.toList());
+ if (fuzzTargetMethods.size() != 1) {
+ throw new IllegalArgumentException(
+ String.format("%s must define exactly one function of this form:%n"
+ + "public static void fuzzerTestOneInput(...)%n",
+ clazz.getName()));
+ }
+ fuzzTargetMethod = fuzzTargetMethods.get(0);
+ } else {
+ Optional<Method> bytesFuzzTarget =
+ targetPublicStaticMethod(clazz, FUZZER_TEST_ONE_INPUT, byte[].class);
+ Optional<Method> dataFuzzTarget =
+ targetPublicStaticMethod(clazz, FUZZER_TEST_ONE_INPUT, FuzzedDataProvider.class);
+ if (bytesFuzzTarget.isPresent() == dataFuzzTarget.isPresent()) {
+ throw new IllegalArgumentException(String.format(
+ "%s must define exactly one of the following two functions:%n"
+ + "public static void fuzzerTestOneInput(byte[] ...)%n"
+ + "public static void fuzzerTestOneInput(FuzzedDataProvider ...)%n"
+ + "Note: Fuzz targets returning boolean are no longer supported; exceptions should be thrown instead of returning true.",
+ clazz.getName()));
+ }
+ fuzzTargetMethod = dataFuzzTarget.orElseGet(bytesFuzzTarget::get);
+ }
+
+ Callable<Object> initialize =
+ Stream
+ .of(targetPublicStaticMethod(clazz, FUZZER_INITIALIZE, String[].class)
+ .map(init -> (Callable<Object>) () -> {
+ init.invoke(null, (Object) Opt.targetArgs.toArray(new String[] {}));
+ return null;
+ }),
+ targetPublicStaticMethod(clazz, FUZZER_INITIALIZE)
+ .map(init -> (Callable<Object>) () -> {
+ init.invoke(null);
+ return null;
+ }))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findFirst()
+ .orElse(() -> null);
+
+ return new FuzzTarget(
+ fuzzTargetMethod, initialize, targetPublicStaticMethod(clazz, FUZZER_TEAR_DOWN));
+ }
+
+ private static Optional<Method> targetPublicStaticMethod(
+ Class<?> clazz, String name, Class<?>... parameterTypes) {
+ try {
+ Method method = clazz.getMethod(name, parameterTypes);
+ if (!Modifier.isStatic(method.getModifiers()) || !Modifier.isPublic(method.getModifiers())) {
+ return Optional.empty();
+ }
+ return Optional.of(method);
+ } catch (NoSuchMethodException e) {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetHolder.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetHolder.java
new file mode 100644
index 00000000..e748c6f7
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetHolder.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.driver;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.lang.reflect.Method;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+
+public class FuzzTargetHolder {
+ public static FuzzTarget autofuzzFuzzTarget(Callable<Object> newInstance) {
+ try {
+ Method fuzzerTestOneInput = com.code_intelligence.jazzer.autofuzz.FuzzTarget.class.getMethod(
+ "fuzzerTestOneInput", FuzzedDataProvider.class);
+ return new FuzzTargetHolder.FuzzTarget(fuzzerTestOneInput, newInstance, Optional.empty());
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static final FuzzTarget AUTOFUZZ_FUZZ_TARGET = autofuzzFuzzTarget(() -> {
+ com.code_intelligence.jazzer.autofuzz.FuzzTarget.fuzzerInitialize(
+ Opt.targetArgs.toArray(new String[0]));
+ return null;
+ });
+
+ /**
+ * The fuzz target that {@link FuzzTargetRunner} should fuzz.
+ */
+ public static FuzzTarget fuzzTarget;
+
+ public static class FuzzTarget {
+ public final Method method;
+ public final Callable<Object> newInstance;
+ public final Optional<Method> tearDown;
+
+ public FuzzTarget(Method method, Callable<Object> newInstance, Optional<Method> tearDown) {
+ this.method = method;
+ this.newInstance = newInstance;
+ this.tearDown = tearDown;
+ }
+
+ public boolean usesFuzzedDataProvider() {
+ return this.method.getParameterCount() == 1
+ && this.method.getParameterTypes()[0] == FuzzedDataProvider.class;
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java
new file mode 100644
index 00000000..aefa5352
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.driver;
+
+import static com.code_intelligence.jazzer.driver.Constants.JAZZER_FINDING_EXIT_CODE;
+import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID;
+import static java.lang.System.exit;
+import static java.util.stream.Collectors.joining;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.autofuzz.FuzzTarget;
+import com.code_intelligence.jazzer.instrumentor.CoverageRecorder;
+import com.code_intelligence.jazzer.mutation.ArgumentsMutator;
+import com.code_intelligence.jazzer.runtime.FuzzTargetRunnerNatives;
+import com.code_intelligence.jazzer.runtime.JazzerInternal;
+import com.code_intelligence.jazzer.utils.Log;
+import com.code_intelligence.jazzer.utils.UnsafeProvider;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import sun.misc.Unsafe;
+
+/**
+ * Executes a fuzz target and reports findings.
+ *
+ * <p>This class maintains global state (both native and non-native) and thus cannot be used
+ * concurrently.
+ */
+public final class FuzzTargetRunner {
+ private static final String OPENTEST4J_TEST_ABORTED_EXCEPTION =
+ "org.opentest4j.TestAbortedException";
+
+ private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
+
+ private static final long BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class);
+
+ // Possible return values for the libFuzzer callback runOne.
+ private static final int LIBFUZZER_CONTINUE = 0;
+ private static final int LIBFUZZER_RETURN_FROM_DRIVER = -2;
+
+ private static boolean invalidCorpusFileWarningShown = false;
+ private static final Set<Long> ignoredTokens = new HashSet<>(Opt.ignore);
+ private static final FuzzedDataProviderImpl fuzzedDataProvider =
+ FuzzedDataProviderImpl.withNativeData();
+ private static final MethodHandle fuzzTargetMethod;
+ private static final boolean useFuzzedDataProvider;
+ // Reused in every iteration analogous to JUnit's PER_CLASS lifecycle.
+ private static final Object fuzzTargetInstance;
+ private static final Method fuzzerTearDown;
+ private static final ArgumentsMutator mutator;
+ private static final ReproducerTemplate reproducerTemplate;
+ private static Predicate<Throwable> findingHandler;
+
+ static {
+ FuzzTargetHolder.FuzzTarget fuzzTarget = FuzzTargetHolder.fuzzTarget;
+ Class<?> fuzzTargetClass = fuzzTarget.method.getDeclaringClass();
+
+ // The method may not be accessible - JUnit test classes and methods are usually declared
+ // without access modifiers and thus package-private.
+ fuzzTarget.method.setAccessible(true);
+ try {
+ fuzzTargetMethod = MethodHandles.lookup().unreflect(fuzzTarget.method);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(e);
+ }
+ useFuzzedDataProvider = fuzzTarget.usesFuzzedDataProvider();
+ if (!useFuzzedDataProvider && IS_ANDROID) {
+ Log.error("Android fuzz targets must use " + FuzzedDataProvider.class.getName());
+ exit(1);
+ throw new IllegalStateException("Not reached");
+ }
+
+ fuzzerTearDown = fuzzTarget.tearDown.orElse(null);
+ reproducerTemplate = new ReproducerTemplate(fuzzTargetClass.getName(), useFuzzedDataProvider);
+
+ JazzerInternal.onFuzzTargetReady(fuzzTargetClass.getName());
+
+ try {
+ fuzzTargetInstance = fuzzTarget.newInstance.call();
+ } catch (Throwable t) {
+ Log.finding(t);
+ exit(1);
+ throw new IllegalStateException("Not reached");
+ }
+
+ if (Opt.experimentalMutator) {
+ if (Modifier.isStatic(fuzzTarget.method.getModifiers())) {
+ mutator = ArgumentsMutator.forStaticMethodOrThrow(fuzzTarget.method);
+ } else {
+ mutator = ArgumentsMutator.forInstanceMethodOrThrow(fuzzTargetInstance, fuzzTarget.method);
+ }
+ Log.info("Using experimental mutator: " + mutator);
+ } else {
+ mutator = null;
+ }
+
+ if (Opt.hooks) {
+ // libFuzzer will clear the coverage map after this method returns and keeps no record of the
+ // coverage accumulated so far (e.g. by static initializers). We record it here to keep it
+ // around for JaCoCo coverage reports.
+ CoverageRecorder.updateCoveredIdsWithCoverageMap();
+ }
+
+ Runtime.getRuntime().addShutdownHook(new Thread(FuzzTargetRunner::shutdown));
+ }
+
+ /**
+ * A test-only convenience wrapper around {@link #runOne(long, int)}.
+ */
+ static int runOne(byte[] data) {
+ long dataPtr = UNSAFE.allocateMemory(data.length);
+ UNSAFE.copyMemory(data, BYTE_ARRAY_OFFSET, null, dataPtr, data.length);
+ try {
+ return runOne(dataPtr, data.length);
+ } finally {
+ UNSAFE.freeMemory(dataPtr);
+ }
+ }
+
+ /**
+ * Executes the user-provided fuzz target once.
+ *
+ * @param dataPtr a native pointer to beginning of the input provided by the fuzzer for this
+ * execution
+ * @param dataLength length of the fuzzer input
+ * @return the value that the native LLVMFuzzerTestOneInput function should return. Currently,
+ * this is always 0. The function may exit the process instead of returning.
+ */
+ private static int runOne(long dataPtr, int dataLength) {
+ Throwable finding = null;
+ byte[] data;
+ Object argument;
+ if (Opt.experimentalMutator) {
+ // TODO: Instead of copying the native data and then reading it in, consider the following
+ // optimizations if they turn out to be worthwhile in benchmarks:
+ // 1. Let libFuzzer pass in a null pointer if the byte array hasn't changed since the last
+ // call to our custom mutator and skip the read entirely.
+ // 2. Implement a InputStream backed by Unsafe to avoid the copyToArray overhead.
+ byte[] buf = copyToArray(dataPtr, dataLength);
+ boolean readExactly = mutator.read(new ByteArrayInputStream(buf));
+
+ // All inputs constructed by the mutator framework can be read exactly, existing corpus files
+ // may not be valid for the current fuzz target anymore, though. In this case, print a warning
+ // once.
+ if (!(invalidCorpusFileWarningShown || readExactly || isFixedLibFuzzerInput(buf))) {
+ invalidCorpusFileWarningShown = true;
+ Log.warn("Some files in the seed corpus do not match the fuzz target signature. "
+ + "This indicates that they were generated with a different signature and may cause issues reproducing previous findings.");
+ }
+ data = null;
+ argument = null;
+ } else if (useFuzzedDataProvider) {
+ fuzzedDataProvider.setNativeData(dataPtr, dataLength);
+ data = null;
+ argument = fuzzedDataProvider;
+ } else {
+ data = copyToArray(dataPtr, dataLength);
+ argument = data;
+ }
+ try {
+ if (Opt.experimentalMutator) {
+ // No need to detach as we are currently reading in the mutator state from bytes in every
+ // iteration.
+ mutator.invoke(false);
+ } else if (fuzzTargetInstance == null) {
+ fuzzTargetMethod.invoke(argument);
+ } else {
+ fuzzTargetMethod.invoke(fuzzTargetInstance, argument);
+ }
+ } catch (Throwable uncaughtFinding) {
+ finding = uncaughtFinding;
+ }
+
+ // When using libFuzzer's -merge flag, only the coverage of the current input is relevant, not
+ // whether it is crashing. Since every crash would cause a restart of the process and thus the
+ // JVM, we can optimize this case by not crashing.
+ //
+ // Incidentally, this makes the behavior of fuzz targets relying on global states more
+ // consistent: Rather than resetting the global state after every crashing input and thus
+ // dependent on the particular ordering of the inputs, we never reset it.
+ if (Opt.mergeInner) {
+ return LIBFUZZER_CONTINUE;
+ }
+
+ // Explicitly reported findings take precedence over uncaught exceptions.
+ if (JazzerInternal.lastFinding != null) {
+ finding = JazzerInternal.lastFinding;
+ JazzerInternal.lastFinding = null;
+ }
+ // Allow skipping invalid inputs in fuzz tests by using e.g. JUnit's assumeTrue.
+ if (finding == null || finding.getClass().getName().equals(OPENTEST4J_TEST_ABORTED_EXCEPTION)) {
+ return LIBFUZZER_CONTINUE;
+ }
+ if (Opt.hooks) {
+ finding = ExceptionUtils.preprocessThrowable(finding);
+ }
+
+ long dedupToken = Opt.dedup ? ExceptionUtils.computeDedupToken(finding) : 0;
+ if (Opt.dedup && !ignoredTokens.add(dedupToken)) {
+ return LIBFUZZER_CONTINUE;
+ }
+
+ if (findingHandler != null) {
+ // We still print the libFuzzer crashing input information, which also dumps the crashing
+ // input as a side effect.
+ printCrashingInput();
+ if (findingHandler.test(finding)) {
+ return LIBFUZZER_CONTINUE;
+ } else {
+ return LIBFUZZER_RETURN_FROM_DRIVER;
+ }
+ }
+
+ // The user-provided fuzz target method has returned. Any further exits are on us and should not
+ // result in a "fuzz target exited" warning being printed by libFuzzer.
+ temporarilyDisableLibfuzzerExitHook();
+
+ Log.finding(finding);
+ if (Opt.dedup) {
+ // Has to be printed to stdout as it is parsed by libFuzzer when minimizing a crash. It does
+ // not necessarily have to appear at the beginning of a line.
+ // https://github.com/llvm/llvm-project/blob/4c106c93eb68f8f9f201202677cd31e326c16823/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L342
+ Log.structuredOutput(String.format(Locale.ROOT, "DEDUP_TOKEN: %016x", dedupToken));
+ }
+ Log.println("== libFuzzer crashing input ==");
+ printCrashingInput();
+ // dumpReproducer needs to be called after libFuzzer printed its final stats as otherwise it
+ // would report incorrect coverage - the reproducer generation involved rerunning the fuzz
+ // target.
+ // It doesn't support @FuzzTest fuzz targets, but these come with an integrated regression test
+ // that satisfies the same purpose.
+ // It also doesn't support the experimental mutator yet as that requires implementing Java code
+ // generation for mutators.
+ if (fuzzTargetInstance == null && !Opt.experimentalMutator) {
+ dumpReproducer(data);
+ }
+
+ if (!Opt.dedup || Long.compareUnsigned(ignoredTokens.size(), Opt.keepGoing) >= 0) {
+ // Reached the maximum amount of findings to keep going for, crash after shutdown. We use
+ // _Exit rather than System.exit to not trigger libFuzzer's exit handlers.
+ if (!Opt.autofuzz.isEmpty() && Opt.dedup) {
+ Log.println("");
+ Log.info(String.format(
+ "To continue fuzzing past this particular finding, rerun with the following additional argument:"
+ + "%n%n --ignore=%s%n%n"
+ + "To ignore all findings of this kind, rerun with the following additional argument:"
+ + "%n%n --autofuzz_ignore=%s",
+ ignoredTokens.stream()
+ .map(token -> Long.toUnsignedString(token, 16))
+ .collect(joining(",")),
+ Stream.concat(Opt.autofuzzIgnore.stream(), Stream.of(finding.getClass().getName()))
+ .collect(joining(","))));
+ }
+ System.exit(JAZZER_FINDING_EXIT_CODE);
+ throw new IllegalStateException("Not reached");
+ }
+ return LIBFUZZER_CONTINUE;
+ }
+
+ private static boolean isFixedLibFuzzerInput(byte[] input) {
+ // Detect special libFuzzer inputs which can not be processed by the mutator framework.
+ // libFuzzer always uses an empty input, and one with a single line feed (10) to indicate
+ // end of initial corpus file processing.
+ return input.length == 0 || (input.length == 1 && input[0] == 10);
+ }
+
+ // Called via JNI, being passed data from LLVMFuzzerCustomMutator.
+ @SuppressWarnings("unused")
+ private static int mutateOne(long data, int size, int maxSize, int seed) {
+ mutate(data, size, seed);
+ return writeToMemory(mutator, data, maxSize);
+ }
+
+ private static void mutate(long data, int size, int seed) {
+ // libFuzzer sends the input "\n" when there are no corpus entries. We use that as a signal to
+ // initialize the mutator instead of just reading that trivial input to produce a more
+ // interesting value.
+ if (size == 1 && UNSAFE.getByte(data) == '\n') {
+ mutator.init(seed);
+ } else {
+ // TODO: See the comment on earlier calls to read for potential optimizations.
+ mutator.read(new ByteArrayInputStream(copyToArray(data, size)));
+ mutator.mutate(seed);
+ }
+ }
+
+ private static long crossOverCount = 0;
+
+ // Called via JNI, being passed data from LLVMFuzzerCustomCrossOver.
+ @SuppressWarnings("unused")
+ private static int crossOver(
+ long data1, int size1, long data2, int size2, long out, int maxOutSize, int seed) {
+ // Custom cross over and custom mutate are the only mutators registered in
+ // libFuzzer, hence cross over is picked as often as mutate, which is way
+ // too frequently. Without custom mutate, cross over would be picked from
+ // the list of default mutators, so ~1/12 of the time. This also seems too
+ // much and is reduced to a configurable frequency, default 1/100, here,
+ // mutate is used in the other cases.
+ if (Opt.experimentalCrossOverFrequency != 0
+ && crossOverCount++ % Opt.experimentalCrossOverFrequency == 0) {
+ mutator.crossOver(new ByteArrayInputStream(copyToArray(data1, size1)),
+ new ByteArrayInputStream(copyToArray(data2, size2)), seed);
+ } else {
+ mutate(data1, size1, seed);
+ }
+ return writeToMemory(mutator, out, maxOutSize);
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static int writeToMemory(ArgumentsMutator mutator, long out, int maxOutSize) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ // TODO: Instead of writing to a byte array and then copying that array's contents into
+ // memory, consider introducing an OutputStream backed by Unsafe.
+ mutator.write(baos);
+ byte[] mutatedBytes = baos.toByteArray();
+ int newSize = Math.min(mutatedBytes.length, maxOutSize);
+ UNSAFE.copyMemory(mutatedBytes, BYTE_ARRAY_OFFSET, null, out, newSize);
+ return newSize;
+ }
+
+ /*
+ * Starts libFuzzer via LLVMFuzzerRunDriver.
+ */
+ public static int startLibFuzzer(List<String> args) {
+ // We always define LLVMFuzzerCustomMutator, but only use it when --experimental_mutator is
+ // specified. libFuzzer contains logic that disables --len_control when it finds the custom
+ // mutator symbol:
+ // https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L202-L207
+ // We thus have to explicitly set --len_control to its default value when not using the new
+ // mutator.
+ // TODO: libFuzzer still emits a message about --len_control being disabled by default even if
+ // we override it via a flag. We may want to patch this out.
+ if (!Opt.experimentalMutator) {
+ // args may not be mutable.
+ args = new ArrayList<>(args);
+ // https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerFlags.def#L19
+ args.add("-len_control=100");
+ }
+
+ for (String arg : args.subList(1, args.size())) {
+ if (!arg.startsWith("-")) {
+ Log.info("using inputs from: " + arg);
+ }
+ }
+
+ if (!IS_ANDROID) {
+ SignalHandler.initialize();
+ }
+ return startLibFuzzer(
+ args.stream().map(str -> str.getBytes(StandardCharsets.UTF_8)).toArray(byte[][] ::new));
+ }
+
+ /**
+ * Registers a custom handler for findings.
+ *
+ * @param findingHandler a consumer for the finding that returns true if the fuzzer should
+ * continue fuzzing and false if it should return from
+ * {@link FuzzTargetRunner#startLibFuzzer(List)}.
+ */
+ public static void registerFindingHandler(Predicate<Throwable> findingHandler) {
+ FuzzTargetRunner.findingHandler = findingHandler;
+ }
+
+ private static void shutdown() {
+ if (!Opt.coverageDump.isEmpty() || !Opt.coverageReport.isEmpty()) {
+ if (!Opt.coverageDump.isEmpty()) {
+ CoverageRecorder.dumpJacocoCoverage(Opt.coverageDump);
+ }
+ if (!Opt.coverageReport.isEmpty()) {
+ CoverageRecorder.dumpCoverageReport(Opt.coverageReport);
+ }
+ }
+
+ if (fuzzerTearDown == null) {
+ return;
+ }
+ Log.info("calling fuzzerTearDown function");
+ try {
+ fuzzerTearDown.invoke(null);
+ } catch (InvocationTargetException e) {
+ Log.finding(e.getCause());
+ System.exit(JAZZER_FINDING_EXIT_CODE);
+ } catch (Throwable t) {
+ Log.error(t);
+ System.exit(1);
+ }
+ }
+
+ private static void dumpReproducer(byte[] data) {
+ if (data == null) {
+ assert useFuzzedDataProvider;
+ fuzzedDataProvider.reset();
+ data = fuzzedDataProvider.consumeRemainingAsBytes();
+ }
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SHA-1 not available", e);
+ }
+ String dataSha1 = toHexString(digest.digest(data));
+
+ if (!Opt.autofuzz.isEmpty()) {
+ fuzzedDataProvider.reset();
+ FuzzTarget.dumpReproducer(fuzzedDataProvider, Opt.reproducerPath, dataSha1);
+ return;
+ }
+
+ String base64Data;
+ if (useFuzzedDataProvider) {
+ fuzzedDataProvider.reset();
+ FuzzedDataProvider recordingFuzzedDataProvider =
+ RecordingFuzzedDataProvider.makeFuzzedDataProviderProxy(fuzzedDataProvider);
+ try {
+ fuzzTargetMethod.invokeExact(recordingFuzzedDataProvider);
+ if (JazzerInternal.lastFinding == null) {
+ Log.warn("Failed to reproduce crash when rerunning with recorder");
+ }
+ } catch (Throwable ignored) {
+ // Expected.
+ }
+ try {
+ base64Data = RecordingFuzzedDataProvider.serializeFuzzedDataProviderProxy(
+ recordingFuzzedDataProvider);
+ } catch (IOException e) {
+ Log.error("Failed to create reproducer", e);
+ // Don't let libFuzzer print a native stack trace.
+ System.exit(1);
+ throw new IllegalStateException("Not reached");
+ }
+ } else {
+ base64Data = Base64.getEncoder().encodeToString(data);
+ }
+
+ reproducerTemplate.dumpReproducer(base64Data, dataSha1);
+ }
+
+ /**
+ * Convert a byte array to a lower-case hex string.
+ *
+ * <p>The returned hex string always has {@code 2 * bytes.length} characters.
+ *
+ * @param bytes the bytes to convert
+ * @return a lower-case hex string representing the bytes
+ */
+ private static String toHexString(byte[] bytes) {
+ String unpadded = new BigInteger(1, bytes).toString(16);
+ int numLeadingZeroes = 2 * bytes.length - unpadded.length();
+ return String.join("", Collections.nCopies(numLeadingZeroes, "0")) + unpadded;
+ }
+
+ // Accessed by fuzz_target_runner.cpp.
+ @SuppressWarnings("unused")
+ private static void dumpAllStackTraces() {
+ ExceptionUtils.dumpAllStackTraces();
+ }
+
+ private static byte[] copyToArray(long ptr, int length) {
+ // TODO: Use Unsafe.allocateUninitializedArray instead once Java 9 is the base.
+ byte[] array = new byte[length];
+ UNSAFE.copyMemory(null, ptr, array, BYTE_ARRAY_OFFSET, length);
+ return array;
+ }
+
+ /**
+ * Starts libFuzzer via LLVMFuzzerRunDriver.
+ *
+ * @param args command-line arguments encoded in UTF-8 (not null-terminated)
+ * @return the return value of LLVMFuzzerRunDriver
+ */
+ private static int startLibFuzzer(byte[][] args) {
+ return FuzzTargetRunnerNatives.startLibFuzzer(
+ args, FuzzTargetRunner.class, Opt.experimentalMutator);
+ }
+
+ /**
+ * Causes libFuzzer to write the current input to disk as a crashing input and emit some
+ * information about it to stderr.
+ */
+ public static void printCrashingInput() {
+ FuzzTargetRunnerNatives.printCrashingInput();
+ }
+
+ /**
+ * Returns the debug string of the current mutator.
+ * If no mutator is used, returns null.
+ */
+ public static String mutatorDebugString() {
+ return mutator != null ? mutator.toString() : null;
+ }
+
+ /**
+ * Returns whether the current mutator has detected invalid corpus files.
+ * If no mutator is used, returns false.
+ */
+ public static boolean invalidCorpusFilesPresent() {
+ return mutator != null && invalidCorpusFileWarningShown;
+ }
+
+ /**
+ * Disables libFuzzer's fuzz target exit detection until the next call to {@link #runOne}.
+ *
+ * <p>Calling {@link System#exit} after having called this method will not trigger libFuzzer's
+ * exit hook that would otherwise print the "fuzz target exited" error message. This method should
+ * thus only be called after control has returned from the user-provided fuzz target.
+ */
+ private static void temporarilyDisableLibfuzzerExitHook() {
+ FuzzTargetRunnerNatives.temporarilyDisableLibfuzzerExitHook();
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzedDataProviderImpl.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzedDataProviderImpl.java
new file mode 100644
index 00000000..08e5298b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzedDataProviderImpl.java
@@ -0,0 +1,257 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.driver;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.utils.UnsafeProvider;
+import com.github.fmeum.rules_jni.RulesJni;
+import sun.misc.Unsafe;
+
+public class FuzzedDataProviderImpl implements FuzzedDataProvider, AutoCloseable {
+ static {
+ RulesJni.loadLibrary("jazzer_fuzzed_data_provider", "/com/code_intelligence/jazzer/driver");
+ nativeInit();
+ }
+
+ private static native void nativeInit();
+
+ private final byte[] javaData;
+ private long originalDataPtr;
+ private int originalRemainingBytes;
+
+ // Accessed in fuzzed_data_provider.cpp.
+ private long dataPtr;
+ private int remainingBytes;
+
+ private FuzzedDataProviderImpl(long dataPtr, int remainingBytes, byte[] javaData) {
+ this.javaData = javaData;
+ this.originalDataPtr = dataPtr;
+ this.dataPtr = dataPtr;
+ this.originalRemainingBytes = remainingBytes;
+ this.remainingBytes = remainingBytes;
+ }
+
+ /**
+ * Creates a {@link FuzzedDataProvider} that consumes bytes from an already existing native array.
+ *
+ * <ul>
+ * <li>{@link #close()} <b>must</b> be called on instances created with this method to free the
+ * native copy of the Java
+ * {@code byte} array.
+ * <li>{@link #setNativeData(long, int)} <b>must not</b> be called on instances created with this
+ * method.
+ *
+ * @param data the raw bytes used as input
+ * @return a {@link FuzzedDataProvider} backed by {@code data}
+ */
+ public static FuzzedDataProviderImpl withJavaData(byte[] data) {
+ return new FuzzedDataProviderImpl(allocateNativeCopy(data), data.length, data);
+ }
+
+ /**
+ * Creates a {@link FuzzedDataProvider} that consumes bytes from an already existing native array.
+ *
+ * <p>The backing array can be set at any time using {@link #setNativeData(long, int)} and is
+ * initially empty.
+ *
+ * @return a {@link FuzzedDataProvider} backed by an empty array.
+ */
+ public static FuzzedDataProviderImpl withNativeData() {
+ return new FuzzedDataProviderImpl(0, 0, null);
+ }
+
+ /**
+ * Replaces the current native backing array.
+ *
+ * <p><b>Must not</b> be called on instances created with {@link #withJavaData(byte[])}.
+ *
+ * @param dataPtr a native pointer to the new backing array
+ * @param dataLength the length of the new backing array
+ */
+ public void setNativeData(long dataPtr, int dataLength) {
+ this.originalDataPtr = dataPtr;
+ this.dataPtr = dataPtr;
+ this.originalRemainingBytes = dataLength;
+ this.remainingBytes = dataLength;
+ }
+
+ /**
+ * Returns the Java byte array used to construct the instance, or null if it was created with
+ * {@link FuzzedDataProviderImpl#withNativeData()};
+ */
+ public byte[] getJavaData() {
+ return javaData;
+ }
+
+ /**
+ * Resets the FuzzedDataProvider state to read from the beginning to the end of its current
+ * backing item.
+ */
+ public void reset() {
+ dataPtr = originalDataPtr;
+ remainingBytes = originalRemainingBytes;
+ }
+
+ /**
+ * Releases native memory allocated for this instance (if any).
+ *
+ * <p>While the instance should not be used after this method returns, no usage of {@link
+ * FuzzedDataProvider} methods can result in memory corruption.
+ */
+ @Override
+ public void close() {
+ if (originalDataPtr == 0) {
+ return;
+ }
+ // We own the native memory iff the instance was created backed by a Java byte array.
+ if (javaData != null) {
+ UNSAFE.freeMemory(originalDataPtr);
+ }
+ // Prevent double-frees and use-after-frees by effectively making all methods no-ops after
+ // close() has been called.
+ originalDataPtr = 0;
+ originalRemainingBytes = 0;
+ dataPtr = 0;
+ remainingBytes = 0;
+ }
+
+ private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
+ private static final long BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class);
+
+ private static long allocateNativeCopy(byte[] data) {
+ long nativeCopy = UNSAFE.allocateMemory(data.length);
+ UNSAFE.copyMemory(data, BYTE_ARRAY_OFFSET, null, nativeCopy, data.length);
+ return nativeCopy;
+ }
+
+ @Override public native boolean consumeBoolean();
+
+ @Override public native boolean[] consumeBooleans(int maxLength);
+
+ @Override public native byte consumeByte();
+
+ @Override
+ public byte consumeByte(byte min, byte max) {
+ if (min > max) {
+ throw new IllegalArgumentException(
+ String.format("min must be <= max (got min: %d, max: %d)", min, max));
+ }
+ return consumeByteUnchecked(min, max);
+ }
+
+ @Override public native short consumeShort();
+
+ @Override
+ public short consumeShort(short min, short max) {
+ if (min > max) {
+ throw new IllegalArgumentException(
+ String.format("min must be <= max (got min: %d, max: %d)", min, max));
+ }
+ return consumeShortUnchecked(min, max);
+ }
+
+ @Override public native short[] consumeShorts(int maxLength);
+
+ @Override public native int consumeInt();
+
+ @Override
+ public int consumeInt(int min, int max) {
+ if (min > max) {
+ throw new IllegalArgumentException(
+ String.format("min must be <= max (got min: %d, max: %d)", min, max));
+ }
+ return consumeIntUnchecked(min, max);
+ }
+
+ @Override public native int[] consumeInts(int maxLength);
+
+ @Override public native long consumeLong();
+
+ @Override
+ public long consumeLong(long min, long max) {
+ if (min > max) {
+ throw new IllegalArgumentException(
+ String.format("min must be <= max (got min: %d, max: %d)", min, max));
+ }
+ return consumeLongUnchecked(min, max);
+ }
+
+ @Override public native long[] consumeLongs(int maxLength);
+
+ @Override public native float consumeFloat();
+
+ @Override public native float consumeRegularFloat();
+
+ @Override
+ public float consumeRegularFloat(float min, float max) {
+ if (min > max) {
+ throw new IllegalArgumentException(
+ String.format("min must be <= max (got min: %f, max: %f)", min, max));
+ }
+ return consumeRegularFloatUnchecked(min, max);
+ }
+
+ @Override public native float consumeProbabilityFloat();
+
+ @Override public native double consumeDouble();
+
+ @Override
+ public double consumeRegularDouble(double min, double max) {
+ if (min > max) {
+ throw new IllegalArgumentException(
+ String.format("min must be <= max (got min: %f, max: %f)", min, max));
+ }
+ return consumeRegularDoubleUnchecked(min, max);
+ }
+
+ @Override public native double consumeRegularDouble();
+
+ @Override public native double consumeProbabilityDouble();
+
+ @Override public native char consumeChar();
+
+ @Override
+ public char consumeChar(char min, char max) {
+ if (min > max) {
+ throw new IllegalArgumentException(
+ String.format("min must be <= max (got min: %c, max: %c)", min, max));
+ }
+ return consumeCharUnchecked(min, max);
+ }
+
+ @Override public native char consumeCharNoSurrogates();
+
+ @Override public native String consumeAsciiString(int maxLength);
+
+ @Override public native String consumeString(int maxLength);
+
+ @Override public native String consumeRemainingAsAsciiString();
+
+ @Override public native String consumeRemainingAsString();
+
+ @Override public native byte[] consumeBytes(int maxLength);
+
+ @Override public native byte[] consumeRemainingAsBytes();
+
+ @Override public native int remainingBytes();
+
+ private native byte consumeByteUnchecked(byte min, byte max);
+ private native short consumeShortUnchecked(short min, short max);
+ private native char consumeCharUnchecked(char min, char max);
+ private native int consumeIntUnchecked(int min, int max);
+ private native long consumeLongUnchecked(long min, long max);
+ private native float consumeRegularFloatUnchecked(float min, float max);
+ private native double consumeRegularDoubleUnchecked(double min, double max);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/OfflineInstrumentor.java b/src/main/java/com/code_intelligence/jazzer/driver/OfflineInstrumentor.java
new file mode 100644
index 00000000..3e779d42
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/OfflineInstrumentor.java
@@ -0,0 +1,179 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.driver;
+
+import com.code_intelligence.jazzer.agent.AgentInstaller;
+import com.code_intelligence.jazzer.utils.Log;
+import com.code_intelligence.jazzer.utils.ZipUtils;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.UnsupportedClassVersionError;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.ZipOutputStream;
+
+public class OfflineInstrumentor {
+ /**
+ * Create a new jar file at <jazzer_path>/<jarBaseName>.instrumented.jar
+ * for each jar in passed in, with classes that have Jazzer instrumentation.
+ *
+ * @param jarLists list of jars to instrument
+ * @return a boolean representing the success status
+ */
+ public static boolean instrumentJars(List<String> jarLists) {
+ AgentInstaller.install(Opt.hooks);
+
+ // Clear Opt.dumpClassesDir before adding new instrumented classes
+ File dumpClassesDir = new File(Opt.dumpClassesDir);
+ if (dumpClassesDir.exists()) {
+ for (String fn : dumpClassesDir.list()) {
+ new File(Opt.dumpClassesDir, fn).delete();
+ }
+ }
+
+ List<String> errorMessages = new ArrayList<>();
+ for (String jarPath : jarLists) {
+ String outputBaseName = jarPath;
+ if (outputBaseName.contains(File.separator)) {
+ outputBaseName = outputBaseName.substring(
+ outputBaseName.lastIndexOf(File.separator) + 1, outputBaseName.length());
+ }
+
+ if (outputBaseName.contains(".jar")) {
+ outputBaseName = outputBaseName.substring(0, outputBaseName.lastIndexOf(".jar"));
+ }
+
+ Log.info("Instrumenting jar file: " + jarPath);
+
+ try {
+ errorMessages = createInstrumentedClasses(jarPath);
+ } catch (IOException e) {
+ errorMessages.add("Failed to instrument jar: " + jarPath
+ + ". Please ensure the file at this location is a jar file. Error Message: " + e);
+ continue;
+ }
+
+ try {
+ createInstrumentedJar(jarPath, Opt.dumpClassesDir + File.separator + outputBaseName,
+ outputBaseName + ".instrumented.jar");
+ } catch (Exception e) {
+ errorMessages.add("Failed to instrument jar: " + jarPath + ". Error: " + e);
+ }
+ }
+
+ // Log all errors at the end
+ for (String error : errorMessages) {
+ Log.error(error);
+ }
+
+ return errorMessages.isEmpty();
+ }
+
+ /**
+ * Loops over all classes in jar file and adds instrumentation. The output
+ * of the instrumented classes will be at --dump-classes-dir
+ *
+ * @param jarPath a path to a jar file to instrument.
+ * @return a list of errors that were hit when trying to instrument all classes in jar
+ */
+ private static List<String> createInstrumentedClasses(String jarPath) throws IOException {
+ List<String> errorMessages = new ArrayList<>();
+ List<String> allClasses = new ArrayList<>();
+
+ // Collect all classes for jar file
+ try (JarFile jarFile = new JarFile(jarPath)) {
+ Enumeration<JarEntry> allEntries = jarFile.entries();
+ while (allEntries.hasMoreElements()) {
+ JarEntry entry = allEntries.nextElement();
+ if (entry.isDirectory()) {
+ continue;
+ }
+
+ String name = entry.getName();
+ if (!name.endsWith(".class")) {
+ Log.info("Skipping instrumenting file: " + name);
+ continue;
+ }
+
+ String className = name.substring(0, name.lastIndexOf(".class"));
+ className = className.replace('/', '.');
+ allClasses.add(className);
+ Log.info("Found class: " + className);
+ }
+ }
+
+ // No classes found, so none to load. Return errors
+ if (allClasses.size() == 0) {
+ errorMessages.add("Classes is empty for jar: " + jarPath);
+ return errorMessages;
+ }
+
+ // Create class loader to load in all classes
+ File file = new File(jarPath);
+ URL url = file.toURI().toURL();
+ URL[] urls = new URL[] {url};
+ ClassLoader cl = new URLClassLoader(urls);
+
+ // Loop through all files and load in all classes, agent will instrument them as they load
+ for (String className : allClasses) {
+ try {
+ cl.loadClass(className);
+ } catch (UnsupportedClassVersionError ucve) {
+ // The classes will still get instrumented here, but warn so the user knows something
+ // happened
+ Log.warn(ucve.toString());
+ } catch (Throwable e) {
+ // Catch all exceptions/errors and keep instrumenting to give user the option to manually
+ // fix one offs if possible
+ errorMessages.add("Failed to instrument class: " + className + ". Error: " + e);
+ }
+ }
+
+ return errorMessages;
+ }
+
+ /**
+ * This will create a new jar out of specified original jar and the merge in the instrumented
+ * classes from the specified instrumented classes dir
+ *
+ * @param originalJarPath a path to the original jar.
+ * @param instrumentedClassesDir a path to the instrumented classes dir.
+ * @param outputZip output file.
+ */
+ private static void createInstrumentedJar(
+ String originalJarPath, String instrumentedClassesDir, String outputZip) throws IOException {
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outputZip))) {
+ Set<String> dirFilesToSkip = new HashSet<>();
+ dirFilesToSkip.add(".original.class");
+ dirFilesToSkip.add(".failed.class");
+ Set<String> filesMerged =
+ ZipUtils.mergeDirectoryToZip(instrumentedClassesDir, zos, dirFilesToSkip);
+
+ ZipUtils.mergeZipToZip(originalJarPath, zos, filesMerged);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Opt.java b/src/main/java/com/code_intelligence/jazzer/driver/Opt.java
new file mode 100644
index 00000000..f1a45c3b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/Opt.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2022 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.driver;
+
+import static com.code_intelligence.jazzer.Constants.JAZZER_VERSION;
+import static com.code_intelligence.jazzer.driver.OptParser.boolSetting;
+import static com.code_intelligence.jazzer.driver.OptParser.ignoreSetting;
+import static com.code_intelligence.jazzer.driver.OptParser.lazyStringListSetting;
+import static com.code_intelligence.jazzer.driver.OptParser.stringListSetting;
+import static com.code_intelligence.jazzer.driver.OptParser.stringSetting;
+import static com.code_intelligence.jazzer.driver.OptParser.uint64Setting;
+import static java.lang.System.exit;
+import static java.util.Collections.unmodifiableList;
+import static java.util.Collections.unmodifiableSet;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+import static java.util.stream.Stream.concat;
+
+import com.code_intelligence.jazzer.utils.Log;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+/**
+ * Static options that determine the runtime behavior of the fuzzer, set via Java properties.
+ *
+ * <p>Each option corresponds to a command-line argument of the driver of the same name.
+ *
+ * <p>Every public field should be deeply immutable.
+ */
+public final class Opt {
+ static {
+ if (Opt.class.getClassLoader() == null) {
+ throw new IllegalStateException("Opt should not be loaded in the bootstrap class loader");
+ }
+ }
+
+ static {
+ // We additionally list system properties supported by the Jazzer JUnit engine that do not
+ // directly map to arguments. These are not shown in help texts.
+ ignoreSetting("instrument");
+ ignoreSetting("valueprofile");
+ // The following arguments are interpreted by the native launcher only. They do appear in the
+ // help text, but aren't read by the driver.
+ stringListSetting("jvm_args",
+ "Arguments to pass to the JVM (separator can be escaped with '\\', native launcher only)");
+ stringListSetting("additional_jvm_args",
+ "Additional arguments to pass to the JVM (separator can be escaped with '\\', native launcher only)");
+ stringSetting(
+ "agent_path", null, "Custom path to jazzer_agent_deploy.jar (native launcher only)");
+ // The following arguments are interpreted by the Jazzer main class directly as they require
+ // starting Jazzer as a subprocess.
+ boolSetting(
+ "asan", false, "Allow fuzzing of native libraries compiled with '-fsanitize=address'");
+ boolSetting(
+ "ubsan", false, "Allow fuzzing of native libraries compiled with '-fsanitize=undefined'");
+ boolSetting("native", false,
+ "Allow fuzzing of native libraries compiled with '-fsanitize=fuzzer' (implied by --asan and --ubsan)");
+ // Options currently used by Android only
+ stringSetting("android_init_options", null,
+ "Which libraries to use when initializing ART (native launcher only)");
+ boolSetting("hwasan", false, "Allow fuzzing of native libraries compiled with hwasan");
+ }
+
+ public static final String autofuzz = stringSetting("autofuzz", "",
+ "Fully qualified reference (optionally with a Javadoc-style signature) to a "
+ + "method on the class path to be fuzzed with automatically generated arguments "
+ + "(examples: java.lang.System.out::println, java.lang.String::new(byte[]))");
+ public static final List<String> autofuzzIgnore = stringListSetting("autofuzz_ignore", ',',
+ "Fully qualified names of exception classes to ignore during fuzzing");
+ public static final String coverageDump = stringSetting("coverage_dump", "",
+ "Path to write a JaCoCo .exec file to when the fuzzer exits (if non-empty)");
+ public static final String coverageReport = stringSetting("coverage_report", "",
+ "Path to write a human-readable coverage report to when the fuzzer exits (if non-empty)");
+ public static final List<String> customHooks =
+ stringListSetting("custom_hooks", "Names of classes to load custom hooks from");
+ public static final List<String> disabledHooks = stringListSetting("disabled_hooks",
+ "Names of classes from which hooks (custom or built-in) should not be loaded from");
+ public static final String dumpClassesDir = stringSetting(
+ "dump_classes_dir", "", "Directory to dump instrumented .class files into (if non-empty)");
+ public static final boolean experimentalMutator =
+ boolSetting("experimental_mutator", false, "Use an experimental structured mutator");
+ public static final long experimentalCrossOverFrequency = uint64Setting(
+ "experimental_cross_over_frequency", 100,
+ "(Used in experimental mutator) Frequency of cross-over mutations actually being executed "
+ + "when the cross-over function is picked by the underlying fuzzing engine (~1/2 of all mutations), "
+ + "other invocations perform type specific mutations via the experimental mutator. "
+ + "(0 = disabled, 1 = every call, 2 = every other call, etc.).");
+ public static final boolean hooks = boolSetting(
+ "hooks", true, "Apply fuzzing instrumentation (use 'trace' for finer-grained control)");
+ public static final String idSyncFile = stringSetting("id_sync_file", null, null);
+ public static final Set<Long> ignore =
+ unmodifiableSet(stringListSetting("ignore", ',',
+ "Hex strings representing deduplication tokens of findings that should be ignored")
+ .stream()
+ .map(token -> Long.parseUnsignedLong(token, 16))
+ .collect(toSet()));
+ public static final long keepGoing = uint64Setting(
+ "keep_going", 1, "Number of distinct findings after which the fuzzer should stop");
+ public static final String reproducerPath = stringSetting("reproducer_path", ".",
+ "Directory in which stand-alone Java reproducers are stored for each finding");
+ public static final String targetClass = stringSetting("target_class", "",
+ "Fully qualified name of the fuzz target class (required unless --autofuzz is specified)");
+ // Used to disambiguate between multiple methods annotated with @FuzzTest in the target class.
+ public static final String targetMethod = stringSetting("target_method", "", null);
+ public static final List<String> trace = stringListSetting("trace",
+ "Types of instrumentation to apply: cmp, cov, div, gep (disabled by default), indir, native");
+
+ // When Jazzer is executed from the command line, these settings are potentially modified by
+ // JUnit's AgentConfigurator after the Driver has initialized Opt, which would result in stale
+ // values being read if the settings weren't evaluated lazily.
+ // TODO: Look into making all settings lazy, but verify that their value never changes after they
+ // have been read once.
+ public static final Supplier<List<String>> customHookIncludes =
+ lazyStringListSetting("custom_hook_includes",
+ "Glob patterns matching names of classes to instrument with hooks (custom and built-in)");
+ public static final Supplier<List<String>> customHookExcludes = lazyStringListSetting(
+ "custom_hook_excludes",
+ "Glob patterns matching names of classes that should not be instrumented with hooks (custom and built-in)");
+ public static final Supplier<List<String>> instrumentationIncludes =
+ lazyStringListSetting("instrumentation_includes",
+ "Glob patterns matching names of classes to instrument for fuzzing");
+ public static final Supplier<List<String>> instrumentationExcludes =
+ lazyStringListSetting("instrumentation_excludes",
+ "Glob patterns matching names of classes that should not be instrumented for fuzzing");
+ // The values of this setting depends on autofuzz.
+ public static final List<String> targetArgs = autofuzz.isEmpty()
+ ? stringListSetting(
+ "target_args", ' ', "Arguments to pass to the fuzz target's fuzzerInitialize method")
+ : unmodifiableList(concat(Stream.of(autofuzz), autofuzzIgnore.stream()).collect(toList()));
+
+ // Default to false if hooks is false to mimic the original behavior of the native fuzz target
+ // runner, but still support hooks = false && dedup = true.
+ public static final boolean dedup =
+ boolSetting("dedup", hooks, "Compute and print a deduplication token for every finding");
+
+ public static final String androidBootclassJarPath = stringSetting("android_bootclass_jar_path",
+ null,
+ "Full path to booclass jar path that will be used on Android runs. If you are using the launcher this will be set for you.");
+
+ public static final String androidBootclassClassesOverrides = stringSetting(
+ "android_bootpath_classes_overrides", null,
+ "Used for fuzzing classes loaded in through the bootstrap class loader on Android. Full path to jar file with the instrumented versions of the classes you want to override.");
+
+ // Whether hook instrumentation should add a check for JazzerInternal#hooksEnabled before
+ // executing hooks. Used to disable hooks during non-fuzz JUnit tests.
+ public static final boolean conditionalHooks =
+ boolSetting("internal.conditional_hooks", false, null);
+
+ static final boolean mergeInner = boolSetting("internal.merge_inner", false, null);
+
+ private static final boolean help =
+ boolSetting("help", false, "Show this list of all available arguments");
+ private static final boolean version = boolSetting("version", false, "Print version information");
+
+ // Methods below currently used by Android only
+ public static final List<String> cp =
+ stringListSetting("cp", "The class path to use for fuzzing (native launcher only)");
+
+ public static final List<String> additionalClassesExcludes =
+ stringListSetting("additional_classes_excludes",
+ "Glob patterns matching names of classes from Java that are not in your jar file, "
+ + "but may be included in your program");
+
+ // Default to false. Sets if fuzzing is taking place on Android device (virtual or physical)
+ public static final boolean isAndroid =
+ boolSetting("android", false, "Jazzer is running on Android");
+
+ // Some scenarios require instrumenting the jar before fuzzing begins
+ public static final List<String> instrumentOnly = stringListSetting("instrument_only", ',',
+ "Comma separated list of jar files to instrument. No fuzzing is performed.");
+
+ static {
+ OptParser.failOnUnknownArgument();
+
+ if (help) {
+ Log.println(OptParser.getHelpText());
+ exit(0);
+ }
+ if (version) {
+ Log.println("Jazzer v" + JAZZER_VERSION);
+ exit(0);
+ }
+ if (!targetClass.isEmpty() && !autofuzz.isEmpty()) {
+ Log.error("--target_class and --autofuzz cannot be specified together");
+ exit(1);
+ }
+ if (!stringListSetting("target_args", ' ', null).isEmpty() && !autofuzz.isEmpty()) {
+ Log.error("--target_args and --autofuzz cannot be specified together");
+ exit(1);
+ }
+ if (autofuzz.isEmpty() && !autofuzzIgnore.isEmpty()) {
+ Log.error("--autofuzz_ignore requires --autofuzz");
+ exit(1);
+ }
+ if ((!ignore.isEmpty() || keepGoing > 1) && !dedup) {
+ Log.error("--nodedup is not supported with --ignore or --keep_going");
+ exit(1);
+ }
+ if (!instrumentOnly.isEmpty() && dumpClassesDir.isEmpty()) {
+ Log.error("--dump_classes_dir must be set with --instrument_only");
+ exit(1);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/OptParser.java b/src/main/java/com/code_intelligence/jazzer/driver/OptParser.java
new file mode 100644
index 00000000..ab096f2b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/OptParser.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2022 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.driver;
+
+import static java.lang.System.exit;
+
+import com.code_intelligence.jazzer.utils.Log;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+final class OptParser {
+ private static final String[] HELP_HEADER = new String[] {
+ "A coverage-guided, in-process fuzzer for the JVM",
+ "",
+ "Usage:",
+ String.format(
+ " java -cp jazzer.jar[%cclasspath_entries] com.code_intelligence.jazzer.Jazzer --target_class=<target class> [args...]",
+ File.separatorChar),
+ String.format(
+ " java -cp jazzer.jar[%cclasspath_entries] com.code_intelligence.jazzer.Jazzer --autofuzz=<method reference> [args...]",
+ File.separatorChar),
+ "",
+ "In addition to the options listed below, Jazzer also accepts all",
+ "libFuzzer options described at:",
+ " https://llvm.org/docs/LibFuzzer.html#options",
+ "",
+ "Options:",
+ };
+ private static final String OPTIONS_PREFIX = "jazzer.";
+
+ // All supported arguments are added to this set by the individual *Setting methods.
+ private static final Map<String, OptDetails> knownArgs = new TreeMap<>();
+
+ static String getHelpText() {
+ return Stream
+ .concat(Arrays.stream(HELP_HEADER),
+ knownArgs.values().stream().filter(Objects::nonNull).map(OptDetails::toString))
+ .collect(Collectors.joining("\n\n"));
+ }
+
+ static void ignoreSetting(String name) {
+ knownArgs.put(name, null);
+ }
+
+ static String stringSetting(String name, String defaultValue, String description) {
+ knownArgs.put(name, OptDetails.create(name, "string", defaultValue, description));
+ return System.getProperty(OPTIONS_PREFIX + name, defaultValue);
+ }
+
+ static List<String> stringListSetting(String name, String description) {
+ return lazyStringListSetting(name, description).get();
+ }
+
+ static List<String> stringListSetting(String name, char separator, String description) {
+ return lazyStringListSetting(name, separator, description).get();
+ }
+
+ static Supplier<List<String>> lazyStringListSetting(String name, String description) {
+ return lazyStringListSetting(name, File.pathSeparatorChar, description);
+ }
+
+ static Supplier<List<String>> lazyStringListSetting(
+ String name, char separator, String description) {
+ knownArgs.put(name,
+ OptDetails.create(
+ name, String.format("list separated by '%c'", separator), "", description));
+ return () -> {
+ String value = System.getProperty(OPTIONS_PREFIX + name);
+ if (value == null || value.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return splitOnUnescapedSeparator(value, separator);
+ };
+ }
+
+ static boolean boolSetting(String name, boolean defaultValue, String description) {
+ knownArgs.put(
+ name, OptDetails.create(name, "boolean", Boolean.toString(defaultValue), description));
+ String value = System.getProperty(OPTIONS_PREFIX + name);
+ if (value == null) {
+ return defaultValue;
+ }
+ return Boolean.parseBoolean(value);
+ }
+
+ static long uint64Setting(String name, long defaultValue, String description) {
+ knownArgs.put(
+ name, OptDetails.create(name, "uint64", Long.toUnsignedString(defaultValue), description));
+ String value = System.getProperty(OPTIONS_PREFIX + name);
+ if (value == null) {
+ return defaultValue;
+ }
+ return Long.parseUnsignedLong(value, 10);
+ }
+
+ static void failOnUnknownArgument() {
+ System.getProperties()
+ .keySet()
+ .stream()
+ .map(key -> (String) key)
+ .filter(key -> key.startsWith("jazzer."))
+ .map(key -> key.substring("jazzer.".length()))
+ .filter(key -> !key.startsWith("internal."))
+ .filter(key -> !knownArgs.containsKey(key))
+ .findFirst()
+ .ifPresent(unknownArg -> {
+ Log.error(String.format(
+ "Unknown argument '--%1$s' or property 'jazzer.%1$s' (list all available arguments with --help)",
+ unknownArg));
+ exit(1);
+ });
+ }
+
+ /**
+ * Split value into non-empty takens separated by separator. Backslashes can be used to escape
+ * separators (or backslashes).
+ *
+ * @param value the string to split
+ * @param separator a single character to split on (backslash is not allowed)
+ * @return an immutable list of tokens obtained by splitting value on separator
+ */
+ static List<String> splitOnUnescapedSeparator(String value, char separator) {
+ if (separator == '\\') {
+ throw new IllegalArgumentException("separator '\\' is not supported");
+ }
+ ArrayList<String> tokens = new ArrayList<>();
+ StringBuilder currentToken = new StringBuilder();
+ boolean inEscapeState = false;
+ for (int pos = 0; pos < value.length(); pos++) {
+ char c = value.charAt(pos);
+ if (inEscapeState) {
+ currentToken.append(c);
+ inEscapeState = false;
+ } else if (c == '\\') {
+ inEscapeState = true;
+ } else if (c == separator) {
+ // Do not emit empty tokens between consecutive separators.
+ if (currentToken.length() > 0) {
+ tokens.add(currentToken.toString());
+ }
+ currentToken.setLength(0);
+ } else {
+ currentToken.append(c);
+ }
+ }
+ if (currentToken.length() > 0) {
+ tokens.add(currentToken.toString());
+ }
+ return Collections.unmodifiableList(tokens);
+ }
+
+ private static final class OptDetails {
+ final String name;
+ final String type;
+ final String defaultValue;
+ final String description;
+
+ private OptDetails(String name, String type, String defaultValue, String description) {
+ this.name = name;
+ this.type = type;
+ this.defaultValue = defaultValue;
+ this.description = description;
+ }
+
+ static OptDetails create(String name, String type, String defaultValue, String description) {
+ if (description == null) {
+ return null;
+ }
+ return new OptDetails(checkNotNullOrEmpty(name, "name"), checkNotNullOrEmpty(type, "type"),
+ defaultValue, checkNotNullOrEmpty(description, "description"));
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "--%s (%s, default: \"%s\")%n %s", name, type, defaultValue, description);
+ }
+
+ private static String checkNotNullOrEmpty(String arg, String name) {
+ if (arg == null) {
+ throw new NullPointerException(name + " must not be null");
+ }
+ if (arg.isEmpty()) {
+ throw new NullPointerException(name + " must not be empty");
+ }
+ return arg;
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/RecordingFuzzedDataProvider.java b/src/main/java/com/code_intelligence/jazzer/driver/RecordingFuzzedDataProvider.java
new file mode 100644
index 00000000..6593f0d9
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/RecordingFuzzedDataProvider.java
@@ -0,0 +1,213 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.driver;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Base64;
+
+// Wraps the native FuzzedDataProviderImpl and serializes all its return values
+// into a Base64-encoded string.
+public final class RecordingFuzzedDataProvider implements FuzzedDataProvider {
+ private final FuzzedDataProvider target;
+ private final ArrayList<Object> recordedReplies = new ArrayList<>();
+
+ private RecordingFuzzedDataProvider(FuzzedDataProvider target) {
+ this.target = target;
+ }
+
+ public static FuzzedDataProvider makeFuzzedDataProviderProxy(FuzzedDataProvider target) {
+ return new RecordingFuzzedDataProvider(target);
+ }
+
+ public static String serializeFuzzedDataProviderProxy(FuzzedDataProvider proxy)
+ throws IOException {
+ return ((RecordingFuzzedDataProvider) proxy).serialize();
+ }
+
+ private <T> T recordAndReturn(T object) {
+ recordedReplies.add(object);
+ return object;
+ }
+
+ private String serialize() throws IOException {
+ byte[] rawOut;
+ try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) {
+ try (ObjectOutputStream objectStream = new ObjectOutputStream(byteStream)) {
+ objectStream.writeObject(recordedReplies);
+ }
+ rawOut = byteStream.toByteArray();
+ }
+ return Base64.getEncoder().encodeToString(rawOut);
+ }
+
+ @Override
+ public boolean consumeBoolean() {
+ return recordAndReturn(target.consumeBoolean());
+ }
+
+ @Override
+ public boolean[] consumeBooleans(int maxLength) {
+ return recordAndReturn(target.consumeBooleans(maxLength));
+ }
+
+ @Override
+ public byte consumeByte() {
+ return recordAndReturn(target.consumeByte());
+ }
+
+ @Override
+ public byte consumeByte(byte min, byte max) {
+ return recordAndReturn(target.consumeByte(min, max));
+ }
+
+ @Override
+ public byte[] consumeBytes(int maxLength) {
+ return recordAndReturn(target.consumeBytes(maxLength));
+ }
+
+ @Override
+ public byte[] consumeRemainingAsBytes() {
+ return recordAndReturn(target.consumeRemainingAsBytes());
+ }
+
+ @Override
+ public short consumeShort() {
+ return recordAndReturn(target.consumeShort());
+ }
+
+ @Override
+ public short consumeShort(short min, short max) {
+ return recordAndReturn(target.consumeShort(min, max));
+ }
+
+ @Override
+ public short[] consumeShorts(int maxLength) {
+ return recordAndReturn(target.consumeShorts(maxLength));
+ }
+
+ @Override
+ public int consumeInt() {
+ return recordAndReturn(target.consumeInt());
+ }
+
+ @Override
+ public int consumeInt(int min, int max) {
+ return recordAndReturn(target.consumeInt(min, max));
+ }
+
+ @Override
+ public int[] consumeInts(int maxLength) {
+ return recordAndReturn(target.consumeInts(maxLength));
+ }
+
+ @Override
+ public long consumeLong() {
+ return recordAndReturn(target.consumeLong());
+ }
+
+ @Override
+ public long consumeLong(long min, long max) {
+ return recordAndReturn(target.consumeLong(min, max));
+ }
+
+ @Override
+ public long[] consumeLongs(int maxLength) {
+ return recordAndReturn(target.consumeLongs(maxLength));
+ }
+
+ @Override
+ public float consumeFloat() {
+ return recordAndReturn(target.consumeFloat());
+ }
+
+ @Override
+ public float consumeRegularFloat() {
+ return recordAndReturn(target.consumeRegularFloat());
+ }
+
+ @Override
+ public float consumeRegularFloat(float min, float max) {
+ return recordAndReturn(target.consumeRegularFloat(min, max));
+ }
+
+ @Override
+ public float consumeProbabilityFloat() {
+ return recordAndReturn(target.consumeProbabilityFloat());
+ }
+
+ @Override
+ public double consumeDouble() {
+ return recordAndReturn(target.consumeDouble());
+ }
+
+ @Override
+ public double consumeRegularDouble() {
+ return recordAndReturn(target.consumeRegularDouble());
+ }
+
+ @Override
+ public double consumeRegularDouble(double min, double max) {
+ return recordAndReturn(target.consumeRegularDouble(min, max));
+ }
+
+ @Override
+ public double consumeProbabilityDouble() {
+ return recordAndReturn(target.consumeProbabilityDouble());
+ }
+
+ @Override
+ public char consumeChar() {
+ return recordAndReturn(target.consumeChar());
+ }
+
+ @Override
+ public char consumeChar(char min, char max) {
+ return recordAndReturn(target.consumeChar(min, max));
+ }
+
+ @Override
+ public char consumeCharNoSurrogates() {
+ return recordAndReturn(target.consumeCharNoSurrogates());
+ }
+
+ @Override
+ public String consumeString(int maxLength) {
+ return recordAndReturn(target.consumeString(maxLength));
+ }
+
+ @Override
+ public String consumeRemainingAsString() {
+ return recordAndReturn(target.consumeRemainingAsString());
+ }
+
+ @Override
+ public String consumeAsciiString(int maxLength) {
+ return recordAndReturn(target.consumeAsciiString(maxLength));
+ }
+
+ @Override
+ public String consumeRemainingAsAsciiString() {
+ return recordAndReturn(target.consumeRemainingAsAsciiString());
+ }
+
+ @Override
+ public int remainingBytes() {
+ return recordAndReturn(target.remainingBytes());
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl b/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl
new file mode 100644
index 00000000..3c441756
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl
@@ -0,0 +1,28 @@
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class Crash_%1$s {
+ static final String base64Bytes = String.join("", "%2$s");
+
+ public static void main(String[] args) throws Throwable {
+ Crash_%1$s.class.getClassLoader().setDefaultAssertionStatus(true);
+ try {
+ Method fuzzerInitialize = %3$s.class.getMethod("fuzzerInitialize");
+ fuzzerInitialize.invoke(null);
+ } catch (NoSuchMethodException ignored) {
+ try {
+ Method fuzzerInitialize = %3$s.class.getMethod("fuzzerInitialize", String[].class);
+ fuzzerInitialize.invoke(null, (Object) args);
+ } catch (NoSuchMethodException ignored1) {
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ %4$s
+ %3$s.fuzzerTestOneInput(input);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java b/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java
new file mode 100644
index 00000000..a69a8dba
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2022 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.driver;
+
+import com.code_intelligence.jazzer.utils.Log;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.stream.Collectors;
+
+final class ReproducerTemplate {
+ // A constant pool CONSTANT_Utf8_info entry should be able to hold data of size
+ // uint16, but somehow this does not seem to be the case and leads to invalid
+ // code crash reproducer code. Reducing the size by one resolves the problem.
+ private static final int DATA_CHUNK_MAX_LENGTH = Short.MAX_VALUE - 1;
+ private static final String RAW_BYTES_INPUT =
+ "byte[] input = java.util.Base64.getDecoder().decode(base64Bytes);";
+ private static final String FUZZED_DATA_PROVIDER_INPUT =
+ "com.code_intelligence.jazzer.api.CannedFuzzedDataProvider input = new com.code_intelligence.jazzer.api.CannedFuzzedDataProvider(base64Bytes);";
+
+ private final String targetClass;
+ private final boolean useFuzzedDataProvider;
+
+ public ReproducerTemplate(String targetClass, boolean useFuzzedDataProvider) {
+ this.targetClass = targetClass;
+ this.useFuzzedDataProvider = useFuzzedDataProvider;
+ }
+
+ /**
+ * Emits a Java reproducer to {@code Crash_HASH.java} in {@code Opt.reproducerPath}.
+ *
+ * @param data the Base64-encoded data to emit as a string literal
+ * @param sha the SHA1 hash of the raw fuzzer input
+ */
+ public void dumpReproducer(String data, String sha) {
+ String targetArg = useFuzzedDataProvider ? FUZZED_DATA_PROVIDER_INPUT : RAW_BYTES_INPUT;
+ String template = new BufferedReader(
+ new InputStreamReader(ReproducerTemplate.class.getResourceAsStream("Reproducer.java.tmpl"),
+ StandardCharsets.UTF_8))
+ .lines()
+ .collect(Collectors.joining("\n"));
+ String chunkedData = chunkStringLiteral(data);
+ String javaSource = String.format(template, sha, chunkedData, targetClass, targetArg);
+ Path javaPath = Paths.get(Opt.reproducerPath, String.format("Crash_%s.java", sha));
+ try {
+ Files.write(javaPath, javaSource.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ Log.error(String.format("Failed to write Java reproducer to %s%n", javaPath));
+ e.printStackTrace();
+ }
+ Log.println(String.format(
+ "reproducer_path='%s'; Java reproducer written to %s%n", Opt.reproducerPath, javaPath));
+ }
+
+ // The serialization of recorded FuzzedDataProvider invocations can get too long to be emitted
+ // into the template as a single String literal. This is mitigated by chunking the data and
+ // concatenating it again in the generated code.
+ private String chunkStringLiteral(String data) {
+ ArrayList<String> chunks = new ArrayList<>();
+ for (int i = 0; i <= data.length() / DATA_CHUNK_MAX_LENGTH; i++) {
+ chunks.add(data.substring(
+ i * DATA_CHUNK_MAX_LENGTH, Math.min((i + 1) * DATA_CHUNK_MAX_LENGTH, data.length())));
+ }
+ return String.join("\", \"", chunks);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/SignalHandler.java b/src/main/java/com/code_intelligence/jazzer/driver/SignalHandler.java
new file mode 100644
index 00000000..215a0479
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/SignalHandler.java
@@ -0,0 +1,31 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.driver;
+
+import com.github.fmeum.rules_jni.RulesJni;
+import sun.misc.Signal;
+
+public final class SignalHandler {
+ static {
+ RulesJni.loadLibrary("jazzer_signal_handler", SignalHandler.class);
+ Signal.handle(new Signal("INT"), sig -> handleInterrupt());
+ }
+
+ public static void initialize() {
+ // Implicitly runs the static initializer.
+ }
+
+ private static native void handleInterrupt();
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/junit/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/driver/junit/BUILD.bazel
new file mode 100644
index 00000000..c715365b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/junit/BUILD.bazel
@@ -0,0 +1,30 @@
+java_library(
+ name = "junit_runner",
+ srcs = ["JUnitRunner.java"],
+ visibility = ["//src/main/java/com/code_intelligence/jazzer/driver:__pkg__"],
+ deps = [
+ ":exit_code_exception",
+ ":junit_compile_only",
+ "//src/main/java/com/code_intelligence/jazzer/driver:constants",
+ "//src/main/java/com/code_intelligence/jazzer/driver:exception_utils",
+ "//src/main/java/com/code_intelligence/jazzer/driver:fuzz_target_runner",
+ "//src/main/java/com/code_intelligence/jazzer/driver:opt",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ "@maven//:org_junit_platform_junit_platform_engine",
+ ],
+)
+
+java_library(
+ name = "exit_code_exception",
+ srcs = ["ExitCodeException.java"],
+ visibility = ["//src/main/java/com/code_intelligence/jazzer/junit:__pkg__"],
+)
+
+java_library(
+ name = "junit_compile_only",
+ neverlink = True,
+ exports = [
+ "@maven//:org_junit_jupiter_junit_jupiter_engine",
+ "@maven//:org_junit_platform_junit_platform_launcher",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/junit/ExitCodeException.java b/src/main/java/com/code_intelligence/jazzer/driver/junit/ExitCodeException.java
new file mode 100644
index 00000000..662fb9b1
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/junit/ExitCodeException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.driver.junit;
+
+public final class ExitCodeException extends Exception {
+ public final int exitCode;
+
+ public ExitCodeException(String message, int exitCode) {
+ super(message);
+ this.exitCode = exitCode;
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/junit/JUnitRunner.java b/src/main/java/com/code_intelligence/jazzer/driver/junit/JUnitRunner.java
new file mode 100644
index 00000000..5bba4342
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/junit/JUnitRunner.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.driver.junit;
+
+import static com.code_intelligence.jazzer.driver.Constants.JAZZER_FINDING_EXIT_CODE;
+import static com.code_intelligence.jazzer.driver.FuzzTargetRunner.printCrashingInput;
+import static org.junit.platform.engine.FilterResult.includedIf;
+import static org.junit.platform.engine.TestExecutionResult.Status.ABORTED;
+import static org.junit.platform.engine.TestExecutionResult.Status.FAILED;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+import static org.junit.platform.launcher.TagFilter.includeTags;
+
+import com.code_intelligence.jazzer.driver.ExceptionUtils;
+import com.code_intelligence.jazzer.driver.Opt;
+import com.code_intelligence.jazzer.utils.Log;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.jupiter.engine.JupiterTestEngine;
+import org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.reporting.ReportEntry;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.PostDiscoveryFilter;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.core.LauncherConfig;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+
+public final class JUnitRunner {
+ private final Launcher launcher;
+ private final TestPlan testPlan;
+
+ private JUnitRunner(Launcher launcher, TestPlan testPlan) {
+ this.launcher = launcher;
+ this.testPlan = testPlan;
+ }
+
+ // Detects the presence of both the JUnit launcher and the Jupiter engine on the classpath.
+ public static boolean isSupported() {
+ try {
+ Class.forName("org.junit.platform.launcher.LauncherDiscoveryRequest");
+ Class.forName("org.junit.jupiter.engine.JupiterTestEngine");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ public static Optional<JUnitRunner> create(String testClassName, List<String> libFuzzerArgs) {
+ // We want the test execution to be as lightweight as possible, so disable all auto-discover and
+ // only register the test engine we are using for @FuzzTest, JUnit Jupiter.
+ LauncherConfig config = LauncherConfig.builder()
+ .addTestEngines(new JupiterTestEngine())
+ .enableLauncherDiscoveryListenerAutoRegistration(false)
+ .enableLauncherSessionListenerAutoRegistration(false)
+ .enablePostDiscoveryFilterAutoRegistration(false)
+ .enableTestEngineAutoRegistration(false)
+ .enableTestExecutionListenerAutoRegistration(false)
+ .build();
+
+ Map<String, String> indexedArgs =
+ IntStream.range(0, libFuzzerArgs.size())
+ .boxed()
+ .collect(Collectors.toMap(i -> "jazzer.internal.arg." + i, libFuzzerArgs::get));
+
+ LauncherDiscoveryRequestBuilder requestBuilder =
+ LauncherDiscoveryRequestBuilder.request()
+ .configurationParameter("jazzer.internal.commandLine", "true")
+ .configurationParameters(indexedArgs)
+ .selectors(selectClass(testClassName))
+ .filters(includeTags("jazzer"));
+ if (!Opt.targetMethod.isEmpty()) {
+ // HACK: This depends on JUnit internals as we need to filter by method name without having to
+ // specify the parameter types of the method.
+ requestBuilder.filters((PostDiscoveryFilter) testDescriptor
+ -> includedIf(!(testDescriptor instanceof MethodBasedTestDescriptor)
+ || ((MethodBasedTestDescriptor) testDescriptor)
+ .getTestMethod()
+ .getName()
+ .equals(Opt.targetMethod)));
+ }
+ LauncherDiscoveryRequest request = requestBuilder.build();
+ Launcher launcher = LauncherFactory.create(config);
+ TestPlan testPlan = launcher.discover(request);
+ if (!testPlan.containsTests()) {
+ return Optional.empty();
+ }
+ return Optional.of(new JUnitRunner(launcher, testPlan));
+ }
+
+ public int run() {
+ AtomicReference<TestExecutionResult> resultHolder =
+ new AtomicReference<>(TestExecutionResult.successful());
+ launcher.execute(testPlan, new TestExecutionListener() {
+ @Override
+ public void executionFinished(
+ TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
+ // Lifecycle methods can fail too, which results in failed execution results on container
+ // nodes. We keep the last failing one with a stack trace. For tests, we also keep the stack
+ // traces of aborted tests so that we can show a warning. In JUnit Jupiter, tests and
+ // containers always fail with a throwable:
+ // https://github.com/junit-team/junit5/blob/ac31e9a7d58973db73496244dab4defe17ae563e/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ThrowableCollector.java#LL176C37-L176C37
+ if ((testIdentifier.isTest() && testExecutionResult.getThrowable().isPresent())
+ || testExecutionResult.getStatus() == FAILED) {
+ resultHolder.set(testExecutionResult);
+ }
+ if (testExecutionResult.getStatus() == FAILED
+ && testExecutionResult.getThrowable().isPresent()) {
+ resultHolder.set(testExecutionResult);
+ }
+ }
+
+ @Override
+ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) {
+ entry.getKeyValuePairs().values().forEach(Log::info);
+ }
+ });
+
+ TestExecutionResult result = resultHolder.get();
+ if (result.getStatus() != FAILED) {
+ // We do not generate a finding for Aborted tests (i.e. tests whose preconditions were not
+ // met) as such tests also wouldn't make a test run fail.
+ if (result.getStatus() == ABORTED) {
+ Log.warn("Fuzz test aborted", result.getThrowable().orElse(null));
+ }
+ return 0;
+ }
+
+ // Safe to unwrap as result is either TestExecutionResult.successful() (initial value) or has
+ // a throwable (set in the TestExecutionListener above).
+ Throwable throwable = result.getThrowable().get();
+ if (throwable instanceof ExitCodeException) {
+ // Jazzer found a regular finding and printed it, so just return the exit code.
+ return ((ExitCodeException) throwable).exitCode;
+ } else {
+ // Jazzer didn't report a finding, but an afterAll-type function threw an exception. Report it
+ // as a finding, cleaning up the stack trace.
+ Log.finding(ExceptionUtils.preprocessThrowable(throwable));
+ Log.println("== libFuzzer crashing input ==");
+ printCrashingInput();
+ return JAZZER_FINDING_EXIT_CODE;
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel
new file mode 100644
index 00000000..bbb449a4
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel
@@ -0,0 +1,41 @@
+load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+load("//bazel:kotlin.bzl", "ktlint")
+
+kt_jvm_library(
+ name = "instrumentor",
+ srcs = [
+ "ClassInstrumentor.kt",
+ "CoverageRecorder.kt",
+ "DescriptorUtils.kt",
+ "DeterministicRandom.kt",
+ "EdgeCoverageInstrumentor.kt",
+ "Hook.kt",
+ "HookInstrumentor.kt",
+ "HookMethodVisitor.kt",
+ "Hooks.kt",
+ "Instrumentor.kt",
+ "StaticMethodStrategy.java",
+ "TraceDataFlowInstrumentor.kt",
+ ],
+ visibility = [
+ "//src/jmh/java/com/code_intelligence/jazzer/instrumentor:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/driver:__pkg__",
+ "//src/test/java/com/code_intelligence/jazzer/instrumentor:__pkg__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap_compile_only",
+ "//src/main/java/com/code_intelligence/jazzer/utils",
+ "//src/main/java/com/code_intelligence/jazzer/utils:class_name_globber",
+ "//src/main/java/com/code_intelligence/jazzer/utils:log",
+ "@com_github_classgraph_classgraph//:classgraph",
+ "@com_github_jetbrains_kotlin//:kotlin-reflect",
+ "@jazzer_jacoco//:jacoco_internal",
+ "@org_ow2_asm_asm//jar",
+ "@org_ow2_asm_asm_commons//jar",
+ "@org_ow2_asm_asm_tree//jar",
+ ],
+)
+
+ktlint()
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt
new file mode 100644
index 00000000..a93e29c7
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt
@@ -0,0 +1,55 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor
+
+import com.code_intelligence.jazzer.runtime.CoverageMap
+
+fun extractClassFileMajorVersion(classfileBuffer: ByteArray): Int {
+ return ((classfileBuffer[6].toInt() and 0xff) shl 8) or (classfileBuffer[7].toInt() and 0xff)
+}
+
+class ClassInstrumentor(private val internalClassName: String, bytecode: ByteArray) {
+
+ var instrumentedBytecode = bytecode
+ private set
+
+ fun coverage(initialEdgeId: Int): Int {
+ val edgeCoverageInstrumentor = EdgeCoverageInstrumentor(
+ defaultEdgeCoverageStrategy,
+ defaultCoverageMap,
+ initialEdgeId,
+ )
+ instrumentedBytecode = edgeCoverageInstrumentor.instrument(internalClassName, instrumentedBytecode)
+ return edgeCoverageInstrumentor.numEdges
+ }
+
+ fun traceDataFlow(instrumentations: Set<InstrumentationType>) {
+ instrumentedBytecode =
+ TraceDataFlowInstrumentor(instrumentations).instrument(internalClassName, instrumentedBytecode)
+ }
+
+ fun hooks(hooks: Iterable<Hook>, classWithHooksEnabledField: String?) {
+ instrumentedBytecode = HookInstrumentor(
+ hooks,
+ java6Mode = extractClassFileMajorVersion(instrumentedBytecode) < 51,
+ classWithHooksEnabledField = classWithHooksEnabledField,
+ ).instrument(internalClassName, instrumentedBytecode)
+ }
+
+ companion object {
+ val defaultEdgeCoverageStrategy = StaticMethodStrategy()
+ val defaultCoverageMap = CoverageMap::class.java
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt
new file mode 100644
index 00000000..56fb5725
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt
@@ -0,0 +1,252 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor
+
+import com.code_intelligence.jazzer.runtime.CoverageMap
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.CoverageBuilder
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionData
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataWriter
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.SessionInfo
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.data.CRC64
+import com.code_intelligence.jazzer.utils.ClassNameGlobber
+import io.github.classgraph.ClassGraph
+import java.io.File
+import java.io.FileOutputStream
+import java.io.OutputStream
+import java.time.Instant
+import java.util.UUID
+
+private data class InstrumentedClassInfo(
+ val classId: Long,
+ val initialEdgeId: Int,
+ val nextEdgeId: Int,
+ val bytecode: ByteArray,
+)
+
+object CoverageRecorder {
+ var classNameGlobber = ClassNameGlobber(emptyList(), emptyList())
+ private val instrumentedClassInfo = mutableMapOf<String, InstrumentedClassInfo>()
+ private var startTimestamp: Instant? = null
+ private val additionalCoverage = mutableSetOf<Int>()
+
+ fun recordInstrumentedClass(internalClassName: String, bytecode: ByteArray, firstId: Int, numIds: Int) {
+ if (startTimestamp == null) {
+ startTimestamp = Instant.now()
+ }
+ instrumentedClassInfo[internalClassName] = InstrumentedClassInfo(
+ CRC64.classId(bytecode),
+ firstId,
+ firstId + numIds,
+ bytecode,
+ )
+ }
+
+ /**
+ * Manually records coverage IDs based on the current state of [CoverageMap].
+ * Should be called after static initializers have run.
+ */
+ @JvmStatic
+ fun updateCoveredIdsWithCoverageMap() {
+ additionalCoverage.addAll(CoverageMap.getCoveredIds())
+ }
+
+ /**
+ * [dumpCoverageReport] dumps a human-readable coverage report of files using any [coveredIds] to [dumpFileName].
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun dumpCoverageReport(dumpFileName: String, coveredIds: IntArray = CoverageMap.getEverCoveredIds()) {
+ File(dumpFileName).bufferedWriter().use { writer ->
+ writer.write(computeFileCoverage(coveredIds))
+ }
+ }
+
+ private fun computeFileCoverage(coveredIds: IntArray): String {
+ fun Double.format(digits: Int) = "%.${digits}f".format(this)
+ val coverage = analyzeCoverage(coveredIds.toSet()) ?: return "No classes were instrumented"
+ return coverage.sourceFiles.joinToString(
+ "\n",
+ prefix = "Branch coverage:\n",
+ postfix = "\n\n",
+ ) { fileCoverage ->
+ val counter = fileCoverage.branchCounter
+ val percentage = 100 * counter.coveredRatio
+ "${fileCoverage.name}: ${counter.coveredCount}/${counter.totalCount} (${percentage.format(2)}%)"
+ } + coverage.sourceFiles.joinToString(
+ "\n",
+ prefix = "Line coverage:\n",
+ postfix = "\n\n",
+ ) { fileCoverage ->
+ val counter = fileCoverage.lineCounter
+ val percentage = 100 * counter.coveredRatio
+ "${fileCoverage.name}: ${counter.coveredCount}/${counter.totalCount} (${percentage.format(2)}%)"
+ } + coverage.sourceFiles.joinToString(
+ "\n",
+ prefix = "Incompletely covered lines:\n",
+ postfix = "\n\n",
+ ) { fileCoverage ->
+ "${fileCoverage.name}: " + (fileCoverage.firstLine..fileCoverage.lastLine).filter {
+ val instructions = fileCoverage.getLine(it).instructionCounter
+ instructions.coveredCount in 1 until instructions.totalCount
+ }.toString()
+ } + coverage.sourceFiles.joinToString(
+ "\n",
+ prefix = "Missed lines:\n",
+ ) { fileCoverage ->
+ "${fileCoverage.name}: " + (fileCoverage.firstLine..fileCoverage.lastLine).filter {
+ val instructions = fileCoverage.getLine(it).instructionCounter
+ instructions.coveredCount == 0 && instructions.totalCount > 0
+ }.toString()
+ }
+ }
+
+ /**
+ * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [dumpFileName].
+ * JaCoCo only exports coverage for files containing at least one coverage data point. The dump
+ * can be used by the JaCoCo report command to create reports also including not covered files.
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun dumpJacocoCoverage(dumpFileName: String, coveredIds: IntArray = CoverageMap.getEverCoveredIds()) {
+ FileOutputStream(dumpFileName).use { outStream ->
+ dumpJacocoCoverage(outStream, coveredIds)
+ }
+ }
+
+ /**
+ * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [outStream].
+ */
+ @JvmStatic
+ fun dumpJacocoCoverage(outStream: OutputStream, coveredIds: IntArray) {
+ // Return if no class has been instrumented.
+ val startTimestamp = startTimestamp ?: return
+
+ // Update the list of covered IDs with the coverage information for the current run.
+ updateCoveredIdsWithCoverageMap()
+
+ val dumpTimestamp = Instant.now()
+ val outWriter = ExecutionDataWriter(outStream)
+ outWriter.visitSessionInfo(
+ SessionInfo(UUID.randomUUID().toString(), startTimestamp.epochSecond, dumpTimestamp.epochSecond),
+ )
+ analyzeJacocoCoverage(coveredIds.toSet()).accept(outWriter)
+ }
+
+ /**
+ * Build up a JaCoCo [ExecutionDataStore] based on [coveredIds] containing the internally gathered coverage information.
+ */
+ private fun analyzeJacocoCoverage(coveredIds: Set<Int>): ExecutionDataStore {
+ val executionDataStore = ExecutionDataStore()
+ val sortedCoveredIds = (additionalCoverage + coveredIds).sorted().toIntArray()
+ for ((internalClassName, info) in instrumentedClassInfo) {
+ // Determine the subarray of coverage IDs in sortedCoveredIds that contains the IDs generated while
+ // instrumenting the current class. Since the ID array is sorted, use binary search.
+ var coveredIdsStart = sortedCoveredIds.binarySearch(info.initialEdgeId)
+ if (coveredIdsStart < 0) {
+ coveredIdsStart = -(coveredIdsStart + 1)
+ }
+ var coveredIdsEnd = sortedCoveredIds.binarySearch(info.nextEdgeId)
+ if (coveredIdsEnd < 0) {
+ coveredIdsEnd = -(coveredIdsEnd + 1)
+ }
+ if (coveredIdsStart == coveredIdsEnd) {
+ // No coverage data for the class.
+ continue
+ }
+ check(coveredIdsStart in 0 until coveredIdsEnd && coveredIdsEnd <= sortedCoveredIds.size) {
+ "Invalid range [$coveredIdsStart, $coveredIdsEnd) with coveredIds.size=${sortedCoveredIds.size}"
+ }
+ // Generate a probes array for the current class only, i.e., mapping info.initialEdgeId to 0.
+ val probes = BooleanArray(info.nextEdgeId - info.initialEdgeId)
+ (coveredIdsStart until coveredIdsEnd).asSequence()
+ .map {
+ val globalEdgeId = sortedCoveredIds[it]
+ globalEdgeId - info.initialEdgeId
+ }
+ .forEach { classLocalEdgeId ->
+ probes[classLocalEdgeId] = true
+ }
+ executionDataStore.visitClassExecution(ExecutionData(info.classId, internalClassName, probes))
+ }
+ return executionDataStore
+ }
+
+ /**
+ * Create a [CoverageBuilder] containing all classes matching the include/exclude pattern and their coverage statistics.
+ */
+ fun analyzeCoverage(coveredIds: Set<Int>): CoverageBuilder? {
+ return try {
+ val coverage = CoverageBuilder()
+ analyzeAllUncoveredClasses(coverage)
+ val executionDataStore = analyzeJacocoCoverage(coveredIds)
+ for ((internalClassName, info) in instrumentedClassInfo) {
+ EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0)
+ .analyze(
+ executionDataStore,
+ coverage,
+ info.bytecode,
+ internalClassName,
+ )
+ }
+ coverage
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ /**
+ * Traverses the entire classpath and analyzes all uncovered classes that match the include/exclude pattern.
+ * The returned [CoverageBuilder] will report coverage information for *all* classes on the classpath, not just
+ * those that were loaded while the fuzzer ran.
+ */
+ private fun analyzeAllUncoveredClasses(coverage: CoverageBuilder): CoverageBuilder {
+ val coveredClassNames = instrumentedClassInfo
+ .keys
+ .asSequence()
+ .map { it.replace('/', '.') }
+ .toSet()
+ ClassGraph()
+ .enableClassInfo()
+ .ignoreClassVisibility()
+ .rejectPackages(
+ // Always exclude Jazzer-internal packages (including ClassGraph itself) from coverage reports. Classes
+ // from the Java standard library are never traversed.
+ "com.code_intelligence.jazzer.*",
+ "jaz",
+ )
+ .scan().use { result ->
+ // ExecutionDataStore is used to look up existing coverage during analysis of the class files,
+ // no entries are added during that. Passing in an empty store is fine for uncovered files.
+ val emptyExecutionDataStore = ExecutionDataStore()
+ result.allClasses
+ .asSequence()
+ .filter { classInfo -> classNameGlobber.includes(classInfo.name) }
+ .filterNot { classInfo -> classInfo.name in coveredClassNames }
+ .forEach { classInfo ->
+ classInfo.resource.use { resource ->
+ EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0).analyze(
+ emptyExecutionDataStore,
+ coverage,
+ resource.load(),
+ classInfo.name.replace('.', '/'),
+ )
+ }
+ }
+ }
+ return coverage
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt
new file mode 100644
index 00000000..9d02c04f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt
@@ -0,0 +1,105 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor
+
+import org.objectweb.asm.Type
+import java.lang.reflect.Constructor
+import java.lang.reflect.Executable
+import java.lang.reflect.Method
+
+val Class<*>.descriptor: String
+ get() = Type.getDescriptor(this)
+
+val Executable.descriptor: String
+ get() = if (this is Method) {
+ Type.getMethodDescriptor(this)
+ } else {
+ Type.getConstructorDescriptor(this as Constructor<*>?)
+ }
+
+internal fun isPrimitiveType(typeDescriptor: String): Boolean {
+ return typeDescriptor in arrayOf("B", "C", "D", "F", "I", "J", "S", "V", "Z")
+}
+
+private fun isPrimitiveType(typeDescriptor: Char) = isPrimitiveType(typeDescriptor.toString())
+
+internal fun getWrapperTypeDescriptor(typeDescriptor: String): String = when (typeDescriptor) {
+ "B" -> "Ljava/lang/Byte;"
+ "C" -> "Ljava/lang/Character;"
+ "D" -> "Ljava/lang/Double;"
+ "F" -> "Ljava/lang/Float;"
+ "I" -> "Ljava/lang/Integer;"
+ "J" -> "Ljava/lang/Long;"
+ "S" -> "Ljava/lang/Short;"
+ "V" -> "Ljava/lang/Void;"
+ "Z" -> "Ljava/lang/Boolean;"
+ else -> typeDescriptor
+}
+
+// Removes the 'L' and ';' prefix/suffix from signatures to get the full class name.
+// Note that array signatures '[Ljava/lang/String;' already have the correct form.
+internal fun extractInternalClassName(typeDescriptor: String): String {
+ return if (typeDescriptor.startsWith("L") && typeDescriptor.endsWith(";")) {
+ typeDescriptor.substring(1, typeDescriptor.length - 1)
+ } else {
+ typeDescriptor
+ }
+}
+
+internal fun extractParameterTypeDescriptors(methodDescriptor: String): List<String> {
+ require(methodDescriptor.startsWith('(')) { "Method descriptor must start with '('" }
+ val endOfParameterPart = methodDescriptor.indexOf(')') - 1
+ require(endOfParameterPart >= 0) { "Method descriptor must contain ')'" }
+ var remainingDescriptorList = methodDescriptor.substring(1..endOfParameterPart)
+ val parameterDescriptors = mutableListOf<String>()
+ while (remainingDescriptorList.isNotEmpty()) {
+ val nextDescriptor = extractNextTypeDescriptor(remainingDescriptorList)
+ parameterDescriptors.add(nextDescriptor)
+ remainingDescriptorList = remainingDescriptorList.removePrefix(nextDescriptor)
+ }
+ return parameterDescriptors
+}
+
+internal fun extractReturnTypeDescriptor(methodDescriptor: String): String {
+ require(methodDescriptor.startsWith('(')) { "Method descriptor must start with '('" }
+ val endBracketPos = methodDescriptor.indexOf(')')
+ require(endBracketPos >= 0) { "Method descriptor must contain ')'" }
+ val startOfReturnValue = endBracketPos + 1
+ return extractNextTypeDescriptor(methodDescriptor.substring(startOfReturnValue))
+}
+
+private fun extractNextTypeDescriptor(input: String): String {
+ require(input.isNotEmpty()) { "Type descriptor must not be empty" }
+ // Skip over arbitrarily many '[' to support multi-dimensional arrays.
+ val firstNonArrayPrefixCharPos = input.indexOfFirst { it != '[' }
+ require(firstNonArrayPrefixCharPos >= 0) { "Array descriptor must contain type" }
+ val firstTypeChar = input[firstNonArrayPrefixCharPos]
+ return when {
+ // Primitive type
+ isPrimitiveType(firstTypeChar) -> {
+ input.substring(0..firstNonArrayPrefixCharPos)
+ }
+ // Object type
+ firstTypeChar == 'L' -> {
+ val endOfClassNamePos = input.indexOf(';')
+ require(endOfClassNamePos > 0) { "Class type indicated by L must end with ;" }
+ input.substring(0..endOfClassNamePos)
+ }
+ // Invalid type
+ else -> {
+ throw IllegalArgumentException("Invalid type: $firstTypeChar")
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt
new file mode 100644
index 00000000..d4210dc4
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt
@@ -0,0 +1,35 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor
+
+import java.security.MessageDigest
+import java.security.SecureRandom
+
+// This RNG is resistant to collisions (even under XOR) but fully deterministic.
+internal class DeterministicRandom(vararg contexts: String) {
+ private val random = SecureRandom.getInstance("SHA1PRNG").apply {
+ val contextHash = MessageDigest.getInstance("SHA-256").run {
+ for (context in contexts) {
+ update(context.toByteArray())
+ }
+ digest()
+ }
+ setSeed(contextHash)
+ }
+
+ fun nextInt(bound: Int) = random.nextInt(bound)
+
+ fun nextInt() = random.nextInt()
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt
new file mode 100644
index 00000000..975f3987
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt
@@ -0,0 +1,187 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor
+
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.Analyzer
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.ICoverageVisitor
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.ClassProbesAdapter
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.ClassProbesVisitor
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.IClassProbesAdapterFactory
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.ClassInstrumenter
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.IProbeArrayStrategy
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.IProbeInserterFactory
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.InstrSupport
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.ProbeInserter
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.MethodVisitor
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles.publicLookup
+import java.lang.invoke.MethodType.methodType
+import kotlin.math.max
+
+/**
+ * A particular way to instrument bytecode for edge coverage using a coverage map class available to
+ * hold the collected coverage data at runtime.
+ */
+interface EdgeCoverageStrategy {
+
+ /**
+ * Inject bytecode instrumentation on a control flow edge with ID [edgeId], with access to the
+ * local variable [variable] that is populated at the beginning of each method by the
+ * instrumentation injected in [loadLocalVariable].
+ */
+ fun instrumentControlFlowEdge(
+ mv: MethodVisitor,
+ edgeId: Int,
+ variable: Int,
+ coverageMapInternalClassName: String,
+ )
+
+ /**
+ * The maximal number of stack elements used by [instrumentControlFlowEdge].
+ */
+ val instrumentControlFlowEdgeStackSize: Int
+
+ /**
+ * The type of the local variable used by the instrumentation in the format used by
+ * [MethodVisitor.visitFrame]'s `local` parameter, or `null` if the instrumentation does not use
+ * one.
+ * @see https://asm.ow2.io/javadoc/org/objectweb/asm/MethodVisitor.html#visitFrame(int,int,java.lang.Object%5B%5D,int,java.lang.Object%5B%5D)
+ */
+ val localVariableType: Any?
+
+ /**
+ * Inject bytecode that loads the coverage counters of the coverage map class described by
+ * [coverageMapInternalClassName] into the local variable [variable].
+ */
+ fun loadLocalVariable(mv: MethodVisitor, variable: Int, coverageMapInternalClassName: String)
+
+ /**
+ * The maximal number of stack elements used by [loadLocalVariable].
+ */
+ val loadLocalVariableStackSize: Int
+}
+
+// An instance of EdgeCoverageInstrumentor should only be used to instrument a single class as it
+// internally tracks the edge IDs, which have to be globally unique.
+class EdgeCoverageInstrumentor(
+ private val strategy: EdgeCoverageStrategy,
+ /**
+ * The class must have the following public static member
+ * - method enlargeIfNeeded(int nextEdgeId): Called before a new edge ID is emitted.
+ */
+ coverageMapClass: Class<*>,
+ private val initialEdgeId: Int,
+) : Instrumentor {
+ private var nextEdgeId = initialEdgeId
+
+ private val coverageMapInternalClassName = coverageMapClass.name.replace('.', '/')
+ private val enlargeIfNeeded: MethodHandle =
+ publicLookup().findStatic(
+ coverageMapClass,
+ "enlargeIfNeeded",
+ methodType(
+ Void::class.javaPrimitiveType,
+ Int::class.javaPrimitiveType,
+ ),
+ )
+
+ override fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray {
+ val reader = InstrSupport.classReaderFor(bytecode)
+ val writer = ClassWriter(reader, 0)
+ val version = InstrSupport.getMajorVersion(reader)
+ val visitor = EdgeCoverageClassProbesAdapter(
+ ClassInstrumenter(edgeCoverageProbeArrayStrategy, edgeCoverageProbeInserterFactory, writer),
+ InstrSupport.needsFrames(version),
+ )
+ reader.accept(visitor, ClassReader.EXPAND_FRAMES)
+ return writer.toByteArray()
+ }
+
+ fun analyze(executionData: ExecutionDataStore, coverageVisitor: ICoverageVisitor, bytecode: ByteArray, internalClassName: String) {
+ Analyzer(executionData, coverageVisitor, edgeCoverageClassProbesAdapterFactory).run {
+ analyzeClass(bytecode, internalClassName)
+ }
+ }
+
+ val numEdges
+ get() = nextEdgeId - initialEdgeId
+
+ private fun nextEdgeId(): Int {
+ enlargeIfNeeded.invokeExact(nextEdgeId)
+ return nextEdgeId++
+ }
+
+ /**
+ * A [ProbeInserter] that injects bytecode instrumentation at every control flow edge and
+ * modifies the stack size and number of local variables accordingly.
+ */
+ private inner class EdgeCoverageProbeInserter(
+ access: Int,
+ name: String,
+ desc: String,
+ mv: MethodVisitor,
+ arrayStrategy: IProbeArrayStrategy,
+ ) : ProbeInserter(access, name, desc, mv, arrayStrategy) {
+ override fun insertProbe(id: Int) {
+ strategy.instrumentControlFlowEdge(mv, id, variable, coverageMapInternalClassName)
+ }
+
+ override fun visitMaxs(maxStack: Int, maxLocals: Int) {
+ val newMaxStack = max(maxStack + strategy.instrumentControlFlowEdgeStackSize, strategy.loadLocalVariableStackSize)
+ val newMaxLocals = maxLocals + if (strategy.localVariableType != null) 1 else 0
+ mv.visitMaxs(newMaxStack, newMaxLocals)
+ }
+
+ override fun getLocalVariableType() = strategy.localVariableType
+ }
+
+ private val edgeCoverageProbeInserterFactory =
+ IProbeInserterFactory { access, name, desc, mv, arrayStrategy ->
+ EdgeCoverageProbeInserter(access, name, desc, mv, arrayStrategy)
+ }
+
+ private inner class EdgeCoverageClassProbesAdapter(private val cpv: ClassProbesVisitor, trackFrames: Boolean) :
+ ClassProbesAdapter(cpv, trackFrames) {
+ override fun nextId(): Int = nextEdgeId()
+
+ override fun visitEnd() {
+ cpv.visitTotalProbeCount(numEdges)
+ // Avoid calling super.visitEnd() as that invokes cpv.visitTotalProbeCount with an
+ // incorrect value of `count`.
+ cpv.visitEnd()
+ }
+ }
+
+ private val edgeCoverageClassProbesAdapterFactory = IClassProbesAdapterFactory { probesVisitor, trackFrames ->
+ EdgeCoverageClassProbesAdapter(probesVisitor, trackFrames)
+ }
+
+ private val edgeCoverageProbeArrayStrategy = object : IProbeArrayStrategy {
+ override fun storeInstance(mv: MethodVisitor, clinit: Boolean, variable: Int): Int {
+ strategy.loadLocalVariable(mv, variable, coverageMapInternalClassName)
+ return strategy.loadLocalVariableStackSize
+ }
+
+ override fun addMembers(cv: ClassVisitor, probeCount: Int) {}
+ }
+}
+
+fun MethodVisitor.push(value: Int) {
+ InstrSupport.push(this, value)
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt
new file mode 100644
index 00000000..077ab10e
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt
@@ -0,0 +1,132 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+
+package com.code_intelligence.jazzer.instrumentor
+
+import com.code_intelligence.jazzer.api.HookType
+import com.code_intelligence.jazzer.api.MethodHook
+import java.lang.invoke.MethodHandle
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+
+class Hook private constructor(
+ private val targetClassName: String,
+ val hookType: HookType,
+ val targetMethodName: String,
+ val targetMethodDescriptor: String?,
+ val additionalClassesToHook: List<String>,
+ val targetInternalClassName: String,
+ private val targetReturnTypeDescriptor: String?,
+ private val targetWrappedReturnTypeDescriptor: String?,
+ private val hookClassName: String,
+ val hookInternalClassName: String,
+ val hookMethodName: String,
+ val hookMethodDescriptor: String,
+) {
+
+ override fun toString(): String {
+ return "$hookType $targetClassName.$targetMethodName: $hookClassName.$hookMethodName $additionalClassesToHook"
+ }
+
+ companion object {
+ fun createAndVerifyHook(hookMethod: Method, hookData: MethodHook, className: String): Hook {
+ return createHook(hookMethod, hookData, className).also {
+ verify(hookMethod, it)
+ }
+ }
+
+ private fun createHook(hookMethod: Method, annotation: MethodHook, targetClassName: String): Hook {
+ val targetReturnTypeDescriptor = annotation.targetMethodDescriptor
+ .takeIf { it.isNotBlank() }?.let { extractReturnTypeDescriptor(it) }
+ val hookClassName: String = hookMethod.declaringClass.name
+ return Hook(
+ targetClassName = targetClassName,
+ hookType = annotation.type,
+ targetMethodName = annotation.targetMethod,
+ targetMethodDescriptor = annotation.targetMethodDescriptor.takeIf { it.isNotBlank() },
+ additionalClassesToHook = annotation.additionalClassesToHook.asList(),
+ targetInternalClassName = targetClassName.replace('.', '/'),
+ targetReturnTypeDescriptor = targetReturnTypeDescriptor,
+ targetWrappedReturnTypeDescriptor = targetReturnTypeDescriptor?.let { getWrapperTypeDescriptor(it) },
+ hookClassName = hookClassName,
+ hookInternalClassName = hookClassName.replace('.', '/'),
+ hookMethodName = hookMethod.name,
+ hookMethodDescriptor = hookMethod.descriptor,
+ )
+ }
+
+ private fun verify(hookMethod: Method, potentialHook: Hook) {
+ // Verify the hook method's modifiers (public static).
+ require(Modifier.isPublic(hookMethod.modifiers)) { "$potentialHook: hook method must be public" }
+ require(Modifier.isStatic(hookMethod.modifiers)) { "$potentialHook: hook method must be static" }
+
+ // Verify the hook method's parameter count.
+ val numParameters = hookMethod.parameters.size
+ when (potentialHook.hookType) {
+ HookType.BEFORE, HookType.REPLACE -> require(numParameters == 4) { "$potentialHook: incorrect number of parameters (expected 4)" }
+ HookType.AFTER -> require(numParameters == 5) { "$potentialHook: incorrect number of parameters (expected 5)" }
+ }
+
+ // Verify the hook method's parameter types.
+ val parameterTypes = hookMethod.parameterTypes
+ require(parameterTypes[0] == MethodHandle::class.java) { "$potentialHook: first parameter must have type MethodHandle" }
+ require(parameterTypes[1] == Object::class.java || parameterTypes[1].name == potentialHook.targetClassName) { "$potentialHook: second parameter must have type Object or ${potentialHook.targetClassName}" }
+ require(parameterTypes[2] == Array<Object>::class.java) { "$potentialHook: third parameter must have type Object[]" }
+ require(parameterTypes[3] == Int::class.javaPrimitiveType) { "$potentialHook: fourth parameter must have type int" }
+
+ // Verify the hook method's return type if possible.
+ when (potentialHook.hookType) {
+ HookType.BEFORE, HookType.AFTER -> require(hookMethod.returnType == Void.TYPE) {
+ "$potentialHook: return type must be void"
+ }
+ HookType.REPLACE -> if (potentialHook.targetReturnTypeDescriptor != null) {
+ if (potentialHook.targetMethodName == "<init>") {
+ require(hookMethod.returnType.name == potentialHook.targetClassName) { "$potentialHook: return type must be ${potentialHook.targetClassName} to match target constructor" }
+ } else if (potentialHook.targetReturnTypeDescriptor == "V") {
+ require(hookMethod.returnType.descriptor == "V") { "$potentialHook: return type must be void" }
+ } else {
+ require(
+ hookMethod.returnType.descriptor in listOf(
+ java.lang.Object::class.java.descriptor,
+ potentialHook.targetReturnTypeDescriptor,
+ potentialHook.targetWrappedReturnTypeDescriptor,
+ ),
+ ) {
+ "$potentialHook: return type must have type Object or match the descriptors ${potentialHook.targetReturnTypeDescriptor} or ${potentialHook.targetWrappedReturnTypeDescriptor}"
+ }
+ }
+ }
+ }
+
+ // AfterMethodHook only: Verify the type of the last parameter if known. Even if not
+ // known, it must not be a primitive value.
+ if (potentialHook.hookType == HookType.AFTER) {
+ if (potentialHook.targetReturnTypeDescriptor != null) {
+ require(
+ parameterTypes[4] == java.lang.Object::class.java ||
+ parameterTypes[4].descriptor == potentialHook.targetWrappedReturnTypeDescriptor,
+ ) {
+ "$potentialHook: fifth parameter must have type Object or match the descriptor ${potentialHook.targetWrappedReturnTypeDescriptor}"
+ }
+ } else {
+ require(!parameterTypes[4].isPrimitive) {
+ "$potentialHook: fifth parameter must not be a primitive type, use a boxed type instead"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt
new file mode 100644
index 00000000..3c0d97c9
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt
@@ -0,0 +1,63 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor
+
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.MethodVisitor
+
+internal class HookInstrumentor(
+ private val hooks: Iterable<Hook>,
+ private val java6Mode: Boolean,
+ private val classWithHooksEnabledField: String?,
+) : Instrumentor {
+
+ private lateinit var random: DeterministicRandom
+
+ override fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray {
+ val reader = ClassReader(bytecode)
+ val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
+ random = DeterministicRandom("hook", reader.className)
+ val interceptor = object : ClassVisitor(Instrumentor.ASM_API_VERSION, writer) {
+ override fun visitMethod(
+ access: Int,
+ name: String?,
+ descriptor: String?,
+ signature: String?,
+ exceptions: Array<String>?,
+ ): MethodVisitor? {
+ val mv = cv.visitMethod(access, name, descriptor, signature, exceptions) ?: return null
+ return if (shouldInstrument(access)) {
+ makeHookMethodVisitor(
+ internalClassName,
+ access,
+ name,
+ descriptor,
+ mv,
+ hooks,
+ java6Mode,
+ random,
+ classWithHooksEnabledField,
+ )
+ } else {
+ mv
+ }
+ }
+ }
+ reader.accept(interceptor, ClassReader.EXPAND_FRAMES)
+ return writer.toByteArray()
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt
new file mode 100644
index 00000000..f5118fd6
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt
@@ -0,0 +1,513 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor
+
+import com.code_intelligence.jazzer.api.HookType
+import org.objectweb.asm.Handle
+import org.objectweb.asm.Label
+import org.objectweb.asm.MethodVisitor
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Type
+import org.objectweb.asm.commons.AnalyzerAdapter
+import org.objectweb.asm.commons.LocalVariablesSorter
+import java.util.concurrent.atomic.AtomicBoolean
+
+internal fun makeHookMethodVisitor(
+ owner: String,
+ access: Int,
+ name: String?,
+ descriptor: String?,
+ methodVisitor: MethodVisitor?,
+ hooks: Iterable<Hook>,
+ java6Mode: Boolean,
+ random: DeterministicRandom,
+ classWithHooksEnabledField: String?,
+): MethodVisitor {
+ return HookMethodVisitor(
+ owner,
+ access,
+ name,
+ descriptor,
+ methodVisitor,
+ hooks,
+ java6Mode,
+ random,
+ classWithHooksEnabledField,
+ ).lvs
+}
+
+private class HookMethodVisitor(
+ owner: String,
+ access: Int,
+ val name: String?,
+ descriptor: String?,
+ methodVisitor: MethodVisitor?,
+ hooks: Iterable<Hook>,
+ private val java6Mode: Boolean,
+ private val random: DeterministicRandom,
+ private val classWithHooksEnabledField: String?,
+) : MethodVisitor(
+ Instrumentor.ASM_API_VERSION,
+ // AnalyzerAdapter computes stack map frames at every instruction, which is needed for the
+ // conditional hook logic as it adds a conditional jump. Before Java 7, stack map frames were
+ // neither included nor required in class files.
+ //
+ // Note: Delegating to AnalyzerAdapter rather than having AnalyzerAdapter delegate to our
+ // MethodVisitor is unusual. We do this since we insert conditional jumps around method calls,
+ // which requires knowing the stack map both before and after the call. If AnalyzerAdapter
+ // delegated to this MethodVisitor, we would only be able to access the stack map before the
+ // method call in visitMethodInsn.
+ if (classWithHooksEnabledField != null && !java6Mode) {
+ AnalyzerAdapter(
+ owner,
+ access,
+ name,
+ descriptor,
+ methodVisitor,
+ )
+ } else {
+ methodVisitor
+ },
+) {
+
+ companion object {
+ private val showUnsupportedHookWarning = AtomicBoolean(true)
+ }
+
+ val lvs = object : LocalVariablesSorter(Instrumentor.ASM_API_VERSION, access, descriptor, this) {
+ override fun updateNewLocals(newLocals: Array<Any>) {
+ // The local variables involved in calling hooks do not need to outlive the current
+ // basic block and should thus not appear in stack map frames. By requesting the
+ // LocalVariableSorter to fill their entries in stack map frames with TOP, they will
+ // be treated like an unused local variable slot.
+ newLocals.fill(Opcodes.TOP)
+ }
+ }
+
+ private val hooks = hooks.groupBy { hook ->
+ var hookKey = "${hook.hookType}#${hook.targetInternalClassName}#${hook.targetMethodName}"
+ if (hook.targetMethodDescriptor != null) {
+ hookKey += "#${hook.targetMethodDescriptor}"
+ }
+ hookKey
+ }
+
+ override fun visitMethodInsn(
+ opcode: Int,
+ owner: String,
+ methodName: String,
+ methodDescriptor: String,
+ isInterface: Boolean,
+ ) {
+ if (!isMethodInvocationOp(opcode)) {
+ mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
+ return
+ }
+ handleMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
+ }
+
+ // Transforms a stack map specification from the form used by the JVM and AnalyzerAdapter, where
+ // LONG and DOUBLE values are followed by an additional TOP entry, to the form accepted by
+ // visitFrame, which doesn't expect this additional entry.
+ private fun dropImplicitTop(stack: Collection<Any>?): Array<Any>? {
+ if (stack == null) {
+ return null
+ }
+ val filteredStack = mutableListOf<Any>()
+ var previousElement: Any? = null
+ for (element in stack) {
+ if (element != Opcodes.TOP || (previousElement != Opcodes.DOUBLE && previousElement != Opcodes.LONG)) {
+ filteredStack.add(element)
+ }
+ previousElement = element
+ }
+ return filteredStack.toTypedArray()
+ }
+
+ private fun storeFrame(aa: AnalyzerAdapter?): Pair<Array<Any>?, Array<Any>?>? {
+ return Pair(dropImplicitTop((aa ?: return null).locals), dropImplicitTop(aa.stack))
+ }
+
+ fun handleMethodInsn(
+ opcode: Int,
+ owner: String,
+ methodName: String,
+ methodDescriptor: String,
+ isInterface: Boolean,
+ ) {
+ val matchingHooks = findMatchingHooks(owner, methodName, methodDescriptor)
+
+ if (matchingHooks.isEmpty()) {
+ mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
+ return
+ }
+
+ val skipHooksLabel = Label()
+ val applyHooksLabel = Label()
+ val useConditionalHooks = classWithHooksEnabledField != null
+ var postCallFrame: Pair<Array<Any>?, Array<Any>?>? = null
+ if (useConditionalHooks) {
+ val preCallFrame = (mv as? AnalyzerAdapter)?.let { storeFrame(it) }
+ // If hooks aren't enabled, skip the hook invocations.
+ mv.visitFieldInsn(
+ Opcodes.GETSTATIC,
+ classWithHooksEnabledField,
+ "hooksEnabled",
+ "Z",
+ )
+ mv.visitJumpInsn(Opcodes.IFNE, applyHooksLabel)
+ mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
+ postCallFrame = (mv as? AnalyzerAdapter)?.let { storeFrame(it) }
+ mv.visitJumpInsn(Opcodes.GOTO, skipHooksLabel)
+ // Needs a stack map frame as both the successor of an unconditional jump and the target
+ // of a jump.
+ mv.visitLabel(applyHooksLabel)
+ if (preCallFrame != null) {
+ mv.visitFrame(
+ Opcodes.F_NEW,
+ preCallFrame.first?.size ?: 0,
+ preCallFrame.first,
+ preCallFrame.second?.size ?: 0,
+ preCallFrame.second,
+ )
+ }
+ // All successor instructions emitted below do not have a stack map frame attached, so
+ // we do not need to emit a NOP to prevent duplicated stack map frames.
+ }
+
+ val paramDescriptors = extractParameterTypeDescriptors(methodDescriptor)
+ val localObjArr = storeMethodArguments(paramDescriptors)
+ // If the method we're hooking is not static there is now a reference to
+ // the object the method was invoked on at the top of the stack.
+ // If the method is static, that object is missing. We make up for it by pushing a null ref.
+ if (opcode == Opcodes.INVOKESTATIC) {
+ mv.visitInsn(Opcodes.ACONST_NULL)
+ }
+
+ // Save the owner object to a new local variable
+ val ownerDescriptor = "L$owner;"
+ val localOwnerObj = lvs.newLocal(Type.getType(ownerDescriptor))
+ mv.visitVarInsn(Opcodes.ASTORE, localOwnerObj) // consume objectref
+ // We now removed all values for the original method call from the operand stack
+ // and saved them to local variables.
+
+ val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor)
+ // Create a local variable to store the return value
+ val localReturnObj = lvs.newLocal(Type.getType(getWrapperTypeDescriptor(returnTypeDescriptor)))
+
+ matchingHooks.forEachIndexed { index, hook ->
+ // The hookId is used to identify a call site.
+ val hookId = random.nextInt()
+
+ // Start to build the arguments for the hook method.
+ if (methodName == "<init>") {
+ // Constructor is invoked on an uninitialized object, and that's still on the stack.
+ // In case of REPLACE pop it from the stack and replace it afterwards with the returned
+ // one from the hook.
+ if (hook.hookType == HookType.REPLACE) {
+ mv.visitInsn(Opcodes.POP)
+ }
+ // Special case for constructors:
+ // We cannot create a MethodHandle for a constructor, so we push null instead.
+ mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
+ // Only pass the this object if it has been initialized by the time the hook is invoked.
+ if (hook.hookType == HookType.AFTER) {
+ mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj)
+ } else {
+ mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
+ }
+ } else {
+ // Push a MethodHandle representing the hooked method.
+ val handleOpcode = when (opcode) {
+ Opcodes.INVOKEVIRTUAL -> Opcodes.H_INVOKEVIRTUAL
+ Opcodes.INVOKEINTERFACE -> Opcodes.H_INVOKEINTERFACE
+ Opcodes.INVOKESTATIC -> Opcodes.H_INVOKESTATIC
+ Opcodes.INVOKESPECIAL -> Opcodes.H_INVOKESPECIAL
+ else -> -1
+ }
+ if (java6Mode) {
+ // MethodHandle constants (type 15) are not supported in Java 6 class files (major version 50).
+ mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
+ } else {
+ mv.visitLdcInsn(
+ Handle(
+ handleOpcode,
+ owner,
+ methodName,
+ methodDescriptor,
+ isInterface,
+ ),
+ ) // push MethodHandle
+ }
+ // Stack layout: ... | MethodHandle (objectref)
+ // Push the owner object again
+ mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj)
+ }
+ // Stack layout: ... | MethodHandle (objectref) | owner (objectref)
+ // Push a reference to our object array with the saved arguments
+ mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
+ // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref)
+ // Push the hook id
+ mv.visitLdcInsn(hookId)
+ // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
+ // How we proceed depends on the type of hook we want to implement
+ when (hook.hookType) {
+ HookType.BEFORE -> {
+ // Call the hook method
+ mv.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ hook.hookInternalClassName,
+ hook.hookMethodName,
+ hook.hookMethodDescriptor,
+ false,
+ )
+
+ // Call the original method if this is the last BEFORE hook. If not, the original method will be
+ // called by the next AFTER hook.
+ if (index == matchingHooks.lastIndex) {
+ // Stack layout: ...
+ // Push the values for the original method call onto the stack again
+ if (opcode != Opcodes.INVOKESTATIC) {
+ mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object
+ }
+ loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments
+ // Stack layout: ... | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ...
+ mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
+ }
+ }
+
+ HookType.REPLACE -> {
+ // Call the hook method
+ mv.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ hook.hookInternalClassName,
+ hook.hookMethodName,
+ hook.hookMethodDescriptor,
+ false,
+ )
+ // Stack layout: ... | [return value (primitive/objectref)]
+ // Check if we need to process the return value
+ if (returnTypeDescriptor != "V") {
+ val hookMethodReturnType = extractReturnTypeDescriptor(hook.hookMethodDescriptor)
+ // if the hook method's return type is primitive we don't need to unwrap or cast it
+ if (!isPrimitiveType(hookMethodReturnType)) {
+ // Check if the returned object type is different than the one that should be returned
+ // If a primitive should be returned we check it's wrapper type
+ val expectedType = getWrapperTypeDescriptor(returnTypeDescriptor)
+ if (expectedType != hookMethodReturnType) {
+ // Cast object
+ mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(expectedType))
+ }
+ // Check if we need to unwrap the returned object
+ unwrapTypeIfPrimitive(returnTypeDescriptor)
+ }
+ }
+ }
+
+ HookType.AFTER -> {
+ // Call the original method before the first AFTER hook
+ if (index == 0 || matchingHooks[index - 1].hookType != HookType.AFTER) {
+ // Push the values for the original method call again onto the stack
+ if (opcode != Opcodes.INVOKESTATIC) {
+ mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object
+ }
+ loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments
+ // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
+ // | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ...
+ mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
+ if (returnTypeDescriptor == "V") {
+ // If the method didn't return anything, we push a nullref as placeholder
+ mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
+ }
+ // Wrap return value if it is a primitive type
+ wrapTypeIfPrimitive(returnTypeDescriptor)
+ mv.visitVarInsn(Opcodes.ASTORE, localReturnObj) // consume objectref
+ }
+ mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref
+
+ // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
+ // | return value (objectref)
+ // Store the result value in a local variable (but keep it on the stack)
+ // Call the hook method
+ mv.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ hook.hookInternalClassName,
+ hook.hookMethodName,
+ hook.hookMethodDescriptor,
+ false,
+ )
+ // Stack layout: ...
+ // Push the return value on the stack after the last AFTER hook if the original method returns a value
+ if (index == matchingHooks.size - 1 && returnTypeDescriptor != "V") {
+ // Push the return value again
+ mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref
+ // Unwrap it, if it was a primitive value
+ unwrapTypeIfPrimitive(returnTypeDescriptor)
+ // Stack layout: ... | return value (primitive/objectref)
+ }
+ }
+ }
+ }
+ if (useConditionalHooks) {
+ // Needs a stack map frame as the target of a jump.
+ mv.visitLabel(skipHooksLabel)
+ if (postCallFrame != null) {
+ mv.visitFrame(
+ Opcodes.F_NEW,
+ postCallFrame.first?.size ?: 0,
+ postCallFrame.first,
+ postCallFrame.second?.size ?: 0,
+ postCallFrame.second,
+ )
+ }
+ // We do not control the next visitor calls, but we must not emit two frames for the
+ // same instruction.
+ mv.visitInsn(Opcodes.NOP)
+ }
+ }
+
+ private fun isMethodInvocationOp(opcode: Int) = opcode in listOf(
+ Opcodes.INVOKEVIRTUAL,
+ Opcodes.INVOKEINTERFACE,
+ Opcodes.INVOKESTATIC,
+ Opcodes.INVOKESPECIAL,
+ )
+
+ private fun findMatchingHooks(owner: String, name: String, descriptor: String): List<Hook> {
+ val result = HookType.values().flatMap { hookType ->
+ val withoutDescriptorKey = "$hookType#$owner#$name"
+ val withDescriptorKey = "$withoutDescriptorKey#$descriptor"
+ hooks[withDescriptorKey].orEmpty() + hooks[withoutDescriptorKey].orEmpty()
+ }.sortedBy { it.hookType }
+ val replaceHookCount = result.count { it.hookType == HookType.REPLACE }
+ check(
+ replaceHookCount == 0 ||
+ (replaceHookCount == 1 && result.size == 1),
+ ) {
+ "For a given method, You can either have a single REPLACE hook or BEFORE/AFTER hooks. Found:\n $result"
+ }
+
+ return result
+ .filter { !isReplaceHookInJava6mode(it) }
+ .sortedByDescending { it.toString() }
+ }
+
+ private fun isReplaceHookInJava6mode(hook: Hook): Boolean {
+ if (java6Mode && hook.hookType == HookType.REPLACE) {
+ if (showUnsupportedHookWarning.getAndSet(false)) {
+ println(
+ """WARN: Some hooks could not be applied to class files built for Java 7 or lower.
+ |WARN: Ensure that the fuzz target and its dependencies are compiled with
+ |WARN: -target 8 or higher to identify as many bugs as possible.
+ """.trimMargin(),
+ )
+ }
+ return true
+ }
+ return false
+ }
+
+ // Stores all arguments for a method call in a local object array.
+ // paramDescriptors: The type descriptors for all method arguments
+ private fun storeMethodArguments(paramDescriptors: List<String>): Int {
+ // Allocate a new Object[] for the methods parameters.
+ mv.visitIntInsn(Opcodes.SIPUSH, paramDescriptors.size)
+ mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object")
+ val localObjArr = lvs.newLocal(Type.getType("[Ljava/lang/Object;"))
+ mv.visitVarInsn(Opcodes.ASTORE, localObjArr)
+
+ // Loop over all arguments in reverse order (because the last argument is on top).
+ for ((argIdx, argDescriptor) in paramDescriptors.withIndex().reversed()) {
+ // If the argument is a primitive type, wrap it in it's wrapper class
+ wrapTypeIfPrimitive(argDescriptor)
+ // Store the argument in our object array, for that we need to shape the stack first.
+ // Stack layout: ... | method argument (objectref)
+ mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
+ // Stack layout: ... | method argument (objectref) | object array (arrayref)
+ mv.visitInsn(Opcodes.SWAP)
+ // Stack layout: ... | object array (arrayref) | method argument (objectref)
+ mv.visitIntInsn(Opcodes.SIPUSH, argIdx)
+ // Stack layout: ... | object array (arrayref) | method argument (objectref) | argument index (int)
+ mv.visitInsn(Opcodes.SWAP)
+ // Stack layout: ... | object array (arrayref) | argument index (int) | method argument (objectref)
+ mv.visitInsn(Opcodes.AASTORE) // consume all three: arrayref, index, value
+ // Stack layout: ...
+ // Continue with the remaining method arguments
+ }
+
+ // Return a reference to the array with the parameters.
+ return localObjArr
+ }
+
+ // Loads all arguments for a method call from a local object array.
+ // argTypeSigs: The type signatures for all method arguments
+ // localObjArr: Index of a local variable containing an object array where the arguments will be loaded from
+ private fun loadMethodArguments(paramDescriptors: List<String>, localObjArr: Int) {
+ // Loop over all arguments
+ for ((argIdx, argDescriptor) in paramDescriptors.withIndex()) {
+ // Push a reference to the object array on the stack
+ mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
+ // Stack layout: ... | object array (arrayref)
+ // Push the index of the current argument on the stack
+ mv.visitIntInsn(Opcodes.SIPUSH, argIdx)
+ // Stack layout: ... | object array (arrayref) | argument index (int)
+ // Load the argument from the array
+ mv.visitInsn(Opcodes.AALOAD)
+ // Stack layout: ... | method argument (objectref)
+ // Cast object to it's original type (or it's wrapper object)
+ val wrapperTypeDescriptor = getWrapperTypeDescriptor(argDescriptor)
+ mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(wrapperTypeDescriptor))
+ // If the argument is a supposed to be a primitive type, unwrap the wrapped type
+ unwrapTypeIfPrimitive(argDescriptor)
+ // Stack layout: ... | method argument (primitive/objectref)
+ // Continue with the remaining method arguments
+ }
+ }
+
+ // Removes a primitive value from the top of the operand stack
+ // and pushes it enclosed in its wrapper type (e.g. removes int, pushes Integer).
+ // This is done by calling .valueOf(...) on the wrapper class.
+ private fun wrapTypeIfPrimitive(unwrappedTypeDescriptor: String) {
+ if (!isPrimitiveType(unwrappedTypeDescriptor) || unwrappedTypeDescriptor == "V") return
+ val wrapperTypeDescriptor = getWrapperTypeDescriptor(unwrappedTypeDescriptor)
+ val wrapperType = extractInternalClassName(wrapperTypeDescriptor)
+ val valueOfDescriptor = "($unwrappedTypeDescriptor)$wrapperTypeDescriptor"
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, wrapperType, "valueOf", valueOfDescriptor, false)
+ }
+
+ // Removes a wrapper object around a given primitive type from the top of the operand stack
+ // and pushes the primitive value it contains (e.g. removes Integer, pushes int).
+ // This is done by calling .intValue(...) / .charValue(...) / ... on the wrapper object.
+ private fun unwrapTypeIfPrimitive(primitiveTypeDescriptor: String) {
+ val (methodName, wrappedTypeDescriptor) = when (primitiveTypeDescriptor) {
+ "B" -> Pair("byteValue", "java/lang/Byte")
+ "C" -> Pair("charValue", "java/lang/Character")
+ "D" -> Pair("doubleValue", "java/lang/Double")
+ "F" -> Pair("floatValue", "java/lang/Float")
+ "I" -> Pair("intValue", "java/lang/Integer")
+ "J" -> Pair("longValue", "java/lang/Long")
+ "S" -> Pair("shortValue", "java/lang/Short")
+ "Z" -> Pair("booleanValue", "java/lang/Boolean")
+ else -> return
+ }
+ mv.visitMethodInsn(
+ Opcodes.INVOKEVIRTUAL,
+ wrappedTypeDescriptor,
+ methodName,
+ "()$primitiveTypeDescriptor",
+ false,
+ )
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt
new file mode 100644
index 00000000..a26c0d6b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt
@@ -0,0 +1,132 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor
+
+import com.code_intelligence.jazzer.api.MethodHook
+import com.code_intelligence.jazzer.api.MethodHooks
+import com.code_intelligence.jazzer.utils.ClassNameGlobber
+import com.code_intelligence.jazzer.utils.Log
+import io.github.classgraph.ClassGraph
+import io.github.classgraph.ScanResult
+import java.lang.instrument.Instrumentation
+import java.lang.reflect.Method
+import java.util.jar.JarFile
+
+data class Hooks(
+ val hooks: List<Hook>,
+ val hookClasses: Set<Class<*>>,
+ val additionalHookClassNameGlobber: ClassNameGlobber,
+) {
+
+ companion object {
+
+ fun appendHooksToBootstrapClassLoaderSearch(instrumentation: Instrumentation, hookClassNames: Set<String>) {
+ hookClassNames.mapNotNull { hook ->
+ val hookClassFilePath = "/${hook.replace('.', '/')}.class"
+ val hookClassFile = Companion::class.java.getResource(hookClassFilePath) ?: return@mapNotNull null
+ if ("jar" != hookClassFile.protocol) {
+ return@mapNotNull null
+ }
+ // hookClassFile.file looks as follows:
+ // file:/tmp/ExampleFuzzerHooks_deploy.jar!/com/example/ExampleFuzzerHooks.class
+ hookClassFile.file.removePrefix("file:").takeWhile { it != '!' }
+ }
+ .toSet()
+ .map { JarFile(it) }
+ .forEach { instrumentation.appendToBootstrapClassLoaderSearch(it) }
+ }
+
+ fun loadHooks(excludeHookClassNames: List<String>, vararg hookClassNames: Set<String>): List<Hooks> {
+ return ClassGraph()
+ .enableClassInfo()
+ .enableSystemJarsAndModules()
+ .rejectPackages("jaz.*", "com.code_intelligence.jazzer.*")
+ .scan()
+ .use { scanResult ->
+ // Capture scanResult in HooksLoader field to not pass it through
+ // all internal hook loading methods.
+ val loader = HooksLoader(scanResult, excludeHookClassNames)
+ hookClassNames.map(loader::load)
+ }
+ }
+
+ private class HooksLoader(private val scanResult: ScanResult, val excludeHookClassNames: List<String>) {
+
+ fun load(hookClassNames: Set<String>): Hooks {
+ val hooksWithHookClasses = hookClassNames.flatMap(::loadHooks)
+ val hooks = hooksWithHookClasses.map { it.first }
+ val hookClasses = hooksWithHookClasses.map { it.second }.toSet()
+ val additionalHookClassNameGlobber = ClassNameGlobber(
+ hooks.flatMap(Hook::additionalClassesToHook),
+ excludeHookClassNames,
+ )
+ return Hooks(hooks, hookClasses, additionalHookClassNameGlobber)
+ }
+
+ private fun loadHooks(hookClassName: String): List<Pair<Hook, Class<*>>> {
+ return try {
+ // We let the static initializers of hook classes execute so that hooks can run
+ // code before the fuzz target class has been loaded (e.g., register themselves
+ // for the onFuzzTargetReady callback).
+ val hookClass =
+ Class.forName(hookClassName, true, Companion::class.java.classLoader)
+ loadHooks(hookClass).also {
+ Log.info("Loaded ${it.size} hooks from $hookClassName")
+ }.map {
+ it to hookClass
+ }
+ } catch (e: ClassNotFoundException) {
+ Log.warn("Failed to load hooks from $hookClassName", e)
+ emptyList()
+ }
+ }
+
+ private fun loadHooks(hookClass: Class<*>): List<Hook> {
+ val hooks = mutableListOf<Hook>()
+ for (method in hookClass.methods.sortedBy { it.descriptor }) {
+ method.getAnnotation(MethodHook::class.java)?.let {
+ hooks.addAll(verifyAndGetHooks(method, it))
+ }
+ method.getAnnotation(MethodHooks::class.java)?.let {
+ it.value.forEach { hookAnnotation ->
+ hooks.addAll(verifyAndGetHooks(method, hookAnnotation))
+ }
+ }
+ }
+ return hooks
+ }
+
+ private fun verifyAndGetHooks(hookMethod: Method, hookData: MethodHook): List<Hook> {
+ return lookupClassesToHook(hookData.targetClassName)
+ .map { className ->
+ Hook.createAndVerifyHook(hookMethod, hookData, className)
+ }
+ }
+
+ private fun lookupClassesToHook(annotationTargetClassName: String): List<String> {
+ // Allowing arbitrary exterior whitespace in the target class name allows for an easy workaround
+ // for mangled hooks due to shading applied to hooks.
+ val targetClassName = annotationTargetClassName.trim()
+ val targetClassInfo = scanResult.getClassInfo(targetClassName) ?: return listOf(targetClassName)
+ val additionalTargetClasses = when {
+ targetClassInfo.isInterface -> scanResult.getClassesImplementing(targetClassName)
+ targetClassInfo.isAbstract -> scanResult.getSubclasses(targetClassName)
+ else -> emptyList()
+ }
+ return (listOf(targetClassName) + additionalTargetClasses.map { it.name }).sorted()
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt
new file mode 100644
index 00000000..c6db94c0
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt
@@ -0,0 +1,45 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor
+
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.tree.MethodNode
+
+enum class InstrumentationType {
+ CMP,
+ COV,
+ DIV,
+ GEP,
+ INDIR,
+ NATIVE,
+}
+
+internal interface Instrumentor {
+ fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray
+
+ fun shouldInstrument(access: Int): Boolean {
+ return (access and Opcodes.ACC_ABSTRACT == 0) &&
+ (access and Opcodes.ACC_NATIVE == 0)
+ }
+
+ fun shouldInstrument(method: MethodNode): Boolean {
+ return shouldInstrument(method.access) &&
+ method.instructions.size() > 0
+ }
+
+ companion object {
+ const val ASM_API_VERSION = Opcodes.ASM9
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java b/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java
new file mode 100644
index 00000000..0512ec2a
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java
@@ -0,0 +1,48 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor;
+
+import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.InstrSupport;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class StaticMethodStrategy implements EdgeCoverageStrategy {
+ @Override
+ public void instrumentControlFlowEdge(
+ MethodVisitor mv, int edgeId, int variable, String coverageMapInternalClassName) {
+ InstrSupport.push(mv, edgeId);
+ mv.visitMethodInsn(
+ Opcodes.INVOKESTATIC, coverageMapInternalClassName, "recordCoverage", "(I)V", false);
+ }
+
+ @Override
+ public int getInstrumentControlFlowEdgeStackSize() {
+ return 1;
+ }
+
+ @Override
+ public Object getLocalVariableType() {
+ return null;
+ }
+
+ @Override
+ public void loadLocalVariable(
+ MethodVisitor mv, int variable, String coverageMapInternalClassName) {}
+
+ @Override
+ public int getLoadLocalVariableStackSize() {
+ return 0;
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt
new file mode 100644
index 00000000..a46e72df
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt
@@ -0,0 +1,268 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.instrumentor
+
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.tree.AbstractInsnNode
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.InsnList
+import org.objectweb.asm.tree.InsnNode
+import org.objectweb.asm.tree.IntInsnNode
+import org.objectweb.asm.tree.LdcInsnNode
+import org.objectweb.asm.tree.LookupSwitchInsnNode
+import org.objectweb.asm.tree.MethodInsnNode
+import org.objectweb.asm.tree.MethodNode
+import org.objectweb.asm.tree.TableSwitchInsnNode
+
+internal class TraceDataFlowInstrumentor(
+ private val types: Set<InstrumentationType>,
+ private val callbackInternalClassName: String = "com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks",
+) : Instrumentor {
+
+ private lateinit var random: DeterministicRandom
+
+ override fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray {
+ val node = ClassNode()
+ val reader = ClassReader(bytecode)
+ reader.accept(node, 0)
+ random = DeterministicRandom("trace", node.name)
+ for (method in node.methods) {
+ if (shouldInstrument(method)) {
+ addDataFlowInstrumentation(method)
+ }
+ }
+
+ val writer = ClassWriter(ClassWriter.COMPUTE_MAXS)
+ node.accept(writer)
+ return writer.toByteArray()
+ }
+
+ private fun addDataFlowInstrumentation(method: MethodNode) {
+ loop@ for (inst in method.instructions.toArray()) {
+ when (inst.opcode) {
+ Opcodes.LCMP -> {
+ if (InstrumentationType.CMP !in types) continue@loop
+ method.instructions.insertBefore(inst, longCmpInstrumentation())
+ method.instructions.remove(inst)
+ }
+ Opcodes.IF_ICMPEQ, Opcodes.IF_ICMPNE,
+ Opcodes.IF_ICMPLT, Opcodes.IF_ICMPLE,
+ Opcodes.IF_ICMPGT, Opcodes.IF_ICMPGE,
+ -> {
+ if (InstrumentationType.CMP !in types) continue@loop
+ method.instructions.insertBefore(inst, intCmpInstrumentation())
+ }
+ Opcodes.IFEQ, Opcodes.IFNE,
+ Opcodes.IFLT, Opcodes.IFLE,
+ Opcodes.IFGT, Opcodes.IFGE,
+ -> {
+ if (InstrumentationType.CMP !in types) continue@loop
+ // The IF* opcodes are often used to branch based on the result of a compare
+ // instruction for a type other than int. The operands of this compare will
+ // already be reported via the instrumentation above (for non-floating point
+ // numbers) and the follow-up compare does not provide a good signal as all
+ // operands will be in {-1, 0, 1}. Skip instrumentation for it.
+ if (inst.previous?.opcode in listOf(Opcodes.DCMPG, Opcodes.DCMPL, Opcodes.FCMPG, Opcodes.DCMPL) ||
+ (inst.previous as? MethodInsnNode)?.name == "traceCmpLongWrapper"
+ ) {
+ continue@loop
+ }
+ method.instructions.insertBefore(inst, ifInstrumentation())
+ }
+ Opcodes.LOOKUPSWITCH, Opcodes.TABLESWITCH -> {
+ if (InstrumentationType.CMP !in types) continue@loop
+ // Mimic the exclusion logic for small label values in libFuzzer:
+ // https://github.com/llvm-mirror/compiler-rt/blob/69445f095c22aac2388f939bedebf224a6efcdaf/lib/fuzzer/FuzzerTracePC.cpp#L520
+ // Case values are reported to libFuzzer via an array of unsigned long values and thus need to be
+ // sorted by unsigned value.
+ val caseValues = when (inst) {
+ is LookupSwitchInsnNode -> {
+ if (inst.keys.isEmpty() || (0 <= inst.keys.first() && inst.keys.last() < 256)) {
+ continue@loop
+ }
+ inst.keys
+ }
+ is TableSwitchInsnNode -> {
+ if (0 <= inst.min && inst.max < 256) {
+ continue@loop
+ }
+ (inst.min..inst.max).filter { caseValue ->
+ val index = caseValue - inst.min
+ // Filter out "gap cases".
+ inst.labels[index].label != inst.dflt.label
+ }.toList()
+ }
+ // Not reached.
+ else -> continue@loop
+ }.sortedBy { it.toUInt() }.map { it.toLong() }.toLongArray()
+ method.instructions.insertBefore(inst, switchInstrumentation(caseValues))
+ }
+ Opcodes.IDIV -> {
+ if (InstrumentationType.DIV !in types) continue@loop
+ method.instructions.insertBefore(inst, intDivInstrumentation())
+ }
+ Opcodes.LDIV -> {
+ if (InstrumentationType.DIV !in types) continue@loop
+ method.instructions.insertBefore(inst, longDivInstrumentation())
+ }
+ Opcodes.AALOAD, Opcodes.BALOAD,
+ Opcodes.CALOAD, Opcodes.DALOAD,
+ Opcodes.FALOAD, Opcodes.IALOAD,
+ Opcodes.LALOAD, Opcodes.SALOAD,
+ -> {
+ if (InstrumentationType.GEP !in types) continue@loop
+ if (!isConstantIntegerPushInsn(inst.previous)) continue@loop
+ method.instructions.insertBefore(inst, gepLoadInstrumentation())
+ }
+ Opcodes.INVOKEINTERFACE, Opcodes.INVOKESPECIAL, Opcodes.INVOKESTATIC, Opcodes.INVOKEVIRTUAL -> {
+ if (InstrumentationType.GEP !in types) continue@loop
+ if (!isGepLoadMethodInsn(inst as MethodInsnNode)) continue@loop
+ if (!isConstantIntegerPushInsn(inst.previous)) continue@loop
+ method.instructions.insertBefore(inst, gepLoadInstrumentation())
+ }
+ }
+ }
+ }
+
+ private fun InsnList.pushFakePc() {
+ add(LdcInsnNode(random.nextInt(512)))
+ }
+
+ private fun longCmpInstrumentation() = InsnList().apply {
+ pushFakePc()
+ // traceCmpLong returns the result of the comparison as duplicating two longs on the stack
+ // is not possible without local variables.
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceCmpLongWrapper", "(JJI)I", false))
+ }
+
+ private fun intCmpInstrumentation() = InsnList().apply {
+ add(InsnNode(Opcodes.DUP2))
+ pushFakePc()
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceCmpInt", "(III)V", false))
+ }
+
+ private fun ifInstrumentation() = InsnList().apply {
+ add(InsnNode(Opcodes.DUP))
+ // All if* instructions are compares to the constant 0.
+ add(InsnNode(Opcodes.ICONST_0))
+ add(InsnNode(Opcodes.SWAP))
+ pushFakePc()
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceConstCmpInt", "(III)V", false))
+ }
+
+ private fun intDivInstrumentation() = InsnList().apply {
+ add(InsnNode(Opcodes.DUP))
+ pushFakePc()
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceDivInt", "(II)V", false))
+ }
+
+ private fun longDivInstrumentation() = InsnList().apply {
+ add(InsnNode(Opcodes.DUP2))
+ pushFakePc()
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceDivLong", "(JI)V", false))
+ }
+
+ private fun switchInstrumentation(caseValues: LongArray) = InsnList().apply {
+ // duplicate {lookup,table}switch key for use as first function argument
+ add(InsnNode(Opcodes.DUP))
+ add(InsnNode(Opcodes.I2L))
+ // Set up array with switch case values. The format libfuzzer expects is created here directly, i.e., the first
+ // two entries are the number of cases and the bit size of values (always 32).
+ add(IntInsnNode(Opcodes.SIPUSH, caseValues.size + 2))
+ add(IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_LONG))
+ // Store number of cases
+ add(InsnNode(Opcodes.DUP))
+ add(IntInsnNode(Opcodes.SIPUSH, 0))
+ add(LdcInsnNode(caseValues.size.toLong()))
+ add(InsnNode(Opcodes.LASTORE))
+ // Store bit size of keys
+ add(InsnNode(Opcodes.DUP))
+ add(IntInsnNode(Opcodes.SIPUSH, 1))
+ add(LdcInsnNode(32.toLong()))
+ add(InsnNode(Opcodes.LASTORE))
+ // Store {lookup,table}switch case values
+ for ((i, caseValue) in caseValues.withIndex()) {
+ add(InsnNode(Opcodes.DUP))
+ add(IntInsnNode(Opcodes.SIPUSH, 2 + i))
+ add(LdcInsnNode(caseValue))
+ add(InsnNode(Opcodes.LASTORE))
+ }
+ pushFakePc()
+ // call the native callback function
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceSwitch", "(J[JI)V", false))
+ }
+
+ /**
+ * Returns true if [node] represents an instruction that possibly pushes a valid, non-zero, constant array index
+ * onto the stack.
+ */
+ private fun isConstantIntegerPushInsn(node: AbstractInsnNode?) = node?.opcode in CONSTANT_INTEGER_PUSH_OPCODES
+
+ /**
+ * Returns true if [node] represents a call to a method that performs an indexed lookup into an array-like
+ * structure.
+ */
+ private fun isGepLoadMethodInsn(node: MethodInsnNode): Boolean {
+ if (!node.desc.startsWith("(I)")) return false
+ val returnType = node.desc.removePrefix("(I)")
+ return MethodInfo(node.owner, node.name, returnType) in GEP_LOAD_METHODS
+ }
+
+ private fun gepLoadInstrumentation() = InsnList().apply {
+ // Duplicate the index and convert to long.
+ add(InsnNode(Opcodes.DUP))
+ add(InsnNode(Opcodes.I2L))
+ pushFakePc()
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceGep", "(JI)V", false))
+ }
+
+ companion object {
+ // Low constants (0, 1) are omitted as they create a lot of noise.
+ val CONSTANT_INTEGER_PUSH_OPCODES = listOf(
+ Opcodes.BIPUSH,
+ Opcodes.SIPUSH,
+ Opcodes.LDC,
+ Opcodes.ICONST_2,
+ Opcodes.ICONST_3,
+ Opcodes.ICONST_4,
+ Opcodes.ICONST_5,
+ )
+
+ data class MethodInfo(val internalClassName: String, val name: String, val returnType: String)
+
+ val GEP_LOAD_METHODS = setOf(
+ MethodInfo("java/util/AbstractList", "get", "Ljava/lang/Object;"),
+ MethodInfo("java/util/ArrayList", "get", "Ljava/lang/Object;"),
+ MethodInfo("java/util/List", "get", "Ljava/lang/Object;"),
+ MethodInfo("java/util/Stack", "get", "Ljava/lang/Object;"),
+ MethodInfo("java/util/Vector", "get", "Ljava/lang/Object;"),
+ MethodInfo("java/lang/CharSequence", "charAt", "C"),
+ MethodInfo("java/lang/String", "charAt", "C"),
+ MethodInfo("java/lang/StringBuffer", "charAt", "C"),
+ MethodInfo("java/lang/StringBuilder", "charAt", "C"),
+ MethodInfo("java/lang/String", "codePointAt", "I"),
+ MethodInfo("java/lang/String", "codePointBefore", "I"),
+ MethodInfo("java/nio/ByteBuffer", "get", "B"),
+ MethodInfo("java/nio/ByteBuffer", "getChar", "C"),
+ MethodInfo("java/nio/ByteBuffer", "getDouble", "D"),
+ MethodInfo("java/nio/ByteBuffer", "getFloat", "F"),
+ MethodInfo("java/nio/ByteBuffer", "getInt", "I"),
+ MethodInfo("java/nio/ByteBuffer", "getLong", "J"),
+ MethodInfo("java/nio/ByteBuffer", "getShort", "S"),
+ )
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/jazzer_shade_rules.jarjar b/src/main/java/com/code_intelligence/jazzer/jazzer_shade_rules.jarjar
new file mode 100644
index 00000000..91073722
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/jazzer_shade_rules.jarjar
@@ -0,0 +1,6 @@
+rule com.github.** com.code_intelligence.jazzer.third_party.@0
+rule io.** com.code_intelligence.jazzer.third_party.@0
+rule kotlin.** com.code_intelligence.jazzer.third_party.@0
+rule net.** com.code_intelligence.jazzer.third_party.@0
+rule nonapi.** com.code_intelligence.jazzer.third_party.@0
+rule org.objectweb.** com.code_intelligence.jazzer.third_party.@0
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java
new file mode 100644
index 00000000..1f286a31
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java
@@ -0,0 +1,72 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.junit;
+
+import static com.code_intelligence.jazzer.junit.Utils.getClassPathBasedInstrumentationFilter;
+import static com.code_intelligence.jazzer.junit.Utils.getLegacyInstrumentationFilter;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+class AgentConfigurator {
+ private static final AtomicBoolean hasBeenConfigured = new AtomicBoolean();
+
+ static void forRegressionTest(ExtensionContext extensionContext) {
+ if (!hasBeenConfigured.compareAndSet(false, true)) {
+ return;
+ }
+
+ applyCommonConfiguration();
+
+ // Add logic to the hook instrumentation that allows us to enable and disable hooks at runtime.
+ System.setProperty("jazzer.internal.conditional_hooks", "true");
+ // Apply all hooks, but no coverage or compare instrumentation.
+ System.setProperty("jazzer.instrumentation_excludes", "**");
+ extensionContext.getConfigurationParameter("jazzer.instrument")
+ .ifPresent(s
+ -> System.setProperty(
+ "jazzer.custom_hook_includes", String.join(File.pathSeparator, s.split(","))));
+ }
+
+ static void forFuzzing(ExtensionContext executionRequest) {
+ if (!hasBeenConfigured.compareAndSet(false, true)) {
+ throw new IllegalStateException("Only a single fuzz test should be executed per fuzzing run");
+ }
+
+ applyCommonConfiguration();
+
+ String instrumentationFilter =
+ executionRequest.getConfigurationParameter("jazzer.instrument")
+ .orElseGet(
+ ()
+ -> getClassPathBasedInstrumentationFilter(System.getProperty("java.class.path"))
+ .orElseGet(()
+ -> getLegacyInstrumentationFilter(
+ executionRequest.getRequiredTestClass())));
+ String filter = String.join(File.pathSeparator, instrumentationFilter.split(","));
+ System.setProperty("jazzer.custom_hook_includes", filter);
+ System.setProperty("jazzer.instrumentation_includes", filter);
+ }
+
+ private static void applyCommonConfiguration() {
+ // Do not hook common IDE and JUnit classes and their dependencies.
+ System.setProperty("jazzer.custom_hook_excludes",
+ String.join(File.pathSeparator, "com.google.testing.junit.**", "com.intellij.**",
+ "org.jetbrains.**", "io.github.classgraph.**", "junit.framework.**", "net.bytebuddy.**",
+ "org.apiguardian.**", "org.assertj.core.**", "org.hamcrest.**", "org.junit.**",
+ "org.opentest4j.**", "org.mockito.**", "org.apache.maven.**", "org.gradle.**"));
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/AgentConfiguringArgumentsProvider.java b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfiguringArgumentsProvider.java
new file mode 100644
index 00000000..e65f028b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfiguringArgumentsProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.junit;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsProvider;
+import org.junit.jupiter.params.support.AnnotationConsumer;
+
+public class AgentConfiguringArgumentsProvider
+ implements ArgumentsProvider, AnnotationConsumer<FuzzTest> {
+ private FuzzTest fuzzTest;
+
+ @Override
+ public void accept(FuzzTest fuzzTest) {
+ this.fuzzTest = fuzzTest;
+ }
+
+ @Override
+ public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext)
+ throws Exception {
+ // FIXME(fmeum): Calling this here feels like a hack. There should be a lifecycle hook that runs
+ // before the argument discovery for a ParameterizedTest is kicked off, but I haven't found
+ // one.
+ FuzzTestExecutor.configureAndInstallAgent(extensionContext, fuzzTest.maxDuration());
+ return Stream.empty();
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel
new file mode 100644
index 00000000..3eb8959f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel
@@ -0,0 +1,99 @@
+load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library")
+
+java_library(
+ name = "junit",
+ visibility = ["//deploy:__pkg__"],
+ runtime_deps = [
+ ":fuzz_test",
+ ],
+)
+
+java_library(
+ name = "agent_configurator",
+ srcs = [
+ "AgentConfigurator.java",
+ ],
+ deps = [
+ ":utils",
+ "@maven//:org_junit_jupiter_junit_jupiter_api",
+ ],
+)
+
+java_library(
+ name = "fuzz_test",
+ srcs = [
+ "AgentConfiguringArgumentsProvider.java",
+ "FuzzTest.java",
+ "FuzzTestExtensions.java",
+ "FuzzingArgumentsProvider.java",
+ "SeedArgumentsProvider.java",
+ ],
+ visibility = [
+ "//examples/junit/src/test/java/com/example:__pkg__",
+ ],
+ runtime_deps = [
+ # The JUnit launcher that is part of the Jazzer driver needs this on the classpath
+ # to run an @FuzzTest with JUnit. This will also result in a transitive dependency
+ # in the generated pom file.
+ "@maven//:org_junit_platform_junit_platform_launcher",
+ ],
+ deps = [
+ ":fuzz_test_executor",
+ ":seed_serializer",
+ ":utils",
+ "@maven//:org_junit_jupiter_junit_jupiter_api",
+ "@maven//:org_junit_jupiter_junit_jupiter_params",
+ "@maven//:org_junit_platform_junit_platform_commons",
+ ],
+)
+
+java_jni_library(
+ name = "fuzz_test_executor",
+ srcs = [
+ "FuzzTestExecutor.java",
+ ],
+ native_libs = [
+ "//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver",
+ ],
+ deps = [
+ ":agent_configurator",
+ ":seed_serializer",
+ ":utils",
+ "//src/main/java/com/code_intelligence/jazzer/agent:agent_installer",
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/autofuzz",
+ "//src/main/java/com/code_intelligence/jazzer/driver:fuzz_target_holder",
+ "//src/main/java/com/code_intelligence/jazzer/driver:fuzz_target_runner",
+ "//src/main/java/com/code_intelligence/jazzer/driver:opt",
+ "//src/main/java/com/code_intelligence/jazzer/driver/junit:exit_code_exception",
+ "//src/main/java/com/code_intelligence/jazzer/mutation",
+ "//src/main/java/com/code_intelligence/jazzer/utils",
+ "@maven//:org_junit_jupiter_junit_jupiter_api",
+ "@maven//:org_junit_jupiter_junit_jupiter_params",
+ "@maven//:org_junit_platform_junit_platform_commons",
+ ],
+)
+
+java_library(
+ name = "seed_serializer",
+ srcs = ["SeedSerializer.java"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/autofuzz",
+ "//src/main/java/com/code_intelligence/jazzer/driver:fuzzed_data_provider_impl",
+ "//src/main/java/com/code_intelligence/jazzer/driver:opt",
+ "//src/main/java/com/code_intelligence/jazzer/mutation",
+ ],
+)
+
+java_library(
+ name = "utils",
+ srcs = ["Utils.java"],
+ visibility = ["//src/test/java/com/code_intelligence/jazzer/junit:__pkg__"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider",
+ "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_utils",
+ "@maven//:org_junit_jupiter_junit_jupiter_api",
+ "@maven//:org_junit_jupiter_junit_jupiter_params",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java
new file mode 100644
index 00000000..041db96d
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java
@@ -0,0 +1,122 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.junit;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.parallel.ResourceAccessMode;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.Resources;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+
+/**
+ * A parameterized test with parameters generated automatically by the Java fuzzer <a
+ * href="https://github.com/CodeIntelligenceTesting/jazzer">Jazzer</a>.
+ *
+ * <h2>Test parameters</h2>
+ *
+ * <p>Methods annotated with {@link FuzzTest} can take either of the following types of parameters:
+ *
+ * <dl>
+ * <dt>{@code byte[]}</dt>
+ * <dd>Raw byte input mutated by the fuzzer. Use this signature when your fuzz test naturally
+ * handles raw bytes (e.g. when fuzzing a binary format parser). This is the most efficient, but
+ * also the least convenient way to write a fuzz test.</dd>
+ *
+ * <dt>{@link com.code_intelligence.jazzer.api.FuzzedDataProvider}</dt>
+ * <dd>Provides convenience methods that generate instances of commonly used Java types from the raw
+ * fuzzer input. This is generally the best way to write fuzz tests.</dd>
+ *
+ * <dt>any non-zero number of parameters of any type</dt>
+ * <dd>In this case, Jazzer will rely on reflection and class path scanning to instantiate concrete
+ * arguments. While convenient and a good way to get started, fuzz tests using this feature will
+ * generally be less efficient than fuzz tests using any of the other possible signatures. Due to
+ * the reliance on class path scanning, any change to the class path may also render previous
+ * findings unreproducible.</dd>
+ * </dl>
+ *
+ * <h2>Test modes</h2>
+ *
+ * A fuzz test can be run in two modes: fuzzing and regression testing.
+ *
+ * <h3>Fuzzing</h3>
+ * <p>When the environment variable {@code JAZZER_FUZZ} is set to any non-empty value, fuzz tests
+ * run in "fuzzing" mode. In this mode, the method annotated with {@link FuzzTest} are invoked
+ * repeatedly with inputs that Jazzer generates and mutates based on feedback obtained from
+ * instrumentation it applies to the test and every class loaded by it.
+ *
+ * <p>When an assertion in the test fails, an exception is thrown but not caught, or Jazzer's
+ * instrumentation detects a security issue (e.g. SQL injection or insecure deserialization), the
+ * fuzz test is reported as failed and the input is collected in the inputs directory for the test
+ * class (see "Regression testing" for details).
+ *
+ * <p>When no issue has been found after the configured {@link FuzzTest#maxDuration()}, the test
+ * passes.
+ *
+ * <p>Only a single fuzz test per test run will be executed in fuzzing mode. All other fuzz tests
+ * will be skipped.
+ *
+ * <h3>Regression testing</h3>
+ * <p>By default, a fuzz test is executed as a regular JUnit {@link ParameterizedTest} running on a
+ * fixed set of inputs. It can be run together with regular unit tests and used to verify that past
+ * findings remain fixed. In IDEs with JUnit 5 integration, it can also be used to conveniently
+ * debug individual findings.
+ *
+ * <p>Fuzz tests are always executed on the empty input as well as all input files contained in the
+ * resource directory called {@code <TestClassName>Inputs} in the current package. For example,
+ * all fuzz tests contained in the class {@code com.example.MyFuzzTests} would run on all files
+ * under {@code src/test/resources/com/example/MyFuzzTestsInputs}.
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@AgentConfiguringArgumentsProviderArgumentsSource
+@ArgumentsSource(SeedArgumentsProvider.class)
+@FuzzingArgumentsProviderArgumentsSource
+@ExtendWith(FuzzTestExtensions.class)
+// {0} is expanded to the basename of the seed by the ArgumentProvider.
+@ParameterizedTest(name = "{0}")
+@Tag("jazzer")
+// Fuzz tests can't run in parallel with other fuzz tests since the last finding is kept in a global
+// variable.
+// Fuzz tests also can't run in parallel with other non-fuzz tests since method hooks are enabled
+// conditionally based on a global variable.
+@ResourceLock(value = Resources.GLOBAL, mode = ResourceAccessMode.READ_WRITE)
+public @interface FuzzTest {
+ /**
+ * A duration string such as "1h 2m 30s" indicating for how long the fuzz test should be executed
+ * during fuzzing.
+ *
+ * <p>This option has no effect during regression testing.
+ */
+ String maxDuration() default "5m";
+}
+
+// Internal use only.
+// These wrappers are needed only because the container annotation for @ArgumentsSource,
+// @ArgumentsSources, can't be applied to annotations.
+@Target({ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@ArgumentsSource(AgentConfiguringArgumentsProvider.class)
+@interface AgentConfiguringArgumentsProviderArgumentsSource {}
+
+@Target({ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@ArgumentsSource(FuzzingArgumentsProvider.class)
+@interface FuzzingArgumentsProviderArgumentsSource {}
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java
new file mode 100644
index 00000000..49252e84
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java
@@ -0,0 +1,282 @@
+// Copyright 2023 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.junit;
+
+import static com.code_intelligence.jazzer.junit.Utils.durationStringToSeconds;
+import static com.code_intelligence.jazzer.junit.Utils.generatedCorpusPath;
+import static com.code_intelligence.jazzer.junit.Utils.inputsDirectoryResourcePath;
+import static com.code_intelligence.jazzer.junit.Utils.inputsDirectorySourcePath;
+
+import com.code_intelligence.jazzer.agent.AgentInstaller;
+import com.code_intelligence.jazzer.driver.FuzzTargetHolder;
+import com.code_intelligence.jazzer.driver.FuzzTargetRunner;
+import com.code_intelligence.jazzer.driver.Opt;
+import com.code_intelligence.jazzer.driver.junit.ExitCodeException;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+import org.junit.platform.commons.support.AnnotationSupport;
+
+class FuzzTestExecutor {
+ private static final AtomicBoolean hasBeenPrepared = new AtomicBoolean();
+ private static final AtomicBoolean agentInstalled = new AtomicBoolean(false);
+
+ private final List<String> libFuzzerArgs;
+ private final Path javaSeedsDir;
+ private final boolean isRunFromCommandLine;
+
+ private FuzzTestExecutor(
+ List<String> libFuzzerArgs, Path javaSeedsDir, boolean isRunFromCommandLine) {
+ this.libFuzzerArgs = libFuzzerArgs;
+ this.javaSeedsDir = javaSeedsDir;
+ this.isRunFromCommandLine = isRunFromCommandLine;
+ }
+
+ public static FuzzTestExecutor prepare(ExtensionContext context, String maxDuration)
+ throws IOException {
+ if (!hasBeenPrepared.compareAndSet(false, true)) {
+ throw new IllegalStateException(
+ "FuzzTestExecutor#prepare can only be called once per test run");
+ }
+
+ Class<?> fuzzTestClass = context.getRequiredTestClass();
+ Method fuzzTestMethod = context.getRequiredTestMethod();
+
+ List<ArgumentsSource> allSources = AnnotationSupport.findRepeatableAnnotations(
+ context.getRequiredTestMethod(), ArgumentsSource.class);
+ // Non-empty as it always contains FuzzingArgumentsProvider.
+ ArgumentsSource lastSource = allSources.get(allSources.size() - 1);
+ // Ensure that our ArgumentsProviders run last so that we can record all the seeds generated by
+ // user-provided ones.
+ if (lastSource.value().getPackage() != FuzzTestExecutor.class.getPackage()) {
+ throw new IllegalArgumentException("@FuzzTest must be the last annotation on a fuzz test,"
+ + " but it came after the (meta-)annotation " + lastSource);
+ }
+
+ Path baseDir =
+ Paths.get(context.getConfigurationParameter("jazzer.internal.basedir").orElse(""))
+ .toAbsolutePath();
+
+ List<String> originalLibFuzzerArgs = getLibFuzzerArgs(context);
+ String argv0 = originalLibFuzzerArgs.isEmpty() ? "fake_argv0" : originalLibFuzzerArgs.remove(0);
+
+ ArrayList<String> libFuzzerArgs = new ArrayList<>();
+ libFuzzerArgs.add(argv0);
+
+ // Add passed in corpus directories (and files) at the beginning of the arguments list.
+ // libFuzzer uses the first directory to store discovered inputs, whereas all others are
+ // only used to provide additional seeds and aren't written into.
+ List<String> corpusDirs = originalLibFuzzerArgs.stream()
+ .filter(arg -> !arg.startsWith("-"))
+ .collect(Collectors.toList());
+ originalLibFuzzerArgs.removeAll(corpusDirs);
+ libFuzzerArgs.addAll(corpusDirs);
+
+ // Use the specified corpus dir, if given, otherwise store the generated corpus in a per-class
+ // directory under the project root, just like cifuzz:
+ // https://github.com/CodeIntelligenceTesting/cifuzz/blob/bf410dcfbafbae2a73cf6c5fbed031cdfe234f2f/internal/cmd/run/run.go#L381
+ // The path is specified relative to the current working directory, which with JUnit is the
+ // project directory.
+ Path generatedCorpusDir = baseDir.resolve(generatedCorpusPath(fuzzTestClass, fuzzTestMethod));
+ Files.createDirectories(generatedCorpusDir);
+ libFuzzerArgs.add(generatedCorpusDir.toAbsolutePath().toString());
+
+ // We can only emit findings into the source tree version of the inputs directory, not e.g. the
+ // copy under Maven's target directory. If it doesn't exist, collect the inputs in the current
+ // working directory, which is usually the project's source root.
+ Optional<Path> findingsDirectory =
+ inputsDirectorySourcePath(fuzzTestClass, fuzzTestMethod, baseDir);
+ if (!findingsDirectory.isPresent()) {
+ context.publishReportEntry(String.format(
+ "Collecting crashing inputs in the project root directory.\nIf you want to keep them "
+ + "organized by fuzz test and automatically run them as regression tests with "
+ + "JUnit Jupiter, create a test resource directory called '%s' in package '%s' "
+ + "and move the files there.",
+ inputsDirectoryResourcePath(fuzzTestClass, fuzzTestMethod),
+ fuzzTestClass.getPackage().getName()));
+ }
+
+ // We prefer the inputs directory on the classpath, if it exists, as that is more reliable than
+ // heuristically looking into the source tree based on the current working directory.
+ Optional<Path> inputsDirectory;
+ URL inputsDirectoryUrl =
+ fuzzTestClass.getResource(inputsDirectoryResourcePath(fuzzTestClass, fuzzTestMethod));
+ if (inputsDirectoryUrl != null && "file".equals(inputsDirectoryUrl.getProtocol())) {
+ // The inputs directory is a regular directory on disk (i.e., the test is not run from a
+ // JAR).
+ try {
+ // Using inputsDirectoryUrl.getFile() fails on Windows.
+ inputsDirectory = Optional.of(Paths.get(inputsDirectoryUrl.toURI()));
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ if (inputsDirectoryUrl != null && !findingsDirectory.isPresent()) {
+ context.publishReportEntry(
+ "When running Jazzer fuzz tests from a JAR rather than class files, the inputs "
+ + "directory isn't used unless it is located under src/test/resources/...");
+ }
+ inputsDirectory = findingsDirectory;
+ }
+
+ // From the second positional argument on, files and directories are used as seeds but not
+ // modified.
+ inputsDirectory.ifPresent(dir -> libFuzzerArgs.add(dir.toAbsolutePath().toString()));
+ Path javaSeedsDir = Files.createTempDirectory("jazzer-java-seeds");
+ libFuzzerArgs.add(javaSeedsDir.toAbsolutePath().toString());
+ libFuzzerArgs.add(String.format("-artifact_prefix=%s%c",
+ findingsDirectory.orElse(baseDir).toAbsolutePath(), File.separatorChar));
+
+ libFuzzerArgs.add("-max_total_time=" + durationStringToSeconds(maxDuration));
+ // Disable libFuzzer's out of memory detection: It is only useful for native library fuzzing,
+ // which we don't support without our native driver, and leads to false positives where it picks
+ // up IntelliJ's memory usage.
+ libFuzzerArgs.add("-rss_limit_mb=0");
+ if (Utils.permissivelyParseBoolean(
+ context.getConfigurationParameter("jazzer.valueprofile").orElse("false"))) {
+ libFuzzerArgs.add("-use_value_profile=1");
+ }
+
+ // Prefer original libFuzzerArgs set via command line by appending them last.
+ libFuzzerArgs.addAll(originalLibFuzzerArgs);
+
+ return new FuzzTestExecutor(libFuzzerArgs, javaSeedsDir, Utils.runFromCommandLine(context));
+ }
+
+ /**
+ * Returns the list of arguments set on the command line.
+ */
+ private static List<String> getLibFuzzerArgs(ExtensionContext extensionContext) {
+ List<String> args = new ArrayList<>();
+ for (int i = 0;; i++) {
+ Optional<String> arg = extensionContext.getConfigurationParameter("jazzer.internal.arg." + i);
+ if (!arg.isPresent()) {
+ break;
+ }
+ args.add(arg.get());
+ }
+ return args;
+ }
+
+ static void configureAndInstallAgent(ExtensionContext extensionContext, String maxDuration)
+ throws IOException {
+ if (!agentInstalled.compareAndSet(false, true)) {
+ return;
+ }
+ if (Utils.isFuzzing(extensionContext)) {
+ FuzzTestExecutor executor = prepare(extensionContext, maxDuration);
+ extensionContext.getRoot().getStore(Namespace.GLOBAL).put(FuzzTestExecutor.class, executor);
+ AgentConfigurator.forFuzzing(extensionContext);
+ } else {
+ AgentConfigurator.forRegressionTest(extensionContext);
+ }
+ AgentInstaller.install(Opt.hooks);
+ }
+
+ static FuzzTestExecutor fromContext(ExtensionContext extensionContext) {
+ return extensionContext.getRoot()
+ .getStore(Namespace.GLOBAL)
+ .get(FuzzTestExecutor.class, FuzzTestExecutor.class);
+ }
+
+ public void addSeed(byte[] bytes) throws IOException {
+ Path seed = Files.createTempFile(javaSeedsDir, "seed", null);
+ Files.write(seed, bytes);
+ }
+
+ @SuppressWarnings("OptionalGetWithoutIsPresent")
+ public Optional<Throwable> execute(
+ ReflectiveInvocationContext<Method> invocationContext, SeedSerializer seedSerializer) {
+ if (seedSerializer instanceof AutofuzzSeedSerializer) {
+ FuzzTargetHolder.fuzzTarget = FuzzTargetHolder.autofuzzFuzzTarget(() -> {
+ // Provide an empty throws declaration to prevent autofuzz from
+ // ignoring the defined test exceptions. All exceptions in tests
+ // should cause them to fail.
+ Map<Executable, Class<?>[]> throwsDeclarations = new HashMap<>(1);
+ throwsDeclarations.put(invocationContext.getExecutable(), new Class[0]);
+
+ com.code_intelligence.jazzer.autofuzz.FuzzTarget.setTarget(
+ new Executable[] {invocationContext.getExecutable()},
+ invocationContext.getTarget().get(), invocationContext.getExecutable().toString(),
+ Collections.emptySet(), throwsDeclarations);
+ return null;
+ });
+ } else {
+ FuzzTargetHolder.fuzzTarget =
+ new FuzzTargetHolder.FuzzTarget(invocationContext.getExecutable(),
+ () -> invocationContext.getTarget().get(), Optional.empty());
+ }
+
+ // Only register a finding handler in case the fuzz test is executed by JUnit.
+ // It short-circuits the handling in FuzzTargetRunner and prevents settings
+ // like --keep_going.
+ AtomicReference<Throwable> atomicFinding = new AtomicReference<>();
+ if (!isRunFromCommandLine) {
+ FuzzTargetRunner.registerFindingHandler(t -> {
+ atomicFinding.set(t);
+ return false;
+ });
+ }
+
+ int exitCode = FuzzTargetRunner.startLibFuzzer(libFuzzerArgs);
+ deleteJavaSeedsDir();
+ Throwable finding = atomicFinding.get();
+ if (finding != null) {
+ return Optional.of(finding);
+ } else if (exitCode != 0) {
+ return Optional.of(
+ new ExitCodeException("Jazzer exited with exit code " + exitCode, exitCode));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private void deleteJavaSeedsDir() {
+ // The directory only consists of files, which we need to delete before deleting the directory
+ // itself.
+ try (Stream<Path> entries = Files.list(javaSeedsDir)) {
+ entries.forEach(FuzzTestExecutor::deleteIgnoringErrors);
+ } catch (IOException ignored) {
+ }
+ deleteIgnoringErrors(javaSeedsDir);
+ }
+
+ private static void deleteIgnoringErrors(Path path) {
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException ignored) {
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java
new file mode 100644
index 00000000..0e8ceec9
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java
@@ -0,0 +1,170 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.junit;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExecutionCondition;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.jupiter.api.extension.InvocationInterceptor;
+import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
+import org.junit.platform.commons.support.AnnotationSupport;
+
+class FuzzTestExtensions implements ExecutionCondition, InvocationInterceptor {
+ private static final String JAZZER_INTERNAL =
+ "com.code_intelligence.jazzer.runtime.JazzerInternal";
+ private static final AtomicReference<Method> fuzzTestMethod = new AtomicReference<>();
+ private static Field lastFindingField;
+ private static Field hooksEnabledField;
+
+ @Override
+ public void interceptTestTemplateMethod(Invocation<Void> invocation,
+ ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext)
+ throws Throwable {
+ FuzzTest fuzzTest =
+ AnnotationSupport.findAnnotation(invocationContext.getExecutable(), FuzzTest.class).get();
+ FuzzTestExecutor.configureAndInstallAgent(extensionContext, fuzzTest.maxDuration());
+ // Skip the invocation of the test method with the special arguments provided by
+ // FuzzTestArgumentsProvider and start fuzzing instead.
+ if (Utils.isMarkedInvocation(invocationContext)) {
+ startFuzzing(invocation, invocationContext, extensionContext);
+ } else {
+ // Blocked by https://github.com/junit-team/junit5/issues/3282:
+ // TODO: The seeds from the input directory are duplicated here as there is no way to
+ // recognize them.
+ // TODO: Error out if there is a non-Jazzer ArgumentsProvider and the SeedSerializer does not
+ // support write.
+ if (Utils.isFuzzing(extensionContext)) {
+ // JUnit verifies that the arguments for this invocation are valid.
+ recordSeedForFuzzing(invocationContext.getArguments(), extensionContext);
+ }
+ runWithHooks(invocation);
+ }
+ }
+
+ /**
+ * Mimics the logic of Jazzer's FuzzTargetRunner, which reports findings in the following way:
+ * <ol>
+ * <li>If a hook used Jazzer#reportFindingFromHook to explicitly report a finding, the last such
+ * finding, as stored in JazzerInternal#lastFinding, is reported. <li>If the fuzz target method
+ * threw a Throwable, that is reported. <li>3. Otherwise, nothing is reported.
+ * </ol>
+ */
+ private static void runWithHooks(Invocation<Void> invocation) throws Throwable {
+ Throwable thrown = null;
+ getLastFindingField().set(null, null);
+ // When running in regression test mode, the agent emits additional bytecode logic in front of
+ // method hook invocations that enables them only while a global variable managed by
+ // withHooksEnabled is true.
+ //
+ // Alternatives considered:
+ // * Using a dedicated class loader for @FuzzTests: First-class support for this isn't
+ // available in JUnit 5 (https://github.com/junit-team/junit5/issues/201), but
+ // third-party extensions have done it:
+ // https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtension.java
+ // However, as this involves launching a new test run as part of running a test, this
+ // introduces a number of inconsistencies if applied on the test method rather than test
+ // class level. For example, @BeforeAll methods will have to be run twice in different class
+ // loaders, which may not be safe if they are using global resources not separated by class
+ // loaders (e.g. files).
+ try (AutoCloseable ignored = withHooksEnabled()) {
+ invocation.proceed();
+ } catch (Throwable t) {
+ thrown = t;
+ }
+ Throwable stored = (Throwable) getLastFindingField().get(null);
+ if (stored != null) {
+ throw stored;
+ } else if (thrown != null) {
+ throw thrown;
+ }
+ }
+
+ private static void startFuzzing(Invocation<Void> invocation,
+ ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext)
+ throws Throwable {
+ invocation.skip();
+ Optional<Throwable> throwable =
+ FuzzTestExecutor.fromContext(extensionContext)
+ .execute(invocationContext, getOrCreateSeedSerializer(extensionContext));
+ if (throwable.isPresent()) {
+ throw throwable.get();
+ }
+ }
+
+ private void recordSeedForFuzzing(List<Object> arguments, ExtensionContext extensionContext)
+ throws IOException {
+ SeedSerializer seedSerializer = getOrCreateSeedSerializer(extensionContext);
+ try {
+ FuzzTestExecutor.fromContext(extensionContext)
+ .addSeed(seedSerializer.write(arguments.toArray()));
+ } catch (UnsupportedOperationException ignored) {
+ }
+ }
+
+ @Override
+ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext extensionContext) {
+ if (!Utils.isFuzzing(extensionContext)) {
+ return ConditionEvaluationResult.enabled(
+ "Regression tests are run instead of fuzzing since JAZZER_FUZZ has not been set to a non-empty value");
+ }
+ // Only fuzz the first @FuzzTest that makes it here.
+ if (FuzzTestExtensions.fuzzTestMethod.compareAndSet(
+ null, extensionContext.getRequiredTestMethod())
+ || extensionContext.getRequiredTestMethod().equals(
+ FuzzTestExtensions.fuzzTestMethod.get())) {
+ return ConditionEvaluationResult.enabled(
+ "Fuzzing " + extensionContext.getRequiredTestMethod());
+ }
+ return ConditionEvaluationResult.disabled(
+ "Only one fuzz test can be run at a time, but multiple tests have been annotated with @FuzzTest");
+ }
+
+ private static SeedSerializer getOrCreateSeedSerializer(ExtensionContext extensionContext) {
+ Method method = extensionContext.getRequiredTestMethod();
+ return extensionContext.getStore(Namespace.create(FuzzTestExtensions.class, method))
+ .getOrComputeIfAbsent(
+ SeedSerializer.class, unused -> SeedSerializer.of(method), SeedSerializer.class);
+ }
+
+ private static Field getLastFindingField() throws ClassNotFoundException, NoSuchFieldException {
+ if (lastFindingField == null) {
+ Class<?> jazzerInternal = Class.forName(JAZZER_INTERNAL);
+ lastFindingField = jazzerInternal.getField("lastFinding");
+ }
+ return lastFindingField;
+ }
+
+ private static Field getHooksEnabledField() throws ClassNotFoundException, NoSuchFieldException {
+ if (hooksEnabledField == null) {
+ Class<?> jazzerInternal = Class.forName(JAZZER_INTERNAL);
+ hooksEnabledField = jazzerInternal.getField("hooksEnabled");
+ }
+ return hooksEnabledField;
+ }
+
+ private static AutoCloseable withHooksEnabled()
+ throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
+ Field hooksEnabledField = getHooksEnabledField();
+ hooksEnabledField.setBoolean(null, true);
+ return () -> hooksEnabledField.setBoolean(null, false);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzingArgumentsProvider.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzingArgumentsProvider.java
new file mode 100644
index 00000000..61a8d3fe
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzingArgumentsProvider.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.junit;
+
+import static com.code_intelligence.jazzer.junit.Utils.isFuzzing;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsProvider;
+
+class FuzzingArgumentsProvider implements ArgumentsProvider {
+ @Override
+ public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
+ if (!isFuzzing(extensionContext)) {
+ return Stream.empty();
+ }
+
+ // When fuzzing, supply a special set of arguments that our InvocationInterceptor uses as a
+ // sign to start fuzzing.
+ // FIXME: This is a hack that is needed only because there does not seem to be a way to
+ // communicate out of band that a certain invocation was triggered by a particular argument
+ // provider. We should get rid of this hack as soon as
+ // https://github.com/junit-team/junit5/issues/3282 has been addressed.
+ return Stream.of(
+ Utils.getMarkedArguments(extensionContext.getRequiredTestMethod(), "Fuzzing..."));
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/SeedArgumentsProvider.java b/src/main/java/com/code_intelligence/jazzer/junit/SeedArgumentsProvider.java
new file mode 100644
index 00000000..687a1f7f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/junit/SeedArgumentsProvider.java
@@ -0,0 +1,225 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.junit;
+
+import static com.code_intelligence.jazzer.junit.Utils.isFuzzing;
+import static com.code_intelligence.jazzer.junit.Utils.runFromCommandLine;
+import static org.junit.jupiter.api.Named.named;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitOption;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiPredicate;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsProvider;
+
+class SeedArgumentsProvider implements ArgumentsProvider {
+ @Override
+ public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext)
+ throws IOException {
+ if (runFromCommandLine(extensionContext)) {
+ // libFuzzer always runs on the file-based seeds first anyway and the additional visual
+ // indication provided by test invocations for seeds isn't effective on the command line, so
+ // we skip these invocations.
+ return Stream.empty();
+ }
+
+ Class<?> testClass = extensionContext.getRequiredTestClass();
+ Method testMethod = extensionContext.getRequiredTestMethod();
+
+ Stream<Map.Entry<String, byte[]>> rawSeeds =
+ Stream.of(new SimpleImmutableEntry<>("<empty input>", new byte[0]));
+ rawSeeds = Stream.concat(rawSeeds, walkInputs(testClass, testMethod));
+
+ if (Utils.isCoverageAgentPresent()
+ && Files.isDirectory(Utils.generatedCorpusPath(testClass, testMethod))) {
+ rawSeeds = Stream.concat(rawSeeds,
+ walkInputsInPath(Utils.generatedCorpusPath(testClass, testMethod), Integer.MAX_VALUE));
+ }
+
+ SeedSerializer serializer = SeedSerializer.of(testMethod);
+ return rawSeeds
+ .map(entry -> {
+ Object[] args = serializer.read(entry.getValue());
+ args[0] = named(entry.getKey(), args[0]);
+ return arguments(args);
+ })
+ .onClose(() -> {
+ if (!isFuzzing(extensionContext)) {
+ extensionContext.publishReportEntry(
+ "No fuzzing has been performed, the fuzz test has only been executed on the fixed "
+ + "set of inputs in the seed corpus.\n"
+ + "To start fuzzing, run a test with the environment variable JAZZER_FUZZ set to a "
+ + "non-empty value.");
+ }
+ if (!serializer.allReadsValid()) {
+ extensionContext.publishReportEntry(
+ "Some files in the seed corpus do not match the fuzz target signature.\n"
+ + "This indicates that they were generated with a different signature and may cause "
+ + "issues reproducing previous findings.");
+ }
+ });
+ }
+
+ /**
+ * Used in regression mode to get test cases for the associated {@code testMethod}
+ * This will return a stream of files consisting of:
+ * <ul>
+ * <li>{@code resources/<classpath>/<testClass name>Inputs/*}</li>
+ * <li>{@code resources/<classpath>/<testClass name>Inputs/<testMethod name>/**}</li>
+ * </ul>
+ * Or the equivalent behavior on resources inside a jar file.
+ * <p>
+ * Note that the first {@code <testClass name>Inputs} path will not recursively search all
+ * directories but only gives files in that directory whereas the {@code <testMethod name>}
+ * directory is searched recursively. This allows for multiple tests to share inputs without
+ * needing to explicitly copy them into each test's directory.
+ *
+ * @param testClass the class of the test being run
+ * @param testMethod the test function being run
+ * @return a stream of findings files to use as inputs for the test function
+ */
+ private Stream<Map.Entry<String, byte[]>> walkInputs(Class<?> testClass, Method testMethod)
+ throws IOException {
+ URL classInputsDirUrl = testClass.getResource(Utils.inputsDirectoryResourcePath(testClass));
+
+ if (classInputsDirUrl == null) {
+ return Stream.empty();
+ }
+ URI classInputsDirUri;
+ try {
+ classInputsDirUri = classInputsDirUrl.toURI();
+ } catch (URISyntaxException e) {
+ throw new IOException("Failed to open inputs resource directory: " + classInputsDirUrl, e);
+ }
+ if (classInputsDirUri.getScheme().equals("file")) {
+ // The test is executed from class files, which usually happens when run from inside an IDE.
+ Path classInputsPath = Paths.get(classInputsDirUri);
+
+ return Stream.concat(
+ walkClassInputs(classInputsPath), walkTestInputs(classInputsPath, testMethod));
+
+ } else if (classInputsDirUri.getScheme().equals("jar")) {
+ FileSystem jar = FileSystems.newFileSystem(classInputsDirUri, new HashMap<>());
+ // inputsDirUrl looks like this:
+ // file:/tmp/testdata/ExampleFuzzTest_deploy.jar!/com/code_intelligence/jazzer/junit/testdata/ExampleFuzzTestInputs
+ String pathInJar =
+ classInputsDirUrl.getFile().substring(classInputsDirUrl.getFile().indexOf('!') + 1);
+
+ Path classPathInJar = jar.getPath(pathInJar);
+
+ return Stream
+ .concat(walkClassInputs(classPathInJar), walkTestInputs(classPathInJar, testMethod))
+ .onClose(() -> {
+ try {
+ jar.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } else {
+ throw new IOException(
+ "Unsupported protocol for inputs resource directory: " + classInputsDirUrl);
+ }
+ }
+
+ /**
+ * Walks over the inputs for the method being tested, recurses into subdirectories
+ * @param classInputsPath the path of the class being tested, used as the base path where the test
+ * method's directory
+ * should be
+ * @param testMethod the method being tested
+ * @return a stream of all files under {@code <classInputsPath>/<testMethod name>}
+ * @throws IOException can be thrown by the underlying call to {@link Files#find}
+ */
+ private static Stream<Map.Entry<String, byte[]>> walkTestInputs(
+ Path classInputsPath, Method testMethod) throws IOException {
+ Path testInputsPath = classInputsPath.resolve(testMethod.getName());
+ try {
+ return walkInputsInPath(testInputsPath, Integer.MAX_VALUE);
+ } catch (NoSuchFileException e) {
+ return Stream.empty();
+ }
+ }
+
+ /**
+ * Walks over the inputs for the class being tested. Does not recurse into subdirectories
+ * @param path the path to search to files
+ * @return a stream of all files (without directories) within {@code path}. If {@code path} is not
+ * found, an empty
+ * stream is returned.
+ * @throws IOException can be thrown by the underlying call to {@link Files#find}
+ */
+ private static Stream<Map.Entry<String, byte[]>> walkClassInputs(Path path) throws IOException {
+ try {
+ // using a depth of 1 will get all files within the given path but does not recurse into
+ // subdirectories
+ return walkInputsInPath(path, 1);
+ } catch (NoSuchFileException e) {
+ return Stream.empty();
+ }
+ }
+
+ /**
+ * Gets a sorted stream of all files (without directories) within under the given {@code path}
+ * @param path the path to walk
+ * @param depth the maximum depth of subdirectories to search from within {@code path}. 1
+ * indicates it should return
+ * only the files directly in {@code path} and not search any of its subdirectories
+ * @return a stream of file name -> file contents as a raw byte array
+ * @throws IOException can be thrown by the call to {@link Files#find(Path, int, BiPredicate,
+ * FileVisitOption...)}
+ */
+ private static Stream<Map.Entry<String, byte[]>> walkInputsInPath(Path path, int depth)
+ throws IOException {
+ // @ParameterTest automatically closes Streams and AutoCloseable instances.
+ // noinspection resource
+ return Files
+ .find(path, depth,
+ (fileOrDir, basicFileAttributes)
+ -> !basicFileAttributes.isDirectory(),
+ FileVisitOption.FOLLOW_LINKS)
+ // JUnit identifies individual runs of a `@ParameterizedTest` via their invocation number.
+ // In order to get reproducible behavior e.g. when trying to debug a particular input, all
+ // inputs thus have to be provided in deterministic order.
+ .sorted()
+ .map(file
+ -> new SimpleImmutableEntry<>(
+ file.getFileName().toString(), readAllBytesUnchecked(file)));
+ }
+
+ private static byte[] readAllBytesUnchecked(Path path) {
+ try {
+ return Files.readAllBytes(path);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/SeedSerializer.java b/src/main/java/com/code_intelligence/jazzer/junit/SeedSerializer.java
new file mode 100644
index 00000000..0cebef2f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/junit/SeedSerializer.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.junit;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.autofuzz.Meta;
+import com.code_intelligence.jazzer.driver.FuzzedDataProviderImpl;
+import com.code_intelligence.jazzer.driver.Opt;
+import com.code_intelligence.jazzer.mutation.ArgumentsMutator;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.Method;
+import java.util.Optional;
+
+interface SeedSerializer {
+ Object[] read(byte[] bytes);
+ default boolean allReadsValid() {
+ return true;
+ }
+
+ // Implementations can assume that the argument array contains valid arguments for the method that
+ // this instance has been constructed for.
+ byte[] write(Object[] args) throws UnsupportedOperationException;
+
+ /**
+ * Creates specialized {@link SeedSerializer} instances for the following method parameters:
+ * <ul>
+ * <li>{@code byte[]}
+ * <li>{@code FuzzDataProvider}
+ * <li>Any other types will attempt to be created using either Autofuzz or the experimental
+ * mutator framework if {@link Opt}'s {@code experimentalMutator} is set.
+ * </ul>
+ */
+ static SeedSerializer of(Method method) {
+ if (method.getParameterCount() == 0) {
+ throw new IllegalArgumentException(
+ "Methods annotated with @FuzzTest must take at least one parameter");
+ }
+ if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == byte[].class) {
+ return new ByteArraySeedSerializer();
+ } else if (method.getParameterCount() == 1
+ && method.getParameterTypes()[0] == FuzzedDataProvider.class) {
+ return new FuzzedDataProviderSeedSerializer();
+ } else {
+ Optional<ArgumentsMutator> argumentsMutator =
+ Opt.experimentalMutator ? ArgumentsMutator.forMethod(method) : Optional.empty();
+ return argumentsMutator.<SeedSerializer>map(ArgumentsMutatorSeedSerializer::new)
+ .orElseGet(() -> new AutofuzzSeedSerializer(method));
+ }
+ }
+}
+
+final class ByteArraySeedSerializer implements SeedSerializer {
+ @Override
+ public Object[] read(byte[] bytes) {
+ return new Object[] {bytes};
+ }
+
+ @Override
+ public byte[] write(Object[] args) {
+ return (byte[]) args[0];
+ }
+}
+
+final class FuzzedDataProviderSeedSerializer implements SeedSerializer {
+ @Override
+ public Object[] read(byte[] bytes) {
+ return new Object[] {FuzzedDataProviderImpl.withJavaData(bytes)};
+ }
+
+ @Override
+ public byte[] write(Object[] args) throws UnsupportedOperationException {
+ // While we could get the underlying bytes, it's not possible to provide Java seeds for fuzz
+ // tests with a FuzzedDataProvider parameter.
+ throw new UnsupportedOperationException();
+ }
+}
+
+final class ArgumentsMutatorSeedSerializer implements SeedSerializer {
+ private final ArgumentsMutator mutator;
+ private boolean allReadsValid;
+
+ public ArgumentsMutatorSeedSerializer(ArgumentsMutator mutator) {
+ this.mutator = mutator;
+ }
+
+ @Override
+ public Object[] read(byte[] bytes) {
+ allReadsValid &= mutator.read(new ByteArrayInputStream(bytes));
+ return mutator.getArguments();
+ }
+
+ @Override
+ public boolean allReadsValid() {
+ return allReadsValid;
+ }
+
+ @Override
+ public byte[] write(Object[] args) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ mutator.writeAny(out, args);
+ return out.toByteArray();
+ }
+}
+
+final class AutofuzzSeedSerializer implements SeedSerializer {
+ private final Meta meta;
+ private final Method method;
+
+ public AutofuzzSeedSerializer(Method method) {
+ this.meta = new Meta(method.getDeclaringClass());
+ this.method = method;
+ }
+
+ @Override
+ public Object[] read(byte[] bytes) {
+ try (FuzzedDataProviderImpl data = FuzzedDataProviderImpl.withJavaData(bytes)) {
+ // The Autofuzz FuzzTarget uses data to construct an instance of the test class before
+ // it constructs the fuzz test arguments. We don't need the instance here, but still
+ // generate it as that mutates the FuzzedDataProvider state.
+ meta.consumeNonStatic(data, method.getDeclaringClass());
+ return meta.consumeArguments(data, method, null);
+ }
+ }
+
+ @Override
+ public byte[] write(Object[] args) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/Utils.java b/src/main/java/com/code_intelligence/jazzer/junit/Utils.java
new file mode 100644
index 00000000..4ab81cd1
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/junit/Utils.java
@@ -0,0 +1,305 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.junit;
+
+import static java.util.Arrays.stream;
+import static java.util.Collections.newSetFromMap;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static org.junit.jupiter.api.Named.named;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import com.code_intelligence.jazzer.utils.UnsafeProvider;
+import com.code_intelligence.jazzer.utils.UnsafeUtils;
+import java.io.File;
+import java.io.IOException;
+import java.lang.invoke.MethodType;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Proxy;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
+import org.junit.jupiter.params.provider.Arguments;
+
+class Utils {
+ /**
+ * Returns the resource path of the inputs directory for a given test class and method. The path
+ * will have the form
+ * {@code <class name>Inputs/<method name>}
+ */
+ static String inputsDirectoryResourcePath(Class<?> testClass, Method testMethod) {
+ return testClass.getSimpleName() + "Inputs"
+ + "/" + testMethod.getName();
+ }
+
+ static String inputsDirectoryResourcePath(Class<?> testClass) {
+ return testClass.getSimpleName() + "Inputs";
+ }
+
+ /**
+ * Returns the file system path of the inputs corpus directory in the source tree, if it exists.
+ * The directory is created if it does not exist, but the test resource directory itself exists.
+ */
+ static Optional<Path> inputsDirectorySourcePath(
+ Class<?> testClass, Method testMethod, Path baseDir) {
+ String inputsResourcePath = Utils.inputsDirectoryResourcePath(testClass, testMethod);
+ // Make the inputs resource path absolute.
+ if (!inputsResourcePath.startsWith("/")) {
+ String inputsPackage = testClass.getPackage().getName().replace('.', '/');
+ inputsResourcePath = "/" + inputsPackage + "/" + inputsResourcePath;
+ }
+
+ // Following the Maven directory layout, we look up the inputs directory under
+ // src/test/resources. This should be correct also for multi-module projects as JUnit is usually
+ // launched in the current module's root directory.
+ Path testResourcesDirectory = baseDir.resolve("src").resolve("test").resolve("resources");
+ Path sourceInputsDirectory = testResourcesDirectory;
+ for (String segment : inputsResourcePath.split("/")) {
+ sourceInputsDirectory = sourceInputsDirectory.resolve(segment);
+ }
+ if (Files.isDirectory(sourceInputsDirectory)) {
+ return Optional.of(sourceInputsDirectory);
+ }
+ // If we can at least find the test resource directory, create the inputs directory.
+ if (!Files.isDirectory(testResourcesDirectory)) {
+ return Optional.empty();
+ }
+ try {
+ return Optional.of(Files.createDirectories(sourceInputsDirectory));
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+
+ static Path generatedCorpusPath(Class<?> testClass, Method testMethod) {
+ return Paths.get(".cifuzz-corpus", testClass.getName(), testMethod.getName());
+ }
+
+ /**
+ * Returns a heuristic default value for jazzer.instrument based on the test class.
+ */
+ static String getLegacyInstrumentationFilter(Class<?> testClass) {
+ // This is an extremely rough "implementation" of the public suffix list algorithm
+ // (https://publicsuffix.org/): It tries to guess the shortest prefix of the package name that
+ // isn't public. It doesn't use the actual list, but instead assumes that every root segment as
+ // well as "com.github" are public. Examples:
+ // - com.example.Test --> com.example.**
+ // - com.example.foobar.Test --> com.example.**
+ // - com.github.someones.repo.Test --> com.github.someones.**
+ String packageName = testClass.getPackage().getName();
+ String[] packageSegments = packageName.split("\\.");
+ int numSegments = 2;
+ if (packageSegments.length > 2 && packageSegments[0].equals("com")
+ && packageSegments[1].equals("github")) {
+ numSegments = 3;
+ }
+ return Stream.concat(Arrays.stream(packageSegments).limit(numSegments), Stream.of("**"))
+ .collect(joining("."));
+ }
+
+ private static final Pattern CLASSPATH_SPLITTER =
+ Pattern.compile(Pattern.quote(File.pathSeparator));
+
+ /**
+ * Returns a heuristic default value for jazzer.instrument based on the files on the provided
+ * classpath.
+ */
+ static Optional<String> getClassPathBasedInstrumentationFilter(String classPath) {
+ List<Path> includes =
+ CLASSPATH_SPLITTER.splitAsStream(classPath)
+ .map(Paths::get)
+ // We consider classpath entries that are directories rather than jar files to contain
+ // the classes of the current project rather than external dependencies. This is just a
+ // heuristic and breaks with build systems that package all classes in jar files, e.g.
+ // with Bazel.
+ .filter(Files::isDirectory)
+ .flatMap(root -> {
+ HashSet<Path> pkgs = new HashSet<>();
+ try {
+ Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult preVisitDirectory(
+ Path dir, BasicFileAttributes basicFileAttributes) throws IOException {
+ try (Stream<Path> entries = Files.list(dir)) {
+ // If a directory contains a .class file, we add an include filter matching it
+ // and all subdirectories.
+ // Special case: If there is a class defined at the root, only the unnamed
+ // package is included, so continue with the traversal of subdirectories
+ // to discover additional includes.
+ if (entries.filter(path -> path.toString().endsWith(".class"))
+ .anyMatch(Files::isRegularFile)) {
+ Path pkgPath = root.relativize(dir);
+ pkgs.add(pkgPath);
+ if (pkgPath.toString().isEmpty()) {
+ return FileVisitResult.CONTINUE;
+ } else {
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+ }
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ } catch (IOException e) {
+ // This is only a best-effort heuristic anyway, ignore this directory.
+ return Stream.of();
+ }
+ return pkgs.stream();
+ })
+ .distinct()
+ .collect(toList());
+ if (includes.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ includes.stream()
+ .map(Path::toString)
+ // For classes without a package, only include the unnamed package.
+ .map(path -> path.isEmpty() ? "*" : path.replace(File.separator, ".") + ".**")
+ .sorted()
+ // jazzer.instrument uses ',' as the separator.
+ .collect(joining(",")));
+ }
+
+ private static final Pattern COVERAGE_AGENT_ARG =
+ Pattern.compile("-javaagent:.*(?:intellij-coverage-agent|jacoco).*");
+ static boolean isCoverageAgentPresent() {
+ return ManagementFactory.getRuntimeMXBean().getInputArguments().stream().anyMatch(
+ s -> COVERAGE_AGENT_ARG.matcher(s).matches());
+ }
+
+ private static final boolean IS_FUZZING_ENV =
+ System.getenv("JAZZER_FUZZ") != null && !System.getenv("JAZZER_FUZZ").isEmpty();
+ static boolean isFuzzing(ExtensionContext extensionContext) {
+ return IS_FUZZING_ENV || runFromCommandLine(extensionContext);
+ }
+
+ static boolean runFromCommandLine(ExtensionContext extensionContext) {
+ return extensionContext.getConfigurationParameter("jazzer.internal.commandLine")
+ .map(Boolean::parseBoolean)
+ .orElse(false);
+ }
+
+ /**
+ * Returns true if and only if the value is equal to "true", "1", or "yes" case-insensitively.
+ */
+ static boolean permissivelyParseBoolean(String value) {
+ return value.equalsIgnoreCase("true") || value.equals("1") || value.equalsIgnoreCase("yes");
+ }
+
+ /**
+ * Convert the string to ISO 8601 (https://en.wikipedia.org/wiki/ISO_8601#Durations). We do not
+ * allow for duration units longer than hours, so we can always prepend PT.
+ */
+ static long durationStringToSeconds(String duration) {
+ String isoDuration =
+ "PT" + duration.replace("sec", "s").replace("min", "m").replace("hr", "h").replace(" ", "");
+ return Duration.parse(isoDuration).getSeconds();
+ }
+
+ /**
+ * Creates {@link Arguments} for a single invocation of a parameterized test that can be
+ * identified as having been created in this way by {@link #isMarkedInvocation}.
+ *
+ * @param displayName the display name to assign to every argument
+ */
+ static Arguments getMarkedArguments(Method method, String displayName) {
+ return arguments(stream(method.getParameterTypes())
+ .map(Utils::getMarkedInstance)
+ // Wrap in named as toString may crash on marked instances.
+ .map(arg -> named(displayName, arg))
+ .toArray(Object[] ::new));
+ }
+
+ /**
+ * @return {@code true} if and only if the arguments for this test method invocation were created
+ * with {@link #getMarkedArguments}
+ */
+ static boolean isMarkedInvocation(ReflectiveInvocationContext<Method> invocationContext) {
+ if (invocationContext.getArguments().stream().anyMatch(Utils::isMarkedInstance)) {
+ if (invocationContext.getArguments().stream().allMatch(Utils::isMarkedInstance)) {
+ return true;
+ }
+ throw new IllegalStateException(
+ "Some, but not all arguments were marked in invocation of " + invocationContext);
+ } else {
+ return false;
+ }
+ }
+
+ private static final ClassValue<Object> uniqueInstanceCache = new ClassValue<Object>() {
+ @Override
+ protected Object computeValue(Class<?> clazz) {
+ return makeMarkedInstance(clazz);
+ }
+ };
+ private static final Set<Object> uniqueInstances = newSetFromMap(new IdentityHashMap<>());
+
+ // Visible for testing.
+ static <T> T getMarkedInstance(Class<T> clazz) {
+ // makeMarkedInstance creates new classes, which is expensive and can cause the JVM to run out
+ // of metaspace. We thus cache the marked instances per class.
+ Object instance = uniqueInstanceCache.get(clazz);
+ uniqueInstances.add(instance);
+ return (T) instance;
+ }
+
+ // Visible for testing.
+ static boolean isMarkedInstance(Object instance) {
+ return uniqueInstances.contains(instance);
+ }
+
+ private static Object makeMarkedInstance(Class<?> clazz) {
+ if (clazz == Class.class) {
+ return new Object() {}.getClass();
+ }
+ if (clazz.isArray()) {
+ return Array.newInstance(clazz.getComponentType(), 0);
+ }
+ if (clazz.isInterface()) {
+ return Proxy.newProxyInstance(
+ Utils.class.getClassLoader(), new Class[] {clazz}, (o, method, objects) -> null);
+ }
+
+ if (clazz.isPrimitive()) {
+ clazz = MethodType.methodType(clazz).wrap().returnType();
+ } else if (Modifier.isAbstract(clazz.getModifiers())) {
+ clazz = UnsafeUtils.defineAnonymousConcreteSubclass(clazz);
+ }
+
+ try {
+ return clazz.cast(UnsafeProvider.getUnsafe().allocateInstance(clazz));
+ } catch (InstantiationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java
new file mode 100644
index 00000000..fabda057
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation;
+
+import static com.code_intelligence.jazzer.mutation.mutator.Mutators.validateAnnotationUsage;
+import static com.code_intelligence.jazzer.mutation.support.InputStreamSupport.extendWithReadExactly;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static com.code_intelligence.jazzer.mutation.support.StreamSupport.toArrayOrEmpty;
+import static java.lang.String.format;
+import static java.util.Arrays.stream;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators;
+import com.code_intelligence.jazzer.mutation.combinator.ProductMutator;
+import com.code_intelligence.jazzer.mutation.engine.SeededPseudoRandom;
+import com.code_intelligence.jazzer.mutation.mutator.Mutators;
+import com.code_intelligence.jazzer.mutation.support.InputStreamSupport.ReadExactlyInputStream;
+import com.code_intelligence.jazzer.mutation.support.Preconditions;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Optional;
+
+public final class ArgumentsMutator {
+ private final Object instance;
+ private final Method method;
+ private final ProductMutator productMutator;
+ private Object[] arguments;
+
+ /**
+ * True if the arguments array has already been passed to a user-provided function or exposed
+ * via {@link #getArguments()} without going through {@link ProductMutator#detach(Object[])}.
+ * In this case the arguments may have been modified externally, which interferes with mutation,
+ * or could have been stored in static state that would be affected by future mutations.
+ * Arguments should either be detached or not be reused after being exposed, which is enforced by
+ * this variable.
+ */
+ private boolean argumentsExposed;
+
+ private ArgumentsMutator(Object instance, Method method, ProductMutator productMutator) {
+ this.instance = instance;
+ this.method = method;
+ this.productMutator = productMutator;
+ }
+
+ private static String prettyPrintMethod(Method method) {
+ return format("%s.%s(%s)", method.getDeclaringClass().getName(), method.getName(),
+ stream(method.getAnnotatedParameterTypes()).map(Object::toString).collect(joining(", ")));
+ }
+
+ public static ArgumentsMutator forInstanceMethodOrThrow(Object instance, Method method) {
+ return forInstanceMethod(Mutators.newFactory(), instance, method)
+ .orElseThrow(()
+ -> new IllegalArgumentException(
+ "Failed to construct mutator for " + prettyPrintMethod(method)));
+ }
+
+ public static ArgumentsMutator forStaticMethodOrThrow(Method method) {
+ return forStaticMethod(Mutators.newFactory(), method)
+ .orElseThrow(()
+ -> new IllegalArgumentException(
+ "Failed to construct mutator for " + prettyPrintMethod(method)));
+ }
+
+ public static Optional<ArgumentsMutator> forMethod(Method method) {
+ return forMethod(Mutators.newFactory(), null, method);
+ }
+
+ public static Optional<ArgumentsMutator> forInstanceMethod(
+ MutatorFactory mutatorFactory, Object instance, Method method) {
+ require(!isStatic(method), "method must not be static");
+ requireNonNull(instance, "instance must not be null");
+ require(method.getDeclaringClass().isInstance(instance),
+ format("instance is a %s, expected %s", instance.getClass(), method.getDeclaringClass()));
+ return forMethod(mutatorFactory, instance, method);
+ }
+
+ public static Optional<ArgumentsMutator> forStaticMethod(
+ MutatorFactory mutatorFactory, Method method) {
+ require(isStatic(method), "method must be static");
+ return forMethod(mutatorFactory, null, method);
+ }
+
+ public static Optional<ArgumentsMutator> forMethod(
+ MutatorFactory mutatorFactory, Object instance, Method method) {
+ require(method.getParameterCount() > 0, "Can't fuzz method without parameters: " + method);
+ for (AnnotatedType parameter : method.getAnnotatedParameterTypes()) {
+ validateAnnotationUsage(parameter);
+ }
+ return toArrayOrEmpty(
+ stream(method.getAnnotatedParameterTypes()).map(mutatorFactory::tryCreate),
+ SerializingMutator<?>[] ::new)
+ .map(MutatorCombinators::mutateProduct)
+ .map(productMutator -> ArgumentsMutator.create(instance, method, productMutator));
+ }
+
+ private static ArgumentsMutator create(
+ Object instance, Method method, ProductMutator productMutator) {
+ method.setAccessible(true);
+
+ return new ArgumentsMutator(instance, method, productMutator);
+ }
+
+ private static boolean isStatic(Method method) {
+ return Modifier.isStatic(method.getModifiers());
+ }
+
+ /**
+ * @throws UncheckedIOException if the underlying InputStream throws
+ */
+ public void crossOver(InputStream data1, InputStream data2, long seed) {
+ try {
+ Object[] objects1 = productMutator.readExclusive(data1);
+ Object[] objects2 = productMutator.readExclusive(data2);
+ PseudoRandom prng = new SeededPseudoRandom(seed);
+ arguments = productMutator.crossOver(objects1, objects2, prng);
+ argumentsExposed = false;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
+ * @return if the given input stream was consumed exactly
+ * @throws UncheckedIOException if the underlying InputStream throws
+ */
+ public boolean read(ByteArrayInputStream data) {
+ try {
+ ReadExactlyInputStream is = extendWithReadExactly(data);
+ arguments = productMutator.readExclusive(is);
+ argumentsExposed = false;
+ return is.isConsumedExactly();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
+ * @throws UncheckedIOException if the underlying OutputStream throws
+ */
+ public void write(OutputStream data) {
+ failIfArgumentsExposed();
+ writeAny(data, arguments);
+ }
+
+ /**
+ * @throws UncheckedIOException if the underlying OutputStream throws
+ */
+ public void writeAny(OutputStream data, Object[] args) throws UncheckedIOException {
+ try {
+ productMutator.writeExclusive(args, data);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public void init(long seed) {
+ init(new SeededPseudoRandom(seed));
+ }
+
+ void init(PseudoRandom prng) {
+ arguments = productMutator.init(prng);
+ argumentsExposed = false;
+ }
+
+ public void mutate(long seed) {
+ mutate(new SeededPseudoRandom(seed));
+ }
+
+ void mutate(PseudoRandom prng) {
+ failIfArgumentsExposed();
+ // TODO: Sometimes mutate the entire byte representation of the current value with libFuzzer's
+ // dictionary and TORC mutations.
+ productMutator.mutate(arguments, prng);
+ }
+
+ public void invoke(boolean detach) throws Throwable {
+ Object[] invokeArguments;
+ if (detach) {
+ invokeArguments = productMutator.detach(arguments);
+ } else {
+ invokeArguments = arguments;
+ argumentsExposed = true;
+ }
+ try {
+ method.invoke(instance, invokeArguments);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("method should have been made accessible", e);
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+
+ public Object[] getArguments() {
+ argumentsExposed = true;
+ return arguments;
+ }
+
+ @Override
+ public String toString() {
+ return "Arguments" + productMutator;
+ }
+
+ private void failIfArgumentsExposed() {
+ Preconditions.check(!argumentsExposed,
+ "Arguments have previously been exposed to user-provided code without calling #detach and may have been modified");
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel
new file mode 100644
index 00000000..9866c2c4
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel
@@ -0,0 +1,16 @@
+java_library(
+ name = "mutation",
+ srcs = glob(["*.java"]),
+ visibility = [
+ "//visibility:public",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/api",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/combinator",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/engine",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/mutator",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/support",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/AppliesTo.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/AppliesTo.java
new file mode 100644
index 00000000..4d4c4a89
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/AppliesTo.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * A meta-annotation that limits the concrete types an annotation for type usages applies to.
+ */
+@Target(ANNOTATION_TYPE)
+@Retention(RUNTIME)
+public @interface AppliesTo {
+ /**
+ * The meta-annotated annotation can be applied to these classes.
+ */
+ Class<?>[] value() default {};
+
+ /**
+ * The meta-annotated annotation can be applied to subclasses of these classes.
+ */
+ Class<?>[] subClassesOf() default {};
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/Ascii.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/Ascii.java
new file mode 100644
index 00000000..190ada09
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/Ascii.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target(TYPE_USE)
+@Retention(RUNTIME)
+@AppliesTo(String.class)
+public @interface Ascii {}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/BUILD.bazel
new file mode 100644
index 00000000..6d6c4da9
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/BUILD.bazel
@@ -0,0 +1,5 @@
+java_library(
+ name = "annotation",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/DoubleInRange.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/DoubleInRange.java
new file mode 100644
index 00000000..27765879
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/DoubleInRange.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target(TYPE_USE)
+@Retention(RUNTIME)
+@AppliesTo({double.class, Double.class})
+public @interface DoubleInRange {
+ double min() default Double.NEGATIVE_INFINITY;
+ double max() default Double.POSITIVE_INFINITY;
+ boolean allowNaN() default true;
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/FloatInRange.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/FloatInRange.java
new file mode 100644
index 00000000..ec54e026
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/FloatInRange.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target(TYPE_USE)
+@Retention(RUNTIME)
+@AppliesTo({float.class, Float.class})
+public @interface FloatInRange {
+ float min() default Float.NEGATIVE_INFINITY;
+ float max() default Float.POSITIVE_INFINITY;
+ boolean allowNaN() default true;
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java
new file mode 100644
index 00000000..a8dc8281
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(TYPE_USE)
+@Retention(RUNTIME)
+@AppliesTo({byte.class, Byte.class, short.class, Short.class, int.class, Integer.class, long.class,
+ Long.class})
+public @interface InRange {
+ long min() default Long.MIN_VALUE;
+
+ long max() default Long.MAX_VALUE;
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/NotNull.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/NotNull.java
new file mode 100644
index 00000000..061eeffc
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/NotNull.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({PARAMETER, TYPE_USE})
+@Retention(RUNTIME)
+@AppliesTo(subClassesOf = Object.class)
+public @interface NotNull {}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithLength.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithLength.java
new file mode 100644
index 00000000..7cee23df
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithLength.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target(TYPE_USE)
+@Retention(RUNTIME)
+@AppliesTo(byte[].class)
+public @interface WithLength {
+ int min() default 0;
+
+ int max() default 1000;
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java
new file mode 100644
index 00000000..32233f4b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.List;
+import java.util.Map;
+
+@Target(TYPE_USE)
+@Retention(RUNTIME)
+@AppliesTo({List.class, Map.class})
+public @interface WithSize {
+ int min() default 0;
+
+ int max() default 1000;
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithUtf8Length.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithUtf8Length.java
new file mode 100644
index 00000000..00b1f463
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithUtf8Length.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that applies to {@link String} to <strong>limit the length of the UTF-8
+ * encoding</strong> of the string. In practical terms, this means that strings given this
+ * annotation will sometimes have a {@link String#length()} of less than
+ * {@code min} but will never exceed {@code max}. <p> Due to the fact that our String mutator is
+ * backed by the byte array mutator, it's difficult to know how many characters we'll get from the
+ * byte array we get from libfuzzer. Rather than reuse {@link WithLength} for strings which may give
+ * the impression that {@link String#length()} will return a value between {@code min} and {@code
+ * max}, we use this annotation to help make clear that the string consists of between
+ * {@code min} and {@code max} UTF-8 bytes, not necessarily (UTF-16) characters.
+ */
+@Target(TYPE_USE)
+@Retention(RUNTIME)
+@AppliesTo(String.class)
+public @interface WithUtf8Length {
+ int min() default 0;
+
+ int max() default 1000;
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/AnySource.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/AnySource.java
new file mode 100644
index 00000000..9f802dfe
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/AnySource.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation.proto;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.code_intelligence.jazzer.mutation.annotation.AppliesTo;
+import com.google.protobuf.Message;
+import com.google.protobuf.Message.Builder;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Controls the mutations of {@link com.google.protobuf.Any} fields in messages of the annotated
+ * type as well as its recursive message fields.
+ */
+@Target(TYPE_USE)
+@Retention(RUNTIME)
+@AppliesTo(subClassesOf = {Message.class, Builder.class})
+public @interface AnySource {
+ /**
+ * A non-empty list of {@link Message}s to use for {@link com.google.protobuf.Any} fields.
+ */
+ Class<? extends Message>[] value();
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/BUILD.bazel
new file mode 100644
index 00000000..8ef6863b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/BUILD.bazel
@@ -0,0 +1,21 @@
+java_library(
+ name = "proto",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ ":protobuf_runtime_compile_only",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
+ ],
+)
+
+java_library(
+ name = "protobuf_runtime_compile_only",
+ # The proto mutator factory detects the presence of Protobuf at runtime and disables itself if
+ # it isn't found. Without something else bringing in the Protobuf runtime, there is no point in
+ # supporting proto mutations.
+ neverlink = True,
+ visibility = ["//src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto:__pkg__"],
+ exports = [
+ "@com_google_protobuf_protobuf_java//jar",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/WithDefaultInstance.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/WithDefaultInstance.java
new file mode 100644
index 00000000..e26d73a2
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/WithDefaultInstance.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.annotation.proto;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.code_intelligence.jazzer.mutation.annotation.AppliesTo;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.Message;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Provides a default instance to use as the base for mutations of the annotated {@link Message} or
+ * {@link DynamicMessage.Builder}.
+ */
+@Target(TYPE_USE)
+@Retention(RUNTIME)
+@AppliesTo(subClassesOf = {Message.class, Message.Builder.class})
+public @interface WithDefaultInstance {
+ /**
+ * The fully qualified name of a static method (e.g.
+ * {@code com.example.MyClass#getDefaultInstance}) with return type assignable to
+ * {@link com.google.protobuf.Message}, which returns a default instance that mutations should be
+ * based on.
+ */
+ String value();
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/api/BUILD.bazel
new file mode 100644
index 00000000..cd5fe60e
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/BUILD.bazel
@@ -0,0 +1,9 @@
+java_library(
+ name = "api",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/support",
+ "@com_google_errorprone_error_prone_annotations//jar",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/ChainedMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/ChainedMutatorFactory.java
new file mode 100644
index 00000000..bf27e81b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/ChainedMutatorFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.api;
+
+import static com.code_intelligence.jazzer.mutation.support.StreamSupport.findFirstPresent;
+import static java.util.Arrays.asList;
+import static java.util.Collections.unmodifiableList;
+
+import com.google.errorprone.annotations.CheckReturnValue;
+import java.lang.reflect.AnnotatedType;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A {@link MutatorFactory} that delegates to the given factories in order.
+ */
+public final class ChainedMutatorFactory extends MutatorFactory {
+ private final List<MutatorFactory> factories;
+
+ /**
+ * Creates a {@link MutatorFactory} that delegates to the given factories in order.
+ *
+ * @param factories a possibly empty collection of factories
+ */
+ public ChainedMutatorFactory(MutatorFactory... factories) {
+ this.factories = unmodifiableList(asList(factories));
+ }
+
+ @Override
+ @CheckReturnValue
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory parent) {
+ return findFirstPresent(factories.stream().map(factory -> factory.tryCreate(type, parent)));
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/Debuggable.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/Debuggable.java
new file mode 100644
index 00000000..df6f8288
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/Debuggable.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.api;
+
+import static java.util.Collections.newSetFromMap;
+import static java.util.Objects.requireNonNull;
+
+import java.util.IdentityHashMap;
+import java.util.Set;
+import java.util.function.Predicate;
+
+public interface Debuggable {
+ /**
+ * Returns a string representation of the object that is meant to be used to make assertions about
+ * its structure in tests.
+ *
+ * @param isInCycle evaluates to {@code true} if a cycle has been detected during recursive calls
+ * of this function. Must be called at most once with {@code this} as the single
+ * argument. Implementing classes that know that their current instance can never
+ * be contained in recursive substructures need not call this method.
+ */
+ String toDebugString(Predicate<Debuggable> isInCycle);
+
+ /**
+ * Returns a string representation of the given {@link Debuggable} that is meant to be used to
+ * make assertions about its structure in tests.
+ */
+ static String getDebugString(Debuggable debuggable) {
+ Set<Debuggable> seen = newSetFromMap(new IdentityHashMap<>());
+ return debuggable.toDebugString(child -> !seen.add(requireNonNull(child)));
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/Detacher.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/Detacher.java
new file mode 100644
index 00000000..d927e505
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/Detacher.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.api;
+
+import com.google.errorprone.annotations.CheckReturnValue;
+
+/**
+ * Knows how to clone a {@code T} such that it shares no mutable state with the original.
+ */
+@FunctionalInterface
+public interface Detacher<T> {
+ /**
+ * Returns an equal instance that shares no mutable state with {@code value}.
+ *
+ * <p>Implementations
+ * <ul>
+ * <li>MUST return an instance that {@link Object#equals(Object)} the argument;
+ * <li>MUST return an instance that cannot be used to mutate the state of the argument through
+ * its API (ignoring uses of {@link sun.misc.Unsafe});
+ * <li>MUST return an instance that is not affected by any changes to the original value made
+ * by any mutator;</li>
+ * <li>MUST be accepted by mutator methods just like the original value;</li>
+ * <li>MAY return the argument itself if it is deeply immutable.
+ * </ul>
+ *
+ * @param value the instance to detach
+ * @return an equal instance that shares no mutable state with {@code value}
+ */
+ @CheckReturnValue T detach(T value);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/InPlaceMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/InPlaceMutator.java
new file mode 100644
index 00000000..cf1b243d
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/InPlaceMutator.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.api;
+
+/**
+ * Knows how to initialize and mutate (parts of) an existing object of type {@code T} in place and
+ * how to incorporate (cross over) parts of another object of the same type.
+ *
+ * <p>Certain types, such as immutable and primitive types, can not be mutated in place. For
+ * example, {@link java.util.List} can be mutated in place whereas {@link String} and {@code int}
+ * can't. In such cases, use {@link ValueMutator} instead.
+ *
+ * <p>Implementations
+ * <ul>
+ * <li>MAY weakly associate mutable state with the identity (not equality class) of objects they
+ * have been passed as arguments or returned from initialization functions;
+ * <li>MAY assume that they are only passed arguments that they have initialized or mutated;</li>
+ * <li>SHOULD use {@link com.code_intelligence.jazzer.mutation.support.WeakIdentityHashMap} for
+ * this purpose;
+ * <li>MUST otherwise be deeply immutable;
+ * <li>SHOULD override {@link Object#toString()} to return {@code
+ * Debuggable.getDebugString(this)}.
+ * </ul>
+ *
+ * @param <T> the reference type this mutator operates on
+ */
+public interface InPlaceMutator<T> extends Debuggable {
+ /**
+ * Implementations
+ * <ul>
+ * <li>MUST accept any mutable instance of {@code T}, not just those it creates itself.
+ * <li>SHOULD, when called repeatedly, initialize the object in ways that are likely to be
+ * distinct.
+ * </ul>
+ */
+ void initInPlace(T reference, PseudoRandom prng);
+
+ /**
+ * Implementations
+ * <ul>
+ * <li>MUST ensure that {@code reference} does not {@link Object#equals(Object)} the state it
+ * had prior to the call (if possible);
+ * <li>MUST accept any mutable instance of {@code T}, not just those it creates itself.
+ * <li>SHOULD, when called repeatedly, be able to eventually reach any valid state of the part
+ * of {@code T} governed by this mutator;
+ * </ul>
+ */
+ void mutateInPlace(T reference, PseudoRandom prng);
+
+ /**
+ * Implementations
+ * <ul>
+ * <li>MUST ensure that {@code reference} does not {@link Object#equals(Object)} the state it
+ * had prior to the call (if possible);
+ * <li>MUST accept any mutable instance of {@code T}, not just those it creates itself.
+ * <li>MUST NOT mutate {@code otherReference}</li>
+ * </ul>
+ */
+ void crossOverInPlace(T reference, T otherReference, PseudoRandom prng);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/MutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/MutatorFactory.java
new file mode 100644
index 00000000..64771285
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/MutatorFactory.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.api;
+
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asAnnotatedType;
+import static java.lang.String.format;
+
+import com.google.errorprone.annotations.CheckReturnValue;
+import java.lang.reflect.AnnotatedType;
+import java.util.Optional;
+
+/**
+ * Instances of this class are not required to be thread safe, but are generally lightweight and can
+ * thus be created as needed.
+ */
+public abstract class MutatorFactory {
+ public final boolean canMutate(AnnotatedType type) {
+ return tryCreate(type).isPresent();
+ }
+
+ public final <T> SerializingMutator<T> createOrThrow(Class<T> clazz) {
+ return (SerializingMutator<T>) createOrThrow(asAnnotatedType(clazz));
+ }
+
+ public final SerializingMutator<?> createOrThrow(AnnotatedType type) {
+ Optional<SerializingMutator<?>> maybeMutator = tryCreate(type);
+ require(maybeMutator.isPresent(), "Failed to create mutator for " + type);
+ return maybeMutator.get();
+ }
+
+ public final SerializingInPlaceMutator<?> createInPlaceOrThrow(AnnotatedType type) {
+ Optional<SerializingInPlaceMutator<?>> maybeMutator = tryCreateInPlace(type);
+ require(maybeMutator.isPresent(), "Failed to create mutator for " + type);
+ return maybeMutator.get();
+ }
+
+ /**
+ * Tries to create a mutator for {@code type} and, if successful, asserts that it is an instance
+ * of {@link SerializingInPlaceMutator}.
+ */
+ public final Optional<SerializingInPlaceMutator<?>> tryCreateInPlace(AnnotatedType type) {
+ return tryCreate(type).map(mutator -> {
+ require(mutator instanceof InPlaceMutator<?>,
+ format("Mutator for %s is not in-place: %s", type, mutator.getClass()));
+ return (SerializingInPlaceMutator<?>) mutator;
+ });
+ }
+
+ @CheckReturnValue
+ public final Optional<SerializingMutator<?>> tryCreate(AnnotatedType type) {
+ return tryCreate(type, this);
+ }
+
+ /**
+ * Attempt to create a {@link SerializingMutator} for the given type.
+ *
+ * @param type the type to mutate
+ * @param factory the factory to use when creating submutators
+ * @return a {@link SerializingMutator} for the given {@code type}, or {@link Optional#empty()}
+ * if this factory can't create such mutators
+ */
+ @CheckReturnValue
+ public abstract Optional<SerializingMutator<?>> tryCreate(
+ AnnotatedType type, MutatorFactory factory);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/PseudoRandom.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/PseudoRandom.java
new file mode 100644
index 00000000..3755a7b1
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/PseudoRandom.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.api;
+
+import com.google.errorprone.annotations.DoNotMock;
+import java.util.List;
+import java.util.function.Supplier;
+
+@DoNotMock("Use TestSupport#mockPseudoRandom instead")
+public interface PseudoRandom {
+ /**
+ * @return a uniformly random {@code boolean}
+ */
+ boolean choice();
+
+ /**
+ * @return a {@code boolean} that is {@code true} with probability {@code 1/inverseFrequencyTrue}
+ */
+ boolean trueInOneOutOf(int inverseFrequencyTrue);
+
+ /**
+ * @throws IllegalArgumentException if {@code array.length == 0}
+ * @return an element from the given array at uniformly random index
+ */
+ <T> T pickIn(T[] array);
+
+ /**
+ * @throws IllegalArgumentException if {@code array.length == 0}
+ * @return an element from the given List at uniformly random index
+ */
+ <T> T pickIn(List<T> array);
+
+ /**
+ * @throws IllegalArgumentException if {@code array.length == 0}
+ * @return a uniformly random index valid for the given array
+ */
+ <T> int indexIn(T[] array);
+
+ /**
+ * @throws IllegalArgumentException if {@code list.size() == 0}
+ * @return a uniformly random index valid for the given list
+ */
+ <T> int indexIn(List<T> list);
+
+ /**
+ * Prefer {@link #indexIn(Object[])} and {@link #indexIn(List)}.
+ *
+ * @throws IllegalArgumentException if {@code range < 1}
+ * @return a uniformly random index in the range {@code [0, range-1]}
+ */
+ int indexIn(int range);
+
+ /**
+ * @throws IllegalArgumentException if {@code array.length < 2}
+ * @return a uniformly random index valid for the given array and different from
+ * {@code currentIndex}
+ */
+ <T> int otherIndexIn(T[] array, int currentIndex);
+
+ /**
+ * @throws IllegalArgumentException if {@code length < 2}
+ * @return a uniformly random {@code int} in the closed range {@code [0, length)} that is
+ * different from {@code currentIndex}
+ */
+ int otherIndexIn(int range, int currentIndex);
+
+ /**
+ * @return a uniformly random {@code int} in the closed range
+ * {@code [lowerInclusive, upperInclusive]}.
+ */
+ int closedRange(int lowerInclusive, int upperInclusive);
+
+ /**
+ * @return a uniformly random {@code long} in the closed range
+ * {@code [lowerInclusive, upperInclusive]}.
+ */
+ long closedRange(long lowerInclusive, long upperInclusive);
+
+ /**
+ * @return a uniformly random {@code float} in the closed range
+ * {@code [lowerInclusive, upperInclusive]}.
+ */
+ float closedRange(float lowerInclusive, float upperInclusive);
+
+ /**
+ * @return a uniformly random {@code double} in the closed range
+ * {@code [lowerInclusive, upperInclusive]}.
+ */
+ double closedRange(double lowerInclusive, double upperInclusive);
+
+ /**
+ * @return a random value in the closed range [0, upperInclusive] that is heavily biased towards
+ * being small
+ */
+ int closedRangeBiasedTowardsSmall(int upperInclusive);
+
+ /**
+ * @return a random value in the closed range [lowerInclusive, upperInclusive] that is heavily
+ * biased towards being small
+ */
+ int closedRangeBiasedTowardsSmall(int lowerInclusive, int upperInclusive);
+
+ /**
+ * Fills the given array with random bytes.
+ */
+ void bytes(byte[] bytes);
+
+ /**
+ * Use the given supplier to produce a value with probability {@code 1/inverseSupplierFrequency},
+ * otherwise randomly return one of the given values.
+ *
+ * @return value produced by the supplier or one of the given values
+ */
+ <T> T pickValue(T value, T otherValue, Supplier<T> supplier, int inverseSupplierFrequency);
+
+ /**
+ * Returns a pseudorandom {@code long} value.
+ *
+ * @return a pseudorandom {@code long} value
+ */
+ long nextLong();
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/Serializer.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/Serializer.java
new file mode 100644
index 00000000..b948b177
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/Serializer.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.api;
+
+import static com.code_intelligence.jazzer.mutation.support.InputStreamSupport.extendWithZeros;
+
+import com.google.errorprone.annotations.CheckReturnValue;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Serializes and deserializes values of type {@code T>} to and from (in-memory or on disk) corpus
+ * entries.
+ *
+ * <p>Binary representations must by default be self-delimiting. For variable-length types, the
+ * {@link #readExclusive(InputStream)} and {@link #writeExclusive(Object, OutputStream)} methods can
+ * optionally be overriden to implement more compact representations that align with existing binary
+ * corpus entries. For example, a {@code Serializer<byte[]>} could implement these optional methods
+ * to read and write the raw bytes without preceding length information whenever it is used in an
+ * already delimited context.
+ */
+public interface Serializer<T> extends Detacher<T> {
+ /**
+ * Reads a {@code T} from an endless stream that is eventually 0.
+ *
+ * <p>Implementations
+ * <ul>
+ * <li>MUST not attempt to consume the entire stream;
+ * <li>MUST return a valid {@code T} and not throw for any (even garbage) stream;
+ * <li>SHOULD short-circuit the creation of nested structures upon reading null bytes.
+ * </ul>
+ *
+ * @param in an endless stream that eventually only reads null bytes
+ * @return a {@code T} constructed from the bytes read
+ * @throws IOException declared, but must not be thrown by implementations unless methods called
+ * on {@code in} do
+ */
+ @CheckReturnValue T read(DataInputStream in) throws IOException;
+
+ /**
+ * Writes a {@code T} to a stream in such a way that an equal object can be recovered from the
+ * written bytes via {@link #read(DataInputStream)}.
+ *
+ * <p>Since {@link #read(DataInputStream)} is called with an endless stream, the binary
+ * representation MUST be self-delimiting. For example, when writing out a list, first write its
+ * length.
+ *
+ * @param value the value to write
+ * @param out the stream to write to
+ * @throws IOException declared, but must not be thrown by implementations unless methods called
+ * on {@code out} do
+ */
+ void write(T value, DataOutputStream out) throws IOException;
+
+ /**
+ * Reads a {@code T} from a finite stream, potentially using a simpler representation than that
+ * read by {@link #read(DataInputStream)}.
+ *
+ * <p>The default implementations call extends the stream with null bytes and then calls
+ * {@link #read(DataInputStream)}.
+ *
+ * <p>Implementations
+ * <ul>
+ * <li>MUST return a valid {@code T} and not throw for any (even garbage) stream;
+ * <li>SHOULD short-circuit the creation of nested structures upon reading null bytes;
+ * <li>SHOULD naturally consume the entire stream.
+ * </ul>
+ *
+ * @param in a finite stream
+ * @return a {@code T} constructed from the bytes read
+ * @throws IOException declared, but must not be thrown by implementations unless methods called
+ * on {@code in} do
+ */
+ @CheckReturnValue
+ default T readExclusive(InputStream in) throws IOException {
+ return read(new DataInputStream(extendWithZeros(in)));
+ }
+
+ /**
+ * Writes a {@code T} to a stream in such a way that an equal object can be recovered from the
+ * written bytes via {@link #readExclusive(InputStream)}.
+ *
+ * <p>The default implementations calls through to {@link #read(DataInputStream)} and should only
+ * be overriden if {@link #readExclusive(InputStream)} is.
+ *
+ * <p>As opposed to {@link #read(DataInputStream)}, {@link #readExclusive(InputStream)} is called
+ * with a finite stream. The binary representation of a {@code T} value thus does not have to be
+ * self-delimiting, which can allow for simpler representations. For example, a {@code byte[]} can
+ * be written to the stream without prepending its length.
+ *
+ * @param value the value to write
+ * @param out the stream to write to
+ * @throws IOException declared, but must not be thrown by implementations unless methods called
+ * on {@code out} do
+ */
+ default void writeExclusive(T value, OutputStream out) throws IOException {
+ write(value, new DataOutputStream(out));
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingInPlaceMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingInPlaceMutator.java
new file mode 100644
index 00000000..b7bc4d4c
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingInPlaceMutator.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.api;
+
+import static com.code_intelligence.jazzer.mutation.support.ExceptionSupport.asUnchecked;
+
+import com.google.errorprone.annotations.ForOverride;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Combines an {@link InPlaceMutator} with a {@link Serializer} for objects of type {@code T}.
+ *
+ * <p>If {@code T} can't be mutated in place, implement {@link SerializingMutator} instead.
+ *
+ * <p>Implementing classes SHOULD be declared final.
+ */
+public abstract class SerializingInPlaceMutator<T>
+ extends SerializingMutator<T> implements InPlaceMutator<T> {
+ // ByteArrayInputStream#close is documented as being a no-op, so it is safe to reuse an instance
+ // here.
+ // TODO: Introduce a dedicated empty InputStream implementation.
+ private static final InputStream emptyInputStream = new ByteArrayInputStream(new byte[0]);
+
+ /**
+ * Constructs a default instance of {@code T}.
+ *
+ * <p>The returned value is immediately passed to {@link #initInPlace(Object, PseudoRandom)}.
+ *
+ * <p>Implementing classes SHOULD provide a more efficient implementation.
+ *
+ * @return a default instance of {@code T}
+ */
+ @ForOverride
+ protected T makeDefaultInstance() {
+ try {
+ return readExclusive(emptyInputStream);
+ } catch (IOException e) {
+ throw asUnchecked(e);
+ }
+ }
+
+ @Override
+ public final T init(PseudoRandom prng) {
+ T value = makeDefaultInstance();
+ initInPlace(value, prng);
+ return value;
+ }
+
+ @Override
+ public final T mutate(T value, PseudoRandom prng) {
+ mutateInPlace(value, prng);
+ return value;
+ }
+
+ @Override
+ public final T crossOver(T value, T otherValue, PseudoRandom prng) {
+ crossOverInPlace(value, otherValue, prng);
+ return value;
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingMutator.java
new file mode 100644
index 00000000..58b2a49b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingMutator.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.api;
+
+import com.google.errorprone.annotations.DoNotMock;
+
+/**
+ * Combines a {@link ValueMutator} with a {@link Serializer} for objects of type {@code T}.
+ *
+ * <p>Implementing classes SHOULD be declared final.
+ *
+ * <p>This is the default fully-featured mutator type. If {@code T} can be mutated fully in place,
+ * consider implementing the more versatile {@link SerializingInPlaceMutator} instead.
+ */
+@DoNotMock("Use TestSupport#mockMutator instead")
+public abstract class SerializingMutator<T> implements Serializer<T>, ValueMutator<T> {
+ @Override
+ public final String toString() {
+ return Debuggable.getDebugString(this);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/ValueMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/ValueMutator.java
new file mode 100644
index 00000000..aa2b551e
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/ValueMutator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.api;
+
+import com.google.errorprone.annotations.CheckReturnValue;
+
+/**
+ * Knows how to initialize and mutate objects of type {@code T} and how to incorporate
+ * (cross over) parts of another object of the same type.
+ *
+ * <p>Certain types can be mutated fully in place. In such cases, prefer implementing the more
+ * versatile {@link InPlaceMutator} instead.
+ *
+ * <p>Implementations
+ * <ul>
+ * <li>MAY weakly associate mutable state with the identity (not equality class) of objects they
+ * have been passed as arguments or returned from initialization functions;
+ * <li>MAY assume that they are only passed arguments that they have initialized or mutated;</li>
+ * <li>SHOULD use {@link com.code_intelligence.jazzer.mutation.support.WeakIdentityHashMap} for
+ * this purpose;
+ * <li>MUST otherwise be deeply immutable;
+ * <li>SHOULD override {@link Object#toString()} to return {@code
+ * Debuggable.getDebugString(this)}.
+ * </ul>
+ *
+ * @param <T> the type this mutator operates on
+ */
+public interface ValueMutator<T> extends Debuggable {
+ /**
+ * Implementations
+ * <ul>
+ * <li>SHOULD, when called repeatedly, return a low amount of duplicates.
+ * </ul>
+ *
+ * @return an instance of {@code T}
+ */
+ @CheckReturnValue T init(PseudoRandom prng);
+
+ /**
+ * Implementations
+ * <ul>
+ * <li>MUST return a value that does not {@link Object#equals(Object)} the argument (if
+ * possible);
+ * <li>SHOULD, when called repeatedly, be able to eventually return any valid value of
+ * type {@code T};
+ * <li>MAY mutate the argument.
+ * </ul>
+ */
+ @CheckReturnValue T mutate(T value, PseudoRandom prng);
+
+ /**
+ * Implementations
+ * <ul>
+ * <li>MUST return a value that does not {@link Object#equals(Object)} the arguments (if
+ * possible);
+ * <li>MAY mutate {@code value}.
+ * <li>MUST NOT mutate {@code otherValue}.
+ * </ul>
+ */
+ @CheckReturnValue T crossOver(T value, T otherValue, PseudoRandom prng);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel
new file mode 100644
index 00000000..639a3d09
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel
@@ -0,0 +1,11 @@
+java_library(
+ name = "combinator",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/api",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/support",
+ "@com_github_jhalterman_typetools//:typetools",
+ "@com_google_errorprone_error_prone_type_annotations//jar",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java
new file mode 100644
index 00000000..18fa0288
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.combinator;
+
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.requireNonNullElements;
+import static java.util.Arrays.stream;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.InPlaceMutator;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.Serializer;
+import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.google.errorprone.annotations.ImmutableTypeParameter;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.function.ToIntFunction;
+import net.jodah.typetools.TypeResolver;
+
+public final class MutatorCombinators {
+ // Inverse frequency in which value mutator should be used in cross over.
+ private final static int INVERSE_PICK_VALUE_SUPPLIER_FREQUENCY = 100;
+
+ private MutatorCombinators() {}
+
+ public static <T, R> InPlaceMutator<T> mutateProperty(
+ Function<T, R> getter, SerializingMutator<R> mutator, BiConsumer<T, R> setter) {
+ requireNonNull(getter);
+ requireNonNull(mutator);
+ requireNonNull(setter);
+ return new InPlaceMutator<T>() {
+ @Override
+ public void initInPlace(T reference, PseudoRandom prng) {
+ setter.accept(reference, mutator.init(prng));
+ }
+
+ @Override
+ public void mutateInPlace(T reference, PseudoRandom prng) {
+ setter.accept(reference, mutator.mutate(getter.apply(reference), prng));
+ }
+
+ @Override
+ public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) {
+ // Most of the time cross over of properties should use one of the
+ // given values and only seldom use the property type specific cross
+ // over function. Other mutator combinators delegate to this one and
+ // don't cross over values themselves.
+ R referenceValue = getter.apply(reference);
+ R otherReferenceValue = getter.apply(otherReference);
+ R crossedOver = prng.pickValue(referenceValue, otherReferenceValue,
+ ()
+ -> mutator.crossOver(referenceValue, otherReferenceValue, prng),
+ INVERSE_PICK_VALUE_SUPPLIER_FREQUENCY);
+ if (crossedOver == otherReferenceValue) {
+ // If otherReference was picked, it needs to be detached as mutating
+ // it is prohibited in cross over.
+ crossedOver = mutator.detach(crossedOver);
+ }
+ setter.accept(reference, crossedOver);
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ Class<?> owningType =
+ TypeResolver.resolveRawArguments(Function.class, getter.getClass())[0];
+ return owningType.getSimpleName() + "." + mutator.toDebugString(isInCycle);
+ }
+
+ @Override
+ public String toString() {
+ return Debuggable.getDebugString(this);
+ }
+ };
+ }
+
+ public static <T, R> InPlaceMutator<T> mutateViaView(
+ Function<T, R> map, InPlaceMutator<R> mutator) {
+ requireNonNull(map);
+ requireNonNull(mutator);
+ return new InPlaceMutator<T>() {
+ @Override
+ public void initInPlace(T reference, PseudoRandom prng) {
+ mutator.initInPlace(map.apply(reference), prng);
+ }
+
+ @Override
+ public void mutateInPlace(T reference, PseudoRandom prng) {
+ mutator.mutateInPlace(map.apply(reference), prng);
+ }
+
+ @Override
+ public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) {
+ mutator.crossOverInPlace(map.apply(reference), map.apply(otherReference), prng);
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ Class<?> owningType = TypeResolver.resolveRawArguments(Function.class, map.getClass())[0];
+ return owningType.getSimpleName() + " via " + mutator.toDebugString(isInCycle);
+ }
+
+ @Override
+ public String toString() {
+ return Debuggable.getDebugString(this);
+ }
+ };
+ }
+
+ /**
+ * Combines multiple in-place mutators for different parts of a {@code T} into one that picks one
+ * at random whenever it mutates.
+ *
+ * <p>Calling this method with no arguments returns a no-op mutator that may decrease fuzzing
+ * efficiency.
+ */
+ @SafeVarargs
+ public static <T> InPlaceMutator<T> combine(InPlaceMutator<T>... partialMutators) {
+ requireNonNullElements(partialMutators);
+ if (partialMutators.length == 0) {
+ return new InPlaceMutator<T>() {
+ @Override
+ public void initInPlace(T reference, PseudoRandom prng) {}
+
+ @Override
+ public void mutateInPlace(T reference, PseudoRandom prng) {}
+
+ @Override
+ public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) {}
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return "{<empty>}";
+ }
+
+ @Override
+ public String toString() {
+ return Debuggable.getDebugString(this);
+ }
+ };
+ }
+
+ final InPlaceMutator<T>[] mutators = Arrays.copyOf(partialMutators, partialMutators.length);
+ return new InPlaceMutator<T>() {
+ @Override
+ public void initInPlace(T reference, PseudoRandom prng) {
+ for (InPlaceMutator<T> mutator : mutators) {
+ mutator.initInPlace(reference, prng);
+ }
+ }
+
+ @Override
+ public void mutateInPlace(T reference, PseudoRandom prng) {
+ mutators[prng.indexIn(mutators)].mutateInPlace(reference, prng);
+ }
+
+ @Override
+ public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) {
+ for (InPlaceMutator<T> mutator : mutators) {
+ mutator.crossOverInPlace(reference, otherReference, prng);
+ }
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return stream(mutators)
+ .map(mutator -> mutator.toDebugString(isInCycle))
+ .collect(joining(", ", "{", "}"));
+ }
+
+ @Override
+ public String toString() {
+ return Debuggable.getDebugString(this);
+ }
+ };
+ }
+
+ public static <T, R> SerializingMutator<R> mutateThenMap(
+ SerializingMutator<T> mutator, Function<T, R> map, Function<R, T> inverse) {
+ return new PostComposedMutator<T, R>(mutator, map, inverse) {};
+ }
+
+ public static <T, R> SerializingMutator<R> mutateThenMap(SerializingMutator<T> mutator,
+ Function<T, R> map, Function<R, T> inverse, Function<Predicate<Debuggable>, String> debug) {
+ return new PostComposedMutator<T, R>(mutator, map, inverse) {
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return debug.apply(isInCycle);
+ }
+ };
+ }
+
+ public static <T, @ImmutableTypeParameter R> SerializingMutator<R> mutateThenMapToImmutable(
+ SerializingMutator<T> mutator, Function<T, R> map, Function<R, T> inverse) {
+ return new PostComposedMutator<T, R>(mutator, map, inverse) {
+ @Override
+ public R detach(R value) {
+ return value;
+ }
+ };
+ }
+
+ public static <T, @ImmutableTypeParameter R> SerializingMutator<R> mutateThenMapToImmutable(
+ SerializingMutator<T> mutator, Function<T, R> map, Function<R, T> inverse,
+ Function<Predicate<Debuggable>, String> debug) {
+ return new PostComposedMutator<T, R>(mutator, map, inverse) {
+ @Override
+ public R detach(R value) {
+ return value;
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return debug.apply(isInCycle);
+ }
+ };
+ }
+
+ public static SerializingMutator<Integer> mutateIndices(int length) {
+ require(length > 1, "There should be at least two indices to choose from");
+ return new SerializingMutator<Integer>() {
+ @Override
+ public Integer read(DataInputStream in) throws IOException {
+ return Math.floorMod(in.readInt(), length);
+ }
+
+ @Override
+ public void write(Integer value, DataOutputStream out) throws IOException {
+ out.writeInt(value);
+ }
+
+ @Override
+ public Integer detach(Integer value) {
+ return value;
+ }
+
+ @Override
+ public Integer init(PseudoRandom prng) {
+ return prng.closedRange(0, length - 1);
+ }
+
+ @Override
+ public Integer mutate(Integer value, PseudoRandom prng) {
+ return prng.otherIndexIn(length, value);
+ }
+
+ @Override
+ public Integer crossOver(Integer value, Integer otherValue, PseudoRandom prng) {
+ return prng.choice() ? value : otherValue;
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return "mutateIndices(" + length + ")";
+ }
+ };
+ }
+
+ /**
+ * Combines multiple mutators for potentially different types into one that mutates an
+ * {@code Object[]} containing one instance per mutator.
+ */
+ @SuppressWarnings("rawtypes")
+ public static ProductMutator mutateProduct(SerializingMutator... mutators) {
+ return new ProductMutator(mutators);
+ }
+
+ /**
+ * Mutates a sum type (e.g. a Protobuf oneof) in place, preferring to mutate the current state
+ * but occasionally switching to a different state.
+ * @param getState a function that returns the current state of the sum type as an index into
+ * {@code perStateMutators}, or -1 if the state is indeterminate.
+ * @param perStateMutators the mutators for each state
+ * @return a mutator that mutates the sum type in place
+ */
+ @SafeVarargs
+ public static <T> InPlaceMutator<T> mutateSumInPlace(
+ ToIntFunction<T> getState, InPlaceMutator<T>... perStateMutators) {
+ final InPlaceMutator<T>[] mutators = Arrays.copyOf(perStateMutators, perStateMutators.length);
+ return new InPlaceMutator<T>() {
+ @Override
+ public void initInPlace(T reference, PseudoRandom prng) {
+ mutators[prng.indexIn(mutators)].initInPlace(reference, prng);
+ }
+
+ @Override
+ public void mutateInPlace(T reference, PseudoRandom prng) {
+ int currentState = getState.applyAsInt(reference);
+ if (currentState == -1) {
+ // The value is in an indeterminate state, initialize it.
+ initInPlace(reference, prng);
+ } else if (prng.trueInOneOutOf(100) && mutators.length > 1) {
+ // Initialize to a different state.
+ mutators[prng.otherIndexIn(mutators, currentState)].initInPlace(reference, prng);
+ } else {
+ // Mutate within the current state.
+ mutators[currentState].mutateInPlace(reference, prng);
+ }
+ }
+
+ @Override
+ public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) {
+ // Try to cross over in current state and leave state changes to the mutate step.
+ int currentState = getState.applyAsInt(reference);
+ int otherState = getState.applyAsInt(otherReference);
+ if (currentState == -1) {
+ // If reference is not initialized to a concrete state yet, try to do so in
+ // the state of other reference, as that's at least some progress.
+ if (otherState == -1) {
+ // If both states are indeterminate, cross over can not be performed.
+ return;
+ }
+ mutators[otherState].initInPlace(reference, prng);
+ } else if (currentState == otherState) {
+ mutators[currentState].crossOverInPlace(reference, otherReference, prng);
+ }
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return stream(mutators)
+ .map(mutator -> mutator.toDebugString(isInCycle))
+ .collect(joining(" | "));
+ }
+ };
+ }
+
+ /**
+ * @return a mutator that behaves identically to the provided one except that its {@link
+ * InPlaceMutator#initInPlace(Object, PseudoRandom)} is a no-op
+ */
+ public static <T> InPlaceMutator<T> withoutInit(InPlaceMutator<T> mutator) {
+ return new InPlaceMutator<T>() {
+ @Override
+ public void initInPlace(T reference, PseudoRandom prng) {
+ // Intentionally left empty.
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return "WithoutInit(" + mutator.toDebugString(isInCycle) + ")";
+ }
+
+ @Override
+ public void mutateInPlace(T reference, PseudoRandom prng) {
+ mutator.mutateInPlace(reference, prng);
+ }
+
+ @Override
+ public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) {
+ mutator.crossOverInPlace(reference, otherReference, prng);
+ }
+ };
+ }
+
+ /**
+ * Constructs a mutator that always returns the provided fixed value.
+ *
+ * <p>Note: This mutator explicitly breaks the contract of the init and mutate methods. Use
+ * sparingly as it may harm the overall effectivity of the mutator.
+ */
+ public static <@ImmutableTypeParameter T> SerializingMutator<T> fixedValue(T value) {
+ return new SerializingMutator<T>() {
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return "FixedValue(" + value + ")";
+ }
+
+ @Override
+ public T read(DataInputStream in) {
+ return value;
+ }
+
+ @Override
+ public void write(T value, DataOutputStream out) {}
+
+ @Override
+ public T detach(T value) {
+ return value;
+ }
+
+ @Override
+ public T init(PseudoRandom prng) {
+ return value;
+ }
+
+ @Override
+ public T mutate(T value, PseudoRandom prng) {
+ return value;
+ }
+
+ @Override
+ public T crossOver(T value, T otherValue, PseudoRandom prng) {
+ return value;
+ }
+ };
+ }
+
+ /**
+ * Assembles the parameters into a full implementation of {@link SerializingInPlaceMutator<T>}:
+ *
+ * @param registerSelf a callback that will receive the uninitialized mutator instance
+ * before {@code lazyMutator} is invoked. For simple cases this can
+ * just do nothing, but it is needed to implement mutators for
+ * structures that are self-referential (e.g. Protobuf message A having
+ * a field of type A).
+ * @param makeDefaultInstance constructs a mutable default instance of {@code T}
+ * @param serializer implementation of the {@link Serializer<T>} part
+ * @param lazyMutator supplies the implementation of the {@link InPlaceMutator<T>} part.
+ * This is guaranteed to be invoked exactly once and only after
+ * {@code registerSelf}.
+ */
+ public static <T> SerializingInPlaceMutator<T> assemble(
+ Consumer<SerializingInPlaceMutator<T>> registerSelf, Supplier<T> makeDefaultInstance,
+ Serializer<T> serializer, Supplier<InPlaceMutator<T>> lazyMutator) {
+ return new DelegatingSerializingInPlaceMutator<>(
+ registerSelf, makeDefaultInstance, serializer, lazyMutator);
+ }
+
+ private static class DelegatingSerializingInPlaceMutator<T> extends SerializingInPlaceMutator<T> {
+ private final Supplier<T> makeDefaultInstance;
+ private final Serializer<T> serializer;
+ private final InPlaceMutator<T> mutator;
+
+ private DelegatingSerializingInPlaceMutator(Consumer<SerializingInPlaceMutator<T>> registerSelf,
+ Supplier<T> makeDefaultInstance, Serializer<T> serializer,
+ Supplier<InPlaceMutator<T>> lazyMutator) {
+ requireNonNull(makeDefaultInstance);
+ requireNonNull(serializer);
+
+ registerSelf.accept(this);
+ this.makeDefaultInstance = makeDefaultInstance;
+ this.serializer = serializer;
+ this.mutator = lazyMutator.get();
+ }
+
+ @Override
+ public void initInPlace(T reference, PseudoRandom prng) {
+ mutator.initInPlace(reference, prng);
+ }
+
+ @Override
+ public void mutateInPlace(T reference, PseudoRandom prng) {
+ mutator.mutateInPlace(reference, prng);
+ }
+
+ @Override
+ public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) {
+ mutator.crossOverInPlace(reference, otherReference, prng);
+ }
+
+ @Override
+ protected T makeDefaultInstance() {
+ return makeDefaultInstance.get();
+ }
+
+ @Override
+ public T read(DataInputStream in) throws IOException {
+ return serializer.read(in);
+ }
+
+ @Override
+ public void write(T value, DataOutputStream out) throws IOException {
+ serializer.write(value, out);
+ }
+
+ @Override
+ public T readExclusive(InputStream in) throws IOException {
+ return serializer.readExclusive(in);
+ }
+
+ @Override
+ public void writeExclusive(T value, OutputStream out) throws IOException {
+ serializer.writeExclusive(value, out);
+ }
+
+ @Override
+ public T detach(T value) {
+ return serializer.detach(value);
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ if (isInCycle.test(this)) {
+ return "(cycle)";
+ } else {
+ return mutator.toDebugString(isInCycle);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/PostComposedMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/PostComposedMutator.java
new file mode 100644
index 00000000..ae8f97cc
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/PostComposedMutator.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.combinator;
+
+import static java.util.Objects.requireNonNull;
+
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import net.jodah.typetools.TypeResolver;
+
+abstract class PostComposedMutator<T, R> extends SerializingMutator<R> {
+ private final SerializingMutator<T> mutator;
+ private final Function<T, R> map;
+ private final Function<R, T> inverse;
+
+ PostComposedMutator(SerializingMutator<T> mutator, Function<T, R> map, Function<R, T> inverse) {
+ this.mutator = requireNonNull(mutator);
+ this.map = requireNonNull(map);
+ this.inverse = requireNonNull(inverse);
+ }
+
+ @Override
+ public R detach(R value) {
+ return map.apply(mutator.detach(inverse.apply(value)));
+ }
+
+ @Override
+ public final R init(PseudoRandom prng) {
+ return map.apply(mutator.init(prng));
+ }
+
+ @Override
+ public final R mutate(R value, PseudoRandom prng) {
+ return map.apply(mutator.mutate(inverse.apply(value), prng));
+ }
+
+ @Override
+ public R crossOver(R value, R otherValue, PseudoRandom prng) {
+ return map.apply(mutator.crossOver(inverse.apply(value), inverse.apply(otherValue), prng));
+ }
+
+ @Override
+ public final R read(DataInputStream in) throws IOException {
+ return map.apply(mutator.read(in));
+ }
+
+ @Override
+ public final void write(R value, DataOutputStream out) throws IOException {
+ mutator.write(inverse.apply(value), out);
+ }
+
+ @Override
+ public final R readExclusive(InputStream in) throws IOException {
+ return map.apply(mutator.readExclusive(in));
+ }
+
+ @Override
+ public final void writeExclusive(R value, OutputStream out) throws IOException {
+ mutator.writeExclusive(inverse.apply(value), out);
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ Class<?> returnType = TypeResolver.resolveRawArguments(Function.class, map.getClass())[1];
+ return mutator.toDebugString(isInCycle) + " -> " + returnType.getSimpleName();
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/ProductMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/ProductMutator.java
new file mode 100644
index 00000000..9057fd35
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/ProductMutator.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.combinator;
+
+import static com.code_intelligence.jazzer.mutation.support.InputStreamSupport.extendWithZeros;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.requireNonNullElements;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.joining;
+
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.function.Predicate;
+
+@SuppressWarnings({"unchecked", "rawtypes"})
+public final class ProductMutator extends SerializingInPlaceMutator<Object[]> {
+ // Inverse frequency in which product type mutators should be used in cross over.
+ private final static int INVERSE_PICK_VALUE_SUPPLIER_FREQUENCY = 100;
+
+ private final SerializingMutator[] mutators;
+
+ ProductMutator(SerializingMutator[] mutators) {
+ requireNonNullElements(mutators);
+ require(mutators.length > 0, "mutators must not be empty");
+ this.mutators = Arrays.copyOf(mutators, mutators.length);
+ }
+
+ @Override
+ public Object[] read(DataInputStream in) throws IOException {
+ Object[] value = new Object[mutators.length];
+ for (int i = 0; i < mutators.length; i++) {
+ value[i] = mutators[i].read(in);
+ }
+ return value;
+ }
+
+ @Override
+ public Object[] readExclusive(InputStream in) throws IOException {
+ Object[] value = new Object[mutators.length];
+ int lastIndex = mutators.length - 1;
+ DataInputStream endlessData = new DataInputStream(extendWithZeros(in));
+ for (int i = 0; i < lastIndex; i++) {
+ value[i] = mutators[i].read(endlessData);
+ }
+ value[lastIndex] = mutators[lastIndex].readExclusive(in);
+ return value;
+ }
+
+ @Override
+ public void write(Object[] value, DataOutputStream out) throws IOException {
+ for (int i = 0; i < mutators.length; i++) {
+ mutators[i].write(value[i], out);
+ }
+ }
+
+ @Override
+ public void writeExclusive(Object[] value, OutputStream out) throws IOException {
+ DataOutputStream dataOut = new DataOutputStream(out);
+ int lastIndex = mutators.length - 1;
+ for (int i = 0; i < lastIndex; i++) {
+ mutators[i].write(value[i], dataOut);
+ }
+ mutators[lastIndex].writeExclusive(value[lastIndex], out);
+ }
+
+ @Override
+ protected Object[] makeDefaultInstance() {
+ return new Object[mutators.length];
+ }
+
+ @Override
+ public void initInPlace(Object[] reference, PseudoRandom prng) {
+ for (int i = 0; i < mutators.length; i++) {
+ reference[i] = mutators[i].init(prng);
+ }
+ }
+
+ @Override
+ public void mutateInPlace(Object[] reference, PseudoRandom prng) {
+ int i = prng.indexIn(mutators);
+ reference[i] = mutators[i].mutate(reference[i], prng);
+ }
+
+ @Override
+ public void crossOverInPlace(Object[] reference, Object[] otherReference, PseudoRandom prng) {
+ for (int i = 0; i < mutators.length; i++) {
+ SerializingMutator mutator = mutators[i];
+ Object value = reference[i];
+ Object otherValue = otherReference[i];
+ Object crossedOver = prng.pickValue(value, otherValue,
+ () -> mutator.crossOver(value, otherValue, prng), INVERSE_PICK_VALUE_SUPPLIER_FREQUENCY);
+ if (crossedOver == otherReference) {
+ // If otherReference was picked, it needs to be detached as mutating
+ // it is prohibited in cross over.
+ crossedOver = mutator.detach(crossedOver);
+ }
+ reference[i] = crossedOver;
+ }
+ }
+
+ @Override
+ public Object[] detach(Object[] value) {
+ Object[] clone = new Object[mutators.length];
+ for (int i = 0; i < mutators.length; i++) {
+ clone[i] = mutators[i].detach(value[i]);
+ }
+ return clone;
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return stream(mutators)
+ .map(mutator -> mutator.toDebugString(isInCycle))
+ .collect(joining(", ", "[", "]"));
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/engine/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/engine/BUILD.bazel
new file mode 100644
index 00000000..50bc180a
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/engine/BUILD.bazel
@@ -0,0 +1,12 @@
+java_library(
+ name = "engine",
+ srcs = glob(["*.java"]),
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation:__pkg__",
+ "//src/test/java/com/code_intelligence/jazzer/mutation:__subpackages__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/api",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/support",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/engine/SeededPseudoRandom.java b/src/main/java/com/code_intelligence/jazzer/mutation/engine/SeededPseudoRandom.java
new file mode 100644
index 00000000..515f345b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/engine/SeededPseudoRandom.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.engine;
+
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.support.Preconditions;
+import com.code_intelligence.jazzer.mutation.support.RandomSupport;
+import java.util.List;
+import java.util.SplittableRandom;
+import java.util.function.Supplier;
+
+public final class SeededPseudoRandom implements PseudoRandom {
+ // We use SplittableRandom instead of Random since it doesn't incur unnecessary synchronization
+ // overhead and uses a much better RNG under the hood that can generate all long values.
+ private final SplittableRandom random;
+
+ public SeededPseudoRandom(long seed) {
+ this.random = new SplittableRandom(seed);
+ }
+
+ @Override
+ public boolean choice() {
+ return random.nextBoolean();
+ }
+
+ @Override
+ public boolean trueInOneOutOf(int inverseFrequencyTrue) {
+ // Ensure that the outcome of the choice isn't fixed.
+ require(inverseFrequencyTrue >= 2);
+ return indexIn(inverseFrequencyTrue) == 0;
+ }
+
+ @Override
+ public <T> T pickIn(T[] array) {
+ return array[indexIn(array.length)];
+ }
+
+ @Override
+ public <T> T pickIn(List<T> list) {
+ return list.get(indexIn(list.size()));
+ }
+
+ @Override
+ public <T> int indexIn(T[] array) {
+ return indexIn(array.length);
+ }
+
+ @Override
+ public <T> int indexIn(List<T> list) {
+ return indexIn(list.size());
+ }
+
+ @Override
+ public int indexIn(int range) {
+ require(range >= 1);
+ // TODO: Replace random.nextInt(length) with the fast version of
+ // https://lemire.me/blog/2016/06/30/fast-random-shuffling/, which avoids a modulo operation.
+ // It's slightly more biased for large bounds, but indices and choices tend to be small and
+ // are generated frequently (e.g. when picking a submutator).
+ return random.nextInt(range);
+ }
+
+ @Override
+ public <T> int otherIndexIn(T[] array, int currentIndex) {
+ return otherIndexIn(array.length, currentIndex);
+ }
+
+ @Override
+ public int otherIndexIn(int range, int currentIndex) {
+ int otherIndex = currentIndex + closedRange(1, range - 1);
+ if (otherIndex < range) {
+ return otherIndex;
+ } else {
+ return otherIndex - range;
+ }
+ }
+
+ @Override
+ public int closedRange(int lowerInclusive, int upperInclusive) {
+ require(lowerInclusive <= upperInclusive);
+ int range = upperInclusive - lowerInclusive + 1;
+ if (range > 0) {
+ return lowerInclusive + random.nextInt(range);
+ } else {
+ // The interval [lowerInclusive, upperInclusive] covers at least half of the
+ // [Integer.MIN_VALUE, Integer.MAX_VALUE] range, fall back to rejection sampling with an
+ // expected number of samples <= 2.
+ int r;
+ do {
+ r = random.nextInt();
+ } while (r < lowerInclusive);
+ return r;
+ }
+ }
+
+ @Override
+ public long closedRange(long lowerInclusive, long upperInclusive) {
+ require(lowerInclusive <= upperInclusive);
+ if (upperInclusive < Long.MAX_VALUE) {
+ // upperInclusive + 1 <= Long.MAX_VALUE
+ return random.nextLong(lowerInclusive, upperInclusive + 1);
+ } else if (lowerInclusive > 0) {
+ // upperInclusive + 1 - lowerInclusive <= Long.MAX_VALUE
+ return lowerInclusive + random.nextLong(upperInclusive + 1 - lowerInclusive);
+ } else {
+ // The interval [lowerInclusive, Long.MAX_VALUE] covers at least half of the
+ // [Long.MIN_VALUE, Long.MAX_VALUE] range, fall back to rejection sampling with an expected
+ // number of samples <= 2.
+ long r;
+ do {
+ r = random.nextLong();
+ } while (r < lowerInclusive);
+ return r;
+ }
+ }
+
+ // This function always returns a finite value
+ @Override
+ public float closedRange(float lowerInclusive, float upperInclusive) {
+ require(lowerInclusive <= upperInclusive);
+ if (lowerInclusive == upperInclusive) {
+ require(Double.isFinite(lowerInclusive));
+ return lowerInclusive;
+ }
+ // Special case: [Float.NEGATIVE_INFINITY, -Float.MAX_VALUE]
+ if (lowerInclusive == Float.NEGATIVE_INFINITY && upperInclusive == -Float.MAX_VALUE)
+ return -Float.MAX_VALUE;
+ // Special case: [Float.MAX_VALUE, Float.POSITIVE_INFINITY]
+ if (lowerInclusive == Float.MAX_VALUE && upperInclusive == Float.POSITIVE_INFINITY)
+ return Float.MAX_VALUE;
+ float limitedLower =
+ lowerInclusive == Float.NEGATIVE_INFINITY ? -Float.MAX_VALUE : lowerInclusive;
+ float limitedUpper =
+ upperInclusive == Float.POSITIVE_INFINITY ? Float.MAX_VALUE : upperInclusive;
+
+ // nextDouble(start, bound) is exclusive of bound, so we use Math.nextUp to extend the bound to
+ // the next representable double. The maximal possible range of a float is always finite when
+ // represented as a double. Therefore, we can safely use nextDouble and convert it to a float.
+ return (float) random.nextDouble((double) limitedLower, Math.nextUp((double) limitedUpper));
+ }
+
+ // This function always returns a finite value
+ @Override
+ public double closedRange(double lowerInclusive, double upperInclusive) {
+ require(lowerInclusive <= upperInclusive);
+ if (lowerInclusive == upperInclusive) {
+ require(Double.isFinite(lowerInclusive));
+ return lowerInclusive;
+ }
+ // Special case: [Double.NEGATIVE_INFINITY, -Double.MAX_VALUE]
+ if (lowerInclusive == Double.NEGATIVE_INFINITY && upperInclusive == -Double.MAX_VALUE)
+ return -Double.MAX_VALUE;
+ // Special case: [Double.MAX_VALUE, Double.POSITIVE_INFINITY)
+ if (lowerInclusive == Double.MAX_VALUE && upperInclusive == Double.POSITIVE_INFINITY)
+ return Double.MAX_VALUE;
+
+ // nextDouble(start, bound) cannot deal with infinite values, so we need to limit them
+ double limitedLower =
+ lowerInclusive == Double.NEGATIVE_INFINITY ? -Double.MAX_VALUE : lowerInclusive;
+ double limitedUpper =
+ upperInclusive == Double.POSITIVE_INFINITY ? Double.MAX_VALUE : upperInclusive;
+
+ // After limiting, the range may contain only a single value: return that
+ if (limitedLower == limitedUpper)
+ return limitedLower;
+
+ // random.nextDouble() is exclusive of the upper bound. To include the upper bound,
+ // we extend the bound to the next double value by using Math.nextUp(limitedUpper).
+ double nextUpper =
+ (limitedUpper == Double.MAX_VALUE) ? limitedUpper : Math.nextUp(limitedUpper);
+
+ // This, however, leads to a problem when the upper bound is Double.MAX_VALUE, because the next
+ // double after that is Double.POSITIVE_INFINITY. This case is treated the same as infinite
+ // range case, in the else branch.
+ boolean couldExtendRange = nextUpper != limitedUpper;
+
+ // nextDouble(start, bound) can only deal with finite ranges
+ if (Double.isFinite(nextUpper - limitedLower) && couldExtendRange) {
+ double result = random.nextDouble(limitedLower, nextUpper);
+ // Clamp random.nextDouble() to the upper bound.
+ // This is a workaround for RandomSupport.nextDouble() that causes it to
+ // return values greater than upper bound.
+ // See https://bugs.openjdk.org/browse/JDK-8281183 for a list of affected JDK versions.
+ if (result > limitedUpper)
+ result = limitedUpper;
+ return result;
+ } else {
+ // Ranges that exceeds the maximum representable double value, or ranges that could not be
+ // extended scale a random n from range [0; 1] onto the range [limitLower, limitUpper]
+ // limitedLower * (1 - n) + limitedUpper * n - is the same as:
+ // limitedLower + (limitedUpper - limitedLower) * n
+ // limitedLower + range * n
+ double n = random.nextDouble(0.0, Math.nextUp(1.0));
+ return limitedLower * (1 - n) + limitedUpper * n;
+ }
+ }
+
+ @Override
+ public void bytes(byte[] bytes) {
+ RandomSupport.nextBytes(random, bytes);
+ }
+
+ @Override
+ public int closedRangeBiasedTowardsSmall(int upperInclusive) {
+ if (upperInclusive == 0) {
+ return 0;
+ }
+ Preconditions.require(upperInclusive > 0);
+ // Modified from (Apache-2.0)
+ // https://github.com/abseil/abseil-cpp/blob/2927340217c37328319b5869285a6dcdbc13e7a7/absl/random/zipf_distribution.h
+ // by inlining the values v = 1 and q = 2.
+ final double kd = upperInclusive;
+ final double hxm = zipf_h(kd + 0.5);
+ final double h0x5 = -1.0 / 1.5;
+ final double elogv_q = 1.0;
+ final double hx0_minus_hxm = (h0x5 - elogv_q) - hxm;
+ final double s = 0.46153846153846123;
+ double k;
+ while (true) {
+ final double v = random.nextDouble();
+ final double u = hxm + v * hx0_minus_hxm;
+ final double x = zipf_hinv(u);
+ k = Math.floor(x + 0.5);
+ if (k > kd) {
+ continue;
+ }
+ if (k - x <= s) {
+ break;
+ }
+ final double h = zipf_h(k + 0.5);
+ final double r = zipf_pow_negative_q(1.0 + k);
+ if (u >= h - r) {
+ break;
+ }
+ }
+ return (int) k;
+ }
+
+ @Override
+ public int closedRangeBiasedTowardsSmall(int lowerInclusive, int upperInclusive) {
+ return lowerInclusive + closedRangeBiasedTowardsSmall(upperInclusive - lowerInclusive);
+ }
+
+ private static double zipf_h(double x) {
+ return -1.0 / (x + 1.0);
+ }
+
+ private static double zipf_hinv(double x) {
+ return -1.0 + -1.0 / x;
+ }
+
+ private static double zipf_pow_negative_q(double x) {
+ return 1.0 / (x * x);
+ }
+
+ @Override
+ public <T> T pickValue(
+ T value, T otherValue, Supplier<T> supplier, int inverseSupplierFrequency) {
+ if (trueInOneOutOf(inverseSupplierFrequency)) {
+ return supplier.get();
+ } else if (choice()) {
+ return value;
+ } else {
+ return otherValue;
+ }
+ }
+
+ @Override
+ public long nextLong() {
+ return random.nextLong();
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/BUILD.bazel
new file mode 100644
index 00000000..b922a86a
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/BUILD.bazel
@@ -0,0 +1,14 @@
+java_library(
+ name = "mutator",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/api",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/support",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java
new file mode 100644
index 00000000..fcc4d7ea
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator;
+
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.visitAnnotatedType;
+import static java.lang.String.format;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.joining;
+
+import com.code_intelligence.jazzer.mutation.annotation.AppliesTo;
+import com.code_intelligence.jazzer.mutation.api.ChainedMutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.mutator.collection.CollectionMutators;
+import com.code_intelligence.jazzer.mutation.mutator.lang.LangMutators;
+import com.code_intelligence.jazzer.mutation.mutator.proto.ProtoMutators;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedType;
+
+public final class Mutators {
+ private Mutators() {}
+
+ public static MutatorFactory newFactory() {
+ return new ChainedMutatorFactory(
+ LangMutators.newFactory(), CollectionMutators.newFactory(), ProtoMutators.newFactory());
+ }
+
+ /**
+ * Throws an exception if any annotation on {@code type} violates the restrictions of its
+ * {@link AppliesTo} meta-annotation.
+ */
+ public static void validateAnnotationUsage(AnnotatedType type) {
+ visitAnnotatedType(type, (clazz, annotations) -> {
+ outer:
+ for (Annotation annotation : annotations) {
+ AppliesTo appliesTo = annotation.annotationType().getAnnotation(AppliesTo.class);
+ if (appliesTo == null) {
+ continue;
+ }
+ for (Class<?> allowedClass : appliesTo.value()) {
+ if (allowedClass == clazz) {
+ continue outer;
+ }
+ }
+ for (Class<?> allowedSuperClass : appliesTo.subClassesOf()) {
+ if (allowedSuperClass.isAssignableFrom(clazz)) {
+ continue outer;
+ }
+ }
+
+ String helpText = "";
+ if (appliesTo.value().length != 0) {
+ helpText = stream(appliesTo.value()).map(Class::getName).collect(joining(", "));
+ }
+ if (appliesTo.subClassesOf().length != 0) {
+ if (!helpText.isEmpty()) {
+ helpText += "as well as ";
+ }
+ helpText += "subclasses of ";
+ helpText += stream(appliesTo.subClassesOf()).map(Class::getName).collect(joining(", "));
+ }
+ // Use the simple name as our annotations live in a single package.
+ throw new IllegalArgumentException(format("%s does not apply to %s, only applies to %s",
+ annotation.annotationType().getSimpleName(), clazz.getName(), helpText));
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/BUILD.bazel
new file mode 100644
index 00000000..288b700a
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/BUILD.bazel
@@ -0,0 +1,13 @@
+java_library(
+ name = "collection",
+ srcs = glob(["*.java"]),
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/mutator:__pkg__",
+ "//src/test/java/com/code_intelligence/jazzer/mutation/mutator:__subpackages__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/api",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/support",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java
new file mode 100644
index 00000000..c124f517
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.collection;
+
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+final class ChunkCrossOvers {
+ private ChunkCrossOvers() {}
+
+ static <T> void insertChunk(List<T> list, List<T> otherList, int maxSize, PseudoRandom prng) {
+ int maxChunkSize = Math.min(maxSize - list.size(), Math.min(list.size(), otherList.size()));
+ withChunk(list, otherList, maxChunkSize, prng,
+ (fromPos, toPos, chunk) -> { list.addAll(toPos, chunk); });
+ }
+
+ static <T> void overwriteChunk(List<T> list, List<T> otherList, PseudoRandom prng) {
+ int maxChunkSize = Math.min(list.size(), otherList.size());
+ withChunkElements(list, otherList, maxChunkSize, prng, list::set);
+ }
+
+ static <T> void crossOverChunk(
+ List<T> list, List<T> otherList, SerializingMutator<T> elementMutator, PseudoRandom prng) {
+ int maxChunkSize = Math.min(list.size(), otherList.size());
+ withChunkElements(list, otherList, maxChunkSize, prng, (toPos, element) -> {
+ list.set(toPos, elementMutator.crossOver(list.get(toPos), element, prng));
+ });
+ }
+
+ @FunctionalInterface
+ private interface ChunkListOperation<T> {
+ void apply(int fromPos, int toPos, List<T> chunk);
+ }
+
+ @FunctionalInterface
+ private interface ChunkListElementOperation<T> {
+ void apply(int toPos, T chunk);
+ }
+
+ static private <T> void withChunk(List<T> list, List<T> otherList, int maxChunkSize,
+ PseudoRandom prng, ChunkListOperation<T> operation) {
+ if (maxChunkSize == 0) {
+ return;
+ }
+ int chunkSize = prng.closedRangeBiasedTowardsSmall(1, maxChunkSize);
+ int fromPos = prng.closedRange(0, otherList.size() - chunkSize);
+ int toPos = prng.closedRange(0, list.size() - chunkSize);
+ List<T> chunk = otherList.subList(fromPos, fromPos + chunkSize);
+ operation.apply(fromPos, toPos, chunk);
+ }
+
+ static private <T> void withChunkElements(List<T> list, List<T> otherList, int maxChunkSize,
+ PseudoRandom prng, ChunkListElementOperation<T> operation) {
+ withChunk(list, otherList, maxChunkSize, prng, (fromPos, toPos, chunk) -> {
+ for (int i = 0; i < chunk.size(); i++) {
+ operation.apply(toPos + i, chunk.get(i));
+ }
+ });
+ }
+
+ static <K, V> void insertChunk(
+ Map<K, V> map, Map<K, V> otherMap, int maxSize, PseudoRandom prng) {
+ int originalSize = map.size();
+ int maxChunkSize = Math.min(maxSize - originalSize, otherMap.size());
+ withChunk(map, otherMap, maxChunkSize, prng, (fromIterator, toIterator, chunkSize) -> {
+ // insertChunk only inserts new entries and does not overwrite existing
+ // ones. As skipping those entries would lead to fewer insertions than
+ // requested, loop over the rest of the map to fill the chunk.
+ while (map.size() < originalSize + chunkSize && fromIterator.hasNext()) {
+ Entry<K, V> entry = fromIterator.next();
+ if (!map.containsKey(entry.getKey())) {
+ map.put(entry.getKey(), entry.getValue());
+ }
+ }
+ });
+ }
+
+ static <K, V> void overwriteChunk(Map<K, V> map, Map<K, V> otherMap, PseudoRandom prng) {
+ int maxChunkSize = Math.min(map.size(), otherMap.size());
+ withChunk(map, otherMap, maxChunkSize, prng, (fromIterator, toIterator, chunkSize) -> {
+ // As keys can not be overwritten, only removed and new ones added, this
+ // cross over overwrites the values. Removal of keys is handled by the
+ // removeChunk mutation. Value equality is not checked here.
+ for (int i = 0; i < chunkSize; i++) {
+ Entry<K, V> from = fromIterator.next();
+ Entry<K, V> to = toIterator.next();
+ to.setValue(from.getValue());
+ }
+ });
+ }
+
+ static <K, V> void crossOverChunk(Map<K, V> map, Map<K, V> otherMap,
+ SerializingMutator<K> keyMutator, SerializingMutator<V> valueMutator, PseudoRandom prng) {
+ if (prng.choice()) {
+ crossOverChunkKeys(map, otherMap, keyMutator, prng);
+ } else {
+ crossOverChunkValues(map, otherMap, valueMutator, prng);
+ }
+ }
+
+ private static <K, V> void crossOverChunkKeys(
+ Map<K, V> map, Map<K, V> otherMap, SerializingMutator<K> keyMutator, PseudoRandom prng) {
+ int maxChunkSize = Math.min(map.size(), otherMap.size());
+ withChunk(map, otherMap, maxChunkSize, prng, (fromIterator, toIterator, chunkSize) -> {
+ Map<K, V> entriesToAdd = new LinkedHashMap<>(chunkSize);
+ for (int i = 0; i < chunkSize; i++) {
+ Entry<K, V> to = toIterator.next();
+ Entry<K, V> from = fromIterator.next();
+
+ // The entry has to be removed from the map before the cross-over, as
+ // mutating its key could cause problems in subsequent lookups.
+ // Furthermore, no new entries may be added while using the iterator,
+ // so crossed-over keys are collected for later addition.
+ K key = to.getKey();
+ V value = to.getValue();
+ toIterator.remove();
+
+ // As cross-overs do not guarantee to mutate the given object, no
+ // checks if the crossed over key already exists in the map are
+ // performed. This potentially overwrites existing entries or
+ // generates equal keys.
+ // In case of cross over this behavior is acceptable.
+ K newKey = keyMutator.crossOver(key, from.getKey(), prng);
+
+ // Prevent null keys, as those are not allowed in some map implementations.
+ if (newKey != null) {
+ entriesToAdd.put(newKey, value);
+ }
+ }
+ map.putAll(entriesToAdd);
+ });
+ }
+
+ private static <K, V> void crossOverChunkValues(
+ Map<K, V> map, Map<K, V> otherMap, SerializingMutator<V> valueMutator, PseudoRandom prng) {
+ int maxChunkSize = Math.min(map.size(), otherMap.size());
+ withChunkElements(map, otherMap, maxChunkSize, prng, (fromEntry, toEntry) -> {
+ // As cross-overs do not guarantee to mutate the given object, no
+ // checks if a new value is produced are performed.
+ V newValue = valueMutator.crossOver(toEntry.getValue(), fromEntry.getValue(), prng);
+
+ // The cross-over could have already mutated value, but explicitly set it
+ // through the iterator to be sure.
+ toEntry.setValue(newValue);
+ });
+ }
+
+ @FunctionalInterface
+ private interface ChunkMapOperation<K, V> {
+ void apply(Iterator<Entry<K, V>> fromIterator, Iterator<Entry<K, V>> toIterator, int chunkSize);
+ }
+
+ @FunctionalInterface
+ private interface ChunkMapElementOperation<K, V> {
+ void apply(Entry<K, V> fromEntry, Entry<K, V> toEntry);
+ }
+
+ static <K, V> void withChunk(Map<K, V> map, Map<K, V> otherMap, int maxChunkSize,
+ PseudoRandom prng, ChunkMapOperation<K, V> operation) {
+ int chunkSize = prng.closedRangeBiasedTowardsSmall(1, maxChunkSize);
+ int fromChunkOffset = prng.closedRange(0, otherMap.size() - chunkSize);
+ int toChunkOffset = prng.closedRange(0, map.size() - chunkSize);
+ Iterator<Entry<K, V>> fromIterator = otherMap.entrySet().iterator();
+ for (int i = 0; i < fromChunkOffset; i++) {
+ fromIterator.next();
+ }
+ Iterator<Entry<K, V>> toIterator = map.entrySet().iterator();
+ for (int i = 0; i < toChunkOffset; i++) {
+ toIterator.next();
+ }
+ operation.apply(fromIterator, toIterator, chunkSize);
+ }
+
+ static <K, V> void withChunkElements(Map<K, V> map, Map<K, V> otherMap, int maxChunkSize,
+ PseudoRandom prng, ChunkMapElementOperation<K, V> operation) {
+ withChunk(map, otherMap, maxChunkSize, prng, (fromIterator, toIterator, chunkSize) -> {
+ for (int i = 0; i < chunkSize; i++) {
+ operation.apply(fromIterator.next(), toIterator.next());
+ }
+ });
+ }
+
+ public enum CrossOverAction {
+ INSERT_CHUNK,
+ OVERWRITE_CHUNK,
+ CROSS_OVER_CHUNK,
+ NOOP;
+
+ public static CrossOverAction pickRandomCrossOverAction(
+ Collection<?> reference, Collection<?> otherReference, int maxSize, PseudoRandom prng) {
+ List<CrossOverAction> actions = new ArrayList<>();
+ if (reference.size() < maxSize && !otherReference.isEmpty()) {
+ actions.add(INSERT_CHUNK);
+ }
+ if (!reference.isEmpty() && !otherReference.isEmpty()) {
+ actions.add(OVERWRITE_CHUNK);
+ actions.add(CROSS_OVER_CHUNK);
+ }
+ if (actions.isEmpty()) {
+ return NOOP; // prevent NPE
+ }
+ return prng.pickIn(actions);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java
new file mode 100644
index 00000000..d5260289
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.collection;
+
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.code_intelligence.jazzer.mutation.api.ValueMutator;
+import com.code_intelligence.jazzer.mutation.support.Preconditions;
+import java.util.AbstractList;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+// Based on (Apache-2.0)
+// https://github.com/google/fuzztest/blob/f81257ed70ec7b9c191b633588cb6e39c42da5e4/fuzztest/internal/domains/container_mutation_helpers.h
+@SuppressWarnings("unchecked")
+final class ChunkMutations {
+ private static final int MAX_FAILED_INSERTION_ATTEMPTS = 100;
+
+ private ChunkMutations() {}
+
+ static <T> void deleteRandomChunk(List<T> list, int minSize, PseudoRandom prng) {
+ int oldSize = list.size();
+ int minFinalSize = Math.max(minSize, oldSize / 2);
+ int chunkSize = prng.closedRangeBiasedTowardsSmall(1, oldSize - minFinalSize);
+ int chunkOffset = prng.closedRange(0, oldSize - chunkSize);
+
+ list.subList(chunkOffset, chunkOffset + chunkSize).clear();
+ }
+
+ static <T> void deleteRandomChunk(Collection<T> collection, int minSize, PseudoRandom prng) {
+ int oldSize = collection.size();
+ int minFinalSize = Math.max(minSize, oldSize / 2);
+ int chunkSize = prng.closedRangeBiasedTowardsSmall(1, oldSize - minFinalSize);
+ int chunkOffset = prng.closedRange(0, oldSize - chunkSize);
+
+ Iterator<T> it = collection.iterator();
+ for (int i = 0; i < chunkOffset; i++) {
+ it.next();
+ }
+ for (int i = chunkOffset; i < chunkOffset + chunkSize; i++) {
+ it.next();
+ it.remove();
+ }
+ }
+
+ static <T> void insertRandomChunk(
+ List<T> list, int maxSize, SerializingMutator<T> elementMutator, PseudoRandom prng) {
+ int oldSize = list.size();
+ int chunkSize = prng.closedRangeBiasedTowardsSmall(1, maxSize - oldSize);
+ int chunkOffset = prng.closedRange(0, oldSize);
+
+ T baseElement = elementMutator.init(prng);
+ T[] chunk = (T[]) new Object[chunkSize];
+ for (int i = 0; i < chunk.length; i++) {
+ chunk[i] = elementMutator.detach(baseElement);
+ }
+ // ArrayList#addAll relies on Collection#toArray, but Arrays#asList returns a List whose
+ // toArray() always makes a copy. We avoid this by using a custom list implementation.
+ list.addAll(chunkOffset, new ArraySharingList<>(chunk));
+ }
+
+ static <T> boolean insertRandomChunk(Set<T> set, Consumer<T> addIfNew, int maxSize,
+ ValueMutator<T> elementMutator, PseudoRandom prng) {
+ int oldSize = set.size();
+ int chunkSize = prng.closedRangeBiasedTowardsSmall(1, maxSize - oldSize);
+ return growBy(set, addIfNew, chunkSize, () -> elementMutator.init(prng));
+ }
+
+ static <T> void mutateRandomChunk(List<T> list, ValueMutator<T> mutator, PseudoRandom prng) {
+ int size = list.size();
+ int chunkSize = prng.closedRangeBiasedTowardsSmall(1, size);
+ int chunkOffset = prng.closedRange(0, size - chunkSize);
+
+ for (int i = chunkOffset; i < chunkOffset + chunkSize; i++) {
+ list.set(i, mutator.mutate(list.get(i), prng));
+ }
+ }
+
+ static <K, V, KW, VW> boolean mutateRandomKeysChunk(
+ Map<K, V> map, SerializingMutator<K> keyMutator, PseudoRandom prng) {
+ int originalSize = map.size();
+ int chunkSize = prng.closedRangeBiasedTowardsSmall(1, originalSize);
+ int chunkOffset = prng.closedRange(0, originalSize - chunkSize);
+
+ // To ensure that mutating keys actually results in the set of keys changing and not just their
+ // values (which is what #mutateRandomValuesChunk is for), we keep the keys to mutate in the
+ // map, try to add new keys (that are therefore distinct from the keys to mutate) and only
+ // remove the successfully mutated keys in the end.
+ ArrayDeque<KW> keysToMutate = new ArrayDeque<>(chunkSize);
+ ArrayDeque<VW> values = new ArrayDeque<>(chunkSize);
+ ArrayList<K> keysToRemove = new ArrayList<>(chunkSize);
+ Iterator<Map.Entry<K, V>> it = map.entrySet().iterator();
+ for (int i = 0; i < chunkOffset; i++) {
+ it.next();
+ }
+ for (int i = chunkOffset; i < chunkOffset + chunkSize; i++) {
+ Map.Entry<K, V> entry = it.next();
+ // ArrayDeque cannot hold null elements, which requires us to replace null with a sentinel.
+ // Also detach the key as keys may be mutable and mutation could destroy them.
+ keysToMutate.add(boxNull(keyMutator.detach(entry.getKey())));
+ values.add(boxNull(entry.getValue()));
+ keysToRemove.add(entry.getKey());
+ }
+
+ Consumer<K> addIfNew = key -> {
+ int sizeBeforeAdd = map.size();
+ map.putIfAbsent(key, unboxNull(values.peekFirst()));
+ // The mutated key was new, try to mutate and add the next in line.
+ if (map.size() > sizeBeforeAdd) {
+ keysToMutate.removeFirst();
+ values.removeFirst();
+ }
+ };
+ Supplier<K> nextCandidate = () -> {
+ // Mutate the next candidate in the queue.
+ K candidate = keyMutator.mutate(unboxNull(keysToMutate.removeFirst()), prng);
+ keysToMutate.addFirst(boxNull(candidate));
+ return candidate;
+ };
+
+ growBy(map.keySet(), addIfNew, chunkSize, nextCandidate);
+ // Remove the original keys that were successfully mutated into new keys. Since the original
+ // keys have been kept in the map up to this point, all keys added were successfully mutated to
+ // be unequal to the original keys.
+ int grownBy = map.size() - originalSize;
+ keysToRemove.stream().limit(grownBy).forEach(map::remove);
+ return grownBy > 0;
+ }
+
+ public static <K, V> void mutateRandomValuesChunk(
+ Map<K, V> map, ValueMutator<V> valueMutator, PseudoRandom prng) {
+ Collection<Map.Entry<K, V>> collection = map.entrySet();
+ int oldSize = collection.size();
+ int chunkSize = prng.closedRangeBiasedTowardsSmall(1, oldSize);
+ int chunkOffset = prng.closedRange(0, oldSize - chunkSize);
+
+ Iterator<Map.Entry<K, V>> it = collection.iterator();
+ for (int i = 0; i < chunkOffset; i++) {
+ it.next();
+ }
+ for (int i = chunkOffset; i < chunkOffset + chunkSize; i++) {
+ Entry<K, V> entry = it.next();
+ entry.setValue(valueMutator.mutate(entry.getValue(), prng));
+ }
+ }
+
+ static <T> boolean growBy(
+ Set<T> set, Consumer<T> addIfNew, int delta, Supplier<T> candidateSupplier) {
+ int oldSize = set.size();
+ Preconditions.require(delta >= 0);
+
+ final int targetSize = oldSize + delta;
+ int remainingAttempts = MAX_FAILED_INSERTION_ATTEMPTS;
+ int currentSize = set.size();
+ while (currentSize < targetSize) {
+ // If addIfNew fails, the size of set will not increase.
+ addIfNew.accept(candidateSupplier.get());
+ int newSize = set.size();
+ if (newSize == currentSize && remainingAttempts-- == 0) {
+ return false;
+ } else {
+ currentSize = newSize;
+ }
+ }
+ return true;
+ }
+
+ private static final Object BOXED_NULL = new Object();
+
+ private static <T, TW> TW boxNull(T object) {
+ return object != null ? (TW) object : (TW) BOXED_NULL;
+ }
+
+ private static <T, TW> T unboxNull(TW object) {
+ return object != BOXED_NULL ? (T) object : null;
+ }
+
+ public enum MutationAction {
+ DELETE_CHUNK,
+ INSERT_CHUNK,
+ MUTATE_CHUNK;
+
+ public static MutationAction pickRandomMutationAction(
+ Collection<?> c, int minSize, int maxSize, PseudoRandom prng) {
+ List<MutationAction> actions = new ArrayList<>();
+ if (c.size() > minSize) {
+ actions.add(DELETE_CHUNK);
+ }
+ if (c.size() < maxSize) {
+ actions.add(INSERT_CHUNK);
+ }
+ if (!c.isEmpty()) {
+ actions.add(MUTATE_CHUNK);
+ }
+ return prng.pickIn(actions);
+ }
+ }
+
+ private static final class ArraySharingList<T> extends AbstractList<T> {
+ private final T[] array;
+
+ ArraySharingList(T[] array) {
+ this.array = array;
+ }
+
+ @Override
+ public T get(int i) {
+ return array[i];
+ }
+
+ @Override
+ public int size() {
+ return array.length;
+ }
+
+ @Override
+ public Object[] toArray() {
+ return array;
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java
new file mode 100644
index 00000000..cf819f11
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.collection;
+
+import com.code_intelligence.jazzer.mutation.api.ChainedMutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+
+public final class CollectionMutators {
+ private CollectionMutators() {}
+
+ public static MutatorFactory newFactory() {
+ return new ChainedMutatorFactory(new ListMutatorFactory(), new MapMutatorFactory());
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorFactory.java
new file mode 100644
index 00000000..86ecbd4f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorFactory.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.collection;
+
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.CrossOverAction.pickRandomCrossOverAction;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.crossOverChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.insertChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.overwriteChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.MutationAction.pickRandomMutationAction;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.deleteRandomChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.insertRandomChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.mutateRandomChunk;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.parameterTypeIfParameterized;
+import static java.lang.Math.min;
+import static java.lang.String.format;
+
+import com.code_intelligence.jazzer.mutation.annotation.WithSize;
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.code_intelligence.jazzer.mutation.support.RandomSupport;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.reflect.AnnotatedType;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+final class ListMutatorFactory extends MutatorFactory {
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ Optional<WithSize> withSize = Optional.ofNullable(type.getAnnotation(WithSize.class));
+ int minSize = withSize.map(WithSize::min).orElse(ListMutator.DEFAULT_MIN_SIZE);
+ int maxSize = withSize.map(WithSize::max).orElse(ListMutator.DEFAULT_MAX_SIZE);
+ return parameterTypeIfParameterized(type, List.class)
+ .flatMap(factory::tryCreate)
+ .map(elementMutator -> new ListMutator<>(elementMutator, minSize, maxSize));
+ }
+
+ private static final class ListMutator<T> extends SerializingInPlaceMutator<List<T>> {
+ private static final int DEFAULT_MIN_SIZE = 0;
+ private static final int DEFAULT_MAX_SIZE = 1000;
+
+ private final SerializingMutator<T> elementMutator;
+ private final int minSize;
+ private final int maxSize;
+
+ ListMutator(SerializingMutator<T> elementMutator, int minSize, int maxSize) {
+ this.elementMutator = elementMutator;
+ this.minSize = minSize;
+ this.maxSize = maxSize;
+ require(maxSize >= 1, format("WithSize#max=%d needs to be greater than 0", maxSize));
+ require(minSize >= 0, format("WithSize#min=%d needs to be positive", minSize));
+ require(minSize <= maxSize,
+ format("WithSize#min=%d needs to be smaller or equal than WithSize#max=%d", minSize,
+ maxSize));
+ }
+
+ @Override
+ public List<T> read(DataInputStream in) throws IOException {
+ int size = RandomSupport.clamp(in.readInt(), minSize, maxSize);
+ ArrayList<T> list = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ list.add(elementMutator.read(in));
+ }
+ return list;
+ }
+
+ @Override
+ public void write(List<T> list, DataOutputStream out) throws IOException {
+ out.writeInt(list.size());
+ for (T element : list) {
+ elementMutator.write(element, out);
+ }
+ }
+
+ @Override
+ protected List<T> makeDefaultInstance() {
+ return new ArrayList<>(maxInitialSize());
+ }
+
+ @Override
+ public void initInPlace(List<T> list, PseudoRandom prng) {
+ int targetSize = prng.closedRange(minInitialSize(), maxInitialSize());
+ list.clear();
+ for (int i = 0; i < targetSize; i++) {
+ list.add(elementMutator.init(prng));
+ }
+ }
+
+ @Override
+ public void mutateInPlace(List<T> list, PseudoRandom prng) {
+ switch (pickRandomMutationAction(list, minSize, maxSize, prng)) {
+ case DELETE_CHUNK:
+ deleteRandomChunk(list, minSize, prng);
+ break;
+ case INSERT_CHUNK:
+ insertRandomChunk(list, maxSize, elementMutator, prng);
+ break;
+ case MUTATE_CHUNK:
+ mutateRandomChunk(list, elementMutator, prng);
+ break;
+ default:
+ throw new IllegalStateException("unsupported action");
+ }
+ }
+
+ @Override
+ public void crossOverInPlace(List<T> reference, List<T> otherReference, PseudoRandom prng) {
+ // These cross-over functions don't remove entries, that is handled by
+ // the appropriate mutations on the result.
+ switch (pickRandomCrossOverAction(reference, otherReference, maxSize, prng)) {
+ case INSERT_CHUNK:
+ insertChunk(reference, otherReference, maxSize, prng);
+ break;
+ case OVERWRITE_CHUNK:
+ overwriteChunk(reference, otherReference, prng);
+ break;
+ case CROSS_OVER_CHUNK:
+ crossOverChunk(reference, otherReference, elementMutator, prng);
+ break;
+ default:
+ // Both lists are empty or could otherwise not be crossed over.
+ }
+ }
+
+ @Override
+ public List<T> detach(List<T> value) {
+ return value.stream()
+ .map(elementMutator::detach)
+ .collect(Collectors.toCollection(() -> new ArrayList<>(value.size())));
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return "List<" + elementMutator.toDebugString(isInCycle) + ">";
+ }
+
+ private int minInitialSize() {
+ return minSize;
+ }
+
+ private int maxInitialSize() {
+ return min(maxSize, minSize + 1);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorFactory.java
new file mode 100644
index 00000000..fca8b5cb
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorFactory.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.collection;
+
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.CrossOverAction.pickRandomCrossOverAction;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.crossOverChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.insertChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.overwriteChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.MutationAction.pickRandomMutationAction;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.deleteRandomChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.growBy;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.insertRandomChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.mutateRandomKeysChunk;
+import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.mutateRandomValuesChunk;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.check;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.parameterTypesIfParameterized;
+import static java.lang.Math.min;
+import static java.lang.String.format;
+import static java.util.stream.Collectors.toMap;
+
+import com.code_intelligence.jazzer.mutation.annotation.WithSize;
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.code_intelligence.jazzer.mutation.support.RandomSupport;
+import com.code_intelligence.jazzer.mutation.support.StreamSupport;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedType;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+final class MapMutatorFactory extends MutatorFactory {
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ return parameterTypesIfParameterized(type, Map.class)
+ .map(parameterTypes
+ -> parameterTypes.stream()
+ .map(factory::tryCreate)
+ .flatMap(StreamSupport::getOrEmpty)
+ .collect(Collectors.toList()))
+ .map(elementMutators -> {
+ check(elementMutators.size() == 2);
+ int min = MapMutator.DEFAULT_MIN_SIZE;
+ int max = MapMutator.DEFAULT_MAX_SIZE;
+ for (Annotation annotation : type.getDeclaredAnnotations()) {
+ if (annotation instanceof WithSize) {
+ WithSize withSize = (WithSize) annotation;
+ min = withSize.min();
+ max = withSize.max();
+ }
+ }
+ return new MapMutator<>(elementMutators.get(0), elementMutators.get(1), min, max);
+ });
+ }
+
+ private static final class MapMutator<K, V> extends SerializingInPlaceMutator<Map<K, V>> {
+ private static final int DEFAULT_MIN_SIZE = 0;
+ private static final int DEFAULT_MAX_SIZE = 1000;
+
+ private final SerializingMutator<K> keyMutator;
+ private final SerializingMutator<V> valueMutator;
+ private final int minSize;
+ private final int maxSize;
+
+ MapMutator(SerializingMutator<K> keyMutator, SerializingMutator<V> valueMutator, int minSize,
+ int maxSize) {
+ this.keyMutator = keyMutator;
+ this.valueMutator = valueMutator;
+ this.minSize = Math.max(minSize, DEFAULT_MIN_SIZE);
+ this.maxSize = Math.min(maxSize, DEFAULT_MAX_SIZE);
+
+ require(maxSize >= 1, format("WithSize#max=%d needs to be greater than 0", maxSize));
+ // TODO: Add support for min > 0 to map. If min > 0, then #read can fail to construct
+ // sufficiently many distinct keys, but the mutation framework currently doesn't offer
+ // a way to handle this situation gracefully. It is also not clear what behavior users
+ // could reasonably expect in this situation in both regression test and fuzzing mode.
+ require(minSize == 0, "@WithSize#min != 0 is not yet supported for Map");
+ }
+
+ @Override
+ public Map<K, V> read(DataInputStream in) throws IOException {
+ int size = RandomSupport.clamp(in.readInt(), minSize, maxSize);
+ Map<K, V> map = new LinkedHashMap<>(size);
+ for (int i = 0; i < size; i++) {
+ map.put(keyMutator.read(in), valueMutator.read(in));
+ }
+ // map may have less than size entries due to the potential for duplicates, but this is fine
+ // as we currently assert that minSize == 0.
+ return map;
+ }
+
+ @Override
+ public void write(Map<K, V> map, DataOutputStream out) throws IOException {
+ out.writeInt(map.size());
+ for (Map.Entry<K, V> entry : map.entrySet()) {
+ keyMutator.write(entry.getKey(), out);
+ valueMutator.write(entry.getValue(), out);
+ }
+ }
+
+ @Override
+ protected Map<K, V> makeDefaultInstance() {
+ // Use a LinkedHashMap to ensure deterministic iteration order, which makes chunk-based
+ // mutations deterministic. The additional overhead compared to HashMap should be minimal.
+ return new LinkedHashMap<>(maxInitialSize());
+ }
+
+ @Override
+ public void initInPlace(Map<K, V> map, PseudoRandom prng) {
+ int targetSize = prng.closedRange(minInitialSize(), maxInitialSize());
+ map.clear();
+ growBy(map.keySet(),
+ key
+ -> map.putIfAbsent(key, valueMutator.init(prng)),
+ targetSize, () -> keyMutator.init(prng));
+ if (map.size() < minSize) {
+ throw new IllegalStateException(String.format(
+ "Failed to create %d distinct elements of type %s to satisfy the @WithSize#minSize constraint on Map",
+ minSize, keyMutator));
+ }
+ }
+
+ @Override
+ public void mutateInPlace(Map<K, V> map, PseudoRandom prng) {
+ switch (pickRandomMutationAction(map.keySet(), minSize, maxSize, prng)) {
+ case DELETE_CHUNK:
+ deleteRandomChunk(map.keySet(), minSize, prng);
+ break;
+ case INSERT_CHUNK:
+ insertRandomChunk(map.keySet(),
+ key -> map.putIfAbsent(key, valueMutator.init(prng)), maxSize, keyMutator, prng);
+ break;
+ case MUTATE_CHUNK:
+ if (prng.choice() || !mutateRandomKeysChunk(map, keyMutator, prng)) {
+ mutateRandomValuesChunk(map, valueMutator, prng);
+ }
+ break;
+ default:
+ throw new IllegalStateException("unsupported action");
+ }
+ }
+
+ @Override
+ public void crossOverInPlace(Map<K, V> reference, Map<K, V> otherReference, PseudoRandom prng) {
+ switch (
+ pickRandomCrossOverAction(reference.keySet(), otherReference.keySet(), maxSize, prng)) {
+ case INSERT_CHUNK:
+ insertChunk(reference, otherReference, maxSize, prng);
+ break;
+ case OVERWRITE_CHUNK:
+ overwriteChunk(reference, otherReference, prng);
+ break;
+ case CROSS_OVER_CHUNK:
+ crossOverChunk(reference, otherReference, keyMutator, valueMutator, prng);
+ break;
+ default:
+ // Both maps are empty or could otherwise not be crossed over.
+ }
+ }
+
+ @Override
+ public Map<K, V> detach(Map<K, V> value) {
+ return value.entrySet().stream().collect(toMap(entry
+ -> keyMutator.detach(entry.getKey()),
+ entry -> valueMutator.detach(entry.getValue())));
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return "Map<" + keyMutator.toDebugString(isInCycle) + ","
+ + valueMutator.toDebugString(isInCycle) + ">";
+ }
+
+ private int minInitialSize() {
+ return minSize;
+ }
+
+ private int maxInitialSize() {
+ return min(maxSize, minSize + 1);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BUILD.bazel
new file mode 100644
index 00000000..5b234cee
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BUILD.bazel
@@ -0,0 +1,16 @@
+java_library(
+ name = "lang",
+ srcs = glob(["*.java"]),
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/mutator:__pkg__",
+ "//src/test/java/com/code_intelligence/jazzer/mutation/mutator:__subpackages__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/api",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/combinator",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/support",
+ "@com_google_errorprone_error_prone_annotations//jar",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BooleanMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BooleanMutatorFactory.java
new file mode 100644
index 00000000..a7dda971
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BooleanMutatorFactory.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.lang;
+
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass;
+
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.google.errorprone.annotations.Immutable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.reflect.AnnotatedType;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+final class BooleanMutatorFactory extends MutatorFactory {
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ return findFirstParentIfClass(type, boolean.class, Boolean.class)
+ .map(parent -> BooleanMutator.INSTANCE);
+ }
+
+ @Immutable
+ private static final class BooleanMutator extends SerializingMutator<Boolean> {
+ private static final BooleanMutator INSTANCE = new BooleanMutator();
+
+ @Override
+ public Boolean read(DataInputStream in) throws IOException {
+ return in.readBoolean();
+ }
+
+ @Override
+ public void write(Boolean value, DataOutputStream out) throws IOException {
+ out.writeBoolean(value);
+ }
+
+ @Override
+ public Boolean init(PseudoRandom prng) {
+ return prng.choice();
+ }
+
+ @Override
+ public Boolean mutate(Boolean value, PseudoRandom prng) {
+ return !value;
+ }
+
+ @Override
+ public Boolean crossOver(Boolean value, Boolean otherValue, PseudoRandom prng) {
+ return prng.choice() ? value : otherValue;
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInLoop) {
+ return "Boolean";
+ }
+
+ @Override
+ public Boolean detach(Boolean value) {
+ return value;
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ByteArrayMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ByteArrayMutatorFactory.java
new file mode 100644
index 00000000..cdd0d881
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ByteArrayMutatorFactory.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.lang;
+
+import static com.code_intelligence.jazzer.mutation.support.InputStreamSupport.readAllBytes;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass;
+
+import com.code_intelligence.jazzer.mutation.annotation.WithLength;
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutator;
+import com.code_intelligence.jazzer.mutation.support.RandomSupport;
+import com.google.errorprone.annotations.Immutable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.AnnotatedType;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+final class ByteArrayMutatorFactory extends MutatorFactory {
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ Optional<WithLength> withLength = Optional.ofNullable(type.getAnnotation(WithLength.class));
+ int minLength = withLength.map(WithLength::min).orElse(ByteArrayMutator.DEFAULT_MIN_LENGTH);
+ int maxLength = withLength.map(WithLength::max).orElse(ByteArrayMutator.DEFAULT_MAX_LENGTH);
+
+ return findFirstParentIfClass(type, byte[].class)
+ .map(parent -> new ByteArrayMutator(minLength, maxLength));
+ }
+
+ @Immutable
+ private static final class ByteArrayMutator extends SerializingMutator<byte[]> {
+ private static final int DEFAULT_MIN_LENGTH = 0;
+ private static final int DEFAULT_MAX_LENGTH = 1000;
+
+ private final int minLength;
+
+ private final int maxLength;
+
+ private ByteArrayMutator(int min, int max) {
+ this.minLength = min;
+ this.maxLength = max;
+ }
+
+ @Override
+ public byte[] read(DataInputStream in) throws IOException {
+ int length = RandomSupport.clamp(in.readInt(), minLength, maxLength);
+ byte[] bytes = new byte[length];
+ in.readFully(bytes);
+ return bytes;
+ }
+
+ @Override
+ public byte[] readExclusive(InputStream in) throws IOException {
+ return readAllBytes(in);
+ }
+
+ @Override
+ public void write(byte[] value, DataOutputStream out) throws IOException {
+ out.writeInt(value.length);
+ out.write(value);
+ }
+
+ @Override
+ public void writeExclusive(byte[] value, OutputStream out) throws IOException {
+ out.write(value);
+ }
+
+ @Override
+ public byte[] detach(byte[] value) {
+ return Arrays.copyOf(value, value.length);
+ }
+
+ @Override
+ public byte[] init(PseudoRandom prng) {
+ int len = prng.closedRange(minInitialSize(), maxInitialSize());
+ byte[] bytes = new byte[len];
+ prng.bytes(bytes);
+ return bytes;
+ }
+
+ private int minInitialSize() {
+ return minLength;
+ }
+
+ private int maxInitialSize() {
+ // Allow some variation in length, but keep the initial elements well within reach of each
+ // other via a single mutation based on a Table of Recent Compares (ToRC) entry, which is
+ // currently limited to 64 bytes.
+ // Compared to List<T>, byte arrays can't result in recursive type hierarchies and thus don't
+ // to limit their expected initial size to be <= 1.
+ return Math.min(minLength + 16, maxLength);
+ }
+
+ @Override
+ public byte[] mutate(byte[] value, PseudoRandom prng) {
+ int maxLengthIncrease = maxLength - value.length;
+ byte[] mutated = LibFuzzerMutator.mutateDefault(value, maxLengthIncrease);
+ return enforceLength(mutated);
+ }
+
+ private byte[] enforceLength(byte[] mutated) {
+ // if the mutated array libfuzzer returns is too long or short, we truncate or extend it
+ // respectively. if we extend it, then copyOf will fill leftover bytes with 0
+ if (mutated.length > maxLength) {
+ return Arrays.copyOf(mutated, maxLength);
+ } else if (mutated.length < minLength) {
+ return Arrays.copyOf(mutated, minLength);
+ } else {
+ return mutated;
+ }
+ }
+
+ @Override
+ public byte[] crossOver(byte[] value, byte[] otherValue, PseudoRandom prng) {
+ // Passed in values are expected to already honor the min/max length constraints.
+ // As there does not seem to be an easy way to call libFuzzer's internal cross over
+ // algorithm, it is re-implemented in native Java. The algorithm is based on:
+ // https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/fuzzer/FuzzerMutate.cpp#L440
+ // https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/fuzzer/FuzzerCrossOver.cpp#L19
+ //
+
+ if (value.length == 0 || otherValue.length == 0) {
+ return value;
+ }
+
+ // TODO: Measure if this is fast enough.
+ byte[] out = null;
+ while (out == null) {
+ switch (prng.indexIn(3)) {
+ case 0:
+ out = intersect(value, otherValue, prng);
+ break;
+ case 1:
+ out = insertPart(value, otherValue, prng);
+ break;
+ case 2:
+ out = overwritePart(value, otherValue, prng);
+ break;
+ default:
+ throw new AssertionError("Invalid cross over function.");
+ }
+ }
+ return enforceLength(out);
+ }
+
+ private static byte[] intersect(byte[] value, byte[] otherValue, PseudoRandom prng) {
+ int maxOutSize = prng.closedRange(0, Math.min(value.length, otherValue.length));
+ byte[] out = new byte[maxOutSize];
+ int outPos = 0;
+ int valuePos = 0;
+ int otherValuePos = 0;
+ boolean usingFirstValue = true;
+ while (outPos < out.length) {
+ if (usingFirstValue && valuePos < value.length) {
+ int extraSize = rndArraycopy(value, valuePos, out, outPos, prng);
+ outPos += extraSize;
+ valuePos += extraSize;
+ } else if (!usingFirstValue && otherValuePos < otherValue.length) {
+ int extraSize = rndArraycopy(otherValue, otherValuePos, out, outPos, prng);
+ outPos += extraSize;
+ otherValuePos += extraSize;
+ }
+ usingFirstValue = !usingFirstValue;
+ }
+ return out;
+ }
+
+ private static int rndArraycopy(
+ byte[] val, int valPos, byte[] out, int outPos, PseudoRandom prng) {
+ int outSizeLeft = out.length - outPos;
+ int inSizeLeft = val.length - valPos;
+ int maxExtraSize = Math.min(outSizeLeft, inSizeLeft);
+ int extraSize = prng.closedRange(0, maxExtraSize);
+ System.arraycopy(val, valPos, out, outPos, extraSize);
+ return extraSize;
+ }
+
+ private static byte[] insertPart(byte[] value, byte[] otherValue, PseudoRandom prng) {
+ int copySize = prng.closedRange(1, otherValue.length);
+ int f = otherValue.length - copySize;
+ int fromPos = f == 0 ? 0 : prng.indexIn(f);
+ int toPos = prng.indexIn(value.length);
+ int tailSize = value.length - toPos;
+
+ byte[] out = new byte[value.length + copySize];
+ System.arraycopy(value, 0, out, 0, toPos);
+ System.arraycopy(otherValue, fromPos, out, toPos, copySize);
+ System.arraycopy(value, toPos, out, toPos + copySize, tailSize);
+ return out;
+ }
+
+ private static byte[] overwritePart(byte[] value, byte[] otherValue, PseudoRandom prng) {
+ int toPos = prng.indexIn(value.length);
+ int copySize = Math.min(prng.closedRange(1, value.length - toPos), otherValue.length);
+ int f = otherValue.length - copySize;
+ int fromPos = f == 0 ? 0 : prng.indexIn(f);
+ System.arraycopy(otherValue, fromPos, value, toPos, copySize);
+ return value;
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return "byte[]";
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/EnumMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/EnumMutatorFactory.java
new file mode 100644
index 00000000..80b9e6c9
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/EnumMutatorFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.lang;
+
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateIndices;
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMap;
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asSubclassOrEmpty;
+
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import java.lang.reflect.AnnotatedType;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+final class EnumMutatorFactory extends MutatorFactory {
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ return asSubclassOrEmpty(type, Enum.class).map(parent -> {
+ require(((Class<Enum<?>>) type.getType()).getEnumConstants().length > 1,
+ String.format(
+ "%s defines less than two enum constants and can't be mutated. Use a constant instead.",
+ parent));
+ Enum<?>[] values = ((Class<Enum<?>>) type.getType()).getEnumConstants();
+ return mutateThenMap(mutateIndices(values.length),
+ (index)
+ -> values[index],
+ Enum::ordinal, (Predicate<Debuggable> inCycle) -> "Enum<" + parent.getSimpleName() + ">");
+ });
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/FloatingPointMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/FloatingPointMutatorFactory.java
new file mode 100644
index 00000000..17890a9f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/FloatingPointMutatorFactory.java
@@ -0,0 +1,597 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.lang;
+
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static java.lang.String.format;
+
+import com.code_intelligence.jazzer.mutation.annotation.DoubleInRange;
+import com.code_intelligence.jazzer.mutation.annotation.FloatInRange;
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutator;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedType;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.DoubleFunction;
+import java.util.function.Predicate;
+import java.util.stream.DoubleStream;
+
+final class FloatingPointMutatorFactory extends MutatorFactory {
+ @SuppressWarnings("unchecked")
+ private static final DoubleFunction<Double>[] mathFunctions =
+ new DoubleFunction[] {Math::acos, Math::asin, Math::atan, Math::cbrt, Math::ceil, Math::cos,
+ Math::cosh, Math::exp, Math::expm1, Math::floor, Math::log, Math::log10, Math::log1p,
+ Math::rint, Math::sin, Math::sinh, Math::sqrt, Math::tan, Math::tanh, Math::toDegrees,
+ Math::toRadians, n -> n * 0.5, n -> n * 2.0, n -> n * 0.333333333333333, n -> n * 3.0};
+
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ if (!(type.getType() instanceof Class)) {
+ return Optional.empty();
+ }
+ Class<?> clazz = (Class<?>) type.getType();
+
+ if (clazz == float.class || clazz == Float.class) {
+ return Optional.of(
+ new FloatMutator(type, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, true));
+ } else if (clazz == double.class || clazz == Double.class) {
+ return Optional.of(
+ new DoubleMutator(type, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, true));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ static final class FloatMutator extends SerializingMutator<Float> {
+ private static final int EXPONENT_INITIAL_BIT = 23;
+ private static final int MANTISSA_MASK = 0x7fffff;
+ private static final int EXPONENT_MASK = 0xff;
+ private static final int MANTISSA_RANDOM_WALK_RANGE = 1000;
+ private static final int EXPONENT_RANDOM_WALK_RANGE = Float.MAX_EXPONENT;
+ private static final int INVERSE_FREQUENCY_SPECIAL_VALUE = 1000;
+
+ // Visible for testing.
+ final float minValue;
+ final float maxValue;
+ final boolean allowNaN;
+ private final float[] specialValues;
+
+ FloatMutator(AnnotatedType type, float defaultMinValueForType, float defaultMaxValueForType,
+ boolean defaultAllowNaN) {
+ float minValue = defaultMinValueForType;
+ float maxValue = defaultMaxValueForType;
+ boolean allowNaN = defaultAllowNaN;
+ // InRange is not repeatable, so the loop body will apply at most once.
+ for (Annotation annotation : type.getAnnotations()) {
+ if (annotation instanceof FloatInRange) {
+ FloatInRange floatInRange = (FloatInRange) annotation;
+ minValue = floatInRange.min();
+ maxValue = floatInRange.max();
+ allowNaN = floatInRange.allowNaN();
+ }
+ }
+
+ require(minValue <= maxValue,
+ format("[%f, %f] is not a valid interval: %s", minValue, maxValue, type));
+ require(minValue != maxValue,
+ format(
+ "[%f, %f] can not be mutated, use a constant instead: %s", minValue, maxValue, type));
+ this.minValue = minValue;
+ this.maxValue = maxValue;
+ this.allowNaN = allowNaN;
+ this.specialValues = collectSpecialValues(minValue, maxValue);
+ }
+
+ private float[] collectSpecialValues(float minValue, float maxValue) {
+ // stream of floats
+ List<Double> specialValues =
+ DoubleStream
+ .of(Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, 0.0f, -0.0f, Float.NaN,
+ Float.MAX_VALUE, Float.MIN_VALUE, -Float.MAX_VALUE, -Float.MIN_VALUE,
+ this.minValue, this.maxValue)
+ .filter(n -> (n >= minValue && n <= maxValue) || allowNaN && Double.isNaN(n))
+ .distinct()
+ .sorted()
+ .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
+
+ float[] specialValuesArray = new float[specialValues.size()];
+ for (int i = 0; i < specialValues.size(); i++) {
+ specialValuesArray[i] = (float) (double) specialValues.get(i);
+ }
+ return specialValuesArray;
+ }
+
+ public float mutateWithLibFuzzer(float value) {
+ return LibFuzzerMutator.mutateDefault(value, this, 0);
+ }
+
+ @Override
+ public Float init(PseudoRandom prng) {
+ if (prng.choice()) {
+ return specialValues[prng.closedRange(0, specialValues.length - 1)];
+ } else {
+ return prng.closedRange(minValue, maxValue);
+ }
+ }
+
+ @Override
+ public Float mutate(Float value, PseudoRandom prng) {
+ float result;
+ // small chance to return a special value
+ if (prng.trueInOneOutOf(INVERSE_FREQUENCY_SPECIAL_VALUE)) {
+ result = specialValues[prng.closedRange(0, specialValues.length - 1)];
+ } else {
+ switch (prng.closedRange(0, 5)) {
+ case 0:
+ result = mutateWithBitFlip(value, prng);
+ break;
+ case 1:
+ result = mutateExponent(value, prng);
+ break;
+ case 2:
+ result = mutateMantissa(value, prng);
+ break;
+ case 3:
+ result = mutateWithMathematicalFn(value, prng);
+ break;
+ case 4:
+ result = mutateWithLibFuzzer(value);
+ break;
+ case 5: // random in range cannot exceed the given bounds (and cannot be NaN)
+ result = prng.closedRange(minValue, maxValue);
+ break;
+ default:
+ throw new IllegalStateException("Unknown mutation case");
+ }
+ }
+ result = forceInRange(result, minValue, maxValue, allowNaN);
+
+ // Repeating values are not allowed.
+ if (Float.compare(result, value) == 0) {
+ if (Float.isNaN(result)) {
+ return prng.closedRange(minValue, maxValue);
+ } else { // Change the value to the neighboring float.
+ if (result > minValue && result < maxValue) {
+ return prng.choice() ? Math.nextAfter(result, Float.NEGATIVE_INFINITY)
+ : Math.nextAfter(result, Float.POSITIVE_INFINITY);
+ } else if (result > minValue) {
+ return Math.nextAfter(result, Float.NEGATIVE_INFINITY);
+ } else
+ return Math.nextAfter(result, Float.POSITIVE_INFINITY);
+ }
+ }
+
+ return result;
+ }
+
+ static float forceInRange(float value, float minValue, float maxValue, boolean allowNaN) {
+ if ((value >= minValue && value <= maxValue) || (Float.isNaN(value) && allowNaN))
+ return value;
+
+ // Clamp infinite values
+ if (value == Float.POSITIVE_INFINITY)
+ return maxValue;
+ if (value == Float.NEGATIVE_INFINITY)
+ return minValue;
+
+ // From here on limits should be finite
+ float finiteMax = Math.min(Float.MAX_VALUE, maxValue);
+ float finiteMin = Math.max(-Float.MAX_VALUE, minValue);
+
+ // If NaN was allowed, it was handled above. Replace it by the midpoint of the range.
+ if (Float.isNaN(value))
+ return finiteMin * 0.5f + finiteMax * 0.5f;
+
+ float range = finiteMax - finiteMin;
+ if (range == 0f)
+ return finiteMin;
+
+ float diff = value - finiteMin;
+
+ if (Float.isFinite(diff) && Float.isFinite(range)) {
+ return finiteMin + Math.abs(diff % range);
+ }
+
+ // diff, range, or both are infinite: divide both by 2, reduce, and multiply by 2.
+ float halfDiff = value * 0.5f - finiteMin * 0.5f;
+
+ return finiteMin + (halfDiff % (finiteMax * 0.5f - finiteMin * 0.5f)) * 2.0f;
+ }
+
+ public float mutateWithMathematicalFn(float value, PseudoRandom prng) {
+ double result = prng.pickIn(mathFunctions).apply(value);
+ return (float) result;
+ }
+
+ private float mutateWithBitFlip(float value, PseudoRandom prng) {
+ int bits = Float.floatToRawIntBits(value);
+ int bitToFlip = prng.closedRange(0, 31);
+ bits ^= 1L << bitToFlip;
+ return Float.intBitsToFloat(bits);
+ }
+
+ private float mutateExponent(float value, PseudoRandom prng) {
+ int bits = Float.floatToRawIntBits(value);
+ int exponent = ((bits >> EXPONENT_INITIAL_BIT) & EXPONENT_MASK)
+ + prng.closedRange(0, EXPONENT_RANDOM_WALK_RANGE);
+ bits = (bits & ~(EXPONENT_MASK << EXPONENT_INITIAL_BIT))
+ | ((exponent % EXPONENT_MASK) << EXPONENT_INITIAL_BIT);
+ return Float.intBitsToFloat(bits);
+ }
+
+ private float mutateMantissa(float value, PseudoRandom prng) {
+ int bits = Float.floatToRawIntBits(value);
+
+ int mantissa = bits & MANTISSA_MASK;
+ switch (prng.closedRange(0, 2)) {
+ case 0: // +
+ mantissa =
+ (mantissa + prng.closedRange(-MANTISSA_RANDOM_WALK_RANGE, MANTISSA_RANDOM_WALK_RANGE))
+ % MANTISSA_MASK;
+ break;
+ case 1: // *
+ mantissa =
+ (mantissa * prng.closedRange(-MANTISSA_RANDOM_WALK_RANGE, MANTISSA_RANDOM_WALK_RANGE))
+ % MANTISSA_MASK;
+ break;
+ case 2: // /
+ int divisor = prng.closedRange(2, MANTISSA_RANDOM_WALK_RANGE);
+ if (prng.choice()) {
+ divisor = -divisor;
+ }
+ mantissa = (mantissa / divisor);
+ break;
+ default:
+ throw new IllegalStateException("Unknown mutation case for mantissa");
+ }
+ bits = (bits & ~MANTISSA_MASK) | mantissa;
+ return Float.intBitsToFloat(bits);
+ }
+
+ @Override
+ public Float crossOver(Float value, Float otherValue, PseudoRandom prng) {
+ float result;
+ switch (prng.closedRange(0, 2)) {
+ case 0:
+ result = crossOverMean(value, otherValue);
+ break;
+ case 1:
+ result = crossOverExponent(value, otherValue);
+ break;
+ case 2:
+ result = crossOverMantissa(value, otherValue);
+ break;
+ default:
+ throw new IllegalStateException("Unknown mutation case");
+ }
+ return forceInRange(result, minValue, maxValue, allowNaN);
+ }
+
+ private float crossOverMean(float value, float otherValue) {
+ return (float) ((((double) value) + ((double) otherValue)) / 2.0);
+ }
+
+ private float crossOverExponent(float value, float otherValue) {
+ int bits = Float.floatToRawIntBits(value);
+ int otherExponent =
+ Float.floatToRawIntBits(otherValue) & (EXPONENT_MASK << EXPONENT_INITIAL_BIT);
+ int bitsWithOtherExponent = (bits & ~(EXPONENT_MASK << EXPONENT_INITIAL_BIT)) | otherExponent;
+ return Float.intBitsToFloat(bitsWithOtherExponent);
+ }
+
+ private float crossOverMantissa(float value, float otherValue) {
+ int bits = Float.floatToRawIntBits(value);
+ int otherMantissa = Float.floatToRawIntBits(otherValue) & MANTISSA_MASK;
+ int bitsWithOtherMantissa = (bits & ~MANTISSA_MASK) | otherMantissa;
+ return Float.intBitsToFloat(bitsWithOtherMantissa);
+ }
+
+ @Override
+ public Float read(DataInputStream in) throws IOException {
+ return forceInRange(in.readFloat(), minValue, maxValue, allowNaN);
+ }
+
+ @Override
+ public void write(Float value, DataOutputStream out) throws IOException {
+ out.writeFloat(value);
+ }
+
+ @Override
+ public Float detach(Float value) {
+ return value;
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return "Float";
+ }
+ }
+
+ static final class DoubleMutator extends SerializingMutator<Double> {
+ private static final long MANTISSA_RANDOM_WALK_RANGE = 1000;
+ private static final int EXPONENT_RANDOM_WALK_RANGE = Double.MAX_EXPONENT;
+ private static final int INVERSE_FREQUENCY_SPECIAL_VALUE = 1000;
+ private static final long MANTISSA_MASK = 0xfffffffffffffL;
+ private static final long EXPONENT_MASK = 0x7ffL;
+ private static final int EXPONENT_INITIAL_BIT = 52;
+
+ // Visible for testing
+ final double minValue;
+ final double maxValue;
+ final boolean allowNaN;
+ private final double[] specialValues;
+
+ DoubleMutator(AnnotatedType type, double defaultMinValueForType, double defaultMaxValueForType,
+ boolean defaultAllowNaN) {
+ double minValue = defaultMinValueForType;
+ double maxValue = defaultMaxValueForType;
+ boolean allowNaN = defaultAllowNaN;
+ // InRange is not repeatable, so the loop body will apply at most once.
+ for (Annotation annotation : type.getAnnotations()) {
+ if (annotation instanceof DoubleInRange) {
+ DoubleInRange doubleInRange = (DoubleInRange) annotation;
+ minValue = doubleInRange.min();
+ maxValue = doubleInRange.max();
+ allowNaN = doubleInRange.allowNaN();
+ }
+ }
+
+ require(!Double.isNaN(minValue) && !Double.isNaN(maxValue),
+ format("[%f, %f] is not a valid interval: %s", minValue, maxValue, type));
+ require(minValue <= maxValue,
+ format("[%f, %f] is not a valid interval: %s", minValue, maxValue, type));
+ require(minValue != maxValue,
+ format(
+ "[%f, %f] can not be mutated, use a constant instead: %s", minValue, maxValue, type));
+ this.minValue = minValue;
+ this.maxValue = maxValue;
+ this.allowNaN = allowNaN;
+ this.specialValues = collectSpecialValues(minValue, maxValue);
+ }
+
+ private double[] collectSpecialValues(double minValue, double maxValue) {
+ double[] specialValues = new double[] {Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
+ 0.0, -0.0, Double.NaN, Double.MAX_VALUE, Double.MIN_VALUE, -Double.MAX_VALUE,
+ -Double.MIN_VALUE, this.minValue, this.maxValue};
+ return Arrays.stream(specialValues)
+ .boxed()
+ .filter(value -> (allowNaN && value.isNaN()) || (value >= minValue && value <= maxValue))
+ .distinct()
+ .sorted()
+ .mapToDouble(Double::doubleValue)
+ .toArray();
+ }
+
+ public double mutateWithLibFuzzer(double value) {
+ return LibFuzzerMutator.mutateDefault(value, this, 0);
+ }
+
+ @Override
+ public Double init(PseudoRandom prng) {
+ if (prng.choice()) {
+ return specialValues[prng.closedRange(0, specialValues.length - 1)];
+ } else {
+ return prng.closedRange(minValue, maxValue);
+ }
+ }
+
+ @Override
+ public Double mutate(Double value, PseudoRandom prng) {
+ double result;
+ // small chance to return a special value
+ if (prng.trueInOneOutOf(INVERSE_FREQUENCY_SPECIAL_VALUE)) {
+ result = specialValues[prng.closedRange(0, specialValues.length - 1)];
+ } else {
+ switch (prng.closedRange(0, 5)) {
+ case 0:
+ result = mutateWithBitFlip(value, prng);
+ break;
+ case 1:
+ result = mutateExponent(value, prng);
+ break;
+ case 2:
+ result = mutateMantissa(value, prng);
+ break;
+ case 3:
+ result = mutateWithMathematicalFn(value, prng);
+ break;
+ case 4:
+ result = mutateWithLibFuzzer(value);
+ break;
+ case 5: // random in range cannot exceed the given bounds (and cannot be NaN)
+ result = prng.closedRange(minValue, maxValue);
+ break;
+ default:
+ throw new IllegalStateException("Unknown mutation case");
+ }
+ }
+ result = forceInRange(result, minValue, maxValue, allowNaN);
+
+ // Repeating values are not allowed.
+ if (Double.compare(result, value) == 0) {
+ if (Double.isNaN(result)) {
+ return prng.closedRange(minValue, maxValue);
+ } else { // Change the value to the neighboring float.
+ if (result > minValue && result < maxValue) {
+ return prng.choice() ? Math.nextAfter(result, Double.NEGATIVE_INFINITY)
+ : Math.nextAfter(result, Double.POSITIVE_INFINITY);
+ } else if (result > minValue) {
+ return Math.nextAfter(result, Double.NEGATIVE_INFINITY);
+ } else
+ return Math.nextAfter(result, Double.POSITIVE_INFINITY);
+ }
+ }
+
+ return result;
+ }
+
+ static double forceInRange(double value, double minValue, double maxValue, boolean allowNaN) {
+ if ((value >= minValue && value <= maxValue) || (Double.isNaN(value) && allowNaN)) {
+ return value;
+ }
+
+ // Clamp infinite values
+ if (value == Double.POSITIVE_INFINITY)
+ return maxValue;
+ if (value == Double.NEGATIVE_INFINITY)
+ return minValue;
+
+ // From here on limits should be finite
+ double finiteMax = Math.min(Double.MAX_VALUE, maxValue);
+ double finiteMin = Math.max(-Double.MAX_VALUE, minValue);
+
+ // If NaN was allowed, it was handled above.
+ // Here we replace NaN by the middle of the clamped finite range.
+ if (Double.isNaN(value)) {
+ // maxValue or minValue may be infinite, so we need to clamp them.
+ return minValue
+ + (Math.min(Double.MAX_VALUE, maxValue) * 0.5
+ - Math.max(-Double.MAX_VALUE, minValue) * 0.5);
+ }
+
+ double range = finiteMax - finiteMin;
+ if (range == 0)
+ return finiteMin;
+
+ double diff = value - finiteMin;
+
+ if (Double.isFinite(diff) && Double.isFinite(range)) {
+ return finiteMin + Math.abs(diff % range);
+ }
+
+ // diff, range, or both are infinite: divide both by 2, reduce, and multiply by 2.
+ double halfDiff = value * 0.5 - finiteMin * 0.5;
+ return finiteMin + (halfDiff % (finiteMax * 0.5 - finiteMin * 0.5)) * 2.0;
+ }
+
+ public double mutateWithMathematicalFn(double value, PseudoRandom prng) {
+ return prng.pickIn(mathFunctions).apply(value);
+ }
+
+ public static double mutateWithBitFlip(double value, PseudoRandom prng) {
+ long bits = Double.doubleToRawLongBits(value);
+ int bitToFlip = prng.closedRange(0, 63);
+ bits ^= 1L << bitToFlip;
+ return Double.longBitsToDouble(bits);
+ }
+
+ private static double mutateExponent(double value, PseudoRandom prng) {
+ long bits = Double.doubleToRawLongBits(value);
+ long exponent = ((bits >> EXPONENT_INITIAL_BIT) & EXPONENT_MASK)
+ + prng.closedRange(0, EXPONENT_RANDOM_WALK_RANGE);
+ bits = (bits & ~(EXPONENT_MASK << EXPONENT_INITIAL_BIT))
+ | ((exponent % EXPONENT_MASK) << EXPONENT_INITIAL_BIT);
+ return Double.longBitsToDouble(bits);
+ }
+
+ public static double mutateMantissa(double value, PseudoRandom prng) {
+ long bits = Double.doubleToRawLongBits(value);
+ long mantissa = bits & MANTISSA_MASK;
+ switch (prng.closedRange(0, 2)) {
+ case 0: // +
+ mantissa =
+ (mantissa + prng.closedRange(-MANTISSA_RANDOM_WALK_RANGE, MANTISSA_RANDOM_WALK_RANGE))
+ % MANTISSA_MASK;
+ break;
+ case 1: // *
+ mantissa =
+ (mantissa * prng.closedRange(-MANTISSA_RANDOM_WALK_RANGE, MANTISSA_RANDOM_WALK_RANGE))
+ % MANTISSA_MASK;
+ break;
+ case 2: // /
+ long divisor = prng.closedRange(2, MANTISSA_RANDOM_WALK_RANGE);
+ if (prng.choice()) {
+ divisor = -divisor;
+ }
+ mantissa = (mantissa / divisor);
+ break;
+ default:
+ throw new IllegalStateException("Unknown mutation case for mantissa");
+ }
+ bits = (bits & ~MANTISSA_MASK) | mantissa;
+ return Double.longBitsToDouble(bits);
+ }
+
+ @Override
+ public Double crossOver(Double value, Double otherValue, PseudoRandom prng) {
+ double result;
+ switch (prng.closedRange(0, 2)) {
+ case 0:
+ result = crossOverMean(value, otherValue);
+ break;
+ case 1:
+ result = crossOverExponent(value, otherValue);
+ break;
+ case 2:
+ result = crossOverMantissa(value, otherValue);
+ break;
+ default:
+ throw new IllegalStateException("Unknown mutation case");
+ }
+ return forceInRange(result, minValue, maxValue, allowNaN);
+ }
+
+ private double crossOverMean(double value, double otherValue) {
+ return (value * 0.5) + (otherValue * 0.5);
+ }
+
+ private double crossOverExponent(double value, double otherValue) {
+ long bits = Double.doubleToRawLongBits(value);
+ long otherExponent =
+ Double.doubleToRawLongBits(otherValue) & (EXPONENT_MASK << EXPONENT_INITIAL_BIT);
+ long bitsWithOtherExponent =
+ (bits & ~(EXPONENT_MASK << EXPONENT_INITIAL_BIT)) | otherExponent;
+ return Double.longBitsToDouble(bitsWithOtherExponent);
+ }
+
+ private double crossOverMantissa(double value, double otherValue) {
+ long bits = Double.doubleToRawLongBits(value);
+ long otherMantissa = Double.doubleToRawLongBits(otherValue) & MANTISSA_MASK;
+ long bitsWithOtherMantissa = (bits & ~MANTISSA_MASK) | otherMantissa;
+ return Double.longBitsToDouble(bitsWithOtherMantissa);
+ }
+
+ @Override
+ public Double read(DataInputStream in) throws IOException {
+ return forceInRange(in.readDouble(), minValue, maxValue, allowNaN);
+ }
+
+ @Override
+ public void write(Double value, DataOutputStream out) throws IOException {
+ out.writeDouble(value);
+ }
+
+ @Override
+ public Double detach(Double value) {
+ return value;
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return "Double";
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java
new file mode 100644
index 00000000..e701e6ad
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.lang;
+
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static java.lang.String.format;
+
+import com.code_intelligence.jazzer.mutation.annotation.InRange;
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutator;
+import com.google.errorprone.annotations.ForOverride;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.ParameterizedType;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.LongStream;
+
+final class IntegralMutatorFactory extends MutatorFactory {
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ if (!(type.getType() instanceof Class)) {
+ return Optional.empty();
+ }
+ Class<?> clazz = (Class<?>) type.getType();
+
+ if (clazz == byte.class || clazz == Byte.class) {
+ return Optional.of(new AbstractIntegralMutator<Byte>(type, Byte.MIN_VALUE, Byte.MAX_VALUE) {
+ @Override
+ protected long mutateWithLibFuzzer(long value) {
+ return LibFuzzerMutator.mutateDefault((byte) value, this, 0);
+ }
+
+ @Override
+ public Byte init(PseudoRandom prng) {
+ return (byte) initImpl(prng);
+ }
+
+ @Override
+ public Byte mutate(Byte value, PseudoRandom prng) {
+ return (byte) mutateImpl(value, prng);
+ }
+
+ @Override
+ public Byte crossOver(Byte value, Byte otherValue, PseudoRandom prng) {
+ return (byte) crossOverImpl(value, otherValue, prng);
+ }
+
+ @Override
+ public Byte read(DataInputStream in) throws IOException {
+ return (byte) forceInRange(in.readByte());
+ }
+
+ @Override
+ public void write(Byte value, DataOutputStream out) throws IOException {
+ out.writeByte(value);
+ }
+ });
+ } else if (clazz == short.class || clazz == Short.class) {
+ return Optional.of(
+ new AbstractIntegralMutator<Short>(type, Short.MIN_VALUE, Short.MAX_VALUE) {
+ @Override
+ protected long mutateWithLibFuzzer(long value) {
+ return LibFuzzerMutator.mutateDefault((short) value, this, 0);
+ }
+
+ @Override
+ public Short init(PseudoRandom prng) {
+ return (short) initImpl(prng);
+ }
+
+ @Override
+ public Short mutate(Short value, PseudoRandom prng) {
+ return (short) mutateImpl(value, prng);
+ }
+
+ @Override
+ public Short crossOver(Short value, Short otherValue, PseudoRandom prng) {
+ return (short) crossOverImpl(value, otherValue, prng);
+ }
+
+ @Override
+ public Short read(DataInputStream in) throws IOException {
+ return (short) forceInRange(in.readShort());
+ }
+
+ @Override
+ public void write(Short value, DataOutputStream out) throws IOException {
+ out.writeShort(value);
+ }
+ });
+ } else if (clazz == int.class || clazz == Integer.class) {
+ return Optional.of(
+ new AbstractIntegralMutator<Integer>(type, Integer.MIN_VALUE, Integer.MAX_VALUE) {
+ @Override
+ protected long mutateWithLibFuzzer(long value) {
+ return LibFuzzerMutator.mutateDefault((int) value, this, 0);
+ }
+
+ @Override
+ public Integer init(PseudoRandom prng) {
+ return (int) initImpl(prng);
+ }
+
+ @Override
+ public Integer mutate(Integer value, PseudoRandom prng) {
+ return (int) mutateImpl(value, prng);
+ }
+
+ @Override
+ public Integer crossOver(Integer value, Integer otherValue, PseudoRandom prng) {
+ return (int) crossOverImpl(value, otherValue, prng);
+ }
+
+ @Override
+ public Integer read(DataInputStream in) throws IOException {
+ return (int) forceInRange(in.readInt());
+ }
+
+ @Override
+ public void write(Integer value, DataOutputStream out) throws IOException {
+ out.writeInt(value);
+ }
+ });
+ } else if (clazz == long.class || clazz == Long.class) {
+ return Optional.of(new AbstractIntegralMutator<Long>(type, Long.MIN_VALUE, Long.MAX_VALUE) {
+ @Override
+ protected long mutateWithLibFuzzer(long value) {
+ return LibFuzzerMutator.mutateDefault(value, this, 0);
+ }
+
+ @Override
+ public Long init(PseudoRandom prng) {
+ return initImpl(prng);
+ }
+
+ @Override
+ public Long mutate(Long value, PseudoRandom prng) {
+ return mutateImpl(value, prng);
+ }
+
+ @Override
+ public Long crossOver(Long value, Long otherValue, PseudoRandom prng) {
+ return crossOverImpl(value, otherValue, prng);
+ }
+
+ @Override
+ public Long read(DataInputStream in) throws IOException {
+ return forceInRange(in.readLong());
+ }
+
+ @Override
+ public void write(Long value, DataOutputStream out) throws IOException {
+ out.writeLong(value);
+ }
+ });
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ // Based on
+ // https://github.com/google/fuzztest/blob/a663ded6c36f050fbdc634a8fc81d553068d71d7/fuzztest/internal/domain.h#L1447
+ // SPDX: Apache-2.0
+ // Copyright 2022 Google LLC
+ //
+ // Visible for testing.
+ static abstract class AbstractIntegralMutator<T extends Number> extends SerializingMutator<T> {
+ private static final long RANDOM_WALK_RANGE = 5;
+ private final long minValue;
+ private final long maxValue;
+ private final int largestMutableBitNegative;
+ private final int largestMutableBitPositive;
+ private final long[] specialValues;
+
+ AbstractIntegralMutator(
+ AnnotatedType type, long defaultMinValueForType, long defaultMaxValueForType) {
+ long minValue = defaultMinValueForType;
+ long maxValue = defaultMaxValueForType;
+ // InRange is not repeatable, so the loop body will apply exactly once.
+ for (Annotation annotation : type.getAnnotations()) {
+ if (annotation instanceof InRange) {
+ InRange inRange = (InRange) annotation;
+ // Since we use a single annotation for all integral types and its min and max fields are
+ // longs, we have to ignore them if they are at their default values.
+ //
+ // This results in a small quirk that is probably acceptable: If someone specifies
+ // @InRange(max = Long.MAX_VALUE) on a byte, we will not fail but silently use
+ // Byte.MAX_VALUE instead. IDEs will warn about the redundant specification of the default
+ // value, so this should not be a problem in practice.
+ if (inRange.min() != Long.MIN_VALUE) {
+ require(inRange.min() >= defaultMinValueForType,
+ format("@InRange.min=%d is out of range: %s", inRange.min(), type.getType()));
+ minValue = inRange.min();
+ }
+ if (inRange.max() != Long.MAX_VALUE) {
+ require(inRange.max() <= defaultMaxValueForType,
+ format("@InRange.max=%d is out of range: %s", inRange.max(), type.getType()));
+ maxValue = inRange.max();
+ }
+ }
+ }
+
+ require(minValue <= maxValue,
+ format("[%d, %d] is not a valid interval: %s", minValue, maxValue, type));
+ require(minValue != maxValue,
+ format(
+ "[%d, %d] can not be mutated, use a constant instead: %s", minValue, maxValue, type));
+ this.minValue = minValue;
+ this.maxValue = maxValue;
+ if (minValue >= 0) {
+ largestMutableBitNegative = 0;
+ largestMutableBitPositive = bitWidth(minValue ^ maxValue);
+ } else if (maxValue < 0) {
+ largestMutableBitNegative = bitWidth(minValue ^ maxValue);
+ largestMutableBitPositive = 0;
+ } else /* minValue < 0 && maxValue >= 0 */ {
+ largestMutableBitNegative = bitWidth(~minValue);
+ largestMutableBitPositive = bitWidth(maxValue);
+ }
+ this.specialValues = collectSpecialValues(minValue, maxValue);
+ }
+
+ private static long[] collectSpecialValues(long minValue, long maxValue) {
+ // Special values can collide or not apply when @InRange is used, so filter appropriately and
+ // remove duplicates - we don't want to weigh certain special values higher than others.
+ return LongStream.of(0, 1, minValue, maxValue)
+ .filter(value -> value >= minValue)
+ .filter(value -> value <= maxValue)
+ .distinct()
+ .sorted()
+ .toArray();
+ }
+
+ private static int bitWidth(long value) {
+ return 64 - Long.numberOfLeadingZeros(value);
+ }
+
+ protected final long initImpl(PseudoRandom prng) {
+ int sentinel = specialValues.length;
+ int choice = prng.closedRange(0, sentinel);
+ if (choice < sentinel) {
+ return specialValues[choice];
+ } else {
+ return prng.closedRange(minValue, maxValue);
+ }
+ }
+
+ protected final long mutateImpl(long value, PseudoRandom prng) {
+ final long previousValue = value;
+ // Mutate in a loop to verify that we really mutated.
+ do {
+ switch (prng.indexIn(4)) {
+ case 0:
+ value = bitFlip(value, prng);
+ break;
+ case 1:
+ value = randomWalk(value, prng);
+ break;
+ case 2:
+ value = prng.closedRange(minValue, maxValue);
+ break;
+ case 3:
+ // TODO: Replace this with a structure-aware dictionary/TORC search similar to fuzztest.
+ value = forceInRange(mutateWithLibFuzzer(value));
+ break;
+ }
+ } while (value == previousValue);
+ return value;
+ }
+
+ protected final long crossOverImpl(long x, long y, PseudoRandom prng) {
+ switch (prng.indexIn(3)) {
+ case 0:
+ return mean(x, y);
+ case 1:
+ return forceInRange(x ^ y);
+ case 2:
+ return bitmask(x, y, prng);
+ default:
+ throw new AssertionError("Invalid cross over function.");
+ }
+ }
+
+ private long bitmask(long x, long y, PseudoRandom prng) {
+ long mask = prng.nextLong();
+ return forceInRange((x & mask) | (y & ~mask));
+ }
+
+ private static long mean(long x, long y) {
+ // Add the common set bits (x & y) and the half of the sum of the
+ // differing bits together ((x ^ y) >> 1), the result will never exceed
+ // the sum of x and y as both parts of the calculation are guaranteed to
+ // be smaller than or equal to x and y.
+ long xor = x ^ y;
+ long mean = (x & y) + (xor >> 1);
+ // Round towards zero (add 1) if rounding is not exact (last xor bit is
+ // set) and result is negative (sign bit is set).
+ return mean + (1 & xor & (mean >>> 31));
+ }
+
+ @ForOverride protected abstract long mutateWithLibFuzzer(long value);
+
+ /**
+ * Force value into the closed interval [minValue, maxValue] while preserving as many of its
+ * bits as possible (e.g. so that mutations that apply to the raw byte representation still have
+ * a good chance to actually mutate the value). Clamping would not have this property.
+ */
+ protected final long forceInRange(long value) {
+ // Fast path for the common case.
+ if (value >= minValue && value <= maxValue) {
+ return value;
+ }
+ return forceInRange(value, minValue, maxValue);
+ }
+
+ // Visible for testing.
+ static long forceInRange(long value, long minValue, long maxValue) {
+ long range = maxValue - minValue;
+ if (range > 0) {
+ return minValue + Math.abs((value - minValue) % range);
+ } else {
+ // [minValue, maxValue] covers at least half of the [Long.MIN_VALUE, Long.MAX_VALUE] range,
+ // so if value doesn't lie in [minValue, maxValue], it will after shifting once.
+ if (value >= minValue && value <= maxValue) {
+ return value;
+ } else {
+ return value + range;
+ }
+ }
+ }
+
+ private long bitFlip(long value, PseudoRandom prng) {
+ int range = value >= 0 ? largestMutableBitPositive : largestMutableBitNegative;
+ value = value ^ (1L << prng.indexIn(range));
+ // The bit flip may violate the range constraint, if so, mutate randomly.
+ if (value > maxValue || value < minValue) {
+ value = prng.closedRange(minValue, maxValue);
+ }
+ return value;
+ }
+
+ private long randomWalk(long value, PseudoRandom prng) {
+ // Prevent overflows by averaging the individual bounds.
+ if (maxValue / 2 - minValue / 2 <= RANDOM_WALK_RANGE) {
+ value = prng.closedRange(minValue, maxValue);
+ } else {
+ // At this point we know that (using non-wrapping arithmetic):
+ // RANDOM_WALK_RANGE < maxValue/2 - minValue/2 <= Long.MAX_VALUE/2 - minValue/2, hence
+ // minValue/2 + RANDOM_WALK_RANGE < Long.MAX_VALUE/2, hence
+ // minValue + 2*RANDOM_WALK_RANGE < Long.MAX_VALUE.
+ // In particular, minValue + RANDOM_WALK_RANGE can't overflow, likewise for maxValue.
+ long lower = minValue;
+ if (value > lower + RANDOM_WALK_RANGE) {
+ lower = value - RANDOM_WALK_RANGE;
+ }
+ long upper = maxValue;
+ if (value < upper - RANDOM_WALK_RANGE) {
+ upper = value + RANDOM_WALK_RANGE;
+ }
+ value = prng.closedRange(lower, upper);
+ }
+ return value;
+ }
+
+ @Override
+ public T detach(T value) {
+ // Always immutable.
+ return value;
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return ((Class<T>) ((ParameterizedType) this.getClass().getGenericSuperclass())
+ .getActualTypeArguments()[0])
+ .getSimpleName();
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java
new file mode 100644
index 00000000..00ea8b47
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.lang;
+
+import com.code_intelligence.jazzer.mutation.api.ChainedMutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+
+public final class LangMutators {
+ private LangMutators() {}
+
+ public static MutatorFactory newFactory() {
+ return new ChainedMutatorFactory(new NullableMutatorFactory(), new BooleanMutatorFactory(),
+ new FloatingPointMutatorFactory(), new IntegralMutatorFactory(),
+ new ByteArrayMutatorFactory(), new StringMutatorFactory(), new EnumMutatorFactory());
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/NullableMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/NullableMutatorFactory.java
new file mode 100644
index 00000000..16e6a132
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/NullableMutatorFactory.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.lang;
+
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.isPrimitive;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull;
+import static java.util.Arrays.stream;
+
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedType;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+final class NullableMutatorFactory extends MutatorFactory {
+ private static boolean isNotNullAnnotation(Annotation annotation) {
+ // There are many NotNull annotations in the wild (including our own) and we want to recognize
+ // them all.
+ return annotation.annotationType().getSimpleName().equals("NotNull");
+ }
+
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ if (isPrimitive(type)
+ || stream(type.getAnnotations()).anyMatch(NullableMutatorFactory::isNotNullAnnotation)) {
+ return Optional.empty();
+ }
+ return factory.tryCreate(notNull(type), factory).map(NullableMutator::new);
+ }
+
+ private static final class NullableMutator<T> extends SerializingMutator<T> {
+ private static final int INVERSE_FREQUENCY_NULL = 100;
+
+ private final SerializingMutator<T> mutator;
+
+ NullableMutator(SerializingMutator<T> mutator) {
+ this.mutator = mutator;
+ }
+
+ @Override
+ public T read(DataInputStream in) throws IOException {
+ if (in.readBoolean()) {
+ return mutator.read(in);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void write(T value, DataOutputStream out) throws IOException {
+ out.writeBoolean(value != null);
+ if (value != null) {
+ mutator.write(value, out);
+ }
+ }
+
+ @Override
+ public T init(PseudoRandom prng) {
+ if (prng.trueInOneOutOf(INVERSE_FREQUENCY_NULL)) {
+ return null;
+ } else {
+ return mutator.init(prng);
+ }
+ }
+
+ @Override
+ public T mutate(T value, PseudoRandom prng) {
+ if (value == null) {
+ return mutator.init(prng);
+ } else if (prng.trueInOneOutOf(INVERSE_FREQUENCY_NULL)) {
+ return null;
+ } else {
+ return mutator.mutate(value, prng);
+ }
+ }
+
+ @Override
+ public T crossOver(T value, T otherValue, PseudoRandom prng) {
+ // Prefer to cross over actual values and only return null if
+ // both are null or at INVERSE_FREQUENCY_NULL probability.
+ if (value != null && otherValue != null) {
+ return mutator.crossOver(value, otherValue, prng);
+ } else if (value == null && otherValue == null) {
+ return null;
+ } else if (prng.trueInOneOutOf(INVERSE_FREQUENCY_NULL)) {
+ return null;
+ } else {
+ return value != null ? value : otherValue;
+ }
+ }
+
+ @Override
+ public T detach(T value) {
+ if (value == null) {
+ return null;
+ } else {
+ return mutator.detach(value);
+ }
+ }
+
+ @Override
+ public String toDebugString(Predicate<Debuggable> isInCycle) {
+ return "Nullable<" + mutator.toDebugString(isInCycle) + ">";
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java
new file mode 100644
index 00000000..d77cb9d3
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.lang;
+
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.*;
+
+import com.code_intelligence.jazzer.mutation.annotation.Ascii;
+import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length;
+import com.code_intelligence.jazzer.mutation.api.Debuggable;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import java.lang.reflect.AnnotatedType;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+final class StringMutatorFactory extends MutatorFactory {
+ private static final int HEADER_MASK = 0b1100_0000;
+ private static final int BODY_MASK = 0b0011_1111;
+ private static final int CONTINUATION_HEADER = 0b1000_0000;
+
+ private static final int DEFAULT_MIN_BYTES = 0;
+
+ private static final int DEFAULT_MAX_BYTES = 1000;
+
+ static void fixUpAscii(byte[] bytes) {
+ for (int i = 0; i < bytes.length; i++) {
+ bytes[i] &= 0x7F;
+ }
+ }
+
+ // Based on
+ // https://github.com/google/libprotobuf-mutator/blob/af3bb18749db3559dc4968dd85319d05168d4b5e/src/utf8_fix.cc#L32
+ // SPDX: Apache-2.0
+ // Copyright 2022 Google LLC
+ static void fixUpUtf8(byte[] bytes) {
+ for (int pos = 0; pos < bytes.length;) {
+ // Leniently read a UTF-8 code point consisting of any byte viewed as the leading byte and up
+ // to three following bytes that have a continuation byte header.
+ //
+ // Since the upper two bits of a byte are 10 with probability 25%, this roughly results in
+ // the following distribution for characters:
+ //
+ // ASCII code point: 75%
+ // two-byte UTF-8: 18.75%
+ // three-byte UTF-8: ~4.7%
+ // four-byte UTF-8: ~1.2%
+ int scanPos = pos + 1;
+ int maxScanPos = Math.min(pos + 4, bytes.length);
+
+ int codePoint = bytes[pos] & 0xFF;
+ for (; scanPos < maxScanPos; scanPos++) {
+ byte b = bytes[scanPos];
+ if ((b & HEADER_MASK) != CONTINUATION_HEADER) {
+ break;
+ }
+ codePoint = (codePoint << 6) + (b & BODY_MASK);
+ }
+
+ int size = scanPos - pos;
+ int nextPos = scanPos;
+ switch (size) {
+ case 1:
+ // Force code point to be ASCII.
+ codePoint &= 0x7F;
+
+ bytes[pos] = (byte) codePoint;
+ break;
+ case 2:
+ codePoint &= 0x7FF;
+ if (codePoint <= 0x7F) {
+ // The code point encoding must not be longer than necessary, so fix up the code point
+ // to actually require two bytes without fixing too many bits.
+ codePoint |= 0x80;
+ }
+
+ bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK));
+ codePoint >>= 6;
+ bytes[pos] = (byte) (0b1100_0000 | codePoint);
+ break;
+ case 3:
+ codePoint &= 0xFFFF;
+ if (codePoint <= 0x7FF) {
+ // The code point encoding must not be longer than necessary, so fix up the code point
+ // to actually require three bytes without fixing too many bits.
+ codePoint |= 0x800;
+ }
+ if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
+ // The code point must not be a low or high UTF-16 surrogate pair, which are not allowed
+ // in UTF-8.
+ codePoint |= (codePoint & ~0xF000) | 0xE000;
+ }
+
+ bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK));
+ codePoint >>= 6;
+ bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK));
+ codePoint >>= 6;
+ bytes[pos] = (byte) (0b1110_0000 | codePoint);
+ break;
+ case 4:
+ codePoint &= 0x1FFFFF;
+ if (codePoint <= 0xFFFF) {
+ // The code point encoding must not be longer than necessary, so fix up the code point
+ // to actually require four bytes without fixing too many bits.
+ codePoint |= 0x100000;
+ }
+ if (codePoint > 0x10FFFF) {
+ // The code point must be in the valid Unicode range, so fix it up by clearing as few
+ // bits as possible.
+ codePoint &= ~0x10FFFF;
+ }
+
+ bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK));
+ codePoint >>= 6;
+ bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK));
+ codePoint >>= 6;
+ bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK));
+ codePoint >>= 6;
+ bytes[pos] = (byte) (0b1111_0000 | codePoint);
+ break;
+ default:
+ throw new IllegalStateException("Not reached as scanPos <= pos + 4");
+ }
+
+ pos = nextPos;
+ }
+ }
+
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ Optional<WithUtf8Length> utf8Length =
+ Optional.ofNullable(type.getAnnotation(WithUtf8Length.class));
+ int min = utf8Length.map(WithUtf8Length::min).orElse(DEFAULT_MIN_BYTES);
+ int max = utf8Length.map(WithUtf8Length::max).orElse(DEFAULT_MAX_BYTES);
+
+ AnnotatedType innerByteArray = notNull(withLength(asAnnotatedType(byte[].class), min, max));
+
+ return findFirstParentIfClass(type, String.class)
+ .flatMap(parent -> factory.tryCreate(innerByteArray))
+ .map(byteArrayMutator -> {
+ boolean fixUpAscii = type.getDeclaredAnnotation(Ascii.class) != null;
+ return mutateThenMapToImmutable((SerializingMutator<byte[]>) byteArrayMutator,
+ bytes
+ -> {
+ if (fixUpAscii) {
+ fixUpAscii(bytes);
+ } else {
+ fixUpUtf8(bytes);
+ }
+ return new String(bytes, StandardCharsets.UTF_8);
+ },
+ string
+ -> string.getBytes(StandardCharsets.UTF_8),
+ (Predicate<Debuggable> inCycle) -> "String");
+ });
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/BUILD.bazel
new file mode 100644
index 00000000..284264bb
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/BUILD.bazel
@@ -0,0 +1,15 @@
+java_library(
+ name = "libfuzzer",
+ srcs = ["LibFuzzerMutator.java"],
+ visibility = [
+ # libFuzzer's mutators should only by used by mutators for primitive types as we want to get
+ # rid of this dependency eventually.
+ "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang:__pkg__",
+ "//src/test/java/com/code_intelligence/jazzer/mutation/mutator/lang:__subpackages__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/api",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/support",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:mutator",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/LibFuzzerMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/LibFuzzerMutator.java
new file mode 100644
index 00000000..c77c75e5
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/LibFuzzerMutator.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.libfuzzer;
+
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+
+import com.code_intelligence.jazzer.mutation.api.Serializer;
+import com.code_intelligence.jazzer.runtime.Mutator;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+public final class LibFuzzerMutator {
+ /**
+ * Key name to give to {@link System#setProperty(String, String)} to control the size of the
+ * returned array for {@link #defaultMutateMock(byte[], int)}. Only used for testing purposes.
+ */
+ public static final String MOCK_SIZE_KEY = "libfuzzermutator.mock.newsize";
+
+ public static byte[] mutateDefault(byte[] data, int maxSizeIncrease) {
+ byte[] mutatedBytes;
+ if (maxSizeIncrease == 0) {
+ mutatedBytes = data;
+ } else {
+ mutatedBytes = Arrays.copyOf(data, data.length + maxSizeIncrease);
+ }
+ int newSize = defaultMutate(mutatedBytes, data.length);
+ if (newSize == 0) {
+ // Mutation failed. This should happen very rarely.
+ return data;
+ }
+ return Arrays.copyOf(mutatedBytes, newSize);
+ }
+
+ public static <T> T mutateDefault(T value, Serializer<T> serializer, int maxSizeIncrease) {
+ require(maxSizeIncrease >= 0);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ serializer.writeExclusive(value, out);
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "writeExclusive is not expected to throw if the underlying stream doesn't", e);
+ }
+
+ byte[] mutatedBytes = mutateDefault(out.toByteArray(), maxSizeIncrease);
+
+ try {
+ return serializer.readExclusive(new ByteArrayInputStream(mutatedBytes));
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "readExclusive is not expected to throw if the underlying stream doesn't", e);
+ }
+ }
+
+ private static int defaultMutate(byte[] buffer, int size) {
+ if (Mutator.SHOULD_MOCK) {
+ return defaultMutateMock(buffer, size);
+ } else {
+ return Mutator.defaultMutateNative(buffer, size);
+ }
+ }
+
+ private static int defaultMutateMock(byte[] buffer, int size) {
+ String newSizeProp = System.getProperty(MOCK_SIZE_KEY);
+ int newSize = Math.min(buffer.length, size + 1);
+ if (newSizeProp != null) {
+ newSize = Integer.parseUnsignedInt(newSizeProp);
+ }
+
+ for (int i = 0; i < newSize; i++) {
+ buffer[i] += i + 1;
+ }
+ return newSize;
+ }
+
+ private LibFuzzerMutator() {}
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BUILD.bazel
new file mode 100644
index 00000000..2c591709
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BUILD.bazel
@@ -0,0 +1,16 @@
+java_library(
+ name = "proto",
+ srcs = glob(["*.java"]),
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/mutator:__pkg__",
+ "//src/test/java/com/code_intelligence/jazzer/mutation/mutator/proto:__pkg__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto:protobuf_runtime_compile_only",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/api",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/combinator",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/support",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderAdapters.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderAdapters.java
new file mode 100644
index 00000000..12dcd406
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderAdapters.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.proto;
+
+import static java.util.Collections.singletonList;
+
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+import com.google.protobuf.Message.Builder;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+final class BuilderAdapters {
+ private BuilderAdapters() {}
+
+ static <T extends Builder, U> List<U> makeMutableRepeatedFieldView(
+ T builder, FieldDescriptor field) {
+ return new AbstractList<U>() {
+ // O(1)
+ @Override
+ public U get(int index) {
+ return (U) builder.getRepeatedField(field, index);
+ }
+
+ // O(1)
+ @Override
+ public int size() {
+ return builder.getRepeatedFieldCount(field);
+ }
+
+ // O(1)
+ @Override
+ public boolean add(U element) {
+ builder.addRepeatedField(field, element);
+ return true;
+ }
+
+ // O(1)
+ @Override
+ public void add(int index, U element) {
+ addAll(index, singletonList(element));
+ }
+
+ // O(size() + other.size())
+ public boolean addAll(int index, Collection<? extends U> other) {
+ // This was benchmarked against the following implementation and found to be faster in all
+ // cases (up to 4x on lists of size 1000):
+ //
+ // for (U element : other) {
+ // builder.addRepeatedField(field, element);
+ // }
+ // Collections.rotate(subList(index, size()), other.size());
+ int otherSize = other.size();
+ if (otherSize == 0) {
+ return false;
+ }
+
+ int originalSize = size();
+ if (index == originalSize) {
+ for (U element : other) {
+ builder.addRepeatedField(field, element);
+ }
+ return true;
+ }
+
+ int newSize = originalSize + otherSize;
+ ArrayList<U> temp = new ArrayList<>(newSize);
+ for (int i = 0; i < index; i++) {
+ temp.add((U) builder.getRepeatedField(field, i));
+ }
+ temp.addAll(other);
+ for (int i = index; i < originalSize; i++) {
+ temp.add((U) builder.getRepeatedField(field, i));
+ }
+
+ replaceWith(temp);
+ return true;
+ }
+
+ // O(1)
+ @Override
+ public U set(int index, U element) {
+ U previous = get(index);
+ builder.setRepeatedField(field, index, element);
+ return previous;
+ }
+
+ // O(size())
+ @Override
+ public U remove(int index) {
+ U removed = get(index);
+ removeRange(index, index + 1);
+ return removed;
+ }
+
+ // O(size() - (toIndex - fromIndex))
+ @Override
+ protected void removeRange(int fromIndex, int toIndex) {
+ int originalSize = size();
+ int newSize = originalSize - (toIndex - fromIndex);
+ if (newSize == 0) {
+ builder.clearField(field);
+ return;
+ }
+
+ // There is no way to remove individual repeated field entries without clearing the entire
+ // field, so we have to iterate over all entries and keep them in a temporary list.
+ ArrayList<U> temp = new ArrayList<>(newSize);
+ for (int i = 0; i < fromIndex; i++) {
+ temp.add((U) builder.getRepeatedField(field, i));
+ }
+ for (int i = toIndex; i < originalSize; i++) {
+ temp.add((U) builder.getRepeatedField(field, i));
+ }
+
+ replaceWith(temp);
+ }
+
+ private void replaceWith(ArrayList<U> temp) {
+ builder.clearField(field);
+ for (U element : temp) {
+ builder.addRepeatedField(field, element);
+ }
+ }
+ };
+ }
+
+ static <T extends Builder, U> U getPresentFieldOrNull(T builder, FieldDescriptor field) {
+ if (builder.hasField(field)) {
+ return (U) builder.getField(field);
+ } else {
+ return null;
+ }
+ }
+
+ static <T extends Builder, U> void setFieldWithPresence(
+ T builder, FieldDescriptor field, U value) {
+ if (value == null) {
+ builder.clearField(field);
+ } else {
+ builder.setField(field, value);
+ }
+ }
+
+ static <T extends Builder, K, V> Map<K, V> getMapField(T builder, FieldDescriptor field) {
+ int size = builder.getRepeatedFieldCount(field);
+ FieldDescriptor keyField = field.getMessageType().getFields().get(0);
+ FieldDescriptor valueField = field.getMessageType().getFields().get(1);
+ HashMap<K, V> map = new HashMap<>(size);
+ for (int i = 0; i < size; i++) {
+ Message entry = (Message) builder.getRepeatedField(field, i);
+ map.put((K) entry.getField(keyField), (V) entry.getField(valueField));
+ }
+ return map;
+ }
+
+ static <T extends Builder, K, V> void setMapField(
+ Builder builder, FieldDescriptor field, Map<K, V> map) {
+ builder.clearField(field);
+ FieldDescriptor keyField = field.getMessageType().getFields().get(0);
+ FieldDescriptor valueField = field.getMessageType().getFields().get(1);
+ Builder entryBuilder = builder.newBuilderForField(field);
+ for (Entry<K, V> entry : map.entrySet()) {
+ entryBuilder.setField(keyField, entry.getKey());
+ entryBuilder.setField(valueField, entry.getValue());
+ builder.addRepeatedField(field, entryBuilder.build());
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java
new file mode 100644
index 00000000..85427c6f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.proto;
+
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.assemble;
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.combine;
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.fixedValue;
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateIndices;
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateProperty;
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateSumInPlace;
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable;
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateViaView;
+import static com.code_intelligence.jazzer.mutation.mutator.proto.BuilderAdapters.getMapField;
+import static com.code_intelligence.jazzer.mutation.mutator.proto.BuilderAdapters.getPresentFieldOrNull;
+import static com.code_intelligence.jazzer.mutation.mutator.proto.BuilderAdapters.makeMutableRepeatedFieldView;
+import static com.code_intelligence.jazzer.mutation.mutator.proto.BuilderAdapters.setFieldWithPresence;
+import static com.code_intelligence.jazzer.mutation.mutator.proto.BuilderAdapters.setMapField;
+import static com.code_intelligence.jazzer.mutation.mutator.proto.TypeLibrary.getDefaultInstance;
+import static com.code_intelligence.jazzer.mutation.mutator.proto.TypeLibrary.withoutInitIfRecursive;
+import static com.code_intelligence.jazzer.mutation.support.InputStreamSupport.cap;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asAnnotatedType;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asSubclassOrEmpty;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withExtraAnnotations;
+import static java.util.Arrays.stream;
+import static java.util.Objects.requireNonNull;
+import static java.util.function.UnaryOperator.identity;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+
+import com.code_intelligence.jazzer.mutation.annotation.proto.AnySource;
+import com.code_intelligence.jazzer.mutation.annotation.proto.WithDefaultInstance;
+import com.code_intelligence.jazzer.mutation.api.ChainedMutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.InPlaceMutator;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.Serializer;
+import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.code_intelligence.jazzer.mutation.support.Preconditions;
+import com.google.protobuf.Any;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.EnumDescriptor;
+import com.google.protobuf.Descriptors.EnumValueDescriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
+import com.google.protobuf.Descriptors.OneofDescriptor;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Message;
+import com.google.protobuf.Message.Builder;
+import com.google.protobuf.UnknownFieldSet;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedType;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+public final class BuilderMutatorFactory extends MutatorFactory {
+ private <T extends Builder, U> InPlaceMutator<T> mutatorForField(
+ FieldDescriptor field, Annotation[] annotations, MutatorFactory factory) {
+ factory = withDescriptorDependentMutatorFactoryIfNeeded(factory, field, annotations);
+ AnnotatedType typeToMutate = TypeLibrary.getTypeToMutate(field);
+ requireNonNull(typeToMutate, () -> "Java class not specified for " + field);
+
+ InPlaceMutator<T> mutator;
+ if (field.isMapField()) {
+ SerializingInPlaceMutator<Map> underlyingMutator =
+ (SerializingInPlaceMutator<Map>) factory.createInPlaceOrThrow(typeToMutate);
+ mutator = mutateProperty(builder
+ -> getMapField(builder, field),
+ underlyingMutator, (builder, value) -> setMapField(builder, field, value));
+ } else if (field.isRepeated()) {
+ SerializingInPlaceMutator<List<U>> underlyingMutator =
+ (SerializingInPlaceMutator<List<U>>) factory.createInPlaceOrThrow(typeToMutate);
+ mutator =
+ mutateViaView(builder -> makeMutableRepeatedFieldView(builder, field), underlyingMutator);
+ } else if (field.hasPresence()) {
+ SerializingMutator<U> underlyingMutator =
+ (SerializingMutator<U>) factory.createOrThrow(typeToMutate);
+ mutator = mutateProperty(builder
+ -> getPresentFieldOrNull(builder, field),
+ underlyingMutator, (builder, value) -> setFieldWithPresence(builder, field, value));
+ } else {
+ SerializingMutator<U> underlyingMutator =
+ (SerializingMutator<U>) factory.createOrThrow(typeToMutate);
+ mutator = mutateProperty(builder
+ -> (U) builder.getField(field),
+ underlyingMutator, (builder, value) -> builder.setField(field, value));
+ }
+
+ // If recursive message fields (i.e. those that have themselves as transitive subfields) are
+ // initialized eagerly, they tend to nest very deeply, which easily results in stack overflows.
+ // We guard against that by making their init a no-op and instead initialize them layer by layer
+ // in mutations.
+ return withoutInitIfRecursive(mutator, field);
+ }
+
+ private MutatorFactory withDescriptorDependentMutatorFactoryIfNeeded(
+ MutatorFactory originalFactory, FieldDescriptor field, Annotation[] annotations) {
+ if (field.getJavaType() == JavaType.ENUM) {
+ // Proto enum fields are special as their type (EnumValueDescriptor) does not encode their
+ // domain - we need the actual EnumDescriptor instance.
+ return new ChainedMutatorFactory(originalFactory, new MutatorFactory() {
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(
+ AnnotatedType type, MutatorFactory factory) {
+ return findFirstParentIfClass(type, EnumValueDescriptor.class).map(parent -> {
+ EnumDescriptor enumType = field.getEnumType();
+ List<EnumValueDescriptor> values = enumType.getValues();
+ String name = enumType.getName();
+ if (values.size() == 1) {
+ // While we generally prefer to error out instead of creating a mutator that can't
+ // actually mutate its domain, we can't do that for proto enum fields as the user
+ // creating the fuzz test may not be in a position to modify the existing proto
+ // definition.
+ return fixedValue(values.get(0));
+ } else {
+ return mutateThenMapToImmutable(mutateIndices(values.size()), values::get,
+ EnumValueDescriptor::getIndex, unused -> "Enum<" + name + ">");
+ }
+ });
+ }
+ });
+ } else if (field.getJavaType() == JavaType.MESSAGE) {
+ Descriptor messageDescriptor;
+ if (field.isMapField()) {
+ // Map fields are represented as messages, but we mutate them as actual Java Maps. In case
+ // the values of the proto map are themselves messages, we need to mutate their type.
+ FieldDescriptor valueField = field.getMessageType().getFields().get(1);
+ if (valueField.getJavaType() != JavaType.MESSAGE) {
+ return originalFactory;
+ }
+ messageDescriptor = valueField.getMessageType();
+ } else {
+ messageDescriptor = field.getMessageType();
+ }
+ return new ChainedMutatorFactory(originalFactory, new MutatorFactory() {
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(
+ AnnotatedType type, MutatorFactory factory) {
+ return asSubclassOrEmpty(type, Message.Builder.class).flatMap(clazz -> {
+ // BuilderMutatorFactory only handles subclasses of Message.Builder and requests
+ // Message.Builder itself for message fields, which we handle here.
+ if (clazz != Message.Builder.class) {
+ return Optional.empty();
+ }
+ // It is important that we use originalFactory here instead of factory: factory has this
+ // field-specific message mutator appended, but this mutator should only be used for
+ // this particular field and not any message subfields.
+ return Optional.of(makeBuilderMutator(originalFactory,
+ DynamicMessage.getDefaultInstance(messageDescriptor), annotations));
+ });
+ }
+ });
+ } else {
+ return originalFactory;
+ }
+ }
+
+ private <T extends Builder> Stream<InPlaceMutator<T>> mutatorsForFields(
+ Optional<OneofDescriptor> oneofField, List<FieldDescriptor> fields, Annotation[] annotations,
+ MutatorFactory factory) {
+ if (oneofField.isPresent()) {
+ // oneof fields are mutated as one as mutating them independently would cause the mutator to
+ // erratically switch between the different states. The individual fields are kept in the
+ // order in which they are defined in the .proto file.
+ OneofDescriptor oneofDescriptor = oneofField.get();
+
+ IdentityHashMap<FieldDescriptor, Integer> indexInOneof =
+ new IdentityHashMap<>(oneofDescriptor.getFieldCount());
+ for (int i = 0; i < oneofDescriptor.getFieldCount(); i++) {
+ indexInOneof.put(oneofDescriptor.getField(i), i);
+ }
+
+ return Stream.of(mutateSumInPlace(
+ (T builder)
+ -> {
+ FieldDescriptor setField = builder.getOneofFieldDescriptor(oneofDescriptor);
+ if (setField == null) {
+ return -1;
+ } else {
+ return indexInOneof.get(setField);
+ }
+ },
+ // Mutating to the unset (-1) state is handled by the individual field mutators, which
+ // are created nullable as oneof fields report that they track presence.
+ fields.stream()
+ .map(field -> mutatorForField(field, annotations, factory))
+ .toArray(InPlaceMutator[] ::new)));
+ } else {
+ // All non-oneof fields are mutated independently, using the order in which they are declared
+ // in the .proto file (which may not coincide with the order by field number).
+ return fields.stream().map(field -> mutatorForField(field, annotations, factory));
+ }
+ }
+
+ private static <M extends Message, B extends Builder> Serializer<B> makeBuilderSerializer(
+ M defaultInstance) {
+ return new Serializer<B>() {
+ @Override
+ public B read(DataInputStream in) throws IOException {
+ int length = Math.max(in.readInt(), 0);
+ return (B) parseLeniently(cap(in, length));
+ }
+
+ @Override
+ public B readExclusive(InputStream in) throws IOException {
+ return (B) parseLeniently(in);
+ }
+
+ private Builder parseLeniently(InputStream in) throws IOException {
+ Builder builder = defaultInstance.toBuilder();
+ try {
+ builder.mergeFrom(in);
+ } catch (InvalidProtocolBufferException ignored) {
+ // builder has been partially modified with what could be decoded before the parser error.
+ }
+ // We never want the fuzz test to see unknown fields and our mutations should never produce
+ // them.
+ builder.setUnknownFields(UnknownFieldSet.getDefaultInstance());
+ // Required fields may not have been set at this point. We set them to default values to
+ // prevent an exception when built.
+ forceInitialized(builder);
+ return builder;
+ }
+
+ private void forceInitialized(Builder builder) {
+ if (builder.isInitialized()) {
+ return;
+ }
+ for (FieldDescriptor field : builder.getDescriptorForType().getFields()) {
+ if (!field.isRequired()) {
+ continue;
+ }
+ if (field.getJavaType() == JavaType.MESSAGE) {
+ forceInitialized(builder.getFieldBuilder(field));
+ } else if (!builder.hasField(field)) {
+ builder.setField(field, field.getDefaultValue());
+ }
+ }
+ }
+
+ @Override
+ public void write(Builder builder, DataOutputStream out) throws IOException {
+ Message message = builder.build();
+ out.writeInt(message.getSerializedSize());
+ message.writeTo(out);
+ }
+
+ @Override
+ public void writeExclusive(Builder builder, OutputStream out) throws IOException {
+ builder.build().writeTo(out);
+ }
+
+ @Override
+ public B detach(Builder builder) {
+ return (B) builder.build().toBuilder();
+ }
+ };
+ }
+
+ /*
+ * Ensures that only a single instance is created per builder class and shared among all mutators
+ * that need it. This ensures that arbitrarily nested recursive structures such as a Protobuf
+ * message type that contains itself as a message field are representable as fixed-size mutator
+ * structures.
+ *
+ * Note: The resulting mutator structures may no longer form a tree: If A is a protobuf message
+ * type with a message field B and B in turn has a message field of type A, then the mutators for
+ * A and B will reference each other, forming a cycle.
+ */
+ private final HashMap<CacheKey, SerializingMutator<? extends Builder>> internedMutators =
+ new HashMap<>();
+
+ private SerializingMutator<Any.Builder> mutatorForAny(
+ AnySource anySource, MutatorFactory factory) {
+ Map<String, Integer> typeUrlToIndex =
+ IntStream.range(0, anySource.value().length)
+ .boxed()
+ .collect(toMap(i -> getTypeUrl(getDefaultInstance(anySource.value()[i])), identity()));
+
+ return assemble(mutator
+ -> internedMutators.put(new CacheKey(Any.getDescriptor(), anySource), mutator),
+ Any.getDefaultInstance()::toBuilder, makeBuilderSerializer(Any.getDefaultInstance()),
+ ()
+ -> mutateSumInPlace(
+ // Corpus entries may contain Anys with arbitrary (and even invalid) messages, so we
+ // fall back to mutating the first message type if the type isn't recognized.
+ (Any.Builder builder)
+ -> typeUrlToIndex.getOrDefault(builder.getTypeUrl(), 0),
+ stream(anySource.value())
+ .map(messageClass -> {
+ SerializingMutator<Message> messageMutator =
+ (SerializingMutator<Message>) factory.createOrThrow(notNull(
+ withExtraAnnotations(asAnnotatedType(messageClass), anySource)));
+ return mutateProperty(
+ (Any.Builder anyBuilder)
+ -> {
+ try {
+ return anyBuilder.build().unpack(messageClass);
+ } catch (InvalidProtocolBufferException e) {
+ // This can only happen if the corpus contains an invalid Any.
+ return getDefaultInstance(messageClass);
+ }
+ },
+ messageMutator,
+ (Any.Builder any, Message message) -> {
+ any.setTypeUrl(getTypeUrl(message));
+ any.setValue(message.toByteString());
+ });
+ })
+ .toArray(InPlaceMutator[] ::new)));
+ }
+
+ private static String getTypeUrl(Message message) {
+ // We only support the default "type.googleapis.com" prefix.
+ // https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/any.proto#L94
+ return "type.googleapis.com/" + message.getDescriptorForType().getFullName();
+ }
+
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ return asSubclassOrEmpty(type, Builder.class).flatMap(builderClass -> {
+ Message defaultInstance;
+ WithDefaultInstance withDefaultInstance = type.getAnnotation(WithDefaultInstance.class);
+ if (withDefaultInstance != null) {
+ defaultInstance = getDefaultInstance(withDefaultInstance);
+ } else if (builderClass == DynamicMessage.Builder.class) {
+ throw new IllegalArgumentException(
+ "To mutate a dynamic message, add a @WithDefaultInstance annotation specifying the"
+ + " fully qualified method name of a static method returning a default instance");
+ } else if (builderClass == Message.Builder.class) {
+ // Handled by a custom mutator factory for message fields that is created in
+ // withDescriptorDependentMutatorFactoryIfNeeded. Without @WithDefaultInstance,
+ // BuilderMutatorFactory only handles proper subclasses, which correspond to generated
+ // message types.
+ return Optional.empty();
+ } else {
+ defaultInstance =
+ getDefaultInstance((Class<? extends Message>) builderClass.getEnclosingClass());
+ }
+
+ return Optional.of(
+ makeBuilderMutator(factory, defaultInstance, type.getDeclaredAnnotations()));
+ });
+ }
+
+ private SerializingMutator<?> makeBuilderMutator(
+ MutatorFactory factory, Message defaultInstance, Annotation[] annotations) {
+ AnySource anySource = (AnySource) stream(annotations)
+ .filter(annotation -> annotation.annotationType() == AnySource.class)
+ .findFirst()
+ .orElse(null);
+ Preconditions.require(anySource == null || anySource.value().length > 0,
+ "@AnySource must list a non-empty list of classes");
+ Descriptor descriptor = defaultInstance.getDescriptorForType();
+
+ CacheKey cacheKey = new CacheKey(descriptor, anySource);
+ if (internedMutators.containsKey(cacheKey)) {
+ return internedMutators.get(cacheKey);
+ }
+
+ // If there is no @AnySource, mutate the Any.Builder fields just like a regular message.
+ // TODO: Determine whether we should show a warning in this case.
+ if (descriptor.equals(Any.getDescriptor()) && anySource != null) {
+ return mutatorForAny(anySource, factory);
+ }
+
+ // assemble inserts the instance of the newly created builder mutator into the
+ // internedMutators map *before* recursively creating the mutators for its fields, which
+ // ensures that the recursion is finite (bounded by the total number of distinct message types
+ // that transitively occur as field types on the current message type).
+ return assemble(mutator
+ -> internedMutators.put(cacheKey, mutator),
+ defaultInstance::toBuilder, makeBuilderSerializer(defaultInstance),
+ ()
+ -> combine(
+ descriptor.getFields()
+ .stream()
+ // Keep oneofs sorted by the first appearance of their fields in the
+ // .proto file.
+ .collect(groupingBy(
+ // groupingBy does not support null keys. We use getRealContainingOneof()
+ // instead of getContainingOneof() as the latter also reports oneofs for
+ // proto3 optional fields, which we handle separately.
+ fieldDescriptor
+ -> Optional.ofNullable(fieldDescriptor.getRealContainingOneof()),
+ LinkedHashMap::new, toList()))
+ .entrySet()
+ .stream()
+ .flatMap(entry
+ -> mutatorsForFields(entry.getKey(), entry.getValue(),
+ anySource == null ? new Annotation[0] : new Annotation[] {anySource},
+ factory))
+ .toArray(InPlaceMutator[] ::new)));
+ }
+
+ private static final class CacheKey {
+ private final Descriptor descriptor;
+ private final AnySource anySource;
+
+ private CacheKey(Descriptor descriptor, AnySource anySource) {
+ this.descriptor = descriptor;
+ this.anySource = anySource;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CacheKey cacheKey = (CacheKey) o;
+ return descriptor == cacheKey.descriptor && Objects.equals(anySource, cacheKey.anySource);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * System.identityHashCode(descriptor) + Objects.hashCode(anySource);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ByteStringMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ByteStringMutatorFactory.java
new file mode 100644
index 00000000..a01ffae6
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ByteStringMutatorFactory.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.proto;
+
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asAnnotatedType;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull;
+
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.google.protobuf.ByteString;
+import java.lang.reflect.AnnotatedType;
+import java.util.Optional;
+
+final class ByteStringMutatorFactory extends MutatorFactory {
+ ByteStringMutatorFactory() {}
+
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) {
+ return findFirstParentIfClass(type, ByteString.class)
+ .flatMap(parent -> factory.tryCreate(notNull(asAnnotatedType(byte[].class))))
+ .map(byteArrayMutator
+ -> mutateThenMapToImmutable((SerializingMutator<byte[]>) byteArrayMutator,
+ ByteString::copyFrom, ByteString::toByteArray));
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/MessageMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/MessageMutatorFactory.java
new file mode 100644
index 00000000..eb3220f5
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/MessageMutatorFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.proto;
+
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asAnnotatedType;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asSubclassOrEmpty;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withExtraAnnotations;
+
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
+import com.google.protobuf.Message;
+import com.google.protobuf.Message.Builder;
+import java.lang.reflect.AnnotatedType;
+import java.util.Arrays;
+import java.util.Optional;
+
+public final class MessageMutatorFactory extends MutatorFactory {
+ @Override
+ public Optional<SerializingMutator<?>> tryCreate(
+ AnnotatedType messageType, MutatorFactory factory) {
+ return asSubclassOrEmpty(messageType, Message.class)
+ // If the Message class doesn't have a nested Builder class, it is not a concrete generated
+ // message and we can't mutate it.
+ .flatMap(messageClass
+ -> Arrays.stream(messageClass.getDeclaredClasses())
+ .filter(clazz -> clazz.getSimpleName().equals("Builder"))
+ .findFirst())
+ .flatMap(builderClass
+ ->
+ // Forward the annotations (e.g. @NotNull) on the Message type to the Builder type.
+ factory.tryCreateInPlace(
+ withExtraAnnotations(asAnnotatedType(builderClass), messageType.getAnnotations())))
+ .map(builderMutator
+ -> mutateThenMapToImmutable(
+ (SerializingMutator<Builder>) builderMutator, Builder::build, Message::toBuilder));
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java
new file mode 100644
index 00000000..acb25cd7
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.proto;
+
+import com.code_intelligence.jazzer.mutation.api.ChainedMutatorFactory;
+import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
+
+public final class ProtoMutators {
+ private ProtoMutators() {}
+
+ public static MutatorFactory newFactory() {
+ try {
+ Class.forName("com.google.protobuf.Message");
+ return new ChainedMutatorFactory(
+ new ByteStringMutatorFactory(), new MessageMutatorFactory(), new BuilderMutatorFactory());
+ } catch (ClassNotFoundException e) {
+ return new ChainedMutatorFactory();
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/TypeLibrary.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/TypeLibrary.java
new file mode 100644
index 00000000..43338f14
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/TypeLibrary.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.mutator.proto;
+
+import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.withoutInit;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.check;
+import static com.code_intelligence.jazzer.mutation.support.StreamSupport.entry;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asAnnotatedType;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.containedInDirectedCycle;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull;
+import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withTypeArguments;
+import static java.lang.String.format;
+import static java.util.Collections.unmodifiableMap;
+import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toMap;
+
+import com.code_intelligence.jazzer.mutation.annotation.NotNull;
+import com.code_intelligence.jazzer.mutation.annotation.proto.WithDefaultInstance;
+import com.code_intelligence.jazzer.mutation.api.InPlaceMutator;
+import com.code_intelligence.jazzer.mutation.support.TypeHolder;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.EnumValueDescriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.Message;
+import com.google.protobuf.Message.Builder;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Stream;
+
+final class TypeLibrary {
+ private static final AnnotatedType RAW_LIST = new TypeHolder<@NotNull List>() {}.annotatedType();
+ private static final AnnotatedType RAW_MAP = new TypeHolder<@NotNull Map>() {}.annotatedType();
+ private static final Map<JavaType, AnnotatedType> BASE_TYPE_WITH_PRESENCE =
+ Stream
+ .of(entry(JavaType.BOOLEAN, Boolean.class), entry(JavaType.BYTE_STRING, ByteString.class),
+ entry(JavaType.DOUBLE, Double.class), entry(JavaType.ENUM, EnumValueDescriptor.class),
+ entry(JavaType.FLOAT, Float.class), entry(JavaType.INT, Integer.class),
+ entry(JavaType.LONG, Long.class), entry(JavaType.MESSAGE, Message.class),
+ entry(JavaType.STRING, String.class))
+ .collect(collectingAndThen(toMap(Entry::getKey, e -> asAnnotatedType(e.getValue())),
+ map -> unmodifiableMap(new EnumMap<>(map))));
+
+ private TypeLibrary() {}
+
+ static <T extends Builder> AnnotatedType getTypeToMutate(FieldDescriptor field) {
+ if (field.isRequired()) {
+ return getBaseType(field);
+ } else if (field.isMapField()) {
+ // Map fields are represented as repeated message fields, so this check has to come before the
+ // one for regular repeated fields.
+ AnnotatedType keyType = getBaseType(field.getMessageType().getFields().get(0));
+ AnnotatedType valueType = getBaseType(field.getMessageType().getFields().get(1));
+ return withTypeArguments(RAW_MAP, keyType, valueType);
+ } else if (field.isRepeated()) {
+ return withTypeArguments(RAW_LIST, getBaseType(field));
+ } else if (field.hasPresence()) {
+ return BASE_TYPE_WITH_PRESENCE.get(field.getJavaType());
+ } else {
+ return getBaseType(field);
+ }
+ }
+
+ private static <T extends Builder> AnnotatedType getBaseType(FieldDescriptor field) {
+ return notNull(BASE_TYPE_WITH_PRESENCE.get(field.getJavaType()));
+ }
+
+ static <T> InPlaceMutator<T> withoutInitIfRecursive(
+ InPlaceMutator<T> mutator, FieldDescriptor field) {
+ if (field.isRequired() || !isRecursiveField(field)) {
+ return mutator;
+ }
+ return withoutInit(mutator);
+ }
+
+ private static boolean isRecursiveField(FieldDescriptor field) {
+ return containedInDirectedCycle(field, f -> {
+ // For map fields, only the value can be a message.
+ FieldDescriptor realField = f.isMapField() ? f.getMessageType().getFields().get(1) : f;
+ if (realField.getJavaType() != JavaType.MESSAGE) {
+ return Stream.empty();
+ }
+ return realField.getMessageType().getFields().stream();
+ });
+ }
+
+ static Message getDefaultInstance(Class<? extends Message> messageClass) {
+ Method getDefaultInstance;
+ try {
+ getDefaultInstance = messageClass.getMethod("getDefaultInstance");
+ check(Modifier.isStatic(getDefaultInstance.getModifiers()));
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException(
+ format("Message class for builder type %s does not have a getDefaultInstance method",
+ messageClass.getName()),
+ e);
+ }
+ try {
+ return (Message) getDefaultInstance.invoke(null);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalStateException(
+ format(getDefaultInstance + " isn't accessible or threw an exception"), e);
+ }
+ }
+
+ static Message getDefaultInstance(WithDefaultInstance withDefaultInstance) {
+ String[] parts = withDefaultInstance.value().split("#");
+ if (parts.length != 2) {
+ throw new IllegalArgumentException(
+ format("Expected @WithDefaultInstance(\"%s\") to specify a fully-qualified method name"
+ + " (e.g. com.example.MyClass#getDefaultInstance)",
+ withDefaultInstance.value()));
+ }
+
+ Class<?> clazz;
+ try {
+ clazz = Class.forName(parts[0]);
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException(
+ format("Failed to find class '%s' specified by @WithDefaultInstance(\"%s\")", parts[0],
+ withDefaultInstance.value()),
+ e);
+ }
+
+ Method method;
+ try {
+ method = clazz.getDeclaredMethod(parts[1]);
+ method.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException(
+ format("Failed to find method specified by @WithDefaultInstance(\"%s\")",
+ withDefaultInstance.value()),
+ e);
+ }
+ if (!Modifier.isStatic(method.getModifiers())) {
+ throw new IllegalArgumentException(
+ format("Expected method specified by @WithDefaultInstance(\"%s\") to be static",
+ withDefaultInstance.value()));
+ }
+ if (!Message.class.isAssignableFrom(method.getReturnType())) {
+ throw new IllegalArgumentException(format(
+ "Expected return type of method specified by @WithDefaultInstance(\"%s\") to be a"
+ + " subtype of %s, got %s",
+ withDefaultInstance.value(), Message.class.getName(), method.getReturnType().getName()));
+ }
+
+ try {
+ return (Message) method.invoke(null);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalArgumentException(
+ format("Failed to execute method specified by @WithDefaultInstance(\"%s\")",
+ withDefaultInstance.value()),
+ e);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel
new file mode 100644
index 00000000..5765ceed
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel
@@ -0,0 +1,11 @@
+java_library(
+ name = "support",
+ srcs = glob(["*.java"]),
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation:__subpackages__",
+ "//src/test/java/com/code_intelligence/jazzer/mutation:__subpackages__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
+ ],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/ExceptionSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/ExceptionSupport.java
new file mode 100644
index 00000000..bd5f434b
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ExceptionSupport.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.support;
+
+public final class ExceptionSupport {
+ /**
+ * Allows throwing any {@link Throwable} unchanged as if it were an unchecked exception.
+ *
+ * <p>Example: {@code throw asUnchecked(new IOException())}
+ */
+ @SuppressWarnings("unchecked")
+ public static <T extends Throwable> T asUnchecked(Throwable t) throws T {
+ throw(T) t;
+ }
+
+ private ExceptionSupport() {}
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/InputStreamSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/InputStreamSupport.java
new file mode 100644
index 00000000..c643fea2
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/InputStreamSupport.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.support;
+
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+import static java.util.Objects.requireNonNull;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Queue;
+
+public final class InputStreamSupport {
+ public static byte[] readAllBytes(InputStream stream) throws IOException {
+ requireNonNull(stream);
+ Queue<byte[]> buffers = new ArrayDeque<>();
+ int arrayLength = 0;
+ outer:
+ while (true) {
+ byte[] buffer = new byte[max(8192, stream.available())];
+ buffers.add(buffer);
+ int off = 0;
+ while (off < buffer.length) {
+ int bytesRead = stream.read(buffer, off, buffer.length - off);
+ if (bytesRead == -1) {
+ break outer;
+ }
+ off += bytesRead;
+ arrayLength += bytesRead;
+ }
+ }
+
+ byte[] result = new byte[arrayLength];
+ int offset = 0;
+ byte[] buffer;
+ int remaining = arrayLength;
+ while ((buffer = buffers.poll()) != null) {
+ int toCopy = min(buffer.length, remaining);
+ System.arraycopy(buffer, 0, result, offset, toCopy);
+ remaining -= toCopy;
+ }
+ return result;
+ }
+
+ private static final InputStream infiniteZerosStream = new ExtendWithNullInputStream();
+
+ /**
+ * @return an infinite stream consisting of 0s
+ */
+ public static InputStream infiniteZeros() {
+ return infiniteZerosStream;
+ }
+
+ /**
+ * @return {@code stream} extended with 0s to an infinite stream
+ */
+ public static InputStream extendWithZeros(InputStream stream) {
+ if (stream instanceof ExtendWithNullInputStream) {
+ return stream;
+ }
+ return new ExtendWithNullInputStream(requireNonNull(stream));
+ }
+
+ public static final class ExtendWithNullInputStream extends InputStream {
+ private static final InputStream ALWAYS_EOF = new ByteArrayInputStream(new byte[0]);
+ private final InputStream stream;
+ private boolean eof;
+
+ private ExtendWithNullInputStream() {
+ this.stream = ALWAYS_EOF;
+ this.eof = true;
+ }
+
+ private ExtendWithNullInputStream(InputStream stream) {
+ this.stream = stream;
+ this.eof = false;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (eof) {
+ return 0;
+ }
+
+ int res = stream.read();
+ if (res != -1) {
+ return res;
+ } else {
+ eof = true;
+ return 0;
+ }
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (eof) {
+ Arrays.fill(b, off, off + len, (byte) 0);
+ } else {
+ int bytesRead = stream.read(b, off, len);
+ if (bytesRead < len) {
+ eof = true;
+ Arrays.fill(b, max(off, off + bytesRead), off + len, (byte) 0);
+ }
+ }
+ return len;
+ }
+
+ @Override
+ public int available() throws IOException {
+ if (eof) {
+ return Integer.MAX_VALUE;
+ } else {
+ return stream.available();
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ stream.close();
+ }
+ }
+
+ /**
+ * @return a stream with the first {@code bytes} bytes of {@code stream}
+ */
+ public static InputStream cap(InputStream stream, long bytes) {
+ requireNonNull(stream);
+ require(bytes >= 0, "bytes must be non-negative");
+ return new CappedInputStream(stream, bytes);
+ }
+
+ private static final class CappedInputStream extends InputStream {
+ private final InputStream stream;
+ private long remaining;
+
+ CappedInputStream(InputStream stream, long remaining) {
+ this.stream = stream;
+ this.remaining = remaining;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (remaining == 0) {
+ return -1;
+ }
+
+ int res = stream.read();
+ if (res != -1) {
+ --remaining;
+ }
+ return res;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (remaining == 0) {
+ return -1;
+ }
+
+ int res = stream.read(b, off, (int) min(len, remaining));
+ if (res != -1) {
+ remaining -= res;
+ }
+ return res;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return (int) min(stream.available(), remaining);
+ }
+
+ @Override
+ public void close() throws IOException {
+ stream.close();
+ }
+ }
+
+ /**
+ * Wraps a given stream with the functionality to detect if it was read exactly.
+ * To do so, the stream must provide an accurate implementation of {@link
+ * InputStream#available()}, hence it's restricted to {@link ByteArrayInputStream} for now.
+ *
+ * @return {@code stream} extended that detects if it was consumed exactly
+ */
+ public static ReadExactlyInputStream extendWithReadExactly(ByteArrayInputStream stream) {
+ return new ReadExactlyInputStream(requireNonNull(stream));
+ }
+
+ public static final class ReadExactlyInputStream extends InputStream {
+ private final InputStream stream;
+ private boolean eof;
+
+ private ReadExactlyInputStream(InputStream stream) {
+ this.stream = stream;
+ this.eof = false;
+ }
+
+ public boolean isConsumedExactly() {
+ try {
+ // Forwards availability check to the underlying ByteInputStream,
+ // which is accurate for the number of available bytes.
+ return !eof && available() == 0;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public int read() throws IOException {
+ int res = stream.read();
+ if (res == -1) {
+ eof = true;
+ }
+ return res;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int read = stream.read(b, off, len);
+ if (read < len) {
+ eof = true;
+ }
+ return read;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return stream.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ stream.close();
+ }
+ }
+
+ private InputStreamSupport() {}
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterHolder.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterHolder.java
new file mode 100644
index 00000000..316a6dfd
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterHolder.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.support;
+
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static java.util.stream.Collectors.toList;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A factory for {@link AnnotatedType} instances capturing method parameters.
+ *
+ * <p>Due to type erasure, this class can only be used by creating an anonymous subclass with a
+ * method called {@code foo} that takes exactly the desired parameter.
+ *
+ * <p>Example: {@code new ParameterHolder {void foo(@NotNull List<String> param)}.annotatedType}
+ */
+public abstract class ParameterHolder {
+ protected ParameterHolder() {}
+
+ public AnnotatedType annotatedType() {
+ return getMethod().getAnnotatedParameterTypes()[0];
+ }
+
+ public Type type() {
+ return annotatedType().getType();
+ }
+
+ public Annotation[] parameterAnnotations() {
+ return getMethod().getParameterAnnotations()[0];
+ }
+
+ private Method getMethod() {
+ List<Method> foos = Arrays.stream(this.getClass().getDeclaredMethods())
+ .filter(method -> method.getName().equals("foo"))
+ .collect(toList());
+ require(foos.size() == 1,
+ this.getClass().getName() + " must define exactly one function named 'foo'");
+ Method foo = foos.get(0);
+ require(foo.getParameterCount() == 1,
+ this.getClass().getName() + "#foo must define exactly one parameter");
+ return foo;
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/Preconditions.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/Preconditions.java
new file mode 100644
index 00000000..a77f65fb
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/Preconditions.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.support;
+
+import static java.util.Objects.requireNonNull;
+
+public final class Preconditions {
+ private Preconditions() {}
+
+ public static void check(boolean property) {
+ if (!property) {
+ throw new IllegalStateException();
+ }
+ }
+
+ public static void check(boolean property, String message) {
+ if (!property) {
+ throw new IllegalStateException(message);
+ }
+ }
+
+ public static void require(boolean property) {
+ if (!property) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ public static void require(boolean property, String message) {
+ if (!property) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ public static <T> T[] requireNonNullElements(T[] array) {
+ requireNonNull(array);
+ for (T element : array) {
+ requireNonNull(element, "array must not contain null elements");
+ }
+ return array;
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/RandomSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/RandomSupport.java
new file mode 100644
index 00000000..5cfa765e
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/RandomSupport.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.support;
+
+import java.util.SplittableRandom;
+
+public final class RandomSupport {
+ private RandomSupport() {}
+
+ /**
+ * Polyfill for {@link SplittableRandom#nextBytes(byte[])}, which is not available in Java 8.
+ */
+ public static void nextBytes(SplittableRandom random, byte[] bytes) {
+ // Taken from the implementation contract
+ // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/RandomGenerator.html#nextBytes(byte%5B%5D)
+ // for interoperability with the RandomGenerator interface available as of Java 17.
+ int i = 0;
+ int len = bytes.length;
+ for (int words = len >> 3; words-- > 0;) {
+ long rnd = random.nextLong();
+ for (int n = 8; n-- > 0; rnd >>>= Byte.SIZE) bytes[i++] = (byte) rnd;
+ }
+ if (i < len)
+ for (long rnd = random.nextLong(); i<len; rnd>>>= Byte.SIZE) bytes[i++] = (byte) rnd;
+ }
+
+ /**
+ * Clamp function for integers, which Java does not yet have
+ *
+ * @param value the value you want to clamp
+ * @param min the minimum allowable value (inclusive)
+ * @param max the maximum allowable value (inclusive)
+ * @return Closest number to {@code value} within the range {@code [min, max]}
+ */
+ public static int clamp(int value, int min, int max) {
+ return Math.min(Math.max(value, min), max);
+ }
+
+ /**
+ * Clamp function for longs, which Java does not yet have
+ *
+ * @param value the value you want to clamp
+ * @param min the minimum allowable value (inclusive)
+ * @param max the maximum allowable value (inclusive)
+ * @return Closest number to {@code value} within the range {@code [min, max]}
+ */
+ public static long clamp(long value, long min, long max) {
+ return Math.min(Math.max(value, min), max);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/StreamSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/StreamSupport.java
new file mode 100644
index 00000000..b61980c4
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/StreamSupport.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.support;
+
+import static java.util.stream.Collectors.toList;
+
+import java.util.AbstractMap.SimpleEntry;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.function.IntFunction;
+import java.util.stream.Stream;
+
+public final class StreamSupport {
+ private StreamSupport() {}
+
+ public static boolean[] toBooleanArray(Stream<Boolean> stream) {
+ List<Boolean> list = stream.collect(toList());
+ boolean[] array = new boolean[list.size()];
+ for (int i = 0; i < list.size(); i++) {
+ array[i] = list.get(i);
+ }
+ return array;
+ }
+
+ /**
+ * @return the first present value, otherwise {@link Optional#empty()}
+ */
+ public static <T> Optional<T> findFirstPresent(Stream<Optional<T>> stream) {
+ return stream.filter(Optional::isPresent).map(Optional::get).findFirst();
+ }
+
+ /**
+ * @return an array with the values if all {@link Optional}s are present, otherwise
+ * {@link Optional#empty()}
+ */
+ public static <T> Optional<T[]> toArrayOrEmpty(
+ Stream<Optional<T>> stream, IntFunction<T[]> newArray) {
+ try {
+ return Optional.of(stream.map(Optional::get).toArray(newArray));
+ } catch (NoSuchElementException e) {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Return a stream containing the optional value if present, otherwise an empty stream.
+ *
+ * @return stream containing the optional value
+ */
+ public static <T> Stream<T> getOrEmpty(Optional<T> optional) {
+ return optional.isPresent() ? Stream.of(optional.get()) : Stream.empty();
+ }
+
+ public static <K, V> SimpleEntry<K, V> entry(K key, V value) {
+ return new SimpleEntry<>(key, value);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeHolder.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeHolder.java
new file mode 100644
index 00000000..e703f9f1
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeHolder.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.support;
+
+import java.lang.reflect.AnnotatedParameterizedType;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.Type;
+
+/**
+ * A factory for {@link AnnotatedType} instances capturing types.
+ *
+ * <p>Due to type erasure, this class can only be used by creating an anonymous subclass.
+ *
+ * <p>Example: {@code new TypeHolder<List<String>> {}.annotatedType}
+ *
+ * @param <T> the type to hold
+ */
+public abstract class TypeHolder<T> {
+ protected TypeHolder() {}
+
+ public AnnotatedType annotatedType() {
+ return ((AnnotatedParameterizedType) this.getClass().getAnnotatedSuperclass())
+ .getAnnotatedActualTypeArguments()[0];
+ }
+
+ public Type type() {
+ return annotatedType().getType();
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java
new file mode 100644
index 00000000..8ce3aefc
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java
@@ -0,0 +1,564 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.support;
+
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.check;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
+import static com.code_intelligence.jazzer.mutation.support.Preconditions.requireNonNullElements;
+import static java.util.Arrays.stream;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toSet;
+
+import com.code_intelligence.jazzer.mutation.annotation.NotNull;
+import com.code_intelligence.jazzer.mutation.annotation.WithLength;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Inherited;
+import java.lang.reflect.AnnotatedArrayType;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.AnnotatedParameterizedType;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.AnnotatedTypeVariable;
+import java.lang.reflect.AnnotatedWildcardType;
+import java.lang.reflect.Array;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public final class TypeSupport {
+ private static final Annotation NOT_NULL =
+ new TypeHolder<@NotNull String>() {}.annotatedType().getAnnotation(NotNull.class);
+
+ private TypeSupport() {}
+
+ public static boolean isPrimitive(AnnotatedType type) {
+ return isPrimitive(type.getType());
+ }
+
+ public static boolean isPrimitive(Type type) {
+ if (!(type instanceof Class<?>) ) {
+ return false;
+ }
+ return ((Class<?>) type).isPrimitive();
+ }
+
+ public static boolean isInheritable(Annotation annotation) {
+ return annotation.annotationType().getDeclaredAnnotation(Inherited.class) != null;
+ }
+
+ /**
+ * Returns {@code type} as a {@code Class<? extends T>} if it is a subclass of T, otherwise
+ * empty.
+ *
+ * <p>This function also returns an empty {@link Optional} for more complex (e.g. parameterized)
+ * types.
+ */
+ public static <T> Optional<Class<? extends T>> asSubclassOrEmpty(
+ AnnotatedType type, Class<T> superclass) {
+ if (!(type.getType() instanceof Class<?>) ) {
+ return Optional.empty();
+ }
+
+ Class<?> actualClazz = (Class<?>) type.getType();
+ if (!superclass.isAssignableFrom(actualClazz)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(actualClazz.asSubclass(superclass));
+ }
+
+ public static AnnotatedType asAnnotatedType(Class<?> clazz) {
+ requireNonNull(clazz);
+ return new AnnotatedType() {
+ @Override
+ public Type getType() {
+ return clazz;
+ }
+
+ @Override
+ public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
+ return annotatedElementGetAnnotation(this, annotationClass);
+ }
+
+ @Override
+ public Annotation[] getAnnotations() {
+ // No directly present annotations, look for inheritable present annotations on the
+ // superclass.
+ if (clazz.getSuperclass() == null) {
+ return new Annotation[0];
+ }
+ return stream(clazz.getSuperclass().getAnnotations())
+ .filter(TypeSupport::isInheritable)
+ .toArray(Annotation[] ::new);
+ }
+
+ @Override
+ public Annotation[] getDeclaredAnnotations() {
+ // No directly present annotations.
+ return new Annotation[0];
+ }
+
+ @Override
+ public String toString() {
+ return annotatedTypeToString(this);
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException(
+ "hashCode() is not supported as its behavior isn't specified");
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ throw new UnsupportedOperationException(
+ "equals() is not supported as its behavior isn't specified");
+ }
+ };
+ }
+
+ /**
+ * Visits the individual classes and their directly present annotations that make up the given
+ * type.
+ *
+ * <p>Classes are visited in left-to-right order as they appear in the type definition, except
+ * that an array class is visited before its component class.
+ *
+ * @throws IllegalArgumentException if the given type contains a wildcard type or type variable
+ */
+ public static void visitAnnotatedType(
+ AnnotatedType type, BiConsumer<Class<?>, Annotation[]> visitor) {
+ visitAnnotatedTypeInternal(type, visitor);
+ }
+
+ private static Class<?> visitAnnotatedTypeInternal(
+ AnnotatedType type, BiConsumer<Class<?>, Annotation[]> visitor) {
+ Class<?> clazz;
+ if (type instanceof AnnotatedWildcardType) {
+ throw new IllegalArgumentException("Wildcard types are not supported: " + type);
+ } else if (type instanceof AnnotatedTypeVariable) {
+ throw new IllegalArgumentException("Type variables are not supported: " + type);
+ } else if (type instanceof AnnotatedParameterizedType) {
+ AnnotatedParameterizedType annotatedParameterizedType = (AnnotatedParameterizedType) type;
+ check(annotatedParameterizedType.getType() instanceof ParameterizedType);
+ Type rawType = ((ParameterizedType) annotatedParameterizedType.getType()).getRawType();
+ check(rawType instanceof Class<?>);
+ clazz = (Class<?>) rawType;
+
+ visitor.accept(clazz, type.getDeclaredAnnotations());
+ for (AnnotatedType typeArgument :
+ annotatedParameterizedType.getAnnotatedActualTypeArguments()) {
+ visitAnnotatedTypeInternal(typeArgument, visitor);
+ }
+ } else if (type instanceof AnnotatedArrayType) {
+ AnnotatedArrayType arrayType = (AnnotatedArrayType) type;
+
+ // Recursively determine the array class before visiting the component type.
+ Class<?> componentClass =
+ visitAnnotatedTypeInternal(arrayType.getAnnotatedGenericComponentType(), (c, a) -> {});
+ clazz = Array.newInstance(componentClass, 0).getClass();
+ visitor.accept(clazz, type.getDeclaredAnnotations());
+ visitAnnotatedTypeInternal(arrayType.getAnnotatedGenericComponentType(), visitor);
+ } else {
+ check(type.getType() instanceof Class<?>);
+ clazz = (Class<?>) type.getType();
+
+ visitor.accept(clazz, type.getDeclaredAnnotations());
+ }
+ return clazz;
+ }
+
+ public static AnnotatedType notNull(AnnotatedType type) {
+ return withExtraAnnotations(type, NOT_NULL);
+ }
+
+ /**
+ * Constructs an anonymous WithLength class that can be applied as an annotation to {@code type}
+ * with the given
+ * {@code min} and {@code max} values.
+ * @param type
+ * @param min
+ * @param max
+ * @return {@code type} with a `WithLength` annotation applied to it
+ */
+ public static AnnotatedType withLength(AnnotatedType type, int min, int max) {
+ WithLength withLength = withLengthImplementation(min, max);
+ return withExtraAnnotations(type, withLength);
+ }
+
+ private static WithLength withLengthImplementation(int min, int max) {
+ return new WithLength() {
+ @Override
+ public int min() {
+ return min;
+ }
+
+ @Override
+ public int max() {
+ return max;
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return WithLength.class;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof WithLength)) {
+ return false;
+ }
+ WithLength other = (WithLength) o;
+ return this.min() == other.min() && this.max() == other.max();
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 0;
+ hash += ("min".hashCode() * 127) ^ Integer.valueOf(this.min()).hashCode();
+ hash += ("max".hashCode() * 127) ^ Integer.valueOf(this.max()).hashCode();
+ return hash;
+ }
+ };
+ }
+
+ public static AnnotatedParameterizedType withTypeArguments(
+ AnnotatedType type, AnnotatedType... typeArguments) {
+ requireNonNull(type);
+ requireNonNullElements(typeArguments);
+ require(typeArguments.length > 0);
+ require(!(type instanceof AnnotatedParameterizedType || type instanceof AnnotatedTypeVariable
+ || type instanceof AnnotatedWildcardType || type instanceof AnnotatedArrayType),
+ "only plain annotated types are supported");
+ require(
+ ((Class<?>) type.getType()).getEnclosingClass() == null, "nested classes aren't supported");
+
+ ParameterizedType filledRawType = new ParameterizedType() {
+ @Override
+ public Type[] getActualTypeArguments() {
+ return stream(typeArguments).map(AnnotatedType::getType).toArray(Type[] ::new);
+ }
+
+ @Override
+ public Type getRawType() {
+ return type.getType();
+ }
+
+ @Override
+ public Type getOwnerType() {
+ // We require the class is top-level.
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return getRawType()
+ + stream(getActualTypeArguments()).map(Type::toString).collect(joining(",", "<", ">"));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ParameterizedType)) {
+ return false;
+ }
+ ParameterizedType other = (ParameterizedType) obj;
+ return getRawType().equals(other.getRawType()) && null == other.getOwnerType()
+ && Arrays.equals(getActualTypeArguments(), other.getActualTypeArguments());
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException(
+ "hashCode() is not supported as its behavior isn't specified");
+ }
+ };
+
+ return new AnnotatedParameterizedType() {
+ @Override
+ public AnnotatedType[] getAnnotatedActualTypeArguments() {
+ return Arrays.copyOf(typeArguments, typeArguments.length);
+ }
+
+ // @Override as of Java 9
+ @SuppressWarnings("Since15")
+ public AnnotatedType getAnnotatedOwnerType() {
+ return null;
+ }
+
+ @Override
+ public Type getType() {
+ return filledRawType;
+ }
+
+ @Override
+ public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
+ return type.getAnnotation(annotationClass);
+ }
+
+ @Override
+ public Annotation[] getAnnotations() {
+ return type.getAnnotations();
+ }
+
+ @Override
+ public Annotation[] getDeclaredAnnotations() {
+ return type.getDeclaredAnnotations();
+ }
+
+ @Override
+ public String toString() {
+ return annotatedTypeToString(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof AnnotatedParameterizedType)) {
+ return false;
+ }
+ AnnotatedParameterizedType other = (AnnotatedParameterizedType) obj;
+ // Can't call getAnnotatedOwnerType on Java 8, but since our own implementation always
+ // returns null, comparing getType().getOwnerType() via getType() is sufficient.
+ return Objects.equals(getType(), other.getType())
+ && Arrays.equals(
+ getAnnotatedActualTypeArguments(), other.getAnnotatedActualTypeArguments())
+ && Arrays.equals(getAnnotations(), other.getAnnotations());
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException(
+ "hashCode() is not supported as its behavior isn't specified");
+ }
+ };
+ }
+
+ public static AnnotatedType withExtraAnnotations(
+ AnnotatedType base, Annotation... extraAnnotations) {
+ requireNonNull(base);
+ requireNonNullElements(extraAnnotations);
+
+ if (extraAnnotations.length == 0) {
+ return base;
+ }
+
+ require(!(base instanceof AnnotatedTypeVariable || base instanceof AnnotatedWildcardType),
+ "Adding annotations to AnnotatedTypeVariables or AnnotatedWildcardTypes is not supported");
+ if (base instanceof AnnotatedArrayType) {
+ return new AugmentedArrayType((AnnotatedArrayType) base, extraAnnotations);
+ } else if (base instanceof AnnotatedParameterizedType) {
+ return new AugmentedParameterizedType((AnnotatedParameterizedType) base, extraAnnotations);
+ } else {
+ return new AugmentedAnnotatedType(base, extraAnnotations);
+ }
+ }
+
+ private static String annotatedTypeToString(AnnotatedType annotatedType) {
+ String annotations =
+ stream(annotatedType.getAnnotations()).map(Annotation::toString).collect(joining(" "));
+ if (annotations.isEmpty()) {
+ return annotatedType.getType().toString();
+ } else {
+ return annotations + " " + annotatedType.getType();
+ }
+ }
+
+ private static <T extends Annotation> T annotatedElementGetAnnotation(
+ AnnotatedElement element, Class<T> annotationClass) {
+ requireNonNull(annotationClass);
+ return stream(element.getAnnotations())
+ .filter(annotation -> annotationClass.equals(annotation.annotationType()))
+ .findFirst()
+ .map(annotationClass::cast)
+ .orElse(null);
+ }
+
+ public static Optional<Class<?>> findFirstParentIfClass(AnnotatedType type, Class<?>... parents) {
+ if (!(type.getType() instanceof Class<?>) ) {
+ return Optional.empty();
+ }
+ Class<?> clazz = (Class<?>) type.getType();
+ return Stream.of(parents).filter(parent -> parent.isAssignableFrom(clazz)).findFirst();
+ }
+
+ public static Optional<AnnotatedType> parameterTypeIfParameterized(
+ AnnotatedType type, Class<?> expectedParent) {
+ return parameterTypesIfParameterized(type, expectedParent).flatMap(typeArguments -> {
+ if (typeArguments.size() != 1) {
+ return Optional.empty();
+ } else {
+ AnnotatedType elementType = typeArguments.get(0);
+ if (!(elementType.getType() instanceof ParameterizedType)
+ && !(elementType.getType() instanceof Class)) {
+ return Optional.empty();
+ } else {
+ return Optional.of(elementType);
+ }
+ }
+ });
+ }
+
+ public static Optional<List<AnnotatedType>> parameterTypesIfParameterized(
+ AnnotatedType type, Class<?> expectedParent) {
+ if (!(type instanceof AnnotatedParameterizedType)) {
+ return Optional.empty();
+ }
+ Class<?> clazz = (Class<?>) ((ParameterizedType) type.getType()).getRawType();
+ if (!expectedParent.isAssignableFrom(clazz)) {
+ return Optional.empty();
+ }
+
+ AnnotatedType[] typeArguments =
+ ((AnnotatedParameterizedType) type).getAnnotatedActualTypeArguments();
+ if (typeArguments.length == 0) {
+ return Optional.empty();
+ }
+ return Optional.of(Collections.unmodifiableList(Arrays.asList(typeArguments)));
+ }
+
+ /**
+ * Whether {@code root} is contained in a directed cycle in the directed graph rooted at it and
+ * defined by the given {@code successors} function.
+ */
+ public static <T> boolean containedInDirectedCycle(T root, Function<T, Stream<T>> successors) {
+ HashSet<T> traversed = new HashSet<>();
+ ArrayDeque<T> toTraverse = new ArrayDeque<>();
+ toTraverse.addLast(root);
+ T currentNode;
+ while ((currentNode = toTraverse.pollLast()) != null) {
+ if (traversed.add(currentNode)) {
+ successors.apply(currentNode).forEachOrdered(toTraverse::addLast);
+ } else if (currentNode.equals(root)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static class AugmentedArrayType
+ extends AugmentedAnnotatedType implements AnnotatedArrayType {
+ private AugmentedArrayType(AnnotatedArrayType base, Annotation[] extraAnnotations) {
+ super(base, extraAnnotations);
+ }
+
+ @Override
+ public AnnotatedType getAnnotatedGenericComponentType() {
+ return ((AnnotatedArrayType) base).getAnnotatedGenericComponentType();
+ }
+
+ // @Override as of Java 9
+ @SuppressWarnings("Since15")
+ public AnnotatedType getAnnotatedOwnerType() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+ }
+
+ private static class AugmentedParameterizedType
+ extends AugmentedAnnotatedType implements AnnotatedParameterizedType {
+ private AugmentedParameterizedType(
+ AnnotatedParameterizedType base, Annotation[] extraAnnotations) {
+ super(base, extraAnnotations);
+ }
+
+ @Override
+ public AnnotatedType[] getAnnotatedActualTypeArguments() {
+ return ((AnnotatedParameterizedType) base).getAnnotatedActualTypeArguments();
+ }
+
+ // @Override as of Java 9
+ @SuppressWarnings("Since15")
+ public AnnotatedType getAnnotatedOwnerType() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+ }
+
+ private static class AugmentedAnnotatedType implements AnnotatedType {
+ protected final AnnotatedType base;
+ private final Annotation[] extraAnnotations;
+
+ private AugmentedAnnotatedType(AnnotatedType base, Annotation[] extraAnnotations) {
+ this.base = requireNonNull(base);
+ this.extraAnnotations = checkExtraAnnotations(base, extraAnnotations);
+ }
+
+ private static Annotation[] checkExtraAnnotations(
+ AnnotatedElement base, Annotation[] extraAnnotations) {
+ requireNonNullElements(extraAnnotations);
+ Set<Class<? extends Annotation>> existingAnnotationTypes =
+ stream(base.getDeclaredAnnotations())
+ .map(Annotation::annotationType)
+ .collect(Collectors.toCollection(HashSet::new));
+ for (Annotation annotation : extraAnnotations) {
+ boolean added = existingAnnotationTypes.add(annotation.annotationType());
+ require(added, annotation + " already directly present on " + base);
+ }
+ return extraAnnotations;
+ }
+
+ @Override
+ public Type getType() {
+ return base.getType();
+ }
+
+ @Override
+ public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
+ return annotatedElementGetAnnotation(this, annotationClass);
+ }
+
+ @Override
+ public Annotation[] getAnnotations() {
+ Set<Class<? extends Annotation>> directlyPresentTypes =
+ stream(getDeclaredAnnotations()).map(Annotation::annotationType).collect(toSet());
+ return Stream
+ .concat(
+ // Directly present annotations.
+ stream(getDeclaredAnnotations()),
+ // Present but not directly present annotations, never added by us as we don't add
+ // annotations to the super class.
+ stream(base.getAnnotations())
+ .filter(
+ annotation -> !directlyPresentTypes.contains(annotation.annotationType())))
+ .toArray(Annotation[] ::new);
+ }
+
+ @Override
+ public Annotation[] getDeclaredAnnotations() {
+ return Stream.concat(stream(base.getDeclaredAnnotations()), stream(extraAnnotations))
+ .toArray(Annotation[] ::new);
+ }
+
+ @Override
+ public String toString() {
+ return annotatedTypeToString(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ throw new UnsupportedOperationException(
+ "equals() is not supported as its behavior isn't specified");
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException(
+ "hashCode() is not supported as its behavior isn't specified");
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/WeakIdentityHashMap.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/WeakIdentityHashMap.java
new file mode 100644
index 00000000..0eef7c24
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/WeakIdentityHashMap.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.mutation.support;
+
+import static java.util.stream.Collectors.toSet;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * An unoptimized version of a {@link java.util.WeakHashMap} with the semantics of a
+ * {@link java.util.IdentityHashMap}.
+ *
+ * <p>If this class ever becomes a bottleneck, e.g. because of the IdentityWeakReference
+ * allocations, it should be replaced by a copy of the * {@link java.util.WeakHashMap} code with all
+ * {@code equals} calls dropped and all {@code hashCode} * calls replaced with {@link
+ * System#identityHashCode}.
+ */
+public final class WeakIdentityHashMap<K, V> implements Map<K, V> {
+ private final HashMap<WeakReference<K>, V> map = new HashMap<>();
+ private final ReferenceQueue<K> weaklyReachables = new ReferenceQueue<>();
+
+ @Override
+ public int size() {
+ removeNewWeaklyReachables();
+ return map.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ removeNewWeaklyReachables();
+ return map.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ removeNewWeaklyReachables();
+ return map.containsKey(new IdentityWeakReference<>(key));
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ removeNewWeaklyReachables();
+ return map.containsValue(value);
+ }
+
+ @Override
+ public V get(Object key) {
+ removeNewWeaklyReachables();
+ return map.get(new IdentityWeakReference<>(key));
+ }
+
+ @Override
+ public V put(K key, V value) {
+ removeNewWeaklyReachables();
+ return map.put(new IdentityWeakReference<>(key, weaklyReachables), value);
+ }
+
+ @Override
+ public V remove(Object key) {
+ removeNewWeaklyReachables();
+ return map.remove(new IdentityWeakReference<>(key));
+ }
+
+ @Override
+ public void putAll(Map<? extends K, ? extends V> otherMap) {
+ removeNewWeaklyReachables();
+ for (Entry<? extends K, ? extends V> entry : otherMap.entrySet()) {
+ map.put(new IdentityWeakReference<>(entry.getKey(), weaklyReachables), entry.getValue());
+ }
+ }
+
+ @Override
+ public void clear() {
+ map.clear();
+ }
+
+ @Override
+ public Set<K> keySet() {
+ removeNewWeaklyReachables();
+ return map.keySet().stream().map(WeakReference::get).filter(Objects::nonNull).collect(toSet());
+ }
+
+ @Override
+ public Collection<V> values() {
+ removeNewWeaklyReachables();
+ return map.values();
+ }
+
+ @Override
+ public Set<Entry<K, V>> entrySet() {
+ removeNewWeaklyReachables();
+ return map.entrySet()
+ .stream()
+ .map(e -> new SimpleEntry<>(e.getKey().get(), e.getValue()))
+ .filter(e -> e.getKey() != null)
+ .collect(toSet());
+ }
+
+ void collectKeysForTesting() {
+ map.keySet().forEach(ref -> {
+ ref.clear();
+ ref.enqueue();
+ });
+ }
+
+ private void removeNewWeaklyReachables() {
+ Reference<? extends K> referent;
+ while ((referent = weaklyReachables.poll()) != null) {
+ map.remove(referent);
+ }
+ }
+
+ private static final class IdentityWeakReference<T> extends WeakReference<T> {
+ private final int referentHashCode;
+
+ public IdentityWeakReference(T referent) {
+ super(referent);
+ this.referentHashCode = System.identityHashCode(referent);
+ }
+
+ public IdentityWeakReference(T referent, ReferenceQueue<? super T> queue) {
+ super(referent, queue);
+ this.referentHashCode = System.identityHashCode(referent);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof WeakReference)) {
+ return false;
+ }
+ T referent = get();
+ if (referent == null) {
+ return false;
+ }
+ return referent == ((WeakReference<?>) other).get();
+ }
+
+ @Override
+ public int hashCode() {
+ return referentHashCode;
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel
new file mode 100644
index 00000000..4b534545
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel
@@ -0,0 +1,16 @@
+load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library")
+
+java_jni_library(
+ name = "replay",
+ srcs = ["Replayer.java"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/driver:fuzzed_data_provider_impl",
+ ],
+)
+
+java_binary(
+ name = "Replayer",
+ visibility = ["//visibility:public"],
+ runtime_deps = [":replay"],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java b/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java
new file mode 100644
index 00000000..dc76328e
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java
@@ -0,0 +1,144 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.replay;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.driver.FuzzedDataProviderImpl;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+
+public class Replayer {
+ public static final int STATUS_FINDING = 77;
+ public static final int STATUS_OTHER_ERROR = 1;
+
+ public static void main(String[] args) {
+ if (args.length < 2) {
+ System.err.println("Usage: <fuzz target class> <input file path> <fuzzerInitialize args>...");
+ System.exit(STATUS_OTHER_ERROR);
+ }
+ ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);
+
+ Class<?> fuzzTargetClass;
+ try {
+ fuzzTargetClass = Class.forName(args[0]);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ System.exit(STATUS_OTHER_ERROR);
+ // Without this return the compiler sees fuzzTargetClass as possibly uninitialized.
+ return;
+ }
+
+ String inputFilePath = args[1];
+ byte[] input = loadInput(inputFilePath);
+
+ String[] fuzzTargetArgs = Arrays.copyOfRange(args, 2, args.length);
+ executeFuzzerInitialize(fuzzTargetClass, fuzzTargetArgs);
+ executeFuzzTarget(fuzzTargetClass, input);
+ }
+
+ private static byte[] loadInput(String inputFilePath) {
+ try {
+ return Files.readAllBytes(Paths.get(inputFilePath));
+ } catch (IOException e) {
+ e.printStackTrace();
+ System.exit(STATUS_OTHER_ERROR);
+ // Without this return the compiler sees loadInput as possibly not returning a value.
+ return null;
+ }
+ }
+
+ private static void executeFuzzerInitialize(Class<?> fuzzTarget, String[] args) {
+ // public static void fuzzerInitialize()
+ try {
+ Method fuzzerInitialize = fuzzTarget.getMethod("fuzzerInitialize");
+ fuzzerInitialize.invoke(null);
+ return;
+ } catch (Exception e) {
+ handleInvokeException(e, fuzzTarget);
+ }
+ // public static void fuzzerInitialize(String[] args)
+ try {
+ Method fuzzerInitialize = fuzzTarget.getMethod("fuzzerInitialize", String[].class);
+ fuzzerInitialize.invoke(null, (Object) args);
+ } catch (Exception e) {
+ handleInvokeException(e, fuzzTarget);
+ }
+ }
+
+ public static void executeFuzzTarget(Class<?> fuzzTarget, byte[] input) {
+ // public static void fuzzerTestOneInput(byte[] input)
+ try {
+ Method fuzzerTestOneInput = fuzzTarget.getMethod("fuzzerTestOneInput", byte[].class);
+ fuzzerTestOneInput.invoke(null, (Object) input);
+ return;
+ } catch (Exception e) {
+ handleInvokeException(e, fuzzTarget);
+ }
+ // public static void fuzzerTestOneInput(FuzzedDataProvider data)
+ try {
+ Method fuzzerTestOneInput =
+ fuzzTarget.getMethod("fuzzerTestOneInput", FuzzedDataProvider.class);
+ try (FuzzedDataProviderImpl fuzzedDataProvider = FuzzedDataProviderImpl.withJavaData(input)) {
+ fuzzerTestOneInput.invoke(null, fuzzedDataProvider);
+ }
+ return;
+ } catch (Exception e) {
+ handleInvokeException(e, fuzzTarget);
+ }
+ System.err.printf("%s must define exactly one of the following two functions:%n"
+ + " public static void fuzzerTestOneInput(byte[] ...)%n"
+ + " public static void fuzzerTestOneInput(FuzzedDataProvider ...)%n"
+ + "Note: Fuzz targets returning boolean are no longer supported; exceptions should%n"
+ + "be thrown instead of returning true.%n",
+ fuzzTarget.getName());
+ System.exit(STATUS_OTHER_ERROR);
+ }
+
+ private static void handleInvokeException(Exception e, Class<?> fuzzTarget) {
+ if (e instanceof NoSuchMethodException)
+ return;
+ if (e instanceof InvocationTargetException) {
+ filterOutOwnStackTraceElements(e.getCause(), fuzzTarget);
+ e.getCause().printStackTrace();
+ System.exit(STATUS_FINDING);
+ } else {
+ e.printStackTrace();
+ System.exit(STATUS_OTHER_ERROR);
+ }
+ }
+
+ private static void filterOutOwnStackTraceElements(Throwable t, Class<?> fuzzTarget) {
+ if (t.getCause() != null)
+ filterOutOwnStackTraceElements(t.getCause(), fuzzTarget);
+ if (t.getStackTrace() == null || t.getStackTrace().length == 0)
+ return;
+ StackTraceElement lowestFrame = t.getStackTrace()[t.getStackTrace().length - 1];
+ if (!lowestFrame.getClassName().equals(Replayer.class.getName())
+ || !lowestFrame.getMethodName().equals("main"))
+ return;
+ for (int i = t.getStackTrace().length - 1; i >= 0; i--) {
+ StackTraceElement frame = t.getStackTrace()[i];
+ if (frame.getClassName().equals(fuzzTarget.getName())
+ && frame.getMethodName().equals("fuzzerTestOneInput")) {
+ t.setStackTrace(Arrays.copyOfRange(t.getStackTrace(), 0, i + 1));
+ break;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
new file mode 100644
index 00000000..c31c86e4
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
@@ -0,0 +1,180 @@
+load("@com_github_johnynek_bazel_jar_jar//:jar_jar.bzl", "jar_jar")
+load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library")
+load("//bazel:compat.bzl", "SKIP_ON_WINDOWS")
+load("//bazel:jar.bzl", "strip_jar")
+
+# The transitive dependencies of this target will be appended to the search path
+# of the bootstrap class loader. They will be visible to all classes - care must
+# be taken to shade everything and generally keep this target as small as
+# possible.
+java_binary(
+ name = "jazzer_bootstrap_unshaded",
+ create_executable = False,
+ runtime_deps = [":jazzer_bootstrap_lib"],
+)
+
+java_library(
+ name = "jazzer_bootstrap_lib",
+ visibility = ["//src/main/java/com/code_intelligence/jazzer:__pkg__"],
+ runtime_deps = [
+ ":runtime",
+ "//sanitizers",
+ ],
+)
+
+# These classes with public Bazel visibility are contained in jazzer_bootstrap.jar
+# and will thus be available on the bootstrap class path. This target can be
+# passed to the `deploy_env` attribute of the Jazzer `java_binary` to ensure that
+# it doesn't bundle in these classes.
+java_binary(
+ name = "jazzer_bootstrap_env",
+ create_executable = False,
+ visibility = ["//src/main/java/com/code_intelligence/jazzer:__pkg__"],
+ runtime_deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
+ "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider",
+ ],
+)
+
+jar_jar(
+ name = "jazzer_bootstrap_unstripped",
+ input_jar = ":jazzer_bootstrap_unshaded_deploy.jar",
+ rules = "bootstrap_shade_rules",
+)
+
+strip_jar(
+ name = "jazzer_bootstrap",
+ out = "jazzer_bootstrap.jar",
+ jar = ":jazzer_bootstrap_unstripped",
+ paths_to_keep = [
+ "com/code_intelligence/jazzer/**",
+ "jaz/**",
+ "META-INF/MANIFEST.MF",
+ ],
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/android:__pkg__",
+ ],
+)
+
+sh_test(
+ name = "jazzer_bootstrap_shading_test",
+ srcs = ["verify_shading.sh"],
+ args = [
+ "$(rootpath jazzer_bootstrap.jar)",
+ ],
+ data = [
+ "jazzer_bootstrap.jar",
+ "@local_jdk//:bin/jar",
+ ],
+ tags = [
+ # Coverage instrumentation necessarily adds files to the jar that we
+ # wouldn't want to release and thus causes this test to fail.
+ "no-coverage",
+ ],
+ target_compatible_with = SKIP_ON_WINDOWS,
+)
+
+# At runtime, the AgentInstaller appends jazzer_bootstrap.jar to the bootstrap
+# class loader's search path - these classes must not be available on the
+# regular classpath. Since dependents should not have to resort to reflection to
+# access these classes they know will be there at runtime, this compile-time
+# only dependency can be used as a replacement.
+java_library(
+ name = "jazzer_bootstrap_compile_only",
+ neverlink = True,
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/autofuzz:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/driver:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/instrumentor:__pkg__",
+ ],
+ exports = [
+ ":fuzz_target_runner_natives",
+ ":runtime",
+ ],
+)
+
+# The following targets must only be referenced directly by tests or native implementations.
+
+java_jni_library(
+ name = "coverage_map",
+ srcs = ["CoverageMap.java"],
+ native_libs = select({
+ "@platforms//os:android": ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver"],
+ "//conditions:default": [],
+ }),
+ visibility = [
+ "//src/jmh/java/com/code_intelligence/jazzer/instrumentor:__pkg__",
+ "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__",
+ "//src/test:__subpackages__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/runtime:constants",
+ "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider",
+ ],
+)
+
+java_jni_library(
+ name = "trace_data_flow_native_callbacks",
+ srcs = ["TraceDataFlowNativeCallbacks.java"],
+ visibility = [
+ "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__",
+ ],
+ deps = ["@org_ow2_asm_asm//jar"],
+)
+
+java_jni_library(
+ name = "fuzz_target_runner_natives",
+ srcs = ["FuzzTargetRunnerNatives.java"],
+ visibility = ["//src/main/native/com/code_intelligence/jazzer/driver:__pkg__"],
+ deps = [
+ ":constants",
+ ],
+)
+
+java_jni_library(
+ name = "mutator",
+ srcs = ["Mutator.java"],
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer:__pkg__",
+ "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__",
+ ],
+)
+
+java_library(
+ name = "runtime",
+ srcs = [
+ "HardToCatchError.java",
+ "JazzerInternal.java",
+ "NativeLibHooks.java",
+ "TraceCmpHooks.java",
+ "TraceDivHooks.java",
+ "TraceIndirHooks.java",
+ ],
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/android:__pkg__",
+ "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__",
+ "//src/test:__subpackages__",
+ ],
+ runtime_deps = [
+ ":fuzz_target_runner_natives",
+ ":mutator",
+ # Access to Unsafe is possible without any tricks if the class that does it is loaded by the
+ # bootstrap loader. We thus want Jazzer to use this class from jazzer_bootstrap.
+ "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider",
+ ],
+ deps = [
+ ":constants",
+ ":coverage_map",
+ ":trace_data_flow_native_callbacks",
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
+ ],
+)
+
+# This target exposes a class that can safely be loaded in both the system and the bootstrap class
+# loader as it provides true constants that do not change over the lifetime of the JVM.
+java_library(
+ name = "constants",
+ srcs = ["Constants.java"],
+ visibility = ["//visibility:public"],
+)
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/Constants.java b/src/main/java/com/code_intelligence/jazzer/runtime/Constants.java
new file mode 100644
index 00000000..92f4a3ca
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/Constants.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.runtime;
+
+public final class Constants {
+ public static final boolean IS_ANDROID = System.getProperty("java.vm.vendor").contains("Android");
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java b/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java
new file mode 100644
index 00000000..a945a30a
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java
@@ -0,0 +1,169 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.runtime;
+
+import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID;
+
+import com.code_intelligence.jazzer.utils.UnsafeProvider;
+import com.github.fmeum.rules_jni.RulesJni;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import sun.misc.Unsafe;
+
+/**
+ * Represents the Java view on a libFuzzer 8 bit counter coverage map. By using a direct ByteBuffer,
+ * the counters are shared directly with native code.
+ */
+final public class CoverageMap {
+ static {
+ RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver");
+ }
+
+ private static final String ENV_MAX_NUM_COUNTERS = "JAZZER_MAX_NUM_COUNTERS";
+
+ private static final int MAX_NUM_COUNTERS = System.getenv(ENV_MAX_NUM_COUNTERS) != null
+ ? Integer.parseInt(System.getenv(ENV_MAX_NUM_COUNTERS))
+ : 1 << 20;
+
+ private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
+ private static final Class<?> LOG;
+ private static final MethodHandle LOG_INFO;
+ private static final MethodHandle LOG_ERROR;
+
+ static {
+ try {
+ LOG = Class.forName(
+ "com.code_intelligence.jazzer.utils.Log", false, ClassLoader.getSystemClassLoader());
+ LOG_INFO = MethodHandles.lookup().findStatic(
+ LOG, "info", MethodType.methodType(void.class, String.class));
+ LOG_ERROR = MethodHandles.lookup().findStatic(
+ LOG, "error", MethodType.methodType(void.class, String.class, Throwable.class));
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * The collection of coverage counters directly interacted with by classes that are instrumented
+ * for coverage. The instrumentation assumes that this is always one contiguous block of memory,
+ * so it is allocated once at maximum size. Using a larger number here increases the memory usage
+ * of all fuzz targets, but has otherwise no impact on performance.
+ */
+ public static final long countersAddress = UNSAFE.allocateMemory(MAX_NUM_COUNTERS);
+
+ static {
+ UNSAFE.setMemory(countersAddress, MAX_NUM_COUNTERS, (byte) 0);
+ initialize(countersAddress);
+ }
+
+ private static final int INITIAL_NUM_COUNTERS = 1 << 9;
+
+ static {
+ registerNewCounters(0, INITIAL_NUM_COUNTERS);
+ }
+
+ /**
+ * The number of coverage counters that are currently registered with libFuzzer. This number grows
+ * dynamically as classes are instrumented and should be kept as low as possible as libFuzzer has
+ * to iterate over the whole map for every execution.
+ */
+ private static int currentNumCounters = INITIAL_NUM_COUNTERS;
+
+ // Called via reflection.
+ @SuppressWarnings("unused")
+ public static void enlargeIfNeeded(int nextId) {
+ int newNumCounters = currentNumCounters;
+ while (nextId >= newNumCounters) {
+ newNumCounters = 2 * newNumCounters;
+ if (newNumCounters > MAX_NUM_COUNTERS) {
+ logError(
+ String.format(
+ "Maximum number (%s) of coverage counters exceeded. Try to limit the scope of a single fuzz target as "
+ + "much as possible to keep the fuzzer fast. If that is not possible, the maximum number of "
+ + "counters can be increased via the %s environment variable.",
+ MAX_NUM_COUNTERS, ENV_MAX_NUM_COUNTERS),
+ null);
+ System.exit(1);
+ }
+ }
+ if (newNumCounters > currentNumCounters) {
+ registerNewCounters(currentNumCounters, newNumCounters);
+ currentNumCounters = newNumCounters;
+ logInfo("New number of coverage counters: " + currentNumCounters);
+ }
+ }
+
+ // Called by the coverage instrumentation.
+ @SuppressWarnings("unused")
+ public static void recordCoverage(final int id) {
+ if (IS_ANDROID) {
+ enlargeIfNeeded(id);
+ }
+
+ final long address = countersAddress + id;
+ final byte counter = UNSAFE.getByte(address);
+ UNSAFE.putByte(address, (byte) (counter == -1 ? 1 : counter + 1));
+ }
+
+ public static Set<Integer> getCoveredIds() {
+ Set<Integer> coveredIds = new HashSet<>();
+ for (int id = 0; id < currentNumCounters; id++) {
+ if (UNSAFE.getByte(countersAddress + id) > 0) {
+ coveredIds.add(id);
+ }
+ }
+ return Collections.unmodifiableSet(coveredIds);
+ }
+
+ public static void replayCoveredIds(Set<Integer> coveredIds) {
+ for (int id : coveredIds) {
+ UNSAFE.putByte(countersAddress + id, (byte) 1);
+ }
+ }
+
+ private static void logInfo(String message) {
+ try {
+ LOG_INFO.invokeExact(message);
+ } catch (Throwable error) {
+ // Should not be reached, Log.error does not throw.
+ error.printStackTrace();
+ System.err.println("Failed to call Log.info:");
+ System.err.println(message);
+ }
+ }
+
+ private static void logError(String message, Throwable t) {
+ try {
+ LOG_ERROR.invokeExact(message, t);
+ } catch (Throwable error) {
+ // Should not be reached, Log.error does not throw.
+ error.printStackTrace();
+ System.err.println("Failed to call Log.error:");
+ System.err.println(message);
+ }
+ }
+
+ // Returns the IDs of all blocks that have been covered in at least one run (not just the current
+ // one).
+ public static native int[] getEverCoveredIds();
+
+ private static native void initialize(long countersAddress);
+
+ private static native void registerNewCounters(int oldNumCounters, int newNumCounters);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/FuzzTargetRunnerNatives.java b/src/main/java/com/code_intelligence/jazzer/runtime/FuzzTargetRunnerNatives.java
new file mode 100644
index 00000000..bbf74fdb
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/FuzzTargetRunnerNatives.java
@@ -0,0 +1,41 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.runtime;
+
+import com.github.fmeum.rules_jni.RulesJni;
+
+/**
+ * The native functions used by FuzzTargetRunner.
+ *
+ * <p>This class has to be loaded by the bootstrap class loader since the native library it loads
+ * links in libFuzzer and the Java hooks, which have to be on the bootstrap path so that they are
+ * seen by Java standard library classes, need to be able to call native libFuzzer callbacks.
+ */
+public class FuzzTargetRunnerNatives {
+ static {
+ if (!Constants.IS_ANDROID && FuzzTargetRunnerNatives.class.getClassLoader() != null) {
+ throw new IllegalStateException(
+ "FuzzTargetRunnerNatives must be loaded in the bootstrap loader");
+ }
+ RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver");
+ }
+
+ public static native int startLibFuzzer(
+ byte[][] args, Class<?> runner, boolean useExperimentalMutator);
+
+ public static native void printCrashingInput();
+
+ public static native void temporarilyDisableLibfuzzerExitHook();
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java b/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java
new file mode 100644
index 00000000..cf136051
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java
@@ -0,0 +1,82 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.runtime;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * An Error that rethrows itself when any of its getters is invoked.
+ */
+public class HardToCatchError extends Error {
+ public HardToCatchError() {
+ super();
+ }
+
+ @Override
+ public String getMessage() {
+ throw this;
+ }
+
+ @Override
+ public String getLocalizedMessage() {
+ throw this;
+ }
+
+ @Override
+ public synchronized Throwable initCause(Throwable cause) {
+ throw this;
+ }
+
+ @Override
+ public String toString() {
+ throw this;
+ }
+
+ @Override
+ public void printStackTrace() {
+ throw this;
+ }
+
+ @Override
+ public void printStackTrace(PrintStream s) {
+ throw this;
+ }
+
+ @Override
+ public void printStackTrace(PrintWriter s) {
+ throw this;
+ }
+
+ @Override
+ public StackTraceElement[] getStackTrace() {
+ throw this;
+ }
+
+ @Override
+ public int hashCode() {
+ throw this;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ throw this;
+ }
+
+ @Override
+ public Object clone() {
+ throw this;
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java
new file mode 100644
index 00000000..3b368531
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java
@@ -0,0 +1,50 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.runtime;
+
+import java.util.ArrayList;
+
+final public class JazzerInternal {
+ public static Throwable lastFinding;
+ // The value is only relevant when regression testing. Read by the bytecode emitted by
+ // HookMethodVisitor to enable hooks only when invoked from a @FuzzTest.
+ //
+ // Alternatives considered:
+ // Making this thread local rather than global may potentially allow to run fuzz tests in
+ // parallel with regular unit tests, but it is next to impossible to determine which thread is
+ // currently doing work for a fuzz test versus a regular unit test. Instead, @FuzzTest is
+ // annotated with @Isolated.
+ @SuppressWarnings("unused") public static boolean hooksEnabled = true;
+
+ private static final ArrayList<Runnable> onFuzzTargetReadyCallbacks = new ArrayList<>();
+
+ // Accessed from api.Jazzer via reflection.
+ public static void reportFindingFromHook(Throwable finding) {
+ lastFinding = finding;
+ // Throw an Error that is hard to catch (short of outright ignoring it) in order to quickly
+ // terminate the execution of the fuzz target. The finding will be reported as soon as the fuzz
+ // target returns even if this Error is swallowed.
+ throw new HardToCatchError();
+ }
+
+ public static void registerOnFuzzTargetReadyCallback(Runnable callback) {
+ onFuzzTargetReadyCallbacks.add(callback);
+ }
+
+ public static void onFuzzTargetReady(String fuzzTargetClass) {
+ onFuzzTargetReadyCallbacks.forEach(Runnable::run);
+ onFuzzTargetReadyCallbacks.clear();
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/Mutator.java b/src/main/java/com/code_intelligence/jazzer/runtime/Mutator.java
new file mode 100644
index 00000000..2d9a7f65
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/Mutator.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.runtime;
+
+import com.github.fmeum.rules_jni.RulesJni;
+
+public final class Mutator {
+ public static final boolean SHOULD_MOCK =
+ Boolean.parseBoolean(System.getenv("JAZZER_MOCK_LIBFUZZER_MUTATOR"));
+
+ static {
+ if (!SHOULD_MOCK) {
+ RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver");
+ }
+ }
+
+ public static native int defaultMutateNative(byte[] buffer, int size);
+
+ private Mutator() {}
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java
new file mode 100644
index 00000000..8572f05a
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java
@@ -0,0 +1,39 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.runtime;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+
+@SuppressWarnings("unused")
+final public class NativeLibHooks {
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime",
+ targetMethod = "loadLibrary", targetMethodDescriptor = "(Ljava/lang/String;)V")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System",
+ targetMethod = "loadLibrary", targetMethodDescriptor = "(Ljava/lang/String;)V")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", targetMethod = "load",
+ targetMethodDescriptor = "(Ljava/lang/String;)V")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "load",
+ targetMethodDescriptor = "(Ljava/lang/String;)V")
+ public static void
+ loadLibraryHook(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ if (Constants.IS_ANDROID) {
+ return;
+ }
+
+ TraceDataFlowNativeCallbacks.handleLibraryLoad();
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java
new file mode 100644
index 00000000..7fb15866
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java
@@ -0,0 +1,598 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.runtime;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+import java.util.*;
+
+@SuppressWarnings("unused")
+final public class TraceCmpHooks {
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte", targetMethod = "compare",
+ targetMethodDescriptor = "(BB)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "(BB)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short", targetMethod = "compare",
+ targetMethodDescriptor = "(SS)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "(SS)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer",
+ targetMethod = "compare", targetMethodDescriptor = "(II)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "(II)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "kotlin.jvm.internal.Intrinsics ",
+ targetMethod = "compare", targetMethodDescriptor = "(II)I")
+ public static void
+ integerCompare(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId) {
+ TraceDataFlowNativeCallbacks.traceCmpInt((int) arguments[0], (int) arguments[1], hookId);
+ }
+
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte",
+ targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Byte;)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short",
+ targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Short;)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer",
+ targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Integer;)I")
+ public static void
+ integerCompareTo(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ TraceDataFlowNativeCallbacks.traceCmpInt((int) thisObject, (int) arguments[0], hookId);
+ }
+
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", targetMethod = "compare",
+ targetMethodDescriptor = "(JJ)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "(JJ)I")
+ public static void
+ longCompare(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ TraceDataFlowNativeCallbacks.traceCmpLong((long) arguments[0], (long) arguments[1], hookId);
+ }
+
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long",
+ targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Long;)I")
+ public static void
+ longCompareTo(MethodHandle method, Long thisObject, Object[] arguments, int hookId) {
+ TraceDataFlowNativeCallbacks.traceCmpLong(thisObject, (long) arguments[0], hookId);
+ }
+
+ @MethodHook(type = HookType.BEFORE, targetClassName = "kotlin.jvm.internal.Intrinsics ",
+ targetMethod = "compare", targetMethodDescriptor = "(JJ)I")
+ public static void
+ longCompareKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId) {
+ TraceDataFlowNativeCallbacks.traceCmpLong((long) arguments[0], (long) arguments[1], hookId);
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String",
+ targetMethod = "equalsIgnoreCase")
+ public static void
+ equals(MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean areEqual) {
+ if (!areEqual && arguments.length == 1 && arguments[0] instanceof String) {
+ // The precise value of the result of the comparison is not used by libFuzzer as long as it is
+ // non-zero.
+ TraceDataFlowNativeCallbacks.traceStrcmp(thisObject, (String) arguments[0], 1, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.Object", targetMethod = "equals")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.CharSequence", targetMethod = "equals")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.Number", targetMethod = "equals")
+ public static void
+ genericEquals(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean areEqual) {
+ if (!areEqual && arguments.length == 1 && arguments[0] != null
+ && thisObject.getClass() == arguments[0].getClass()) {
+ TraceDataFlowNativeCallbacks.traceGenericCmp(thisObject, arguments[0], hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "clojure.lang.Util", targetMethod = "equiv")
+ public static void genericStaticEquals(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean areEqual) {
+ if (!areEqual && arguments.length == 2 && arguments[0] != null && arguments[1] != null
+ && arguments[1].getClass() == arguments[0].getClass()) {
+ TraceDataFlowNativeCallbacks.traceGenericCmp(arguments[0], arguments[1], hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "compareTo")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String",
+ targetMethod = "compareToIgnoreCase")
+ public static void
+ compareTo(
+ MethodHandle method, String thisObject, Object[] arguments, int hookId, Integer returnValue) {
+ if (returnValue != 0 && arguments.length == 1 && arguments[0] instanceof String) {
+ TraceDataFlowNativeCallbacks.traceStrcmp(
+ thisObject, (String) arguments[0], returnValue, hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "contentEquals")
+ public static void
+ contentEquals(MethodHandle method, String thisObject, Object[] arguments, int hookId,
+ Boolean areEqualContents) {
+ if (!areEqualContents && arguments.length == 1 && arguments[0] instanceof CharSequence) {
+ TraceDataFlowNativeCallbacks.traceStrcmp(
+ thisObject, ((CharSequence) arguments[0]).toString(), 1, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String",
+ targetMethod = "regionMatches", targetMethodDescriptor = "(ZILjava/lang/String;II)Z")
+ public static void
+ regionsMatches5(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ if (!returnValue) {
+ int toffset = (int) arguments[1];
+ String other = (String) arguments[2];
+ int ooffset = (int) arguments[3];
+ int len = (int) arguments[4];
+ regionMatchesInternal((String) thisObject, toffset, other, ooffset, len, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String",
+ targetMethod = "regionMatches", targetMethodDescriptor = "(ILjava/lang/String;II)Z")
+ public static void
+ regionMatches4(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ if (!returnValue) {
+ int toffset = (int) arguments[0];
+ String other = (String) arguments[1];
+ int ooffset = (int) arguments[2];
+ int len = (int) arguments[3];
+ regionMatchesInternal((String) thisObject, toffset, other, ooffset, len, hookId);
+ }
+ }
+
+ private static void regionMatchesInternal(
+ String thisString, int toffset, String other, int ooffset, int len, int hookId) {
+ if (toffset < 0 || ooffset < 0)
+ return;
+ int cappedThisStringEnd = Math.min(toffset + len, thisString.length());
+ int cappedOtherStringEnd = Math.min(ooffset + len, other.length());
+ String thisPart = thisString.substring(toffset, cappedThisStringEnd);
+ String otherPart = other.substring(ooffset, cappedOtherStringEnd);
+ TraceDataFlowNativeCallbacks.traceStrcmp(thisPart, otherPart, 1, hookId);
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "contains")
+ public static void
+ contains(
+ MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean doesContain) {
+ if (!doesContain && arguments.length == 1 && arguments[0] instanceof CharSequence) {
+ TraceDataFlowNativeCallbacks.traceStrstr(
+ thisObject, ((CharSequence) arguments[0]).toString(), hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "indexOf")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "lastIndexOf")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.StringBuffer", targetMethod = "indexOf")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.StringBuffer",
+ targetMethod = "lastIndexOf")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.StringBuilder", targetMethod = "indexOf")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.StringBuilder",
+ targetMethod = "lastIndexOf")
+ public static void
+ indexOf(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) {
+ if (returnValue == -1 && arguments.length >= 1 && arguments[0] instanceof String) {
+ TraceDataFlowNativeCallbacks.traceStrstr(
+ thisObject.toString(), (String) arguments[0], hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "startsWith")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "endsWith")
+ public static void
+ startsWith(MethodHandle method, String thisObject, Object[] arguments, int hookId,
+ Boolean doesStartOrEndsWith) {
+ if (!doesStartOrEndsWith && arguments.length >= 1 && arguments[0] instanceof String) {
+ TraceDataFlowNativeCallbacks.traceStrstr(thisObject, (String) arguments[0], hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "replace",
+ targetMethodDescriptor =
+ "(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;")
+ public static void
+ replace(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, String returnValue) {
+ String original = (String) thisObject;
+ // Report only if the replacement was not successful.
+ if (original.equals(returnValue)) {
+ String target = arguments[0].toString();
+ TraceDataFlowNativeCallbacks.traceStrstr(original, target, hookId);
+ }
+ }
+
+ // For standard Kotlin packages, which are named according to the pattern kotlin.*, we append a
+ // whitespace to the package name of the target class so that they are not mangled due to shading.
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.jvm.internal.Intrinsics ",
+ targetMethod = "areEqual")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "equals")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "equals$default")
+ public static void
+ equalsKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId,
+ Boolean equalStrings) {
+ if (!equalStrings && arguments.length >= 2 && arguments[0] instanceof String
+ && arguments[1] instanceof String) {
+ TraceDataFlowNativeCallbacks.traceStrcmp(
+ (String) arguments[0], (String) arguments[1], 1, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "contentEquals")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "contentEquals$default")
+ public static void
+ contentEqualKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId,
+ Boolean equalStrings) {
+ if (!equalStrings && arguments.length >= 2 && arguments[0] instanceof CharSequence
+ && arguments[1] instanceof CharSequence) {
+ TraceDataFlowNativeCallbacks.traceStrcmp(
+ arguments[0].toString(), arguments[1].toString(), 1, hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "compareTo")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "compareTo$default")
+ public static void
+ compareToKt(
+ MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Integer returnValue) {
+ if (returnValue != 0 && arguments.length >= 2 && arguments[0] instanceof String
+ && arguments[1] instanceof String) {
+ TraceDataFlowNativeCallbacks.traceStrcmp(
+ (String) arguments[0], (String) arguments[1], 1, hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "endsWith")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "endsWith$default")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "startsWith")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "startsWith$default")
+ public static void
+ startsWithKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId,
+ Boolean doesStartOrEndsWith) {
+ if (!doesStartOrEndsWith && arguments.length >= 2 && arguments[0] instanceof CharSequence
+ && arguments[1] instanceof CharSequence) {
+ TraceDataFlowNativeCallbacks.traceStrstr(
+ arguments[0].toString(), arguments[1].toString(), hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "contains")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "contains$default")
+ public static void
+ containsKt(
+ MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Boolean doesContain) {
+ if (!doesContain && arguments.length >= 2 && arguments[0] instanceof CharSequence
+ && arguments[1] instanceof CharSequence) {
+ TraceDataFlowNativeCallbacks.traceStrstr(
+ arguments[0].toString(), arguments[1].toString(), hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "indexOf")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "indexOf$default")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "lastIndexOf")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "lastIndexOf$default")
+ public static void
+ indexOfKt(
+ MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Integer returnValue) {
+ if (returnValue != -1 || arguments.length < 2 || !(arguments[0] instanceof CharSequence)) {
+ return;
+ }
+ if (arguments[1] instanceof String) {
+ TraceDataFlowNativeCallbacks.traceStrstr(
+ arguments[0].toString(), (String) arguments[1], hookId);
+ } else if (arguments[1] instanceof Character) {
+ TraceDataFlowNativeCallbacks.traceStrstr(
+ arguments[0].toString(), ((Character) arguments[1]).toString(), hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "replace")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replace$default")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replaceAfter")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replaceAfter$default")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replaceAfterLast")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replaceAfterLast$default")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replaceBefore")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replaceBefore$default")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replaceBeforeLast")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replaceBeforeLast$default")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replaceFirst")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "replaceFirst$default")
+ public static void
+ replaceKt(
+ MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, String returnValue) {
+ if (arguments.length < 2 || !(arguments[0] instanceof String)) {
+ return;
+ }
+ String original = (String) arguments[0];
+ if (!original.equals(returnValue)) {
+ return;
+ }
+
+ // We currently don't handle the overloads that take a regex as a second argument.
+ if (arguments[1] instanceof String || arguments[1] instanceof Character) {
+ TraceDataFlowNativeCallbacks.traceStrstr(original, arguments[1].toString(), hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "regionMatches",
+ targetMethodDescriptor = "(Ljava/lang/String;ILjava/lang/String;IIZ)Z")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "regionMatches$default",
+ targetMethodDescriptor = "(Ljava/lang/String;ILjava/lang/String;IIZILjava/lang/Object;)Z")
+ public static void
+ regionMatchesKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId,
+ Boolean doesRegionMatch) {
+ if (!doesRegionMatch) {
+ String thisString = arguments[0].toString();
+ int thisOffset = (int) arguments[1];
+ String other = arguments[2].toString();
+ int otherOffset = (int) arguments[3];
+ int length = (int) arguments[4];
+ regionMatchesInternal(thisString, thisOffset, other, otherOffset, length, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "indexOfAny")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "indexOfAny$default")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "lastIndexOfAny")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "lastIndexOfAny$default")
+ public static void
+ indexOfAnyKt(
+ MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Integer returnValue) {
+ if (returnValue == -1 && arguments.length >= 2 && arguments[0] instanceof CharSequence) {
+ guideTowardContainmentOfFirstElement(arguments[0].toString(), arguments[1], hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "findAnyOf")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "findAnyOf$default")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "findLastAnyOf")
+ @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ",
+ targetMethod = "findLastAnyOf$default")
+ public static void
+ findAnyKt(
+ MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Object returnValue) {
+ if (returnValue == null && arguments.length >= 2 && arguments[0] instanceof CharSequence) {
+ guideTowardContainmentOfFirstElement(arguments[0].toString(), arguments[1], hookId);
+ }
+ }
+
+ private static void guideTowardContainmentOfFirstElement(
+ String containingString, Object candidateCollectionObj, int hookId) {
+ if (candidateCollectionObj instanceof Collection<?>) {
+ Collection<?> strings = (Collection<?>) candidateCollectionObj;
+ if (strings.isEmpty()) {
+ return;
+ }
+ Object firstElementObj = strings.iterator().next();
+ if (firstElementObj instanceof CharSequence) {
+ TraceDataFlowNativeCallbacks.traceStrstr(
+ containingString, firstElementObj.toString(), hookId);
+ }
+ } else if (candidateCollectionObj.getClass().isArray()) {
+ if (candidateCollectionObj.getClass().getComponentType() == char.class) {
+ char[] chars = (char[]) candidateCollectionObj;
+ if (chars.length > 0) {
+ TraceDataFlowNativeCallbacks.traceStrstr(
+ containingString, Character.toString(chars[0]), hookId);
+ }
+ }
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "equals",
+ targetMethodDescriptor = "([B[B)Z")
+ public static void
+ arraysEquals(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ if (returnValue)
+ return;
+ byte[] first = (byte[]) arguments[0];
+ byte[] second = (byte[]) arguments[1];
+ TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId);
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "equals",
+ targetMethodDescriptor = "([BII[BII)Z")
+ public static void
+ arraysEqualsRange(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ if (returnValue)
+ return;
+ byte[] first =
+ Arrays.copyOfRange((byte[]) arguments[0], (int) arguments[1], (int) arguments[2]);
+ byte[] second =
+ Arrays.copyOfRange((byte[]) arguments[3], (int) arguments[4], (int) arguments[5]);
+ TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId);
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare",
+ targetMethodDescriptor = "([B[B)I")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "([B[B)I")
+ public static void
+ arraysCompare(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) {
+ if (returnValue == 0)
+ return;
+ byte[] first = (byte[]) arguments[0];
+ byte[] second = (byte[]) arguments[1];
+ TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId);
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare",
+ targetMethodDescriptor = "([BII[BII)I")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "([BII[BII)I")
+ public static void
+ arraysCompareRange(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) {
+ if (returnValue == 0)
+ return;
+ byte[] first =
+ Arrays.copyOfRange((byte[]) arguments[0], (int) arguments[1], (int) arguments[2]);
+ byte[] second =
+ Arrays.copyOfRange((byte[]) arguments[3], (int) arguments[4], (int) arguments[5]);
+ TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId);
+ }
+
+ // The maximal number of elements of a non-TreeMap Map that will be sorted and searched for the
+ // key closest to the current lookup key in the mapGet hook.
+ private static final int MAX_NUM_KEYS_TO_ENUMERATE = 100;
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Map", targetMethod = "get")
+ public static void mapGet(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) {
+ if (returnValue != null)
+ return;
+ if (arguments.length != 1) {
+ return;
+ }
+ if (thisObject == null)
+ return;
+ final Map map = (Map) thisObject;
+ if (map.size() == 0)
+ return;
+ final Object currentKey = arguments[0];
+ if (currentKey == null)
+ return;
+ // Find two valid map keys that bracket currentKey.
+ // This is a generalization of libFuzzer's __sanitizer_cov_trace_switch:
+ // https://github.com/llvm/llvm-project/blob/318942de229beb3b2587df09e776a50327b5cef0/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L564
+ Object lowerBoundKey = null;
+ Object upperBoundKey = null;
+ try {
+ if (map instanceof TreeMap) {
+ final TreeMap treeMap = (TreeMap) map;
+ try {
+ lowerBoundKey = treeMap.floorKey(currentKey);
+ upperBoundKey = treeMap.ceilingKey(currentKey);
+ } catch (ClassCastException ignored) {
+ // Can be thrown by floorKey and ceilingKey if currentKey is of a type that can't be
+ // compared to the maps keys.
+ }
+ } else if (currentKey instanceof Comparable) {
+ final Comparable comparableCurrentKey = (Comparable) currentKey;
+ // Find two keys that bracket currentKey.
+ // Note: This is not deterministic if map.size() > MAX_NUM_KEYS_TO_ENUMERATE.
+ int enumeratedKeys = 0;
+ for (Object validKey : map.keySet()) {
+ if (!(validKey instanceof Comparable))
+ continue;
+ final Comparable comparableValidKey = (Comparable) validKey;
+ // If the key sorts lower than the non-existing key, but higher than the current lower
+ // bound, update the lower bound and vice versa for the upper bound.
+ try {
+ if (comparableValidKey.compareTo(comparableCurrentKey) < 0
+ && (lowerBoundKey == null || comparableValidKey.compareTo(lowerBoundKey) > 0)) {
+ lowerBoundKey = validKey;
+ }
+ if (comparableValidKey.compareTo(comparableCurrentKey) > 0
+ && (upperBoundKey == null || comparableValidKey.compareTo(upperBoundKey) < 0)) {
+ upperBoundKey = validKey;
+ }
+ } catch (ClassCastException ignored) {
+ // Can be thrown by floorKey and ceilingKey if currentKey is of a type that can't be
+ // compared to the maps keys.
+ }
+ if (enumeratedKeys++ > MAX_NUM_KEYS_TO_ENUMERATE)
+ break;
+ }
+ }
+ } catch (ConcurrentModificationException ignored) {
+ // map was modified by another thread, skip this invocation
+ return;
+ }
+ // Modify the hook ID so that compares against distinct valid keys are traced separately.
+ if (lowerBoundKey != null) {
+ TraceDataFlowNativeCallbacks.traceGenericCmp(
+ currentKey, lowerBoundKey, hookId + lowerBoundKey.hashCode());
+ }
+ if (upperBoundKey != null) {
+ TraceDataFlowNativeCallbacks.traceGenericCmp(
+ currentKey, upperBoundKey, hookId + upperBoundKey.hashCode());
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "org.junit.jupiter.api.Assertions",
+ targetMethod = "assertNotEquals",
+ targetMethodDescriptor = "(Ljava/lang/Object;Ljava/lang/Object;)V")
+ @MethodHook(type = HookType.AFTER, targetClassName = "org.junit.jupiter.api.Assertions",
+ targetMethod = "assertNotEquals",
+ targetMethodDescriptor = "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;)V")
+ @MethodHook(type = HookType.AFTER, targetClassName = "org.junit.jupiter.api.Assertions",
+ targetMethod = "assertNotEquals",
+ targetMethodDescriptor =
+ "(Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/Supplier;)V")
+ public static void
+ assertEquals(MethodHandle method, Object node, Object[] args, int hookId, Object alwaysNull) {
+ if (args[0] != null && args[1] != null && args[0].getClass() == args[1].getClass()) {
+ TraceDataFlowNativeCallbacks.traceGenericCmp(args[0], args[1], hookId);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java
new file mode 100644
index 00000000..777eb0a2
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java
@@ -0,0 +1,125 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.runtime;
+
+import com.github.fmeum.rules_jni.RulesJni;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import org.objectweb.asm.Type;
+
+@SuppressWarnings("unused")
+final public class TraceDataFlowNativeCallbacks {
+ // Note that we are not encoding as modified UTF-8 here: The FuzzedDataProvider transparently
+ // converts CESU8 into modified UTF-8 by coding null bytes on two bytes. Since the fuzzer is more
+ // likely to insert literal null bytes, having both the fuzzer input and the reported string
+ // comparisons be CESU8 should perform even better than the current implementation using modified
+ // UTF-8.
+ private static final Charset FUZZED_DATA_CHARSET = Charset.forName("CESU8");
+
+ static {
+ RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver");
+ }
+
+ // It is possible for RulesJni#loadLibrary to trigger a hook even though it isn't instrumented if
+ // it uses regexes, which it does with at least some JDKs due to its use of String#format. This
+ // led to exceptions in the past when the hook ended up calling traceStrcmp or traceStrstr before
+ // the static initializer was run: FUZZED_DATA_CHARSET used to be initialized after the call and
+ // thus still had the value null when encodeForLibFuzzer was called, resulting in an NPE in
+ // String#getBytes(Charset). Just switching the order may actually make this bug worse: It could
+ // now lead to traceMemcmp being called before the native library has been loaded. We guard
+ // against this by making the hooks noops when static initialization of this class hasn't
+ // completed yet.
+ private static final boolean NATIVE_INITIALIZED = true;
+
+ public static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc);
+
+ public static void traceStrcmp(String s1, String s2, int result, int pc) {
+ if (NATIVE_INITIALIZED) {
+ traceMemcmp(encodeForLibFuzzer(s1), encodeForLibFuzzer(s2), result, pc);
+ }
+ }
+
+ public static void traceStrstr(String s1, String s2, int pc) {
+ if (NATIVE_INITIALIZED) {
+ traceStrstr0(encodeForLibFuzzer(s2), pc);
+ }
+ }
+
+ public static void traceReflectiveCall(Executable callee, int pc) {
+ String className = callee.getDeclaringClass().getCanonicalName();
+ String executableName = callee.getName();
+ String descriptor;
+ if (callee instanceof Method) {
+ descriptor = Type.getMethodDescriptor((Method) callee);
+ } else {
+ descriptor = Type.getConstructorDescriptor((Constructor<?>) callee);
+ }
+ tracePcIndir(Arrays.hashCode(new String[] {className, executableName, descriptor}), pc);
+ }
+
+ public static int traceCmpLongWrapper(long arg1, long arg2, int pc) {
+ traceCmpLong(arg1, arg2, pc);
+ // Long.compare serves as a substitute for the lcmp opcode, which can't be used directly
+ // as the stack layout required for the call can't be achieved without local variables.
+ return Long.compare(arg1, arg2);
+ }
+
+ // The caller has to ensure that arg1 and arg2 have the same class.
+ public static void traceGenericCmp(Object arg1, Object arg2, int pc) {
+ if (arg1 instanceof CharSequence) {
+ traceStrcmp(arg1.toString(), arg2.toString(), 1, pc);
+ } else if (arg1 instanceof Integer) {
+ traceCmpInt((int) arg1, (int) arg2, pc);
+ } else if (arg1 instanceof Long) {
+ traceCmpLong((long) arg1, (long) arg2, pc);
+ } else if (arg1 instanceof Short) {
+ traceCmpInt((short) arg1, (short) arg2, pc);
+ } else if (arg1 instanceof Byte) {
+ traceCmpInt((byte) arg1, (byte) arg2, pc);
+ } else if (arg1 instanceof Character) {
+ traceCmpInt((char) arg1, (char) arg2, pc);
+ } else if (arg1 instanceof Number) {
+ traceCmpLong(((Number) arg1).longValue(), ((Number) arg2).longValue(), pc);
+ } else if (arg1 instanceof byte[]) {
+ traceMemcmp((byte[]) arg1, (byte[]) arg2, 1, pc);
+ }
+ }
+
+ /* trace-cmp */
+ public static native void traceCmpInt(int arg1, int arg2, int pc);
+ public static native void traceConstCmpInt(int arg1, int arg2, int pc);
+ public static native void traceCmpLong(long arg1, long arg2, int pc);
+ public static native void traceSwitch(long val, long[] cases, int pc);
+ /* trace-div */
+ public static native void traceDivInt(int val, int pc);
+ public static native void traceDivLong(long val, int pc);
+ /* trace-gep */
+ public static native void traceGep(long val, int pc);
+ /* indirect-calls */
+ public static native void tracePcIndir(int callee, int caller);
+
+ public static native void handleLibraryLoad();
+
+ private static byte[] encodeForLibFuzzer(String str) {
+ // libFuzzer string hooks only ever consume the first 64 bytes, so we can definitely cut the
+ // string off after 64 characters.
+ return str.substring(0, Math.min(str.length(), 64)).getBytes(FUZZED_DATA_CHARSET);
+ }
+
+ private static native void traceStrstr0(byte[] needle, int pc);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java
new file mode 100644
index 00000000..c4991eb5
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java
@@ -0,0 +1,47 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.runtime;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+
+@SuppressWarnings("unused")
+final public class TraceDivHooks {
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer",
+ targetMethod = "divideUnsigned", targetMethodDescriptor = "(II)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer",
+ targetMethod = "remainderUnsigned", targetMethodDescriptor = "(II)I")
+ public static void
+ intUnsignedDivide(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ // Since the arguments are to be treated as unsigned integers we need a long to fit the
+ // divisor.
+ TraceDataFlowNativeCallbacks.traceDivLong(Integer.toUnsignedLong((int) arguments[1]), hookId);
+ }
+
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long",
+ targetMethod = "divideUnsigned", targetMethodDescriptor = "(JJ)J")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long",
+ targetMethod = "remainderUnsigned", targetMethodDescriptor = "(JJ)J")
+ public static void
+ longUnsignedDivide(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ long divisor = (long) arguments[1];
+ // Run the callback only if the divisor, which is regarded as an unsigned long, fits in a
+ // signed long, i.e., does not have the sign bit set.
+ if (divisor > 0) {
+ TraceDataFlowNativeCallbacks.traceDivLong(divisor, hookId);
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java
new file mode 100644
index 00000000..897ede6c
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java
@@ -0,0 +1,35 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.runtime;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+import java.lang.reflect.Executable;
+
+@SuppressWarnings("unused")
+final public class TraceIndirHooks {
+ // The reflection hook is of type AFTER as it should only report calls that did not fail because
+ // of incorrect arguments passed.
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.reflect.Method", targetMethod = "invoke")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.reflect.Constructor",
+ targetMethod = "newInstance")
+ public static void
+ methodInvoke(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) {
+ TraceDataFlowNativeCallbacks.traceReflectiveCall((Executable) thisObject, hookId);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/bootstrap_shade_rules b/src/main/java/com/code_intelligence/jazzer/runtime/bootstrap_shade_rules
new file mode 100644
index 00000000..0cafcf0a
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/bootstrap_shade_rules
@@ -0,0 +1,4 @@
+rule com.github.fmeum.rules_jni.** com.code_intelligence.jazzer.bootstrap.@0
+rule kotlin.** com.code_intelligence.jazzer.bootstrap.@0
+rule net.sf.jsqlparser.** com.code_intelligence.jazzer.bootstrap.@0
+rule org.objectweb.asm.** com.code_intelligence.jazzer.bootstrap.@0
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/verify_shading.sh b/src/main/java/com/code_intelligence/jazzer/runtime/verify_shading.sh
new file mode 100755
index 00000000..b3a74ea9
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/verify_shading.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env sh
+# Copyright 2022 Code Intelligence GmbH
+#
+# 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.
+
+[ -f "$1" ] || exit 1
+# List all files in the jar and exclude an allowed list of files.
+# Since grep fails if there is no match, ! ... | grep ... fails if there is a
+# match.
+! external/local_jdk/bin/jar tf "$1" | \
+ grep -v \
+ -e '^com/$' \
+ -e '^com/code_intelligence/$' \
+ -e '^com/code_intelligence/jazzer/' \
+ -e '^jaz/' \
+ -e '^META-INF/$' \
+ -e '^META-INF/MANIFEST.MF$'
diff --git a/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel
new file mode 100644
index 00000000..ff9e0d3c
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel
@@ -0,0 +1,70 @@
+load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+load("//bazel:kotlin.bzl", "ktlint")
+
+kt_jvm_library(
+ name = "utils",
+ srcs = ["Utils.kt"],
+ visibility = ["//visibility:public"],
+)
+
+kt_jvm_library(
+ name = "class_name_globber",
+ srcs = ["ClassNameGlobber.kt"],
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/instrumentor:__pkg__",
+ ],
+ deps = [":simple_glob_matcher"],
+)
+
+java_library(
+ name = "log",
+ srcs = ["Log.java"],
+ visibility = ["//visibility:public"],
+)
+
+kt_jvm_library(
+ name = "manifest_utils",
+ srcs = ["ManifestUtils.kt"],
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__",
+ "//src/main/java/com/code_intelligence/jazzer/driver:__pkg__",
+ ],
+ deps = [":log"],
+)
+
+kt_jvm_library(
+ name = "simple_glob_matcher",
+ srcs = ["SimpleGlobMatcher.kt"],
+ visibility = [
+ "//src/main/java/com/code_intelligence/jazzer/autofuzz:__pkg__",
+ ],
+)
+
+java_library(
+ name = "unsafe_provider",
+ srcs = ["UnsafeProvider.java"],
+ visibility = [
+ "//:__subpackages__",
+ ],
+)
+
+java_library(
+ name = "unsafe_utils",
+ srcs = ["UnsafeUtils.java"],
+ visibility = [
+ "//:__subpackages__",
+ ],
+ deps = [
+ ":unsafe_provider",
+ "@org_ow2_asm_asm//jar",
+ ],
+)
+
+java_library(
+ name = "zip_utils",
+ srcs = ["ZipUtils.java"],
+ visibility = ["//visibility:public"],
+)
+
+ktlint()
diff --git a/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt b/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt
new file mode 100644
index 00000000..c6fa20a7
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt
@@ -0,0 +1,66 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.utils
+
+private val BASE_INCLUDED_CLASS_NAME_GLOBS = listOf(
+ "**", // everything
+)
+
+// We use both a strong indicator for running as a Bazel test together with an indicator for a
+// Bazel coverage run to rule out false positives.
+private val IS_BAZEL_COVERAGE_RUN = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") != null &&
+ System.getenv("COVERAGE_DIR") != null
+
+private val ADDITIONAL_EXCLUDED_NAME_GLOBS_FOR_BAZEL_COVERAGE = listOf(
+ "com.google.testing.coverage.**",
+ "org.jacoco.**",
+)
+
+private val BASE_EXCLUDED_CLASS_NAME_GLOBS = listOf(
+ // JDK internals
+ "\\[**", // array types
+ "java.**",
+ "javax.**",
+ "jdk.**",
+ "sun.**",
+ "com.sun.**", // package for Proxy objects
+ // Azul JDK internals
+ "com.azul.tooling.**",
+ // Kotlin internals
+ "kotlin.**",
+ // Jazzer internals
+ "com.code_intelligence.jazzer.**",
+ "jaz.Ter", // safe companion of the honeypot class used by sanitizers
+ "jaz.Zer", // honeypot class used by sanitizers
+ // Test and instrumentation tools
+ "org.junit.**", // dependency of @FuzzTest
+ "org.mockito.**", // can cause instrumentation cycles
+ "net.bytebuddy.**", // ignore Byte Buddy, though it's probably shaded
+ "org.jetbrains.**", // ignore JetBrains products (coverage agent)
+) + if (IS_BAZEL_COVERAGE_RUN) ADDITIONAL_EXCLUDED_NAME_GLOBS_FOR_BAZEL_COVERAGE else listOf()
+
+class ClassNameGlobber(includes: List<String>, excludes: List<String>) {
+ // If no include globs are provided, start with all classes.
+ private val includeMatchers = includes.ifEmpty { BASE_INCLUDED_CLASS_NAME_GLOBS }
+ .map(::SimpleGlobMatcher)
+
+ // If no include globs are provided, additionally exclude stdlib classes as well as our own classes.
+ private val excludeMatchers = (if (includes.isEmpty()) BASE_EXCLUDED_CLASS_NAME_GLOBS + excludes else excludes)
+ .map(::SimpleGlobMatcher)
+
+ fun includes(className: String): Boolean {
+ return includeMatchers.any { it.matches(className) } && excludeMatchers.none { it.matches(className) }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/utils/Log.java b/src/main/java/com/code_intelligence/jazzer/utils/Log.java
new file mode 100644
index 00000000..bccd3a32
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/utils/Log.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.utils;
+
+import java.io.PrintStream;
+
+/**
+ * Provides static functions that should be used for any kind of output (structured or unstructured)
+ * emitted by the fuzzer.
+ *
+ * <p>Output is printed to {@link System#err} and {@link System#out} until {@link
+ * Log#fixOutErr(PrintStream, PrintStream)} is called, which locks in the {@link PrintStream}s to be
+ * used from there on.
+ */
+public class Log {
+ // Don't use directly, always use getOut() and getErr() instead - when these fields haven't been
+ // set yet, we want to resolve them dynamically as System.out and System.err, which may change
+ // over the course of the fuzzer's lifetime.
+ private static PrintStream fixedOut;
+ private static PrintStream fixedErr;
+
+ /**
+ * The {@link PrintStream}s to use for all output from this call on.
+ */
+ public static void fixOutErr(PrintStream out, PrintStream err) {
+ if (out == null) {
+ throw new IllegalArgumentException("out must not be null");
+ }
+ if (err == null) {
+ throw new IllegalArgumentException("err must not be null");
+ }
+ Log.fixedOut = out;
+ Log.fixedErr = err;
+ }
+
+ public static void println(String message) {
+ getErr().println(message);
+ }
+
+ public static void structuredOutput(String output) {
+ getOut().println(output);
+ }
+
+ public static void info(String message) {
+ println("INFO: ", message, null);
+ }
+
+ public static void warn(String message) {
+ warn(message, null);
+ }
+
+ public static void warn(String message, Throwable t) {
+ println("WARN: ", message, t);
+ }
+
+ public static void error(String message) {
+ error(message, null);
+ }
+
+ public static void error(Throwable t) {
+ error(null, t);
+ }
+
+ public static void error(String message, Throwable t) {
+ println("ERROR: ", message, t);
+ }
+
+ public static void finding(Throwable t) {
+ println("\n== Java Exception: ", null, t);
+ }
+
+ private static void println(String prefix, String message, Throwable t) {
+ PrintStream err = getErr();
+ err.print(prefix);
+ if (message != null) {
+ err.println(message + (t != null ? ":" : ""));
+ }
+ if (t != null) {
+ t.printStackTrace(err);
+ }
+ }
+
+ private static PrintStream getOut() {
+ return fixedOut != null ? fixedOut : System.out;
+ }
+
+ private static PrintStream getErr() {
+ return fixedErr != null ? fixedErr : System.err;
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt b/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt
new file mode 100644
index 00000000..9d413a0e
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt
@@ -0,0 +1,50 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.utils
+
+import java.util.jar.Manifest
+
+object ManifestUtils {
+
+ private const val FUZZ_TARGET_CLASS = "Jazzer-Fuzz-Target-Class"
+ const val HOOK_CLASSES = "Jazzer-Hook-Classes"
+
+ fun combineManifestValues(attribute: String): List<String> {
+ val manifests = ManifestUtils::class.java.classLoader.getResources("META-INF/MANIFEST.MF")
+ return manifests.asSequence().mapNotNull { url ->
+ url.openStream().use { inputStream ->
+ val manifest = Manifest(inputStream)
+ manifest.mainAttributes.getValue(attribute)
+ }
+ }.toList()
+ }
+
+ /**
+ * Returns the value of the `Fuzz-Target-Class` manifest attribute if there is a unique one among all manifest
+ * files in the classpath.
+ */
+ @JvmStatic
+ fun detectFuzzTargetClass(): String? {
+ val fuzzTargets = combineManifestValues(FUZZ_TARGET_CLASS)
+ return when (fuzzTargets.size) {
+ 0 -> null
+ 1 -> fuzzTargets.first()
+ else -> {
+ Log.warn("More than one Jazzer-Fuzz-Target-Class manifest entry detected on the classpath.")
+ null
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt b/src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt
new file mode 100644
index 00000000..fb497fda
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt
@@ -0,0 +1,71 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.utils
+
+class SimpleGlobMatcher(val glob: String) {
+ private enum class Type {
+ // foo.bar (matches foo.bar only)
+ FULL_MATCH,
+
+ // foo.** (matches foo.bar and foo.bar.baz)
+ PATH_WILDCARD_SUFFIX,
+
+ // foo.* (matches foo.bar, but not foo.bar.baz)
+ SEGMENT_WILDCARD_SUFFIX,
+ }
+
+ private val type: Type
+ private val prefix: String
+
+ init {
+ // Remain compatible with globs such as "\\[" that use escaping.
+ val pattern = glob.replace("\\", "")
+ when {
+ !pattern.contains('*') -> {
+ type = Type.FULL_MATCH
+ prefix = pattern
+ }
+ // Ends with "**" and contains no other '*'.
+ pattern.endsWith("**") && pattern.indexOf('*') == pattern.length - 2 -> {
+ type = Type.PATH_WILDCARD_SUFFIX
+ prefix = pattern.removeSuffix("**")
+ }
+ // Ends with "*" and contains no other '*'.
+ pattern.endsWith('*') && pattern.indexOf('*') == pattern.length - 1 -> {
+ type = Type.SEGMENT_WILDCARD_SUFFIX
+ prefix = pattern.removeSuffix("*")
+ }
+ else -> throw IllegalArgumentException(
+ "Unsupported glob pattern (only foo.bar, foo.* and foo.** are supported): $pattern",
+ )
+ }
+ }
+
+ /**
+ * Checks whether [maybeInternalClassName], which may be internal (foo/bar) or not (foo.bar), matches [glob].
+ */
+ fun matches(maybeInternalClassName: String): Boolean {
+ val className = maybeInternalClassName.replace('/', '.')
+ return when (type) {
+ Type.FULL_MATCH -> className == prefix
+ Type.PATH_WILDCARD_SUFFIX -> className.startsWith(prefix)
+ Type.SEGMENT_WILDCARD_SUFFIX -> {
+ // className starts with prefix and contains no further '.'.
+ className.startsWith(prefix) &&
+ className.indexOf('.', startIndex = prefix.length) == -1
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/utils/UnsafeProvider.java b/src/main/java/com/code_intelligence/jazzer/utils/UnsafeProvider.java
new file mode 100644
index 00000000..e36e64c4
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/utils/UnsafeProvider.java
@@ -0,0 +1,56 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// 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.
+
+package com.code_intelligence.jazzer.utils;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import sun.misc.Unsafe;
+
+public final class UnsafeProvider {
+ private static final Unsafe UNSAFE = getUnsafeInternal();
+
+ public static Unsafe getUnsafe() {
+ return UNSAFE;
+ }
+
+ private static Unsafe getUnsafeInternal() {
+ try {
+ // The Jazzer runtime is loaded by the bootstrap class loader and should thus pass the
+ // security checks in getUnsafe, so try that first.
+ return Unsafe.getUnsafe();
+ } catch (Throwable unused) {
+ // If not running as an agent, use the classical reflection trick to get an Unsafe instance,
+ // taking into account that the private field may have a name other than "theUnsafe":
+ // https://android.googlesource.com/platform/libcore/+/gingerbread/luni/src/main/java/sun/misc/Unsafe.java#32
+ for (Field f : Unsafe.class.getDeclaredFields()) {
+ if (f.getType() == Unsafe.class) {
+ f.setAccessible(true);
+ try {
+ return (Unsafe) f.get(null);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(
+ "Please file a bug at https://github.com/CodeIntelligenceTesting/jazzer/issues/new "
+ + "with this information: Failed to access Unsafe member on Unsafe class",
+ e);
+ }
+ }
+ }
+ throw new IllegalStateException(String.format(
+ "Please file a bug at https://github.com/CodeIntelligenceTesting/jazzer/issues/new with "
+ + "this information: Failed to find Unsafe member on Unsafe class, have: "
+ + Arrays.deepToString(Unsafe.class.getDeclaredFields())));
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/utils/UnsafeUtils.java b/src/main/java/com/code_intelligence/jazzer/utils/UnsafeUtils.java
new file mode 100644
index 00000000..30c88dc9
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/utils/UnsafeUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.utils;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Optional;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Opcodes;
+
+public final class UnsafeUtils {
+ /**
+ * Dynamically creates a concrete class implementing the given abstract class.
+ *
+ * <p>The returned class will not be functional and should only be used to construct instances
+ * via {@link sun.misc.Unsafe#allocateInstance(Class)}.
+ */
+ public static <T> Class<? extends T> defineAnonymousConcreteSubclass(Class<T> abstractClass) {
+ if (!Modifier.isAbstract(abstractClass.getModifiers())) {
+ throw new IllegalArgumentException(abstractClass + " is not abstract");
+ }
+
+ ClassWriter cw = new ClassWriter(0);
+ String superClassName = abstractClass.getName().replace('.', '/');
+ // Only the package of the class name matters, the actual name is generated. defineHiddenClass
+ // requires the package of the new class to match the one of the lookup.
+ String className = UnsafeUtils.class.getPackage().getName().replace('.', '/') + "/Anonymous";
+ cw.visit(Opcodes.V1_8, 0, className, null, superClassName, null);
+ cw.visitEnd();
+
+ try {
+ Optional<Method> defineHiddenClass =
+ Arrays.stream(Lookup.class.getMethods())
+ .filter(method -> method.getName().equals("defineHiddenClass"))
+ .findFirst();
+ Optional<Class<?>> classOption =
+ Arrays.stream(Lookup.class.getClasses())
+ .filter(clazz -> clazz.getSimpleName().equals("ClassOption"))
+ .findFirst();
+ // MethodHandles.Lookup#defineHiddenClass is available as of Java 15.
+ // Unsafe#defineAnonymousClass has been removed in Java 17.
+ if (defineHiddenClass.isPresent() && classOption.isPresent()) {
+ return ((MethodHandles.Lookup) defineHiddenClass.get().invoke(MethodHandles.lookup(),
+ cw.toByteArray(), true, Array.newInstance(classOption.get(), 0)))
+ .lookupClass()
+ .asSubclass(abstractClass);
+ } else {
+ return (Class<? extends T>) UnsafeProvider.getUnsafe()
+ .getClass()
+ .getMethod("defineAnonymousClass", Class.class, byte[].class, Object[].class)
+ .invoke(UnsafeProvider.getUnsafe(), UnsafeUtils.class, cw.toByteArray(), null);
+ }
+ } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private UnsafeUtils() {}
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt b/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt
new file mode 100644
index 00000000..2de47820
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt
@@ -0,0 +1,45 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// 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.
+@file:JvmName("Utils")
+
+package com.code_intelligence.jazzer.utils
+
+import java.lang.reflect.Executable
+
+val Class<*>.readableDescriptor: String
+ get() = when {
+ isPrimitive -> {
+ when (this) {
+ Boolean::class.javaPrimitiveType -> "boolean"
+ Byte::class.javaPrimitiveType -> "byte"
+ Char::class.javaPrimitiveType -> "char"
+ Short::class.javaPrimitiveType -> "short"
+ Int::class.javaPrimitiveType -> "int"
+ Long::class.javaPrimitiveType -> "long"
+ Float::class.javaPrimitiveType -> "float"
+ Double::class.javaPrimitiveType -> "double"
+ java.lang.Void::class.javaPrimitiveType -> "void"
+ else -> throw IllegalStateException("Unknown primitive type: $name")
+ }
+ }
+ isArray -> "${componentType.readableDescriptor}[]"
+ java.lang.Object::class.java.isAssignableFrom(this) -> name
+ else -> throw IllegalArgumentException("Unknown class type: $name")
+ }
+
+// This does not include the return type as the parameter descriptors already uniquely identify the executable.
+val Executable.readableDescriptor: String
+ get() = parameterTypes.joinToString(separator = ",", prefix = "(", postfix = ")") { parameterType ->
+ parameterType.readableDescriptor
+ }
diff --git a/src/main/java/com/code_intelligence/jazzer/utils/ZipUtils.java b/src/main/java/com/code_intelligence/jazzer/utils/ZipUtils.java
new file mode 100644
index 00000000..4da35c3f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/utils/ZipUtils.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * 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.
+ */
+
+package com.code_intelligence.jazzer.utils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.IllegalArgumentException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+public final class ZipUtils {
+ private ZipUtils() {}
+
+ public static Set<String> mergeZipToZip(String src, ZipOutputStream zos, Set<String> skipFiles)
+ throws IOException {
+ HashSet<String> filesAdded = new HashSet<>();
+ try (JarFile jarFile = new JarFile(src)) {
+ // Copy entries from src to dst (jarFile to ZipOutputStream)
+ Enumeration<JarEntry> allEntries = jarFile.entries();
+ while (allEntries.hasMoreElements()) {
+ JarEntry entry = allEntries.nextElement();
+ if (skipFiles != null && skipFiles.contains(entry.getName())) {
+ continue;
+ }
+
+ zos.putNextEntry(new ZipEntry(entry.getName()));
+ try (InputStream is = jarFile.getInputStream(entry)) {
+ byte[] buf = new byte[1024];
+ int i = 0;
+ while ((i = is.read(buf)) != -1) {
+ zos.write(buf, 0, i);
+ }
+
+ zos.closeEntry();
+ filesAdded.add(entry.getName());
+ }
+ }
+ }
+
+ return filesAdded;
+ }
+
+ public static Set<String> mergeDirectoryToZip(String src, ZipOutputStream zos,
+ Set<String> skipFiles) throws IllegalArgumentException, IOException {
+ HashSet<String> filesAdded = new HashSet<>();
+ File sourceDir = new File(src);
+ if (!sourceDir.isDirectory()) {
+ throw new IllegalArgumentException("Argument src must be a directory.");
+ }
+
+ Files.walkFileTree(sourceDir.toPath(), new SimpleFileVisitor<Path>() {
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ String zipPath = sourceDir.toPath().relativize(file).toString();
+ if (skipFiles.stream().anyMatch(zipPath::endsWith)) {
+ return FileVisitResult.CONTINUE;
+ }
+
+ zos.putNextEntry(new ZipEntry(zipPath));
+ Files.copy(file, zos);
+ filesAdded.add(zipPath);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+
+ return filesAdded;
+ }
+
+ public static void extractFile(String srcZip, String targetFile, String outputFilePath)
+ throws IOException {
+ try (OutputStream out = new FileOutputStream(outputFilePath);
+ ZipInputStream zis = new ZipInputStream(new FileInputStream(srcZip));) {
+ ZipEntry ze = zis.getNextEntry();
+ while (ze != null) {
+ if (ze.getName().equals(targetFile)) {
+ byte[] buf = new byte[1024];
+ int read = 0;
+
+ while ((read = zis.read(buf)) > -1) {
+ out.write(buf, 0, read);
+ }
+
+ out.close();
+ break;
+ }
+
+ ze = zis.getNextEntry();
+ }
+ }
+ }
+}